From ea205420673b10e542cf2cb0683e197bdbe374f7 Mon Sep 17 00:00:00 2001 From: Nathaniel Tucker Date: Wed, 29 Apr 2026 09:20:12 -0400 Subject: [PATCH] feat(endpoint): allow shared Collection context keys Allow one Collection schema to provide both argsKey and nestKey so it can be reused top-level and nested while preserving shared collection state. Made-with: Cursor --- .changeset/collection-context-detection.md | 24 +++++ .cursor/skills/data-client-schema/SKILL.md | 2 +- .../data-client-v0.17-migration/SKILL.md | 38 +++++++ docs/rest/api/Collection.md | 94 ++++++++++-------- packages/endpoint/src/schemaTypes.ts | 13 ++- packages/endpoint/src/schemas/Collection.ts | 91 +++++++++-------- .../src/schemas/__tests__/Collection.test.ts | 98 ++++++++++++++++++- ...2026-04-24-v0.17-scalar-typed-downloads.md | 46 +++++++++ .../editor-types/@data-client/core.d.ts | 9 +- .../editor-types/@data-client/endpoint.d.ts | 22 +++-- .../editor-types/@data-client/graphql.d.ts | 22 +++-- .../editor-types/@data-client/normalizr.d.ts | 9 +- .../editor-types/@data-client/rest.d.ts | 22 +++-- .../Playground/editor-types/globals.d.ts | 22 +++-- .../Playground/editor-types/uuid.d.ts | 15 +-- 15 files changed, 396 insertions(+), 131 deletions(-) create mode 100644 .changeset/collection-context-detection.md diff --git a/.changeset/collection-context-detection.md b/.changeset/collection-context-detection.md new file mode 100644 index 000000000000..009a631cec4b --- /dev/null +++ b/.changeset/collection-context-detection.md @@ -0,0 +1,24 @@ +--- +'@data-client/endpoint': minor +'@data-client/rest': minor +'@data-client/graphql': minor +'@data-client/normalizr': minor +'@data-client/core': minor +'@data-client/react': minor +'@data-client/vue': minor +--- + +Allow one `Collection` schema to be used both top-level and nested. + +Before: + +```ts +const getTodos = new Collection([Todo], { argsKey }); +const userTodos = new Collection([Todo], { nestKey }); +``` + +After: + +```ts +const userTodos = new Collection([Todo], { argsKey, nestKey }); +``` diff --git a/.cursor/skills/data-client-schema/SKILL.md b/.cursor/skills/data-client-schema/SKILL.md index f582163425cf..13bde650c5e3 100644 --- a/.cursor/skills/data-client-schema/SKILL.md +++ b/.cursor/skills/data-client-schema/SKILL.md @@ -105,7 +105,7 @@ export const EventResource = resource({ ### pk routing -`pk()` uses `argsKey(...args)` or `nestKey(parent, key)`, then serializes the result. Without either option, it defaults to `argsKey: params => ({ ...params })`, using all endpoint args as the collection key. +`pk()` uses `nestKey(parent, key)` when nested in an Entity and available; otherwise it uses `argsKey(...args)`, then serializes the result. Without options, it defaults to `argsKey: params => ({ ...params })`, using all endpoint args as the collection key. Provide both `argsKey` and `nestKey` to reuse one Collection definition top-level and nested. - `argsKey` — derive pk from endpoint arguments (default) - `nestKey` — derive pk from parent entity for nested shared-state collections diff --git a/.cursor/skills/data-client-v0.17-migration/SKILL.md b/.cursor/skills/data-client-v0.17-migration/SKILL.md index 4c316ec93a7f..ffa78a08e6f3 100644 --- a/.cursor/skills/data-client-v0.17-migration/SKILL.md +++ b/.cursor/skills/data-client-v0.17-migration/SKILL.md @@ -146,6 +146,44 @@ These are rare; do them by hand: `Schema.normalize()` and the `visit()` callback gain an optional trailing `parentEntity` parameter — the nearest enclosing entity-like schema, tracked automatically by the visit walker. Existing schemas don't need changes; new schemas can opt in. +### Optional Collection cleanup + +`Collection` can now define both `argsKey` and `nestKey` on the same instance. During normalization it uses `argsKey` when top-level and `nestKey` when nested in an Entity, so paired definitions can be consolidated: + +```ts +// before: two separate but equivalent Collections +export const getTodos = new RestEndpoint({ + path: '/todos', + searchParams: {} as { userId?: string }, + schema: new Collection([Todo]), +}); + +class User extends Entity { + static schema = { + todos: new Collection([Todo], { + nestKey: parent => ({ userId: parent.id }), + }), + }; +} + +// after: one shared Collection +export const userTodos = new Collection([Todo], { + nestKey: parent => ({ userId: parent.id }), +}); + +export const getTodos = new RestEndpoint({ + path: '/todos', + searchParams: {} as { userId?: string }, + schema: userTodos, +}); + +class User extends Entity { + static schema = { + todos: userTodos, + }; +} +``` + ## Where to find affected code Search for these patterns in your codebase: diff --git a/docs/rest/api/Collection.md b/docs/rest/api/Collection.md index 548ff0a78703..04f706ef24df 100644 --- a/docs/rest/api/Collection.md +++ b/docs/rest/api/Collection.md @@ -60,7 +60,7 @@ delay: 150, }, ]}> -```ts title="api/Todo" {15} collapsed +```ts title="api/Todo" {15-18,24} collapsed import { Entity, RestEndpoint, Collection } from '@data-client/rest'; export class Todo extends Entity { @@ -72,16 +72,20 @@ export class Todo extends Entity { static key = 'Todo'; } +export const userTodos = new Collection([Todo], { + nestKey: (parent: { id: string }) => ({ userId: parent.id }), +}); + export const getTodos = new RestEndpoint({ path: '/todos', searchParams: {} as { userId?: string }, - schema: new Collection([Todo]), + schema: userTodos, }); ``` -```ts title="api/User" {13-17} collapsed +```ts title="api/User" {13} collapsed import { Entity, RestEndpoint, Collection } from '@data-client/rest'; -import { Todo } from './Todo'; +import { Todo, userTodos } from './Todo'; export class User extends Entity { id = ''; @@ -92,11 +96,7 @@ export class User extends Entity { static key = 'User'; static schema = { - todos: new Collection([Todo], { - nestKey: (parent, key) => ({ - userId: parent.id, - }), - }), + todos: userTodos, }; } @@ -247,7 +247,10 @@ await ctrl.fetch(StatsResource.getList.assign, { ## Options -One of `argsKey` or `nestKey` is used to compute the `Collection's` [pk](#pk). +`argsKey` and `nestKey` compute the `Collection's` [pk](#pk). `argsKey` is used +when the Collection is normalized as a top-level endpoint result; `nestKey` is +used when the same Collection is nested in an [Entity](./Entity.md). Provide both +to reuse one Collection definition in both contexts. ### argsKey(...args): Object {#argsKey} @@ -257,17 +260,24 @@ on Endpoint arguments. ```ts {7-9} import { RestEndpoint, Collection } from '@data-client/rest'; +const userTodos = new Collection([Todo], { + argsKey: (urlParams: { userId?: string }) => ({ + ...urlParams, + }), + nestKey: (parent: { id: string }) => ({ + userId: parent.id, + }), +}); + const getTodos = new RestEndpoint({ path: '/todos', searchParams: {} as { userId?: string }, - schema: new Collection([Todo], { - argsKey: (urlParams: { userId?: string }) => ({ - ...urlParams, - }), - }), + schema: userTodos, }); ``` +When omitted, `argsKey` defaults to `params => ({ ...params })`. + ### nestKey(parent, key): Object {#nestKey} Returns a serializable Object whose members uniquely define this collection based @@ -275,18 +285,12 @@ on the parent it is nested inside. Nested `Collection's` [pk](#pk) are better defined by what they are nested inside. This allows the nested Collection to share its state with other instances whose key has the same value. +When `argsKey` and `nestKey` return the same object shape, top-level and nested +reads resolve to the same collection state. -```ts {28-30} -import { Collection, Entity } from '@data-client/rest'; - -class Todo extends Entity { - id = ''; - userId = ''; - title = ''; - completed = false; - - static key = 'Todo'; -} +```ts {13} +import { Entity } from '@data-client/rest'; +import { Todo, userTodos } from './Todo'; class User extends Entity { id = ''; @@ -297,17 +301,21 @@ class User extends Entity { static key = 'User'; static schema = { - todos: new Collection([Todo], { - nestKey: (parent, key) => ({ - userId: parent.id, - }), - }), + todos: userTodos, }; } ``` In this case, `user.todos` and getTodos() response (from the argsKey example) will always -be the same (referentially equal) Array. +be the same (referentially equal) Array. Add both key functions to the shared +Collection definition: + +```ts +const userTodos = new Collection([Todo], { + argsKey: ({ userId }: { userId?: string }) => ({ userId }), + nestKey: (parent: User) => ({ userId: parent.id }), +}); +``` ### nonFilterArgumentKeys? {#nonFilterArgumentKeys} @@ -684,16 +692,24 @@ static mergeWithStore( `mergeWithStore()` is called during normalization when a processed entity is already found in the store. -### pk: (parent?, key?, args?): pk? {#pk} +### pk: (parent?, key?, args?, parentEntity?): pk? {#pk} -`pk()` calls [argsKey](#argsKey) or [nestKey](#nestKey) depending on which are specified, and -then serializes the result for the pk string. +`pk()` calls [nestKey](#nestKey) when nested in an Entity and available; +otherwise it calls [argsKey](#argsKey). It then serializes the result for the pk +string. ```ts -pk(value: any, parent: any, key: string, args: readonly any[]) { - const obj = this.argsKey - ? this.argsKey(...args) - : this.nestKey(parent, key); +pk( + value: any, + parent: any, + key: string, + args: readonly any[], + parentEntity?: any, +) { + const obj = + parentEntity && this.nestKey + ? this.nestKey(parent, key) + : this.argsKey(...args); for (const key in obj) { if (typeof obj[key] !== 'string') obj[key] = `${obj[key]}`; } diff --git a/packages/endpoint/src/schemaTypes.ts b/packages/endpoint/src/schemaTypes.ts index 3a3b2a701126..c9460a97cab8 100644 --- a/packages/endpoint/src/schemaTypes.ts +++ b/packages/endpoint/src/schemaTypes.ts @@ -88,14 +88,22 @@ export interface CollectionInterface< /** * A unique identifier for each Collection * - * Calls argsKey or nestKey depending on which are specified, and then serializes the result for the pk string. + * Calls nestKey when nested in an Entity and available; otherwise calls + * argsKey. The resulting object is serialized for the pk string. * * @param [parent] When normalizing, the object which included the entity * @param [key] When normalizing, the key where this entity was found * @param [args] ...args sent to Endpoint + * @param [parentEntity] Entity class containing this Collection when nested * @see https://dataclient.io/docs/api/Collection#pk */ - pk(value: any, parent: any, key: string, args: any[]): string; + pk( + value: any, + parent: any, + key: string, + args: any[], + parentEntity?: any, + ): string; normalize( input: any, parent: Parent, @@ -103,6 +111,7 @@ export interface CollectionInterface< args: any[], visit: (...args: any) => any, delegate: INormalizeDelegate, + parentEntity?: any, ): string; /** Creates new instance copying over defined values of arguments diff --git a/packages/endpoint/src/schemas/Collection.ts b/packages/endpoint/src/schemas/Collection.ts index 5df1d25dbba4..1dba8138e107 100644 --- a/packages/endpoint/src/schemas/Collection.ts +++ b/packages/endpoint/src/schemas/Collection.ts @@ -63,9 +63,9 @@ export default class CollectionSchema< Args extends any[] = DefaultArgs, Parent = any, > implements Mergeable { - declare protected nestKey: (parent: any, key: string) => Record; + declare protected nestKey?: (parent: any, key: string) => Record; - declare protected argsKey?: (...args: any) => Record; + declare protected argsKey: (...args: any) => Record; declare readonly schema: S; @@ -123,16 +123,9 @@ export default class CollectionSchema< constructor(schema: S, options?: CollectionOptions) { this.schema = Array.isArray(schema) ? (new ArraySchema(schema[0]) as any) : schema; - if (!options) { - this.argsKey = params => ({ ...params }); - } else { - if ('nestKey' in options) { - (this as any).nestKey = options.nestKey; - } else if ('argsKey' in options) { - this.argsKey = options.argsKey; - } else { - this.argsKey = params => ({ ...params }); - } + this.argsKey = options?.argsKey ?? (params => ({ ...params })); + if (options?.nestKey) { + this.nestKey = options.nestKey; } this.key = keyFromSchema(this.schema); if ((options as any)?.nonFilterArgumentKeys) { @@ -186,9 +179,17 @@ export default class CollectionSchema< }; } - pk(value: any, parent: any, key: string, args: readonly any[]) { + pk( + value: any, + parent: any, + key: string, + args: readonly any[], + parentEntity?: any, + ) { const obj = - this.argsKey ? this.argsKey(...args) : this.nestKey(parent, key); + parentEntity && this.nestKey ? + this.nestKey(parent, key) + : this.argsKey(...args); for (const key in obj) { if (['number', 'boolean'].includes(typeof obj[key])) obj[key] = `${obj[key]}`; @@ -205,6 +206,7 @@ export default class CollectionSchema< args: any[], visit: (...args: any) => any, delegate: INormalizeDelegate, + parentEntity?: any, ): string { const normalizedValue = this.schema.normalize( input, @@ -214,7 +216,7 @@ export default class CollectionSchema< visit, delegate, ); - const id = this.pk(normalizedValue, parent, key, args); + const id = this.pk(normalizedValue, parent, key, args, parentEntity); delegate.mergeEntity(this, id, normalizedValue); return id; @@ -266,11 +268,9 @@ export default class CollectionSchema< // >>>>>>>>>>>>>>DENORMALIZE<<<<<<<<<<<<<< queryKey(args: Args, unvisit: unknown, delegate: IQueryDelegate): any { - if (this.argsKey) { - const pk = this.pk(undefined, undefined, '', args); - // ensure this actually has entity or we shouldn't try to use it in our query - if (delegate.getEntity(this.key, pk)) return pk; - } + const pk = this.pk(undefined, undefined, '', args); + // ensure this actually has entity or we shouldn't try to use it in our query + if (delegate.getEntity(this.key, pk)) return pk; } declare createIfValid: (value: any) => any | undefined; @@ -286,40 +286,35 @@ export default class CollectionSchema< export type CollectionOptions< Args extends any[] = DefaultArgs, Parent = any, -> = ( +> = { + /** Defines lookups for Collections nested in other schemas. + * + * @see https://dataclient.io/rest/api/Collection#nestKey + */ + nestKey?: (parent: Parent, key: string) => Record; + /** Defines lookups top-level Collections using ...args. + * + * @see https://dataclient.io/rest/api/Collection#argsKey + */ + argsKey?: (...args: Args) => Record; +} & ( | { - /** Defines lookups for Collections nested in other schemas. + /** Sets a default createCollectionFilter for addWith(), push, unshift, and assign. * - * @see https://dataclient.io/rest/api/Collection#nestKey + * @see https://dataclient.io/rest/api/Collection#createcollectionfilter */ - nestKey?: (parent: Parent, key: string) => Record; + createCollectionFilter?: ( + ...args: Args + ) => (collectionKey: Record) => boolean; } | { - /** Defines lookups top-level Collections using ...args. + /** Test to determine which arg keys should **not** be used for filtering results. * - * @see https://dataclient.io/rest/api/Collection#argsKey + * @see https://dataclient.io/rest/api/Collection#nonfilterargumentkeys */ - argsKey?: (...args: Args) => Record; + nonFilterArgumentKeys?: ((key: string) => boolean) | string[] | RegExp; } -) & - ( - | { - /** Sets a default createCollectionFilter for addWith(), push, unshift, and assign. - * - * @see https://dataclient.io/rest/api/Collection#createcollectionfilter - */ - createCollectionFilter?: ( - ...args: Args - ) => (collectionKey: Record) => boolean; - } - | { - /** Test to determine which arg keys should **not** be used for filtering results. - * - * @see https://dataclient.io/rest/api/Collection#nonfilterargumentkeys - */ - nonFilterArgumentKeys?: ((key: string) => boolean) | string[] | RegExp; - } - ); +); function derivedProperties( collection: CollectionSchema, @@ -362,6 +357,7 @@ function normalizeCreate( args: readonly any[], visit: ((...args: any) => any) & { creating?: boolean }, delegate: INormalizeDelegate, + _parentEntity?: any, ): any { if (process.env.NODE_ENV !== 'production') { // means 'this is a creation endpoint' - so real PKs are not required @@ -409,6 +405,7 @@ function CreateMover>( args: readonly any[], visit: any, delegate: INormalizeDelegate, + parentEntity?: any, ) { return normalizeMove.call( this, @@ -419,6 +416,7 @@ function CreateMover>( args, visit, delegate, + parentEntity, ); }, ), @@ -440,6 +438,7 @@ function normalizeMove( args: readonly any[], visit: ((...args: any) => any) & { creating?: boolean }, delegate: INormalizeDelegate, + _parentEntity?: any, ): any { const isArray = this.schema instanceof ArraySchema; const entitySchema = this.schema.schema; diff --git a/packages/endpoint/src/schemas/__tests__/Collection.test.ts b/packages/endpoint/src/schemas/__tests__/Collection.test.ts index 1464eff028e4..18f2f43e68a8 100644 --- a/packages/endpoint/src/schemas/__tests__/Collection.test.ts +++ b/packages/endpoint/src/schemas/__tests__/Collection.test.ts @@ -49,6 +49,27 @@ const userTodos = new Collection(new schema.Array(Todo), { }), }); +const sharedUserTodos = new Collection(new schema.Array(Todo), { + argsKey: ({ userId }: { userId: string }) => ({ + userId, + }), + nestKey: (parent: { id: string }) => ({ + userId: parent.id, + }), +}); + +class UserWithSharedTodos extends IDEntity { + name = ''; + username = ''; + email = ''; + todos: Todo[] = []; + + static key = 'UserWithSharedTodos'; + static schema = { + todos: sharedUserTodos, + }; +} + test('key works with custom schema', () => { class CustomArray extends PolymorphicSchema { declare schema: any; @@ -162,6 +183,29 @@ describe(`${schema.Collection.name} normalization`, () => { const b: Record | undefined = state.result; }); + test('normalizes the same collection nested and top level', () => { + const nestedState = normalize(UserWithSharedTodos, { + id: '1', + username: 'bob', + todos: [{ id: '5', title: 'finish collections' }], + }); + const topLevelState = normalize( + sharedUserTodos, + [{ id: '10', title: 'top level item', userId: 1 }], + [{ userId: '1' }], + nestedState, + ); + + expect(nestedState.result).toBe('1'); + expect(topLevelState.result).toBe('{"userId":"1"}'); + expect( + nestedState.entities[sharedUserTodos.key]?.['{"userId":"1"}'], + ).toEqual(['5']); + expect( + topLevelState.entities[sharedUserTodos.key]?.['{"userId":"1"}'], + ).toEqual(['10']); + }); + test('normalizes already processed entities', () => { const state = normalize(User, { id: '1', @@ -2022,6 +2066,56 @@ describe(`${schema.Collection.name} denormalization`, () => { ).toMatchSnapshot(); }); + test('same collection denormalizes nested and top level with push propagation', () => { + const normalized = normalize(UserWithSharedTodos, { + id: '1', + username: 'bob', + todos: [{ id: '5', title: 'finish collections' }], + }); + const memo = new SimpleMemoCache(); + + const todos = memo.denormalize( + sharedUserTodos, + '{"userId":"1"}', + normalized.entities, + [{ userId: '1' }], + ); + const user = memo.denormalize( + UserWithSharedTodos, + normalized.result, + normalized.entities, + [{ id: '1' }], + ); + expect(user).toBeDefined(); + expect(user).not.toEqual(expect.any(Symbol)); + if (typeof user === 'symbol' || !user) return; + expect(todos).toBe(user.todos); + + const pushedState = normalize( + sharedUserTodos.push, + [{ id: '10', title: 'create new items' }], + [{ userId: '1' }], + normalized, + ); + const pushedTodos = memo.denormalize( + sharedUserTodos, + '{"userId":"1"}', + pushedState.entities, + [{ userId: '1' }], + ); + const pushedUser = memo.denormalize( + UserWithSharedTodos, + normalized.result, + pushedState.entities, + [{ id: '1' }], + ); + expect(pushedUser).toBeDefined(); + expect(pushedUser).not.toEqual(expect.any(Symbol)); + if (typeof pushedUser === 'symbol' || !pushedUser) return; + expect(pushedUser.todos.length).toBe(2); + expect(pushedTodos).toBe(pushedUser.todos); + }); + describe('caching', () => { const memo = new SimpleMemoCache(); test('denormalizes nested and top level share referential equality', () => { @@ -2221,14 +2315,14 @@ describe(`${schema.Collection.name} denormalization`, () => { expect(queryKey).toBeUndefined(); }); - it('should buildQueryKey undefined with nested Collection', () => { + it('should buildQueryKey with nested Collection using default argsKey', () => { const memo = new MemoCache(); const queryKey = memo.buildQueryKey( User.schema.todos, [{ userId: '1' }], normalizeNested, ); - expect(queryKey).toBeUndefined(); + expect(queryKey).toBe('{"userId":"1"}'); }); it('pk should serialize differently with nested args', () => { diff --git a/website/blog/2026-04-24-v0.17-scalar-typed-downloads.md b/website/blog/2026-04-24-v0.17-scalar-typed-downloads.md index a91c6e42682e..d335670228a6 100644 --- a/website/blog/2026-04-24-v0.17-scalar-typed-downloads.md +++ b/website/blog/2026-04-24-v0.17-scalar-typed-downloads.md @@ -293,6 +293,52 @@ argument tracking the nearest enclosing entity-like schema. This is **additive** don't need changes. New schemas can opt in to discover their containing entity at normalize time (used internally by [Scalar](/rest/api/Scalar)). +#### Optional: consolidate Collection definitions + +`Collection` can now use both [`argsKey`](/rest/api/Collection#argsKey) and +[`nestKey`](/rest/api/Collection#nestKey) on the same instance. As an optional +cleanup, replace paired top-level and nested Collections with one shared +definition: + + + +```ts title="Before" +export const getTodos = new RestEndpoint({ + path: '/todos', + searchParams: {} as { userId?: string }, + schema: new Collection([Todo]), +}); + +class User extends Entity { + static schema = { + todos: new Collection([Todo], { + nestKey: parent => ({ userId: parent.id }), + }), + }; +} +``` + +```ts title="After" +export const userTodos = new Collection([Todo], { + //argsKey: params => ({ ...params }), - this is default, so it's not needed + nestKey: parent => ({ userId: parent.id }), +}); + +export const getTodos = new RestEndpoint({ + path: '/todos', + searchParams: {} as { userId?: string }, + schema: userTodos, +}); + +class User extends Entity { + static schema = { + todos: userTodos, + }; +} +``` + + + ### Upgrade support As usual, if you have any troubles or questions, feel free to join our [![Chat](https://img.shields.io/discord/768254430381735967.svg?style=flat-square&colorB=758ED3)](https://discord.gg/wXGV27xm6t) or [file a bug](https://github.com/reactive/data-client/issues/new/choose) 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 333bd457db51..91d81d7e1479 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 @@ -243,7 +243,14 @@ declare class WeakDependencyMap { get(entity: K, getDependency: GetDependency, args?: readonly any[]): readonly [undefined, undefined] | readonly [V, Path[]]; /** Slow path: dep chain may interleave entity and `argsKey`-style deps. */ private _getMixed; - set(dependencies: Dep[], value: V, args?: readonly any[]): void; + set(dependencies: Dep[], value: V, args?: readonly any[], + /** Optional consumer-facing journey returned to `get()` callers verbatim. + * Defaults to `dependencies.map(d => d.path)`. Pass an explicit array to + * skip the per-write `.map(...)` and (more importantly) to skip per-hit + * post-processing — see `GlobalCache.getResults` for the read-side + * payoff. The array becomes a shared reference held by every subsequent + * cache hit; callers MUST NOT mutate it. */ + journey?: Path[]): void; /** True once any `argsKey`-style dep has been written. Consumers can use * this to skip function-stripping work on the hit path when false. */ get hasStringDeps(): boolean; diff --git a/website/src/components/Playground/editor-types/@data-client/endpoint.d.ts b/website/src/components/Playground/editor-types/@data-client/endpoint.d.ts index 8f5a410b9a66..63b592809798 100644 --- a/website/src/components/Playground/editor-types/@data-client/endpoint.d.ts +++ b/website/src/components/Playground/editor-types/@data-client/endpoint.d.ts @@ -819,8 +819,13 @@ declare class Scalar implements Mergeable { /** * The bound Entity's pk for a standalone scalar cell. * - * Prefers the surrounding map key (authoritative for `Values(Scalar)`), - * then falls back to the bound `Entity.pk(...)`. + * Prefers the surrounding map key (authoritative for `Values(Scalar)`, + * where `parent[key] === input`), then falls back to the bound + * `Entity.pk(...)`. Other shapes — `[Scalar]` top-level (where `key` is + * `undefined`) or nested under a plain object schema like + * `{ stock: [Scalar] }` (where `Array.normalize` forwards the parent + * object's field name as `key`, but `parent[key]` is the enclosing array, + * not the item) — must derive pk from the item itself. * * @see https://dataclient.io/rest/api/Scalar#entityPk * @param [input] the scalar cell input @@ -876,19 +881,18 @@ declare class Scalar implements Mergeable { queryKey(args: readonly any[], unvisit: any, delegate: IQueryDelegate): string[] | undefined; } -type CollectionOptions = ({ +type CollectionOptions = { /** Defines lookups for Collections nested in other schemas. * * @see https://dataclient.io/rest/api/Collection#nestKey */ nestKey?: (parent: Parent, key: string) => Record; -} | { /** Defines lookups top-level Collections using ...args. * * @see https://dataclient.io/rest/api/Collection#argsKey */ argsKey?: (...args: Args) => Record; -}) & ({ +} & ({ /** Sets a default createCollectionFilter for addWith(), push, unshift, and assign. * * @see https://dataclient.io/rest/api/Collection#createcollectionfilter @@ -940,15 +944,17 @@ interface CollectionInterface any, delegate: INormalizeDelegate): string; + pk(value: any, parent: any, key: string, args: any[], parentEntity?: any): string; + normalize(input: any, parent: Parent, key: string, args: any[], visit: (...args: any) => any, delegate: INormalizeDelegate, parentEntity?: any): string; /** Creates new instance copying over defined values of arguments * * @see https://dataclient.io/docs/api/Collection#merge diff --git a/website/src/components/Playground/editor-types/@data-client/graphql.d.ts b/website/src/components/Playground/editor-types/@data-client/graphql.d.ts index cde55ab8173c..84ecb353d8a5 100644 --- a/website/src/components/Playground/editor-types/@data-client/graphql.d.ts +++ b/website/src/components/Playground/editor-types/@data-client/graphql.d.ts @@ -819,8 +819,13 @@ declare class Scalar implements Mergeable { /** * The bound Entity's pk for a standalone scalar cell. * - * Prefers the surrounding map key (authoritative for `Values(Scalar)`), - * then falls back to the bound `Entity.pk(...)`. + * Prefers the surrounding map key (authoritative for `Values(Scalar)`, + * where `parent[key] === input`), then falls back to the bound + * `Entity.pk(...)`. Other shapes — `[Scalar]` top-level (where `key` is + * `undefined`) or nested under a plain object schema like + * `{ stock: [Scalar] }` (where `Array.normalize` forwards the parent + * object's field name as `key`, but `parent[key]` is the enclosing array, + * not the item) — must derive pk from the item itself. * * @see https://dataclient.io/rest/api/Scalar#entityPk * @param [input] the scalar cell input @@ -876,19 +881,18 @@ declare class Scalar implements Mergeable { queryKey(args: readonly any[], unvisit: any, delegate: IQueryDelegate): string[] | undefined; } -type CollectionOptions = ({ +type CollectionOptions = { /** Defines lookups for Collections nested in other schemas. * * @see https://dataclient.io/rest/api/Collection#nestKey */ nestKey?: (parent: Parent, key: string) => Record; -} | { /** Defines lookups top-level Collections using ...args. * * @see https://dataclient.io/rest/api/Collection#argsKey */ argsKey?: (...args: Args) => Record; -}) & ({ +} & ({ /** Sets a default createCollectionFilter for addWith(), push, unshift, and assign. * * @see https://dataclient.io/rest/api/Collection#createcollectionfilter @@ -940,15 +944,17 @@ interface CollectionInterface any, delegate: INormalizeDelegate): string; + pk(value: any, parent: any, key: string, args: any[], parentEntity?: any): string; + normalize(input: any, parent: Parent, key: string, args: any[], visit: (...args: any) => any, delegate: INormalizeDelegate, parentEntity?: any): string; /** Creates new instance copying over defined values of arguments * * @see https://dataclient.io/docs/api/Collection#merge 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 c78fa87967a1..cdfd4b43f051 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 @@ -281,7 +281,14 @@ declare class WeakDependencyMap { get(entity: K, getDependency: GetDependency, args?: readonly any[]): readonly [undefined, undefined] | readonly [V, Path[]]; /** Slow path: dep chain may interleave entity and `argsKey`-style deps. */ private _getMixed; - set(dependencies: Dep[], value: V, args?: readonly any[]): void; + set(dependencies: Dep[], value: V, args?: readonly any[], + /** Optional consumer-facing journey returned to `get()` callers verbatim. + * Defaults to `dependencies.map(d => d.path)`. Pass an explicit array to + * skip the per-write `.map(...)` and (more importantly) to skip per-hit + * post-processing — see `GlobalCache.getResults` for the read-side + * payoff. The array becomes a shared reference held by every subsequent + * cache hit; callers MUST NOT mutate it. */ + journey?: Path[]): void; /** True once any `argsKey`-style dep has been written. Consumers can use * this to skip function-stripping work on the hit path when false. */ get hasStringDeps(): boolean; diff --git a/website/src/components/Playground/editor-types/@data-client/rest.d.ts b/website/src/components/Playground/editor-types/@data-client/rest.d.ts index 641d3925d8a4..49875188a970 100644 --- a/website/src/components/Playground/editor-types/@data-client/rest.d.ts +++ b/website/src/components/Playground/editor-types/@data-client/rest.d.ts @@ -817,8 +817,13 @@ declare class Scalar implements Mergeable { /** * The bound Entity's pk for a standalone scalar cell. * - * Prefers the surrounding map key (authoritative for `Values(Scalar)`), - * then falls back to the bound `Entity.pk(...)`. + * Prefers the surrounding map key (authoritative for `Values(Scalar)`, + * where `parent[key] === input`), then falls back to the bound + * `Entity.pk(...)`. Other shapes — `[Scalar]` top-level (where `key` is + * `undefined`) or nested under a plain object schema like + * `{ stock: [Scalar] }` (where `Array.normalize` forwards the parent + * object's field name as `key`, but `parent[key]` is the enclosing array, + * not the item) — must derive pk from the item itself. * * @see https://dataclient.io/rest/api/Scalar#entityPk * @param [input] the scalar cell input @@ -874,19 +879,18 @@ declare class Scalar implements Mergeable { queryKey(args: readonly any[], unvisit: any, delegate: IQueryDelegate): string[] | undefined; } -type CollectionOptions = ({ +type CollectionOptions = { /** Defines lookups for Collections nested in other schemas. * * @see https://dataclient.io/rest/api/Collection#nestKey */ nestKey?: (parent: Parent, key: string) => Record; -} | { /** Defines lookups top-level Collections using ...args. * * @see https://dataclient.io/rest/api/Collection#argsKey */ argsKey?: (...args: Args) => Record; -}) & ({ +} & ({ /** Sets a default createCollectionFilter for addWith(), push, unshift, and assign. * * @see https://dataclient.io/rest/api/Collection#createcollectionfilter @@ -938,15 +942,17 @@ interface CollectionInterface any, delegate: INormalizeDelegate): string; + pk(value: any, parent: any, key: string, args: any[], parentEntity?: any): string; + normalize(input: any, parent: Parent, key: string, args: any[], visit: (...args: any) => any, delegate: INormalizeDelegate, parentEntity?: any): string; /** Creates new instance copying over defined values of arguments * * @see https://dataclient.io/docs/api/Collection#merge diff --git a/website/src/components/Playground/editor-types/globals.d.ts b/website/src/components/Playground/editor-types/globals.d.ts index 8f04089476a8..8fa3942cf661 100644 --- a/website/src/components/Playground/editor-types/globals.d.ts +++ b/website/src/components/Playground/editor-types/globals.d.ts @@ -821,8 +821,13 @@ declare class Scalar implements Mergeable { /** * The bound Entity's pk for a standalone scalar cell. * - * Prefers the surrounding map key (authoritative for `Values(Scalar)`), - * then falls back to the bound `Entity.pk(...)`. + * Prefers the surrounding map key (authoritative for `Values(Scalar)`, + * where `parent[key] === input`), then falls back to the bound + * `Entity.pk(...)`. Other shapes — `[Scalar]` top-level (where `key` is + * `undefined`) or nested under a plain object schema like + * `{ stock: [Scalar] }` (where `Array.normalize` forwards the parent + * object's field name as `key`, but `parent[key]` is the enclosing array, + * not the item) — must derive pk from the item itself. * * @see https://dataclient.io/rest/api/Scalar#entityPk * @param [input] the scalar cell input @@ -878,19 +883,18 @@ declare class Scalar implements Mergeable { queryKey(args: readonly any[], unvisit: any, delegate: IQueryDelegate): string[] | undefined; } -type CollectionOptions = ({ +type CollectionOptions = { /** Defines lookups for Collections nested in other schemas. * * @see https://dataclient.io/rest/api/Collection#nestKey */ nestKey?: (parent: Parent, key: string) => Record; -} | { /** Defines lookups top-level Collections using ...args. * * @see https://dataclient.io/rest/api/Collection#argsKey */ argsKey?: (...args: Args) => Record; -}) & ({ +} & ({ /** Sets a default createCollectionFilter for addWith(), push, unshift, and assign. * * @see https://dataclient.io/rest/api/Collection#createcollectionfilter @@ -942,15 +946,17 @@ interface CollectionInterface any, delegate: INormalizeDelegate): string; + pk(value: any, parent: any, key: string, args: any[], parentEntity?: any): string; + normalize(input: any, parent: Parent, key: string, args: any[], visit: (...args: any) => any, delegate: INormalizeDelegate, parentEntity?: any): string; /** Creates new instance copying over defined values of arguments * * @see https://dataclient.io/docs/api/Collection#merge diff --git a/website/src/components/Playground/editor-types/uuid.d.ts b/website/src/components/Playground/editor-types/uuid.d.ts index 170ec2fe2d4e..2aa09d6753ad 100644 --- a/website/src/components/Playground/editor-types/uuid.d.ts +++ b/website/src/components/Playground/editor-types/uuid.d.ts @@ -1,3 +1,7 @@ +declare const _default$1: "ffffffff-ffff-ffff-ffff-ffffffffffff"; + +declare const _default: "00000000-0000-0000-0000-000000000000"; + type UUIDTypes = string | TBuf; type Version1Options = { node?: Uint8Array; @@ -19,12 +23,9 @@ type Version7Options = { seq?: number; rng?: () => Uint8Array; }; +type NonSharedArrayBuffer = ReturnType; -declare const _default$1: "ffffffff-ffff-ffff-ffff-ffffffffffff"; - -declare const _default: "00000000-0000-0000-0000-000000000000"; - -declare function parse(uuid: string): Uint8Array; +declare function parse(uuid: string): NonSharedArrayBuffer; declare function stringify(arr: Uint8Array, offset?: number): string; @@ -32,7 +33,7 @@ declare function v1(options?: Version1Options, buf?: undefined, offset?: number) declare function v1(options: Version1Options | undefined, buf: Buf, offset?: number): Buf; declare function v1ToV6(uuid: string): string; -declare function v1ToV6(uuid: Uint8Array): Uint8Array; +declare function v1ToV6(uuid: Uint8Array): NonSharedArrayBuffer; declare function v3(value: string | Uint8Array, namespace: UUIDTypes, buf?: undefined, offset?: number): string; declare function v3(value: string | Uint8Array, namespace: UUIDTypes, buf: TBuf, offset?: number): TBuf; @@ -64,4 +65,4 @@ declare function validate(uuid: unknown): boolean; declare function version(uuid: string): number; -export { _default$1 as MAX, _default as NIL, type UUIDTypes, type Version1Options, type Version4Options, type Version6Options, type Version7Options, parse, stringify, v1, v1ToV6, v3, v4, v5, v6, v6ToV1, v7, validate, version }; +export { _default$1 as MAX, _default as NIL, type NonSharedArrayBuffer, type UUIDTypes, type Version1Options, type Version4Options, type Version6Options, type Version7Options, parse, stringify, v1, v1ToV6, v3, v4, v5, v6, v6ToV1, v7, validate, version };