feat(entity): support polymorphic-table scoping via EntityConfiguration.inherentFilters#615
Draft
szdziedzic wants to merge 1 commit into
Draft
feat(entity): support polymorphic-table scoping via EntityConfiguration.inherentFilters#615szdziedzic wants to merge 1 commit into
szdziedzic wants to merge 1 commit into
Conversation
Author
This stack of pull requests is managed by Graphite. Learn more about stacking. |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #615 +/- ##
==========================================
Coverage 100.00% 100.00%
==========================================
Files 110 110
Lines 17694 18002 +308
Branches 911 944 +33
==========================================
+ Hits 17694 18002 +308
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
c7cf4b2 to
ce1dd94
Compare
ce1dd94 to
0eece4d
Compare
…on.inherentFilters
## Summary
Adds an optional `inherentFilters` field on `EntityConfiguration` that AND's extra
field-equality conditions into every fetch against the entity's table. Useful for
polymorphic tables where multiple entity classes share a row layout but each represents
a distinct scope (e.g., a discriminator column or a class-specific non-null column).
Each class registers its own scope-disambiguating filter, and the framework guarantees
that wrong-scope rows are invisible to that class on every load path — `loadByID`,
`loadByField`, conjunction loads, cascade deletes — rather than tripping a
constructor-level invariant.
## Motivation
Polymorphic tables today require constructor-level invariants to discriminate scope.
That works for the happy path but fails the moment any code loads a row of the wrong
scope through the wrong class — including, notably, the inbound-edge cascade delete
loop, which loads every child row matching the foreign key regardless of scope. The
result is that a parent-delete cascade involving a polymorphic table throws on
construction of the wrong-scope rows and aborts mid-cascade. Consumers have been
working around this with bespoke helpers; this PR lifts the fix into the framework.
The earlier iteration of this PR scoped the filter to inbound-edge associations only.
Per review feedback, the right place for a polymorphic invariant is the entity
configuration itself — there is no situation in which one of these entities should load
its rows without the filter — so this version makes `inherentFilters` always-applied.
## Mechanism
- New `FieldEqualityCondition` types in the base `@expo/entity` package.
- New `EntityConfiguration.inherentFilters` field; precomputed
`inherentFiltersCacheKeyComponent` derived from a deterministic serialization of the
filters.
- `EntityDatabaseAdapter.fetchManyWhereAsync` / `fetchOneWhereAsync` route through
`fetchManyByFieldEqualityConjunctionAsync` for single-field keys when inherent filters
are present, so adapters with native conjunction support (knex) push the filters all
the way down to SQL; composite-key loads apply them as an in-memory post-filter.
- `fetchManyByFieldEqualityConjunctionAsync` (base + knex override) AND's inherent
filters into the operand list.
- `SingleFieldHolder` and `CompositeFieldHolder` include
`inherentFiltersCacheKeyComponent` in their cache-key parts so all cache adapters
(stub, local-memory, redis) automatically scope cache namespaces per inherent-filter
set without per-adapter changes.
- `EntityCompanionProvider.tableDataCoordinatorMap` is now keyed by `EntityConfiguration`
identity rather than `tableName`, so two configurations sharing a table but with
different inherent filters get separate coordinators (and separate database/cache
adapter instances). Two entity classes sharing one configuration still share a
coordinator (the existing `TwoEntitySameTableDisjointRows` pattern).
- New `loadManyByFieldEqualityConjunctionAsync` on both `AuthorizationResultBasedEntityLoader`
and `EnforcingEntityLoader` — useful in its own right, and the cascade-load /
inherent-filter plumbing routes through it.
## Example
```ts
const scopeAChildEntityConfiguration = new EntityConfiguration<PolyChildFields, 'id'>({
idField: 'id',
tableName: 'poly_children',
inherentFilters: [{ fieldName: 'scope', fieldValue: 'A' }],
schema: {
id: new UUIDField({ columnName: 'id', cache: true }),
parent_id: new UUIDField({
columnName: 'parent_id',
association: {
associatedEntityClass: PolyParentEntity,
edgeDeletionBehavior: EntityEdgeDeletionBehavior.CASCADE_DELETE,
},
}),
scope: new StringField({ columnName: 'scope' }),
},
databaseAdapterFlavor: 'postgres',
cacheAdapterFlavor: 'redis',
});
```
A `loadByID` of a scope-B row through `ScopeAChildEntity` returns `null`. A cascade
delete of the parent only deletes scope-A children through `ScopeAChildEntity` and
scope-B children through `ScopeBChildEntity`.
## Related
- Original consumer motivation: expo/universe#27183
- Prototype helper in consumer code: expo/universe#27406
## Test plan
- [x] `yarn tsc`
- [x] `yarn lint`
- [x] `yarn test` — 741 unit tests, including 4 new in `EntityInherentFilters-test.ts`
covering: scoped `loadByID` (wrong-scope returns null), scoped `loadByField`, cascade
isolation, and no-op cascade.
- [x] `yarn integration` — 123 integration tests, including 2 new in
`EntityInherentFiltersIntegration-test.ts` exercising the native SQL conjunction path
plus Redis cache key scoping against real Postgres + Redis.
0eece4d to
0152777
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Summary
Adds an optional
inherentFiltersfield onEntityConfigurationthat AND's extra field-equality conditions into every fetch against the entity's table. Useful for polymorphic tables where multiple entity classes share a row layout but each represents a distinct scope (e.g., a discriminator column or a class-specific non-null column). Each class registers its own scope-disambiguating filter, and the framework guarantees that wrong-scope rows are invisible to that class on every load path —loadByID,loadByField, conjunction loads, cascade deletes — rather than tripping a constructor-level invariant.Motivation
Polymorphic tables today require constructor-level invariants to discriminate scope. That works for the happy path but fails the moment any code loads a row of the wrong scope through the wrong class — including, notably, the inbound-edge cascade delete loop, which loads every child row matching the foreign key regardless of scope. The result is that a parent-delete cascade involving a polymorphic table throws on construction of the wrong-scope rows and aborts mid-cascade. Consumers have been working around this with bespoke helpers; this PR lifts the fix into the framework.
An earlier iteration of this PR scoped the filter to inbound-edge associations only. Per review feedback, the right place for a polymorphic invariant is the entity configuration itself — there is no situation in which one of these entities should load its rows without the filter — so this version makes
inherentFiltersalways-applied.Mechanism
FieldEqualityConditiontypes in the base@expo/entitypackage.EntityConfiguration.inherentFiltersfield; precomputedinherentFiltersCacheKeyComponentderived from a deterministic serialization of the filters.EntityDatabaseAdapter.fetchManyWhereAsync/fetchOneWhereAsyncroute throughfetchManyByFieldEqualityConjunctionAsyncfor single-field keys when inherent filters are present, so adapters with native conjunction support (knex) push the filters all the way down to SQL; composite-key loads apply them as an in-memory post-filter.fetchManyByFieldEqualityConjunctionAsync(base + knex override) AND's inherent filters into the operand list.SingleFieldHolderandCompositeFieldHolderincludeinherentFiltersCacheKeyComponentin their cache-key parts so all cache adapters (stub, local-memory, redis) automatically scope cache namespaces per inherent-filter set without per-adapter changes.EntityCompanionProvider.tableDataCoordinatorMapis now keyed byEntityConfigurationidentity rather thantableName, so two configurations sharing a table but with different inherent filters get separate coordinators (and separate database/cache adapter instances). Two entity classes sharing one configuration still share a coordinator (the existingTwoEntitySameTableDisjointRowspattern).loadManyByFieldEqualityConjunctionAsyncon bothAuthorizationResultBasedEntityLoaderandEnforcingEntityLoader— useful in its own right, and the inherent-filter plumbing routes through it.Example
A
loadByIDof a scope-B row throughScopeAChildEntityreturnsnull. A cascade delete of the parent only deletes scope-A children throughScopeAChildEntityand scope-B children throughScopeBChildEntity.Related
Test plan
yarn tscyarn lintyarn test— 741 unit tests, including 4 new inEntityInherentFilters-test.tscovering: scopedloadByID(wrong-scope returns null), scopedloadByField, cascade isolation, and no-op cascade.yarn integration— 123 integration tests, including 2 new inEntityInherentFiltersIntegration-test.tsexercising the native SQL conjunction path plus Redis cache key scoping against real Postgres + Redis.