Skip to content
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

BREAKING CHANGE: make using non-enforcing entity loader explicit #238

Merged
merged 2 commits into from
Jun 12, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,9 @@ describe(GenericLocalMemoryCacher, () => {
// simulate non existent db fetch, should write negative result ('') to cache
const nonExistentId = uuidv4();

const entityNonExistentResult =
await LocalMemoryTestEntity.loader(viewerContext).loadByIDAsync(nonExistentId);
const entityNonExistentResult = await LocalMemoryTestEntity.loader(viewerContext)
.nonEnforcing()
.loadByIDAsync(nonExistentId);
expect(entityNonExistentResult.ok).toBe(false);

const nonExistentCachedResult = await entitySpecificGenericCacher.loadManyAsync([
Expand All @@ -75,12 +76,15 @@ describe(GenericLocalMemoryCacher, () => {
});

// load again through entities framework to ensure it reads negative result
const entityNonExistentResult2 =
await LocalMemoryTestEntity.loader(viewerContext).loadByIDAsync(nonExistentId);
const entityNonExistentResult2 = await LocalMemoryTestEntity.loader(viewerContext)
.nonEnforcing()
.loadByIDAsync(nonExistentId);
expect(entityNonExistentResult2.ok).toBe(false);

// invalidate from cache to ensure it invalidates correctly
await LocalMemoryTestEntity.loader(viewerContext).invalidateFieldsAsync(entity1.getAllFields());
await LocalMemoryTestEntity.loader(viewerContext)
.nonEnforcing()
.invalidateFieldsAsync(entity1.getAllFields());
const cachedResultMiss = await entitySpecificGenericCacher.loadManyAsync([
cacheKeyMaker('id', entity1.getID()),
]);
Expand Down Expand Up @@ -131,8 +135,9 @@ describe(GenericLocalMemoryCacher, () => {
// a non existent db fetch should try to write negative result ('') but it's a noop cache, so it should be a miss
const nonExistentId = uuidv4();

const entityNonExistentResult =
await LocalMemoryTestEntity.loader(viewerContext).loadByIDAsync(nonExistentId);
const entityNonExistentResult = await LocalMemoryTestEntity.loader(viewerContext)
.nonEnforcing()
.loadByIDAsync(nonExistentId);
expect(entityNonExistentResult.ok).toBe(false);

const nonExistentCachedResult = await entitySpecificGenericCacher.loadManyAsync([
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,19 +145,23 @@ describe(GenericRedisCacher, () => {

// simulate non existent db fetch, should write negative result ('') to cache
const nonExistentId = uuidv4();
const entityNonExistentResult =
await RedisTestEntity.loader(viewerContext).loadByIDAsync(nonExistentId);
const entityNonExistentResult = await RedisTestEntity.loader(viewerContext)
.nonEnforcing()
.loadByIDAsync(nonExistentId);
expect(entityNonExistentResult.ok).toBe(false);
const cacheKeyNonExistent = cacheKeyMaker('id', nonExistentId);
const nonExistentCachedValue = await redis.get(cacheKeyNonExistent);
expect(nonExistentCachedValue).toEqual('');
// load again through entities framework to ensure it reads negative result
const entityNonExistentResult2 =
await RedisTestEntity.loader(viewerContext).loadByIDAsync(nonExistentId);
const entityNonExistentResult2 = await RedisTestEntity.loader(viewerContext)
.nonEnforcing()
.loadByIDAsync(nonExistentId);
expect(entityNonExistentResult2.ok).toBe(false);

// invalidate from cache to ensure it invalidates correctly in both caches
await RedisTestEntity.loader(viewerContext).invalidateFieldsAsync(entity1.getAllFields());
await RedisTestEntity.loader(viewerContext)
.nonEnforcing()
.invalidateFieldsAsync(entity1.getAllFields());
await expect(redis.get(cacheKeyEntity1)).resolves.toBeNull();
await expect(redis.get(cacheKeyEntity1NameField)).resolves.toBeNull();
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,9 @@ describe(GenericRedisCacher, () => {
// simulate non existent db fetch, should write negative result ('') to cache
const nonExistentId = uuidv4();

const entityNonExistentResult =
await RedisTestEntity.loader(viewerContext).loadByIDAsync(nonExistentId);
const entityNonExistentResult = await RedisTestEntity.loader(viewerContext)
.nonEnforcing()
.loadByIDAsync(nonExistentId);
expect(entityNonExistentResult.ok).toBe(false);

const nonExistentCachedValue = await (genericRedisCacheContext.redisClient as Redis).get(
Expand All @@ -77,12 +78,15 @@ describe(GenericRedisCacher, () => {
expect(nonExistentCachedValue).toEqual('');

// load again through entities framework to ensure it reads negative result
const entityNonExistentResult2 =
await RedisTestEntity.loader(viewerContext).loadByIDAsync(nonExistentId);
const entityNonExistentResult2 = await RedisTestEntity.loader(viewerContext)
.nonEnforcing()
.loadByIDAsync(nonExistentId);
expect(entityNonExistentResult2.ok).toBe(false);

// invalidate from cache to ensure it invalidates correctly
await RedisTestEntity.loader(viewerContext).invalidateFieldsAsync(entity1.getAllFields());
await RedisTestEntity.loader(viewerContext)
.nonEnforcing()
.invalidateFieldsAsync(entity1.getAllFields());
const cachedValueNull = await (genericRedisCacheContext.redisClient as Redis).get(
cacheKeyMaker('id', entity1.getID()),
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import {
OrderByOrdering,
createUnitTestEntityCompanionProvider,
enforceResultsAsync,
ViewerContext,
TransactionIsolationLevel,
} from '@expo/entity';
Expand Down Expand Up @@ -92,7 +91,7 @@ describe('postgres entity integration', () => {
PostgresTestEntity.creator(vc1).setField('name', 'hello').createAsync(),
);

await enforceAsyncResult(PostgresTestEntity.loader(vc1).loadByIDAsync(firstEntity.getID()));
await PostgresTestEntity.loader(vc1).enforcing().loadByIDAsync(firstEntity.getID());

const errorToThrow = new Error('Intentional error');

Expand All @@ -111,9 +110,9 @@ describe('postgres entity integration', () => {
),
).rejects.toEqual(errorToThrow);

const entities = await enforceResultsAsync(
PostgresTestEntity.loader(vc1).loadManyByFieldEqualingAsync('name', 'hello'),
);
const entities = await PostgresTestEntity.loader(vc1)
.enforcing()
.loadManyByFieldEqualingAsync('name', 'hello');
expect(entities).toHaveLength(1);
});

Expand Down
12 changes: 9 additions & 3 deletions packages/entity-example/src/routers/notesRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ router.get('/', async (ctx) => {

router.get('/:id', async (ctx) => {
const viewerContext = ctx.state.viewerContext;
const noteResult = await NoteEntity.loader(viewerContext).loadByIDAsync(ctx.params['id']!);
const noteResult = await NoteEntity.loader(viewerContext)
.nonEnforcing()
.loadByIDAsync(ctx.params['id']!);
if (!noteResult.ok) {
ctx.throw(403, noteResult.reason);
return;
Expand Down Expand Up @@ -72,7 +74,9 @@ router.put('/:id', async (ctx) => {
const viewerContext = ctx.state.viewerContext;
const { title, body } = ctx.request.body as any;

const noteLoadResult = await NoteEntity.loader(viewerContext).loadByIDAsync(ctx.params['id']!);
const noteLoadResult = await NoteEntity.loader(viewerContext)
.nonEnforcing()
.loadByIDAsync(ctx.params['id']!);
if (!noteLoadResult.ok) {
ctx.throw(403, noteLoadResult.reason);
return;
Expand All @@ -95,7 +99,9 @@ router.put('/:id', async (ctx) => {
router.delete('/:id', async (ctx) => {
const viewerContext = ctx.state.viewerContext;

const noteLoadResult = await NoteEntity.loader(viewerContext).loadByIDAsync(ctx.params['id']!);
const noteLoadResult = await NoteEntity.loader(viewerContext)
.nonEnforcing()
.loadByIDAsync(ctx.params['id']!);
if (!noteLoadResult.ok) {
ctx.throw(403, noteLoadResult.reason);
return;
Expand Down
9 changes: 5 additions & 4 deletions packages/entity/src/EnforcingEntityLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,16 @@ import {
QuerySelectionModifiers,
QuerySelectionModifiersWithOrderByRaw,
} from './EntityDatabaseAdapter';
import EntityLoader from './EntityLoader';
import EntityPrivacyPolicy from './EntityPrivacyPolicy';
import NonEnforcingEntityLoader from './NonEnforcingEntityLoader';
import ReadonlyEntity from './ReadonlyEntity';
import ViewerContext from './ViewerContext';
import { mapMap } from './utils/collections/maps';

/**
* Enforcing view on an entity loader. All loads through this loader will throw
* if the loads are not successful.
* Enforcing entity loader. All normal loads are batched,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of scope, but renaming this in the future could make sense. The word "enforcing" is confusing except when working with Result objects.

* cached, and authorized against the entity's EntityPrivacyPolicy. All loads
* through this loader will throw if the load is not successful.
*/
export default class EnforcingEntityLoader<
TFields extends object,
Expand All @@ -28,7 +29,7 @@ export default class EnforcingEntityLoader<
TSelectedFields extends keyof TFields,
> {
constructor(
private readonly entityLoader: EntityLoader<
private readonly entityLoader: NonEnforcingEntityLoader<
TFields,
TID,
TViewerContext,
Expand Down
25 changes: 12 additions & 13 deletions packages/entity/src/EntityAssociationLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,9 @@ export default class EntityAssociationLoader<
.getLoaderFactory()
.forLoad(queryContext, { previousValue: null, cascadingDeleteCause: null });

return (await loader.loadByIDAsync(associatedEntityID as unknown as TAssociatedID)) as Result<
return (await loader
.nonEnforcing()
.loadByIDAsync(associatedEntityID as unknown as TAssociatedID)) as Result<
null extends TFields[TIdentifyingField] ? TAssociatedEntity | null : TAssociatedEntity
>;
}
Expand Down Expand Up @@ -129,10 +131,9 @@ export default class EntityAssociationLoader<
.getViewerScopedEntityCompanionForClass(associatedEntityClass)
.getLoaderFactory()
.forLoad(queryContext, { previousValue: null, cascadingDeleteCause: null });
return await loader.loadManyByFieldEqualingAsync(
associatedEntityFieldContainingThisID,
thisID as any,
);
return await loader
.nonEnforcing()
.loadManyByFieldEqualingAsync(associatedEntityFieldContainingThisID, thisID as any);
}

/**
Expand Down Expand Up @@ -186,10 +187,9 @@ export default class EntityAssociationLoader<
.getViewerScopedEntityCompanionForClass(associatedEntityClass)
.getLoaderFactory()
.forLoad(queryContext, { previousValue: null, cascadingDeleteCause: null });
return await loader.loadByFieldEqualingAsync(
associatedEntityLookupByField,
associatedFieldValue as any,
);
return await loader
.nonEnforcing()
.loadByFieldEqualingAsync(associatedEntityLookupByField, associatedFieldValue as any);
}

/**
Expand Down Expand Up @@ -244,10 +244,9 @@ export default class EntityAssociationLoader<
.getViewerScopedEntityCompanionForClass(associatedEntityClass)
.getLoaderFactory()
.forLoad(queryContext, { previousValue: null, cascadingDeleteCause: null });
return await loader.loadManyByFieldEqualingAsync(
associatedEntityLookupByField,
associatedFieldValue as any,
);
return await loader
.nonEnforcing()
.loadManyByFieldEqualingAsync(associatedEntityLookupByField, associatedFieldValue as any);
}

/**
Expand Down
Loading
Loading