diff --git a/.changeset/stale-socks-accept.md b/.changeset/stale-socks-accept.md new file mode 100644 index 000000000000..6180a04613bf --- /dev/null +++ b/.changeset/stale-socks-accept.md @@ -0,0 +1,18 @@ +--- +'@data-client/normalizr': minor +--- + +MemoCache.query returns `{ data, paths }` just like denormalize. `data` could be INVALID + +#### Before + +```ts +return this.memo.query(schema, args, state); +``` + +#### After + +```ts +const { data } = this.memo.query(schema, args, state); +return typeof data === 'symbol' ? undefined : (data as any); +``` \ No newline at end of file diff --git a/docs/rest/api/schema.md b/docs/rest/api/schema.md index 6f803d2caf0e..dbbebc8f9812 100644 --- a/docs/rest/api/schema.md +++ b/docs/rest/api/schema.md @@ -222,8 +222,7 @@ is an Array of paths of all entities included in the result. const data = memo.query( Article, args, - normalizedData.entities, - normalizedData.indexes, + normalizedData, ); ``` diff --git a/packages/core/src/controller/Controller.ts b/packages/core/src/controller/Controller.ts index 09151b20f215..2e8cf0288b3b 100644 --- a/packages/core/src/controller/Controller.ts +++ b/packages/core/src/controller/Controller.ts @@ -589,7 +589,8 @@ export default class Controller< .slice(0, rest.length - 1) .map(ensurePojo) as SchemaArgs; - return this.memo.query(schema, args, state); + const { data } = this.memo.query(schema, args, state); + return typeof data === 'symbol' ? undefined : (data as any); } /** @@ -612,27 +613,10 @@ export default class Controller< .slice(0, rest.length - 1) .map(ensurePojo) as SchemaArgs; - // TODO: breaking: Switch back to this.memo.query(schema, args, state.entities as any, state.indexes) to do - // this logic - const input = this.memo.buildQueryKey( - schema, - args, - state, - JSON.stringify(args), - ); + const { data, paths } = this.memo.query(schema, args, state); - if (!input) { - return { data: undefined, countRef: () => () => undefined }; - } - - const { data, paths } = this.memo.denormalize( - schema, - input, - state.entities, - args, - ); return { - data: typeof data === 'symbol' ? undefined : (data as any), + data: typeof data === 'symbol' ? undefined : data, countRef: this.gcPolicy.createCountRef({ paths }), }; } diff --git a/packages/endpoint/src/schemas/__tests__/All.test.ts b/packages/endpoint/src/schemas/__tests__/All.test.ts index 7cecf80727a4..c0ad864cccb3 100644 --- a/packages/endpoint/src/schemas/__tests__/All.test.ts +++ b/packages/endpoint/src/schemas/__tests__/All.test.ts @@ -135,7 +135,9 @@ describe.each([ indexes: {}, }); // use memocache because we don't support 'object' schemas in controller yet - expect(new MemoCache().query(catSchema, [], state)).toMatchSnapshot(); + expect( + new MemoCache().query(catSchema, [], state).data, + ).toMatchSnapshot(); }); test('denormalizes nested in object with primitive', () => { @@ -150,7 +152,7 @@ describe.each([ }, indexes: {}, }); - const value = new MemoCache().query(catSchema, [], state); + const value = new MemoCache().query(catSchema, [], state).data; expect(value).not.toEqual(expect.any(Symbol)); if (typeof value === 'symbol' || value === undefined) return; expect(createOutput(value.results)).toMatchSnapshot(); @@ -171,7 +173,7 @@ describe.each([ }, indexes: {}, }); - const value = new MemoCache().query(catSchema, [], state); + const value = new MemoCache().query(catSchema, [], state).data; expect(value).not.toEqual(expect.any(Symbol)); if (typeof value === 'symbol' || value === undefined) return; expect(createOutput(value.results).length).toBe(2); @@ -194,11 +196,11 @@ describe.each([ indexes: {}, }; const memo = new MemoCache(); - const value = memo.query(catSchema, [], state); + const value = memo.query(catSchema, [], state).data; expect(createOutput(value).results?.length).toBe(2); expect(createOutput(value).results).toMatchSnapshot(); - const value2 = memo.query(catSchema, [], state); + const value2 = memo.query(catSchema, [], state).data; expect(createOutput(value).results[0]).toBe( createOutput(value2).results[0], ); @@ -214,7 +216,7 @@ describe.each([ }, }, }; - const value3 = memo.query(catSchema, [], state); + const value3 = memo.query(catSchema, [], state).data; expect(createOutput(value3).results?.length).toBe(3); expect(createOutput(value3).results).toMatchSnapshot(); expect(createOutput(value).results[0]).toBe( @@ -238,8 +240,8 @@ describe.each([ }, indexes: {}, }); - const value = new MemoCache().query(catSchema, [], state); - expect(createOutput(value)).toBeUndefined(); + const value = new MemoCache().query(catSchema, [], state).data; + expect(createOutput(value)).toEqual(expect.any(Symbol)); }); test('denormalizes should not be found when no entities are present (polymorphic)', () => { @@ -270,8 +272,8 @@ describe.each([ }, indexes: {}, }); - const value = new MemoCache().query(listSchema, [], state); - expect(createOutput(value)).toBeUndefined(); + const value = new MemoCache().query(listSchema, [], state).data; + expect(createOutput(value)).toEqual(expect.any(Symbol)); }); test('returns the input value if is null', () => { @@ -333,7 +335,7 @@ describe.each([ }, indexes: {}, }); - const value = new MemoCache().query(listSchema, [], state); + const value = new MemoCache().query(listSchema, [], state).data; expect(value).not.toEqual(expect.any(Symbol)); if (typeof value === 'symbol') return; expect(value).toMatchSnapshot(); diff --git a/packages/endpoint/src/schemas/__tests__/Query.test.ts b/packages/endpoint/src/schemas/__tests__/Query.test.ts index 4335f3ca7dd0..583ac302123c 100644 --- a/packages/endpoint/src/schemas/__tests__/Query.test.ts +++ b/packages/endpoint/src/schemas/__tests__/Query.test.ts @@ -76,7 +76,7 @@ describe.each([ }, }); const users: DenormalizeNullable | symbol = - new MemoCache().query(sortedUsers, [], state); + new MemoCache().query(sortedUsers, [], state).data; expect(users).not.toEqual(expect.any(Symbol)); if (typeof users === 'symbol') return; expect(users && users[0].name).toBe('Zeta'); @@ -101,7 +101,7 @@ describe.each([ }, }); expect( - new MemoCache().query(sortedUsers, [{ asc: true }], state), + new MemoCache().query(sortedUsers, [{ asc: true }], state).data, ).toMatchSnapshot(); }); @@ -115,9 +115,9 @@ describe.each([ }, }, }; - const data = new MemoCache().query(sortedUsers, [], state); + const { data } = new MemoCache().query(sortedUsers, [], state); - expect(createOutput(data)).toEqual(undefined); + expect(createOutput(data)).not.toEqual(expect.any(Array)); }); test('denormalize aggregates', () => { @@ -152,7 +152,7 @@ describe.each([ }); const totalCount: | DenormalizeNullable - | symbol = new MemoCache().query(userCountByAdmin, [], state); + | symbol = new MemoCache().query(userCountByAdmin, [], state).data; expect(totalCount).toBe(4); const nonAdminCount: @@ -161,7 +161,7 @@ describe.each([ userCountByAdmin, [{ isAdmin: false }], state, - ); + ).data; expect(nonAdminCount).toBe(3); const adminCount: | DenormalizeNullable @@ -169,7 +169,7 @@ describe.each([ userCountByAdmin, [{ isAdmin: true }], state, - ); + ).data; expect(adminCount).toBe(1); if (typeof totalCount === 'symbol') return; @@ -209,7 +209,7 @@ describe('top level schema', () => { }, }, }; - const users = new MemoCache().query(sortedUsers, [], state); + const users = new MemoCache().query(sortedUsers, [], state).data; expect(users).not.toEqual(expect.any(Symbol)); if (typeof users === 'symbol') return; expect(users && users[0].name).toBe('Zeta'); @@ -228,7 +228,7 @@ describe('top level schema', () => { }, }, }; - const users = new MemoCache().query(sortedUsers, [], state); + const users = new MemoCache().query(sortedUsers, [], state).data; expect(users).toBeUndefined(); }); @@ -243,8 +243,8 @@ describe('top level schema', () => { return sorted.reverse(); }, ); - const users = new MemoCache().query(allSortedUsers, [], initialState); - expect(users).toBeUndefined(); + const users = new MemoCache().query(allSortedUsers, [], initialState).data; + expect(users).toEqual(expect.any(Symbol)); }); test('works with nested schemas', () => { @@ -258,8 +258,8 @@ describe('top level schema', () => { return sorted.reverse(); }, ); - const users = new MemoCache().query(allSortedUsers, [], initialState); - expect(users).toBeUndefined(); + const users = new MemoCache().query(allSortedUsers, [], initialState).data; + expect(users).toEqual(expect.any(Symbol)); }); test('denormalizes should not be found when no entities are present', () => { @@ -273,7 +273,7 @@ describe('top level schema', () => { }, }; - const value = new MemoCache().query(sortedUsers, [], state); + const value = new MemoCache().query(sortedUsers, [], state).data; expect(value).toEqual(undefined); }); diff --git a/packages/normalizr/README.md b/packages/normalizr/README.md index c7d616049401..ce8ff84d4c54 100644 --- a/packages/normalizr/README.md +++ b/packages/normalizr/README.md @@ -204,14 +204,13 @@ const memo = new MemoCache(); const { data, paths } = memo.denormalize(schema, input, state.entities, args); -const data = memo.query(schema, args, state.entities, state.indexes); +({ data, paths } = memo.query(schema, args, state)); function query(schema, args, state, key) { const queryKey = memo.buildQueryKey( schema, args, - state.entities, - state.indexes, + state, key, ); const { data } = this.denormalize(schema, queryKey, state.entities, args); diff --git a/packages/normalizr/src/__tests__/MemoCache.ts b/packages/normalizr/src/__tests__/MemoCache.ts index d7b8d0956265..e654cd3e4af5 100644 --- a/packages/normalizr/src/__tests__/MemoCache.ts +++ b/packages/normalizr/src/__tests__/MemoCache.ts @@ -1038,11 +1038,11 @@ describe('MemoCache', () => { }); test('works with indexes', () => { - const m = new MemoCache().query(Cat, [{ username: 'm' }], state); + const m = new MemoCache().query(Cat, [{ username: 'm' }], state).data; expect(m).toBeDefined(); expect(m).toMatchSnapshot(); expect( - new MemoCache().query(Cat, [{ username: 'doesnotexist' }], state), + new MemoCache().query(Cat, [{ username: 'doesnotexist' }], state).data, ).toBeUndefined(); }); @@ -1051,7 +1051,7 @@ describe('MemoCache', () => { expect(m).toBeDefined(); expect(m).toMatchSnapshot(); expect( - new MemoCache().query(Cat, [{ id: 'doesnotexist' }], state), + new MemoCache().query(Cat, [{ id: 'doesnotexist' }], state).data, ).toBeUndefined(); }); }); diff --git a/packages/normalizr/src/__tests__/__snapshots__/MemoCache.ts.snap b/packages/normalizr/src/__tests__/__snapshots__/MemoCache.ts.snap index d005485dc688..16f58d463274 100644 --- a/packages/normalizr/src/__tests__/__snapshots__/MemoCache.ts.snap +++ b/packages/normalizr/src/__tests__/__snapshots__/MemoCache.ts.snap @@ -9,10 +9,18 @@ Cat { `; exports[`MemoCache query (direct) works with pk 1`] = ` -Cat { - "id": "1", - "name": "Milo", - "username": "m", +{ + "data": Cat { + "id": "1", + "name": "Milo", + "username": "m", + }, + "paths": [ + { + "key": "Cat", + "pk": "1", + }, + ], } `; @@ -25,10 +33,18 @@ Cat { `; exports[`MemoCache query (immutable) works with pk 1`] = ` -Cat { - "id": "1", - "name": "Milo", - "username": "m", +{ + "data": Cat { + "id": "1", + "name": "Milo", + "username": "m", + }, + "paths": [ + { + "key": "Cat", + "pk": "1", + }, + ], } `; diff --git a/packages/normalizr/src/memo/MemoCache.ts b/packages/normalizr/src/memo/MemoCache.ts index 832b5cd14686..5c7a90e5d26e 100644 --- a/packages/normalizr/src/memo/MemoCache.ts +++ b/packages/normalizr/src/memo/MemoCache.ts @@ -71,15 +71,17 @@ export default class MemoCache { state: StateInterface, // NOTE: different orders can result in cache busting here; but since it's just a perf penalty we will allow for now argsKey: string = JSON.stringify(args), - ): DenormalizeNullable | undefined { + ): { + data: DenormalizeNullable | symbol; + paths: EntityPath[]; + } { const input = this.buildQueryKey(schema, args, state, argsKey); if (!input) { - return; + return { data: undefined as any, paths: [] }; } - const { data } = this.denormalize(schema, input, state.entities, args); - return typeof data === 'symbol' ? undefined : (data as any); + return this.denormalize(schema, input, state.entities, args); } buildQueryKey( diff --git a/website/src/components/Playground/editor-types/@data-client/core.d.ts b/website/src/components/Playground/editor-types/@data-client/core.d.ts index 1b2daf6e7728..f6608e9e6365 100644 --- a/website/src/components/Playground/editor-types/@data-client/core.d.ts +++ b/website/src/components/Playground/editor-types/@data-client/core.d.ts @@ -224,17 +224,20 @@ declare class MemoCache { paths: EntityPath[]; }; /** Compute denormalized form maintaining referential equality for same inputs */ - query(schema: S, args: readonly any[], entities: Record | undefined> | { - getIn(k: string[]): any; - }, indexes: NormalizedIndex | { - getIn(k: string[]): any; - }, argsKey?: string): DenormalizeNullable | undefined; - buildQueryKey(schema: S, args: readonly any[], entities: Record | undefined> | { + query(schema: S, args: readonly any[], state: StateInterface, argsKey?: string): { + data: DenormalizeNullable | symbol; + paths: EntityPath[]; + }; + buildQueryKey(schema: S, args: readonly any[], state: StateInterface, argsKey?: string): NormalizeNullable; +} +type StateInterface = { + entities: Record | undefined> | { getIn(k: string[]): any; - }, indexes: NormalizedIndex | { + }; + indexes: NormalizedIndex | { getIn(k: string[]): any; - }, argsKey?: string): NormalizeNullable; -} + }; +}; /** https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-4.html#the-noinfer-utility-type */ type NI = NoInfer; diff --git a/website/src/components/Playground/editor-types/@data-client/normalizr.d.ts b/website/src/components/Playground/editor-types/@data-client/normalizr.d.ts index 1746ee504f41..227fc5b14157 100644 --- a/website/src/components/Playground/editor-types/@data-client/normalizr.d.ts +++ b/website/src/components/Playground/editor-types/@data-client/normalizr.d.ts @@ -264,17 +264,20 @@ declare class MemoCache { paths: EntityPath[]; }; /** Compute denormalized form maintaining referential equality for same inputs */ - query(schema: S, args: readonly any[], entities: Record | undefined> | { - getIn(k: string[]): any; - }, indexes: NormalizedIndex | { - getIn(k: string[]): any; - }, argsKey?: string): DenormalizeNullable | undefined; - buildQueryKey(schema: S, args: readonly any[], entities: Record | undefined> | { + query(schema: S, args: readonly any[], state: StateInterface, argsKey?: string): { + data: DenormalizeNullable | symbol; + paths: EntityPath[]; + }; + buildQueryKey(schema: S, args: readonly any[], state: StateInterface, argsKey?: string): NormalizeNullable; +} +type StateInterface = { + entities: Record | undefined> | { getIn(k: string[]): any; - }, indexes: NormalizedIndex | { + }; + indexes: NormalizedIndex | { getIn(k: string[]): any; - }, argsKey?: string): NormalizeNullable; -} + }; +}; /** https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-4.html#the-noinfer-utility-type */ type NI = NoInfer;