-
Notifications
You must be signed in to change notification settings - Fork 4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: support entity fields subset of db fields #49
Conversation
Codecov Report
@@ Coverage Diff @@
## master #49 +/- ##
==========================================
+ Coverage 93.95% 94.07% +0.11%
==========================================
Files 57 58 +1
Lines 1357 1384 +27
Branches 147 147
==========================================
+ Hits 1275 1302 +27
Misses 81 81
Partials 1 1
Continue to review full report at Codecov.
|
In general I think we should avoid compile-time only features like this. I often say, "Just because it works doesn't mean it's right," but if there is a simple way (like an One way we could address this is by specifying the selected fields (the exposed subset) statically in a data structure that's available at runtime as part of the entity configuration. The entity library would use this data structure for enforcement at runtime. And since it's statically defined, TypeScript would use it for static enforcement. Continuing this train of thought, don't we already declare the fields (namely, the mapping from database fields to entity fields) in a static, runtime-available data structure that specifies what fields to load? What I'm wondering/proposing is to invert this PR a bit and use this existing data structure to know what fields to expose on an entity instance (i.e. the subset). By default, this data structure also would specify what fields to fetch from the database (i.e. the superset). Entities that share tables (e.g. robots & users) would be able to provide a second data structure that specifies the fields to fetch from the database, since they need a strict superset. For correctness, the RobotEntityConfiguration and UserEntityConfiguration would want to share this data structure so the loaders for both entities fetch the same fields. It would also be correct (but in some cases possibly inefficient) for there to be a way for this second data structure to say "SELECT *". |
I tried this and it turned out pretty well; I'll put up a commit here. It supports filtering the fields at runtime for the
I tried this before this PR actually. The issue is that TypeScript doesn't have a great way of expressing subsets of types (as far as I know). It supports the inverse through |
Made a pretty large update to this. Adding the test for entities that can both reference the same row was critical and an excellent suggestion. |
The general idea LGTM (I mostly looked at the tests) but I think the API could be more streamlined.
Untested draft of an idea: type Example1EntityFields = {
id: string;
field_a: string;
};
type Example2EntityFields = {
id: string;
field_b: string;
};
// TS will make sure that shared fields ("id" in this example) have compatible types or else they get typed as "never"
type ExampleEntityDatabaseFields = Example1EntityFields & Example2EntityFields;
const sharedExampleEntityConfiguration = new EntityConfiguration<ExampleEntityDatabaseFields>({
idField: 'id',
tableName: 'entities',
schema: {
id: new UUIDField({ columnName: 'custom_id', cache: true }),
field_a: new StringField({ columnName: 'field_a', cache: true }),
field_b: new StringField({ columnName: 'field_b', cache: true }),
},
});
class Example1Entity extends Entity<Example1EntityFields, string, ViewerContext, ExampleEntityDatabaseFields> { ... }
abstract class Entity<
TFields extends Record<string, unknown>,
TID,
TViewerContext extends ViewerContext,
TDatabaseFields extends TFields = TFields
> { }
|
The trick in the above proposal code is that In the PR, the entity field names are values of the types ( In the proposal snippet, the entity field names are properties of the types ( |
packages/entity-example/src/entities/AllowIfUserOwnerPrivacyRule.ts
Outdated
Show resolved
Hide resolved
packages/entity/src/__tests__/cases/TwoEntitySameTableOverlappingRows-test.ts
Outdated
Show resolved
Hide resolved
In its current form, the default is that both sets of fields are the same and no extra field specification is necessary. There's only one set of fields and it only needs to be specified once. It's only when the user wants to specify two entities that use the same underlying set of fields that they need to specify the extra parameters.
I had actually tried this first (before doing the field key selection stuff). I'll try to articulate the reasons I ended up going with the key subset approach:
I'll put up a stacked PR with my most recent attempt at this though just in case you have any insight. #51 |
7ec4e8a
to
1272324
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Approving this since this is the direction we want to go in and API changes wouldn't need complicated refactors.
Future work:
Here is an example of why I think TS behaves the way it does and a potential fix:
Why
This is the other approach mentioned in #47 (comment):
EntityDataManager
and everything below it doesn't operate on the current entity's "fields". Rather an entity is a selection of fields exposed from the full set of fields loaded.This PR adds the following concept: If an entity wants to only expose a subset of its database row's fields via
getField
andgetAllFields
, it can do so by specifying a newTSelectedFields
type parameter. To make the change non-breaking,TSelectedFields
defaults to all fields onTFields
.The underlying mutators and loaders still operate on the full set of fields, as not doing so creates an interesting case: Hypothetically, let's say that two entities reference the same table and also the same rows. Entity A has field selection a, b, c. Entity B has field selection a, c. Entities are cached by all fields from both entities.
Instance A of Entity A is created with a, b, c.
Instance B is loaded with ID of Instance A.
Instance B is mutated, changing field c to something else.
Now, all the cache entries of the underlying fields of B must be invalidated. If only cache entries for Entity B's fields were invalidated, then the following load would be inconsistent.
Instance A is loaded by field b. Because the cache was invalidated by all fields (including field b) this will be consistent.
How
= keyof TFields
(to ensure no cases were missed).EntityTableDataCoordinator
, which is essentially whatEntityCompanion
used to be but shared amongst all entity companions that use the same table. This way the dataloaders and caches remain consistent.Test Plan
Run new tests to ensure it works as expected.