kossnocorp / typesaurus

🦕 Type-safe TypeScript-first ODM for Firestore
https://typesaurus.com
414 stars 35 forks source link

Variable Collection type with variable subCollection types. #135

Closed drewdearing closed 3 months ago

drewdearing commented 3 months ago

I have a collection with variable types and a subcollection, that ideally has a different set of types depending on the parent type.

export const db = schema(($) => ({
  models: $.collection<[ModelA, ModelB]>().sub({
    items:
      $.collection<[ModelAItem, OtherModelAItem, ModelBItem]>(),
  }),
}));

In order to make this work, I have to narrow the items depending on the model type.

const useModelAItems= (
  modelA: ModelADoc,
): [ModelAItemDoc[] | undefined, boolean, unknown] => {
  const [items, { loading, error }] = useRead(
    db.models(modelA.ref.id).items.all().on
  );
  //TODO useMemo
  const modelAItems = items
    ?.map((item) =>
      item.narrow<ModelAItem>(
        (data) => data.modelType === "A" && data
      )
    )
    .filter((item) => item) as ModelAItemDoc[] | undefined;
  return [modelAItems, loading, error];
};

I think it makes more sense to structure the db like this.

export const db = schema(($) => ({
  aModels: $.collection<ModelA>().sub({
    items:
      $.collection<[ModelAItem, OtherModelAItem]>(),
  }),
 bModels: $.collection<ModelB>().sub({
    items:
      $.collection<[ModelBItem]>(),
  }),
}));

But then I lose the ability to query both ModelA and ModelB in a single query. Ideally, I'd like to either have the ability to union collection queries across collection types or define subcollections by mapping variable types to a list of subcollections.

const [models] = useRead(db.aModels.all().union(db.bModels.all()).on);

or

export const db = schema(($) => ({
  models: [
    $.collection<ModelA>().sub({
      items: $.collection<[ModelAItem, OtherModelAItem]>(),
    }),
    $.collection<ModelB>().sub({
      items: $.collection<ModelBItem>(),
    }),
  ],
}));
kossnocorp commented 3 months ago

Hey! That's an interesting problem. I can see how it can be helpful, but I'd appreciate it if you could share examples closer to the real problem you are solving. I don't need to see complete types, but having real-world collections and model names with a brief description of what it's for will help me.

You probably considered it, but my solution to the problem would be to avoid using subcollections and have a separate collection for each variant unless they really have to be mixed, something like that:

export const db = schema(($) => ({
  models: $.collection<[ModelA, ModelB]>(),

  aItems: $.collection<[ModelAItem, OtherModelAItem]>(),

  bItems: $.collection<[ModelBItem]>()
}));

Will it work?

Another approach is to use collection groups and nest models into different collections:

export const db = schema(($) => ({
  a: $.collection<{}>().sub({
    models: $.collection<ModelA>().sub({
      items:
        $.collection<[ModelAItem, OtherModelAItem]>(),
    }),
  }),
  b: $.collection<{}>().sub({
    models: $.collection<ModelB>().sub({
      items:
        $.collection<[ModelBItem]>(),
    }),
  }),
}));

So that you can query both models and items collection groups and have them completely isolated.

But again, I need to understand the actual data model to tell if it's acceptable.


Re this approach:

export const db = schema(($) => ({
  models: [
    $.collection<ModelA>().sub({
      items: $.collection<[ModelAItem, OtherModelAItem]>(),
    }),
    $.collection<ModelB>().sub({
      items: $.collection<ModelBItem>(),
    }),
  ],
}));

My immediate feedback is that as a model document can update from ModelA to ModelB at any time, its subcollection might become invalid, so there must be a strict mechanism to separate their ids.

Maybe something like that:

export const db = schema(($) => ({
  models: $.var({
    a: $.collection<ModelA>().sub({
      items: $.collection<[ModelAItem, OtherModelAItem]>(),
    }),

    b: $.collection<ModelB>().sub({
      items: $.collection<ModelBItem>(),
    }),
  })
}));

So then you'll be able to work with them like that:

db.models.var.a.add(/* ... */);

db.models.query(/* ... */);
drewdearing commented 3 months ago

Hey! Thanks for the quick reply. I didn't consider the fact that the models could change types, leading to the sub collection types becoming out of sync. Good call on that. I think with the below solutioning, the addition of this feature isn't highly necessary, but it would be a lot easier to think about this setup.

As for current solutioning, while I did consider your second option, it is a lot less than ideal.

export const db = schema(($) => ({
  a: $.collection<{}>().sub({
    models: $.collection<ModelA>().sub({
      items:
        $.collection<[ModelAItem, OtherModelAItem]>(),
    }),
  }),
  b: $.collection<{}>().sub({
    models: $.collection<ModelB>().sub({
      items:
        $.collection<[ModelBItem]>(),
    }),
  }),
}));

I came around to this solution in the end, which I'm pretty sure is a pretty similar approach to your first solution, and has the same benefits. Although, with the below, you don't need to filter the sub collections on model id. Thanks for your feedback on my designs.

export const db = schema(($) => ({
  items: $.collection<[ModelA, ModelB]>().sub({
    aItems: $.collection<[ModelAItem, OtherModelAItem]>(),
    bItems: $.collection<[ModelBItem]>(),
  }),
}));
kossnocorp commented 3 months ago

You're welcome! I'll keep in mind this problem, and maybe one day I'll come up with an elegant solution.