diff --git a/.changeset/denormalize-delegate.md b/.changeset/denormalize-delegate.md new file mode 100644 index 000000000000..c79488d52dad --- /dev/null +++ b/.changeset/denormalize-delegate.md @@ -0,0 +1,53 @@ +--- +'@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 +--- + +**BREAKING**: `Schema.denormalize()` is now `(input, delegate)` instead +of the previous `(input, args, unvisit)` 3-parameter signature. + +```ts +// before +denormalize(input, args, unvisit) { + return unvisit(this.schema, input); +} + +// after +denormalize(input, delegate) { + return delegate.unvisit(this.schema, input); +} +``` + +The new [`IDenormalizeDelegate`](https://dataclient.io/docs/api/Schema) +exposes `unvisit`, `args`, and a new `argsKey(fn)` helper that registers +a memoization dimension when output varies with endpoint args. Reading +`delegate.args` directly does *not* contribute to cache invalidation — +schemas that branch on args must call `argsKey`. The `fn` reference +doubles as the cache path key, so it must be **referentially stable** +— define it on the instance or at module scope, not inline per call: + +```ts +class LensSchema { + constructor({ lens }) { + this.lensSelector = lens; // stable reference across calls + } + denormalize(input, delegate) { + const portfolio = delegate.argsKey(this.lensSelector); + return this.lookup(input, portfolio); + } +} +``` + +All built-in schemas (`Array`, `Object`, `Values`, `Union`, `Query`, +`Invalidate`, `Lazy`, `Collection`) have been updated. Custom schemas +implementing `SchemaSimple` must update their `denormalize` signature. + +`Schema.normalize()` and the `visit()` callback also gain an optional +trailing `parentEntity` argument tracking the nearest enclosing +entity-like schema. This is additive — existing schemas don't need +changes unless they want to use it. diff --git a/.changeset/scalar-entity-binding.md b/.changeset/scalar-entity-binding.md new file mode 100644 index 000000000000..6ae363a785ba --- /dev/null +++ b/.changeset/scalar-entity-binding.md @@ -0,0 +1,70 @@ +--- +'@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 +--- + +Add [Scalar](https://dataclient.io/rest/api/Scalar) schema for lens-dependent entity fields. + +`Scalar` models entity fields whose values vary by a runtime "lens" (such as the +selected portfolio, currency, or locale). Multiple components can render the +same entity through different lenses simultaneously — each sees the correct +values without the entity itself ever being mutated. Lens-dependent values are +stored in a separate cell table and joined at denormalize time from endpoint +args. + +New exports: `Scalar`, `schema.Scalar`. + +A single `Scalar` instance can serve both as an `Entity.schema` field (parent +entity inferred from the visit) and standalone — inside `Values(Scalar)`, +`[Scalar]`, or `Collection([Scalar])` — for cheap column-only refreshes +(entity bound explicitly via `entity`). Cell pks are derived from the map key +or via `Scalar.entityPk()`, which defaults to `Entity.pk()` so custom and +composite primary keys work with no override: + +```ts +import { Collection, Entity, RestEndpoint, Scalar } from '@data-client/rest'; + +class Company extends Entity { + id = ''; + price = 0; + pct_equity = 0; + shares = 0; +} +const PortfolioScalar = new Scalar({ + lens: args => args[0]?.portfolio, + key: 'portfolio', + entity: Company, +}); +Company.schema = { + pct_equity: PortfolioScalar, + shares: PortfolioScalar, +}; + +// Full load — Company rows + scalar cells for the current portfolio +export const getCompanies = new RestEndpoint({ + path: '/companies', + searchParams: {} as { portfolio: string }, + schema: new Collection([Company], { argsKey: () => ({}) }), +}); +// Lens-only refresh — writes to the same Scalar(portfolio) cell table +export const getPortfolioColumns = new RestEndpoint({ + path: '/companies/columns', + searchParams: {} as { portfolio: string }, + schema: new Collection([PortfolioScalar], { + argsKey: ({ portfolio }) => ({ portfolio }), + }), +}); +``` + +`useSuspense(getCompanies, { portfolio: 'A' })` and +`useSuspense(getCompanies, { portfolio: 'B' })` resolve to different +`pct_equity` / `shares` while sharing the same `Company` row. + +`Scalar.queryKey` enumerates cells in its table for the current lens, so +endpoints that use `Scalar` directly as their top-level schema reconstruct +from cache without a network round-trip once the cells are present. diff --git a/.cursor/skills/data-client-schema/SKILL.md b/.cursor/skills/data-client-schema/SKILL.md index a131941bb884..dd252d29c3cc 100644 --- a/.cursor/skills/data-client-schema/SKILL.md +++ b/.cursor/skills/data-client-schema/SKILL.md @@ -12,9 +12,11 @@ to represent the data expected. ### Object - [Entity](references/Entity.md) - represents a single unique object (denormalized) +- [EntityMixin](references/EntityMixin.md) - turn any pre-existing class into an Entity - [new Union(Entity)](references/Union.md) - polymorphic objects (A | B) - `{[key:string]: Schema}` - immutable objects - [new Invalidate(Entity|Union)](references/Invalidate.md) - to delete an Entity +- [new Lazy(() => Schema)](references/Lazy.md) - break circular imports / defer deep recursive denormalization ### List @@ -27,6 +29,10 @@ to represent the data expected. - `new Collection(Values(Schema))` - mutable/growable maps - [new Values(Schema)](references/Values.md) - immutable maps +### Lens-dependent entity fields + +- [new Scalar({ lens, key, entity? })](references/Scalar.md) - per-field cells that vary by a runtime "lens" (portfolio, currency, locale) without mutating the underlying entity + ### Derived / selector pattern - [new Query(Queryable)](references/Query.md) - memoized programmatic selectors @@ -156,10 +162,13 @@ See [partial-entities](references/partial-entities.md) for patterns and examples For detailed API documentation, see the [references](references/) directory: - [Entity](references/Entity.md) - Normalized data class +- [EntityMixin](references/EntityMixin.md) - Turn any class into an Entity - [Collection](references/Collection.md) - Mutable/growable lists - [Union](references/Union.md) - Polymorphic schemas - [Query](references/Query.md) - Programmatic selectors - [Invalidate](references/Invalidate.md) - Delete entities +- [Lazy](references/Lazy.md) - Deferred / circular schemas +- [Scalar](references/Scalar.md) - Lens-dependent entity fields - [Values](references/Values.md) - Map schemas - [All](references/All.md) - List all entities of a kind - [Array](references/Array.md) - Immutable list schema diff --git a/.cursor/skills/data-client-schema/references/EntityMixin.md b/.cursor/skills/data-client-schema/references/EntityMixin.md new file mode 120000 index 000000000000..dd3773c826ba --- /dev/null +++ b/.cursor/skills/data-client-schema/references/EntityMixin.md @@ -0,0 +1 @@ +../../../../docs/rest/api/EntityMixin.md \ No newline at end of file diff --git a/.cursor/skills/data-client-schema/references/Lazy.md b/.cursor/skills/data-client-schema/references/Lazy.md new file mode 120000 index 000000000000..e40397b7975c --- /dev/null +++ b/.cursor/skills/data-client-schema/references/Lazy.md @@ -0,0 +1 @@ +../../../../docs/rest/api/Lazy.md \ No newline at end of file diff --git a/.cursor/skills/data-client-schema/references/Scalar.md b/.cursor/skills/data-client-schema/references/Scalar.md new file mode 120000 index 000000000000..f68bf486582f --- /dev/null +++ b/.cursor/skills/data-client-schema/references/Scalar.md @@ -0,0 +1 @@ +../../../../docs/rest/api/Scalar.md \ No newline at end of file diff --git a/.cursor/skills/data-client-v0.17-migration/SKILL.md b/.cursor/skills/data-client-v0.17-migration/SKILL.md new file mode 100644 index 000000000000..4c316ec93a7f --- /dev/null +++ b/.cursor/skills/data-client-v0.17-migration/SKILL.md @@ -0,0 +1,163 @@ +--- +name: data-client-v0.17-migration +description: Migrate custom @data-client schemas from the v0.16 denormalize(input, args, unvisit) signature to the v0.17 denormalize(input, delegate) signature. Use when upgrading to v0.17, when seeing TS errors about unvisit not being callable, or when adapting custom Schema implementations. +--- + +# @data-client v0.17 Migration + +Applies to anyone implementing a custom [`Schema`](https://dataclient.io/docs/api/Schema) — `SchemaSimple`, `SchemaClass`, polymorphic wrappers, or types that subclass `EntityMixin` directly. Built-in schemas (`Entity`, `resource()`, `Collection`, `Union`, `Values`, `Array`, `Object`, `Query`, `Invalidate`, `Lazy`) are migrated by the library. + +The automated codemod handles the common cases: + +```bash +npx jscodeshift -t https://dataclient.io/codemods/v0.17.js --extensions=ts,tsx,js,jsx src/ +``` + +This skill describes what it does and how to handle the cases it can't. + +## What changed + +`Schema.denormalize()` now takes a single `delegate` instead of `(args, unvisit)`. Reading `delegate.args` does **not** contribute to cache invalidation — schemas whose output varies with endpoint args must register that dependency through `delegate.argsKey(fn)`. + +```ts +// before +denormalize(input, args, unvisit) { + return unvisit(this.schema, input); +} + +// after +denormalize(input, delegate) { + return delegate.unvisit(this.schema, input); +} +``` + +Full delegate surface ([`IDenormalizeDelegate`](https://dataclient.io/docs/api/Schema)): + +```ts +interface IDenormalizeDelegate { + unvisit(schema: any, input: any): any; + readonly args: readonly any[]; + argsKey(fn: (args: readonly any[]) => string | undefined): string | undefined; +} +``` + +## Migration rules + +### Class methods + +`(input, args, unvisit)` → `(input, delegate)`. Inside the body: + +- `unvisit(schema, value)` → `delegate.unvisit(schema, value)` +- bare `args` references (including spreads) → `delegate.args` +- pass-through `someSchema.denormalize(input, args, unvisit)` → `someSchema.denormalize(input, delegate)` + +```ts +// before +class Wrapper { + denormalize(input: {}, args: readonly any[], unvisit: any) { + const value = unvisit(this.schema, input); + return this.process(value, ...args); + } +} + +// after +class Wrapper { + denormalize(input: {}, delegate: IDenormalizeDelegate) { + const value = delegate.unvisit(this.schema, input); + return this.process(value, ...delegate.args); + } +} +``` + +### TypeScript signatures + +Update method signatures and `declare` fields the same way: + +```ts +// before +interface MySchema { + denormalize(input: {}, args: readonly any[], unvisit: (s: any, v: any) => any): any; +} + +class Lazy { + declare _denormalizeNullable: ( + input: {}, + args: readonly any[], + unvisit: (s: any, v: any) => any, + ) => any; +} + +// after — the codemod adds `IDenormalizeDelegate` to your existing +// `@data-client/{rest,endpoint,normalizr,...}` import as an inline +// `type` specifier. Only when no such import exists does it create a +// new `import type { IDenormalizeDelegate } from '@data-client/endpoint'`. +import { Entity, type IDenormalizeDelegate } from '@data-client/rest'; + +interface MySchema { + denormalize(input: {}, delegate: IDenormalizeDelegate): any; +} + +class Lazy { + declare _denormalizeNullable: (input: {}, delegate: IDenormalizeDelegate) => any; +} +``` + +The codemod matches `denormalize`, `_denormalize`, and `_denormalizeNullable` on type declarations. + +### args-dependent output (manual) + +The codemod will rewrite `args` to `delegate.args`, but if your schema's *return value* depends on those args, you must also register an [`argsKey`](https://dataclient.io/docs/api/Schema) so memoization invalidates correctly. The codemod cannot do this for you. + +`argsKey` returns `fn(args)` for convenience **and** the function reference doubles as the cache path key on `WeakDependencyMap` — so `fn` must be **referentially stable**. Bind it in the constructor or at module scope; an inline arrow creates a new reference per call and misses the cache every time. + +```ts +// before — args read directly +class LensSchema { + denormalize(input, args, unvisit) { + const lens = args[0]?.portfolio; + return this.lookup(input, lens); + } +} + +// after — stable instance field + declared memo dimension +class LensSchema { + constructor({ lens }) { + this.lensSelector = lens; + } + denormalize(input, delegate) { + const lens = delegate.argsKey(this.lensSelector); + return this.lookup(input, lens); + } +} +``` + +See [`Scalar`](https://dataclient.io/rest/api/Scalar) for a real-world example. + +## What the codemod skips + +These are rare; do them by hand: + +- **Computed/string-keyed methods**: only literal `denormalize` keys are matched. +- **Methods reassigned dynamically** (`obj.denormalize = function(input, args, unvisit) { ... }`). +- **Custom helper functions** that wrap `(args, unvisit)` and are passed around — you'll need to update both the helper and its callers. +- **`argsKey` registration** for schemas whose output varies with `args` (see above). + +## New (additive, no migration needed) + +`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. + +## Where to find affected code + +Search for these patterns in your codebase: + +- `denormalize(input` followed by 3 params — both class methods and bare functions +- `unvisit(` calls inside a `denormalize` body +- Spread `...args` inside a `denormalize` body +- TS interfaces / `declare` fields with the 3-param signature +- Custom `Schema` / `SchemaClass` / `SchemaSimple` implementations + +## Reference + +- Changeset: `.changeset/denormalize-delegate.md` +- Built-in schema diffs: `packages/endpoint/src/schemas/{Array,Object,Values,Union,Query,Invalidate,Lazy,Collection}.ts` +- New interface: [`IDenormalizeDelegate`](https://dataclient.io/docs/api/Schema) diff --git a/AGENTS.md b/AGENTS.md index a90478f10d33..f72fbff4a350 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,6 @@ # Reactive Data Client -Monorepo for `@data-client` npm packages. +Monorepo for `@data-client` high performance npm packages. ## Architecture diff --git a/README.md b/README.md index fbd5d25c570f..b31d2abec0bf 100644 --- a/README.md +++ b/README.md @@ -437,6 +437,13 @@ For the small price of 9kb gziped. [🏁Get started now](https://da
| Name | +Price | +% Equity | +Shares | +
|---|---|---|---|
| {c.name} | +${c.price.toFixed(2)} | +{formatPercent(c.pct_equity)} | +{formatShares(c.shares)} | +
{ - const value = unvisit(this.schema, input); + denormalize(input: {}, delegate: IDenormalizeDelegate): ReturnType
{ + const value = delegate.unvisit(this.schema, input); return ( - typeof value === 'symbol' ? value : this.process(value, ...args)) as any; + typeof value === 'symbol' ? value : ( + this.process(value, ...delegate.args) + )) as any; } queryKey( @@ -42,8 +48,7 @@ export default class Query< declare _denormalizeNullable: ( input: {}, - args: readonly any[], - unvisit: (schema: any, input: any) => any, + delegate: IDenormalizeDelegate, ) => ReturnType
| undefined;
declare _normalizeNullable: () => NormalizeNullable;
diff --git a/packages/endpoint/src/schemas/Scalar.ts b/packages/endpoint/src/schemas/Scalar.ts
new file mode 100644
index 000000000000..d980b4960bc4
--- /dev/null
+++ b/packages/endpoint/src/schemas/Scalar.ts
@@ -0,0 +1,285 @@
+import type {
+ IDenormalizeDelegate,
+ INormalizeDelegate,
+ IQueryDelegate,
+ Mergeable,
+ Visit,
+} from '../interface.js';
+
+interface ScalarOptions {
+ /**
+ * Selects the lens value from Endpoint args.
+ *
+ * The returned value is part of the stored cell key, so it must be stable
+ * for a given lens selection.
+ */
+ lens: (args: readonly any[]) => string | undefined;
+ /**
+ * Unique namespace for this Scalar's internal entity table.
+ */
+ key: string;
+ /**
+ * Entity class this Scalar stores cells for.
+ *
+ * Optional when used as a field on `Entity.schema`, where the parent Entity
+ * is inferred. Required for standalone usage such as `schema.Values`.
+ */
+ entity?: {
+ key: string;
+ pk?: (...args: any[]) => string | number | undefined;
+ };
+}
+
+/**
+ * Represents lens-dependent scalar fields on entities.
+ *
+ * Scalar stores values that belong to an Entity but vary by Endpoint args,
+ * such as portfolio-, currency-, or locale-specific columns. Use it as an
+ * `Entity.schema` field, or bind `entity` when using it standalone in
+ * `schema.Values`.
+ *
+ * @see https://dataclient.io/rest/api/Scalar
+ */
+export default class Scalar implements Mergeable {
+ readonly key: string;
+ readonly lensSelector: (args: readonly any[]) => string | undefined;
+ readonly entity: ScalarOptions['entity'];
+ readonly entityKey: string | undefined;
+ /**
+ * Allow normalize to receive primitive field values.
+ *
+ * Scalar stores per-cell values like `0.5`, so the visit walker must not
+ * apply its primitive short-circuit before dispatching to `normalize()`.
+ */
+ readonly acceptsPrimitives = true;
+
+ /**
+ * Represents lens-dependent scalar fields on entities.
+ *
+ * @see https://dataclient.io/rest/api/Scalar
+ */
+ constructor(options: ScalarOptions) {
+ this.key = `Scalar(${options.key})`;
+ this.lensSelector = options.lens;
+ this.entity = options.entity;
+ this.entityKey = options.entity?.key;
+ }
+
+ /**
+ * The bound Entity's pk for a standalone scalar cell.
+ *
+ * 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
+ * @param [parent] When normalizing, the object which included the cell
+ * @param [key] When normalizing, the surrounding map key (if any)
+ * @param [args] ...args sent to Endpoint
+ */
+ entityPk(
+ input: any,
+ parent: any,
+ key: string | undefined,
+ args: readonly any[],
+ ): string | number | undefined {
+ // Only trust `key` when the enclosing container literally maps it to this
+ // cell — i.e. `Values(Scalar)`, where the map key is the entity pk by
+ // construction. `Array.normalize` forwards the *outer* object's field name
+ // as `key` for every element (see Array.ts), so `key !== undefined` alone
+ // would collapse every item onto the same compound pk.
+ if (key !== undefined && parent != null && parent[key] === input) {
+ return key;
+ }
+ return this.entity?.pk?.(input, parent, key, args);
+ }
+
+ createIfValid(props: any) {
+ return { ...props };
+ }
+
+ merge(existing: any, incoming: any) {
+ return { ...existing, ...incoming };
+ }
+
+ /**
+ * Determines whether an incoming write is older than the stored cell.
+ *
+ * Defaults to comparing `fetchedAt`, matching Entity behavior so older
+ * responses do not overwrite newer values.
+ */
+ shouldReorder(
+ existingMeta: { date: number; fetchedAt: number },
+ incomingMeta: { date: number; fetchedAt: number },
+ existing: any,
+ incoming: any,
+ ) {
+ return incomingMeta.fetchedAt < existingMeta.fetchedAt;
+ }
+
+ mergeWithStore(
+ existingMeta: { date: number; fetchedAt: number },
+ incomingMeta: { date: number; fetchedAt: number },
+ existing: any,
+ incoming: any,
+ ) {
+ return this.shouldReorder(existingMeta, incomingMeta, existing, incoming) ?
+ this.merge(incoming, existing)
+ : this.merge(existing, incoming);
+ }
+
+ mergeMetaWithStore(
+ existingMeta: { fetchedAt: number; date: number; expiresAt: number },
+ incomingMeta: { fetchedAt: number; date: number; expiresAt: number },
+ existing: any,
+ incoming: any,
+ ) {
+ return this.shouldReorder(existingMeta, incomingMeta, existing, incoming) ?
+ existingMeta
+ : incomingMeta;
+ }
+
+ normalize(
+ input: any,
+ parent: any,
+ key: any,
+ args: any[],
+ visit: Visit,
+ delegate: INormalizeDelegate,
+ parentEntity: any,
+ ): any {
+ const lensValue = this.lensSelector(args);
+
+ // Without a lens we cannot derive a retrievable cell key — writing to
+ // `${…}|undefined` would silently corrupt the Scalar table (literal
+ // "undefined" collides across rows) and denormalize would never find
+ // the data. A missing lens during normalize is a configuration bug;
+ // throw rather than mask it.
+ if (lensValue === undefined) {
+ throw new Error(
+ `${this.key}: lens returned undefined during normalize. Ensure endpoint args contain the lens value.`,
+ );
+ }
+
+ // Entity-field path: the visit walker supplies the enclosing Entity
+ // class as `parentEntity`. `input` is the per-cell scalar value
+ // (e.g. 0.5), `parent` is the entity data row, `key` is the field name
+ // (always a string from Object.keys, no coercion needed).
+ if (typeof parentEntity === 'function' && parentEntity.pk) {
+ const entityKey: string = parentEntity.key;
+ // TODO: this re-derives the enclosing entity's pk without the enclosing
+ // `parent`/`key` context that `EntityMixin.normalize` used to compute
+ // the authoritative id (see EntityMixin.ts around `this.pk(processedEntity, parent, key, args)`).
+ // If a custom `pk()` reads its 2nd/3rd args, we will key the Scalar cell
+ // differently than the entity is stored under and denormalize will miss.
+ // Fix by threading the enclosing parent/key through the visit walker
+ // (e.g. via a parentContext param on `visit` / INormalizeDelegate) and
+ // forwarding them here instead of `undefined, undefined`.
+ const id = `${parentEntity.pk(parent, undefined, undefined, args)}`;
+ // Merge only this field's value — EntityMixin's loop calls us once
+ // per scalar field, and `merge` accumulates them into the cell.
+ delegate.mergeEntity(this, `${entityKey}|${id}|${lensValue}`, {
+ [key]: input,
+ });
+ // Wrapper is a tuple: `[entityPk, fieldName, entityKey]`. Using an
+ // array (not an object) lets denormalize distinguish it from cell
+ // data via `Array.isArray` without a brand property — and produces
+ // smaller serialized state for SSR. `entityKey` is recorded so a
+ // single Scalar shared across entity types resolves the correct cell.
+ return [id, key, entityKey];
+ }
+
+ // Standalone path: input is scalar cell data, and pk is derived from either
+ // the item itself (`[Scalar]`) or the surrounding key (`Values(Scalar)`).
+ // No parent entity to infer from — require explicit `entity` binding.
+ if (this.entityKey === undefined) {
+ throw new Error(
+ 'Scalar used standalone (e.g. inside `schema.Values`) requires an `entity` option to bind it to an Entity class.',
+ );
+ }
+ const id = this.entityPk(input, parent, key, args);
+ if (id === undefined) {
+ throw new Error(
+ `${this.key}: cannot derive entity pk for cell. Provide items with an ` +
+ '`id` field, a pk-keyed map key, or override Scalar.entityPk().',
+ );
+ }
+ // cpk = compound pk: `"entityKey|entityPk|lensValue"`; used throughout this file.
+ const cpk = `${this.entityKey}|${id}|${lensValue}`;
+ delegate.mergeEntity(this, cpk, { ...input });
+ return cpk;
+ }
+
+ denormalize(input: any, delegate: IDenormalizeDelegate): any {
+ // Legit inputs are wrapper array, cpk string, cell object, or symbol.
+ // Any other primitive — falsy (`0`, `''`, `false`, `null`, `undefined`)
+ // or truthy (`0.5`, `true`, `42`, `bigint`, `symbol`, etc.) — cannot be
+ // resolved via the entity table and must short-circuit here. Without
+ // this, non-string truthy primitives would infinite-recurse through the
+ // Values branch below: `unvisit` re-dispatches back to us because
+ // `isEntity(this)` is false and its string-only fast path misses. This
+ // can happen during schema migration when a Scalar is added to an entity
+ // with cached raw numeric/boolean field values still in the store.
+ if (!input || (typeof input !== 'object' && typeof input !== 'string')) {
+ return input;
+ }
+
+ // Entity-field wrapper `[entityPk, fieldName, entityKey]`: cpk is derived
+ // from current `args`, so record `lensSelector` on the enclosing Entity's
+ // cache frame. `lensSelector` must be ctor-bound (stable ref); see WeakDependencyMap.
+ if (Array.isArray(input)) {
+ const lensValue = delegate.argsKey(this.lensSelector);
+ if (lensValue === undefined) return undefined;
+ const cellData = delegate.unvisit(
+ this,
+ `${input[2]}|${input[0]}|${lensValue}`,
+ );
+ return cellData && typeof cellData !== 'symbol' ?
+ cellData[input[1]]
+ : undefined;
+ }
+
+ // Cell data passes through unchanged (recursive call from `unvisit`
+ // when looking up the cell via its compound pk).
+ if (typeof input === 'object') return input;
+
+ // String-cpk fallback. Walker-driven Values(Scalar) never reaches this —
+ // `unvisit.ts` intercepts string+createIfValid before `schema.denormalize`.
+ // No `argsKey`: cpk is fixed at normalize-time; per-entity WDM keys on cpk.
+ return delegate.unvisit(this, input);
+ }
+
+ /**
+ * Returns the cpks of cells matching the current lens, or undefined.
+ *
+ * Only consulted when `Scalar` is an endpoint's top-level schema; field
+ * usage resolves through the parent entity. Relies on `lens` not
+ * containing the cpk delimiter `|`.
+ */
+ queryKey(
+ args: readonly any[],
+ unvisit: any,
+ delegate: IQueryDelegate,
+ ): string[] | undefined {
+ const lens = this.lensSelector(args);
+ if (lens === undefined) return undefined;
+
+ const cells = delegate.getEntities(this.key);
+ if (!cells) return undefined;
+
+ const cpks: string[] = [];
+ for (const [cpk, entity] of cells.entries()) {
+ if (!entity || typeof entity === 'symbol') continue;
+ const lastPipe = cpk.lastIndexOf('|');
+ if (lastPipe !== -1 && cpk.slice(lastPipe + 1) === lens) {
+ cpks.push(cpk);
+ }
+ }
+ return cpks.length ? cpks : undefined;
+ }
+}
diff --git a/packages/endpoint/src/schemas/Union.ts b/packages/endpoint/src/schemas/Union.ts
index ef91dcaa8b34..810c3395f029 100644
--- a/packages/endpoint/src/schemas/Union.ts
+++ b/packages/endpoint/src/schemas/Union.ts
@@ -1,5 +1,5 @@
import PolymorphicSchema from './Polymorphic.js';
-import { Visit } from '../interface.js';
+import { IDenormalizeDelegate, Visit } from '../interface.js';
/**
* Represents polymorphic values.
@@ -22,12 +22,8 @@ export default class UnionSchema extends PolymorphicSchema {
return this.normalizeValue(input, parent, key, args, visit);
}
- denormalize(
- input: {},
- args: readonly any[],
- unvisit: (schema: any, input: any) => any,
- ) {
- return this.denormalizeValue(input, unvisit);
+ denormalize(input: {}, delegate: IDenormalizeDelegate) {
+ return this.denormalizeValue(input, delegate.unvisit);
}
queryKey(args: any, unvisit: (schema: any, args: any) => any) {
diff --git a/packages/endpoint/src/schemas/Values.ts b/packages/endpoint/src/schemas/Values.ts
index dc36237d00f7..60fb84ce8177 100644
--- a/packages/endpoint/src/schemas/Values.ts
+++ b/packages/endpoint/src/schemas/Values.ts
@@ -1,5 +1,5 @@
import PolymorphicSchema from './Polymorphic.js';
-import { Visit } from '../interface.js';
+import { IDenormalizeDelegate, Visit } from '../interface.js';
/**
* Represents variably sized objects
@@ -26,17 +26,13 @@ export default class ValuesSchema extends PolymorphicSchema {
return output;
}
- denormalize(
- input: {},
- args: readonly any[],
- unvisit: (schema: any, input: any) => any,
- ): any {
+ denormalize(input: {}, delegate: IDenormalizeDelegate): any {
const output: Record🛑
; queryKey(args: ProcessParameters
, unvisit: (schema: any, args: any) => any): any; - _denormalizeNullable: (input: {}, args: readonly any[], unvisit: (schema: any, input: any) => any) => ReturnType
| undefined; + _denormalizeNullable: (input: {}, delegate: IDenormalizeDelegate) => ReturnType
| undefined;
_normalizeNullable: () => NormalizeNullable;
}
type ProcessParameters
= P extends (entries: any, ...args: infer Par) => any ? Par extends [] ? SchemaArgs ;
queryKey(args: ProcessParameters , unvisit: (schema: any, args: any) => any): any;
- _denormalizeNullable: (input: {}, args: readonly any[], unvisit: (schema: any, input: any) => any) => ReturnType | undefined;
+ _denormalizeNullable: (input: {}, delegate: IDenormalizeDelegate) => ReturnType | undefined;
_normalizeNullable: () => NormalizeNullable = P extends (entries: any, ...args: infer Par) => any ? Par extends [] ? SchemaArgs ;
queryKey(args: ProcessParameters , unvisit: (schema: any, args: any) => any): any;
- _denormalizeNullable: (input: {}, args: readonly any[], unvisit: (schema: any, input: any) => any) => ReturnType | undefined;
+ _denormalizeNullable: (input: {}, delegate: IDenormalizeDelegate) => ReturnType | undefined;
_normalizeNullable: () => NormalizeNullable = P extends (entries: any, ...args: infer Par) => any ? Par extends [] ? SchemaArgs ;
queryKey(args: ProcessParameters , unvisit: (schema: any, args: any) => any): any;
- _denormalizeNullable: (input: {}, args: readonly any[], unvisit: (schema: any, input: any) => any) => ReturnType | undefined;
+ _denormalizeNullable: (input: {}, delegate: IDenormalizeDelegate) => ReturnType | undefined;
_normalizeNullable: () => NormalizeNullable = P extends (entries: any, ...args: infer Par) => any ? Par extends [] ? SchemaArgs : Par & SchemaArgs : SchemaArgs;
+interface ScalarOptions {
+ /**
+ * Selects the lens value from Endpoint args.
+ *
+ * The returned value is part of the stored cell key, so it must be stable
+ * for a given lens selection.
+ */
+ lens: (args: readonly any[]) => string | undefined;
+ /**
+ * Unique namespace for this Scalar's internal entity table.
+ */
+ key: string;
+ /**
+ * Entity class this Scalar stores cells for.
+ *
+ * Optional when used as a field on `Entity.schema`, where the parent Entity
+ * is inferred. Required for standalone usage such as `schema.Values`.
+ */
+ entity?: {
+ key: string;
+ pk?: (...args: any[]) => string | number | undefined;
+ };
+}
+/**
+ * Represents lens-dependent scalar fields on entities.
+ *
+ * Scalar stores values that belong to an Entity but vary by Endpoint args,
+ * such as portfolio-, currency-, or locale-specific columns. Use it as an
+ * `Entity.schema` field, or bind `entity` when using it standalone in
+ * `schema.Values`.
+ *
+ * @see https://dataclient.io/rest/api/Scalar
+ */
+declare class Scalar implements Mergeable {
+ readonly key: string;
+ readonly lensSelector: (args: readonly any[]) => string | undefined;
+ readonly entity: ScalarOptions['entity'];
+ readonly entityKey: string | undefined;
+ /**
+ * Allow normalize to receive primitive field values.
+ *
+ * Scalar stores per-cell values like `0.5`, so the visit walker must not
+ * apply its primitive short-circuit before dispatching to `normalize()`.
+ */
+ readonly acceptsPrimitives = true;
+ /**
+ * Represents lens-dependent scalar fields on entities.
+ *
+ * @see https://dataclient.io/rest/api/Scalar
+ */
+ constructor(options: ScalarOptions);
+ /**
+ * 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(...)`.
+ *
+ * @see https://dataclient.io/rest/api/Scalar#entityPk
+ * @param [input] the scalar cell input
+ * @param [parent] When normalizing, the object which included the cell
+ * @param [key] When normalizing, the surrounding map key (if any)
+ * @param [args] ...args sent to Endpoint
+ */
+ entityPk(input: any, parent: any, key: string | undefined, args: readonly any[]): string | number | undefined;
+ createIfValid(props: any): any;
+ merge(existing: any, incoming: any): any;
+ /**
+ * Determines whether an incoming write is older than the stored cell.
+ *
+ * Defaults to comparing `fetchedAt`, matching Entity behavior so older
+ * responses do not overwrite newer values.
+ */
+ shouldReorder(existingMeta: {
+ date: number;
+ fetchedAt: number;
+ }, incomingMeta: {
+ date: number;
+ fetchedAt: number;
+ }, existing: any, incoming: any): boolean;
+ mergeWithStore(existingMeta: {
+ date: number;
+ fetchedAt: number;
+ }, incomingMeta: {
+ date: number;
+ fetchedAt: number;
+ }, existing: any, incoming: any): any;
+ mergeMetaWithStore(existingMeta: {
+ fetchedAt: number;
+ date: number;
+ expiresAt: number;
+ }, incomingMeta: {
+ fetchedAt: number;
+ date: number;
+ expiresAt: number;
+ }, existing: any, incoming: any): {
+ fetchedAt: number;
+ date: number;
+ expiresAt: number;
+ };
+ normalize(input: any, parent: any, key: any, args: any[], visit: Visit, delegate: INormalizeDelegate, parentEntity: any): any;
+ denormalize(input: any, delegate: IDenormalizeDelegate): any;
+ /**
+ * Returns the cpks of cells matching the current lens, or undefined.
+ *
+ * Only consulted when `Scalar` is an endpoint's top-level schema; field
+ * usage resolves through the parent entity. Relies on `lens` not
+ * containing the cpk delimiter `|`.
+ */
+ queryKey(args: readonly any[], unvisit: any, delegate: IQueryDelegate): string[] | undefined;
+}
+
type CollectionOptions implements SchemaClass {
denormalize(
input: {},
- args: readonly any[],
- unvisit: (schema: any, input: any) => any,
+ delegate: IDenormalizeDelegate,
): (S extends EntityMap)[];
queryKey(
@@ -982,8 +1132,7 @@ declare class All<
denormalize(
input: {},
- args: readonly any[],
- unvisit: (schema: any, input: any) => any,
+ delegate: IDenormalizeDelegate,
): (S extends EntityMap)[];
queryKey(
@@ -1021,11 +1170,7 @@ declare class Object$1<
_denormalizeNullable(): DenormalizeNullableObject, ...args: any) => any> = Query;
declare const schema_d_Query: typeof Query;
+type schema_d_Scalar = Scalar;
+declare const schema_d_Scalar: typeof Scalar;
type schema_d_SchemaAttributeFunction = SchemaAttributeFunction;
type schema_d_SchemaClass implements SchemaSimple {
*/
constructor(schema: S);
normalize(input: any, parent: any, key: any, args: any[], visit: (...args: any) => any, _delegate: any): any;
- denormalize(input: {}, _args: readonly any[], _unvisit: any): any;
+ denormalize(input: {}, _delegate: IDenormalizeDelegate): any;
queryKey(_args: readonly any[], _unvisit: (...args: any) => any, _delegate: any): undefined;
/** Queryable schema for use with useQuery() to resolve lazy relationships */
get query(): LazyQuery;
private _query;
- _denormalizeNullable: (input: {}, args: readonly any[], unvisit: (schema: any, input: any) => any) => any;
+ _denormalizeNullable: (input: {}, delegate: IDenormalizeDelegate) => any;
_normalizeNullable: () => NormalizeNullable;
}
/**
@@ -691,12 +731,12 @@ declare class Lazy implements SchemaSimple {
declare class LazyQuery> {
schema: S;
constructor(schema: S);
- denormalize(input: {}, args: readonly any[], unvisit: (schema: any, input: any) => any): Denormalize;
+ denormalize(input: {}, delegate: IDenormalizeDelegate): Denormalize;
queryKey(args: Args, unvisit: (...args: any) => any, delegate: {
getEntity: any;
getIndex: any;
}): any;
- _denormalizeNullable: (input: {}, args: readonly any[], unvisit: (schema: any, input: any) => any) => DenormalizeNullable;
+ _denormalizeNullable: (input: {}, delegate: IDenormalizeDelegate) => DenormalizeNullable;
}
/**
@@ -716,15 +756,126 @@ declare class Query;
+ denormalize(input: {}, delegate: IDenormalizeDelegate): ReturnType;
}
type ProcessParameters : Par & SchemaArgs : SchemaArgs;
+interface ScalarOptions {
+ /**
+ * Selects the lens value from Endpoint args.
+ *
+ * The returned value is part of the stored cell key, so it must be stable
+ * for a given lens selection.
+ */
+ lens: (args: readonly any[]) => string | undefined;
+ /**
+ * Unique namespace for this Scalar's internal entity table.
+ */
+ key: string;
+ /**
+ * Entity class this Scalar stores cells for.
+ *
+ * Optional when used as a field on `Entity.schema`, where the parent Entity
+ * is inferred. Required for standalone usage such as `schema.Values`.
+ */
+ entity?: {
+ key: string;
+ pk?: (...args: any[]) => string | number | undefined;
+ };
+}
+/**
+ * Represents lens-dependent scalar fields on entities.
+ *
+ * Scalar stores values that belong to an Entity but vary by Endpoint args,
+ * such as portfolio-, currency-, or locale-specific columns. Use it as an
+ * `Entity.schema` field, or bind `entity` when using it standalone in
+ * `schema.Values`.
+ *
+ * @see https://dataclient.io/rest/api/Scalar
+ */
+declare class Scalar implements Mergeable {
+ readonly key: string;
+ readonly lensSelector: (args: readonly any[]) => string | undefined;
+ readonly entity: ScalarOptions['entity'];
+ readonly entityKey: string | undefined;
+ /**
+ * Allow normalize to receive primitive field values.
+ *
+ * Scalar stores per-cell values like `0.5`, so the visit walker must not
+ * apply its primitive short-circuit before dispatching to `normalize()`.
+ */
+ readonly acceptsPrimitives = true;
+ /**
+ * Represents lens-dependent scalar fields on entities.
+ *
+ * @see https://dataclient.io/rest/api/Scalar
+ */
+ constructor(options: ScalarOptions);
+ /**
+ * 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(...)`.
+ *
+ * @see https://dataclient.io/rest/api/Scalar#entityPk
+ * @param [input] the scalar cell input
+ * @param [parent] When normalizing, the object which included the cell
+ * @param [key] When normalizing, the surrounding map key (if any)
+ * @param [args] ...args sent to Endpoint
+ */
+ entityPk(input: any, parent: any, key: string | undefined, args: readonly any[]): string | number | undefined;
+ createIfValid(props: any): any;
+ merge(existing: any, incoming: any): any;
+ /**
+ * Determines whether an incoming write is older than the stored cell.
+ *
+ * Defaults to comparing `fetchedAt`, matching Entity behavior so older
+ * responses do not overwrite newer values.
+ */
+ shouldReorder(existingMeta: {
+ date: number;
+ fetchedAt: number;
+ }, incomingMeta: {
+ date: number;
+ fetchedAt: number;
+ }, existing: any, incoming: any): boolean;
+ mergeWithStore(existingMeta: {
+ date: number;
+ fetchedAt: number;
+ }, incomingMeta: {
+ date: number;
+ fetchedAt: number;
+ }, existing: any, incoming: any): any;
+ mergeMetaWithStore(existingMeta: {
+ fetchedAt: number;
+ date: number;
+ expiresAt: number;
+ }, incomingMeta: {
+ fetchedAt: number;
+ date: number;
+ expiresAt: number;
+ }, existing: any, incoming: any): {
+ fetchedAt: number;
+ date: number;
+ expiresAt: number;
+ };
+ normalize(input: any, parent: any, key: any, args: any[], visit: Visit, delegate: INormalizeDelegate, parentEntity: any): any;
+ denormalize(input: any, delegate: IDenormalizeDelegate): any;
+ /**
+ * Returns the cpks of cells matching the current lens, or undefined.
+ *
+ * Only consulted when `Scalar` is an endpoint's top-level schema; field
+ * usage resolves through the parent entity. Relies on `lens` not
+ * containing the cpk delimiter `|`.
+ */
+ queryKey(args: readonly any[], unvisit: any, delegate: IQueryDelegate): string[] | undefined;
+}
+
type CollectionOptions implements SchemaClass {
denormalize(
input: {},
- args: readonly any[],
- unvisit: (schema: any, input: any) => any,
+ delegate: IDenormalizeDelegate,
): (S extends EntityMap)[];
queryKey(
@@ -982,8 +1132,7 @@ declare class All<
denormalize(
input: {},
- args: readonly any[],
- unvisit: (schema: any, input: any) => any,
+ delegate: IDenormalizeDelegate,
): (S extends EntityMap)[];
queryKey(
@@ -1021,11 +1170,7 @@ declare class Object$1<
_denormalizeNullable(): DenormalizeNullableObject, ...args: any) => any> = Query;
declare const schema_d_Query: typeof Query;
+type schema_d_Scalar = Scalar;
+declare const schema_d_Scalar: typeof Scalar;
type schema_d_SchemaAttributeFunction = SchemaAttributeFunction;
type schema_d_SchemaClass(schema: S | undefined, input: any
declare function isEntity(schema: Schema): schema is EntityInterface;
+/** Function path used by `argsKey` deps. Distinguished from object paths
+ * via `typeof === 'function'`. */
+type KeyFn = (args: readonly any[]) => string | undefined;
/** Maps a (ordered) list of dependencies to a value.
*
* Useful as a memoization cache for flat/normalized stores.
*
- * All dependencies are only weakly referenced, allowing automatic garbage collection
- * when any dependencies are no longer used.
+ * Object dependencies are weakly referenced (via `WeakMap`), allowing
+ * automatic garbage collection when the dependency is no longer used.
+ * String-keyed dependencies (used by `argsKey`) sit on a `Map` keyed by the
+ * value returned from `path(args)`, branching on a stable function reference.
*/
declare class WeakDependencyMap implements SchemaSimple {
*/
constructor(schema: S);
normalize(input: any, parent: any, key: any, args: any[], visit: (...args: any) => any, _delegate: any): any;
- denormalize(input: {}, _args: readonly any[], _unvisit: any): any;
+ denormalize(input: {}, _delegate: IDenormalizeDelegate): any;
queryKey(_args: readonly any[], _unvisit: (...args: any) => any, _delegate: any): undefined;
/** Queryable schema for use with useQuery() to resolve lazy relationships */
get query(): LazyQuery;
private _query;
- _denormalizeNullable: (input: {}, args: readonly any[], unvisit: (schema: any, input: any) => any) => any;
+ _denormalizeNullable: (input: {}, delegate: IDenormalizeDelegate) => any;
_normalizeNullable: () => NormalizeNullable;
}
/**
@@ -689,12 +729,12 @@ declare class Lazy implements SchemaSimple {
declare class LazyQuery> {
schema: S;
constructor(schema: S);
- denormalize(input: {}, args: readonly any[], unvisit: (schema: any, input: any) => any): Denormalize;
+ denormalize(input: {}, delegate: IDenormalizeDelegate): Denormalize;
queryKey(args: Args, unvisit: (...args: any) => any, delegate: {
getEntity: any;
getIndex: any;
}): any;
- _denormalizeNullable: (input: {}, args: readonly any[], unvisit: (schema: any, input: any) => any) => DenormalizeNullable;
+ _denormalizeNullable: (input: {}, delegate: IDenormalizeDelegate) => DenormalizeNullable;
}
/**
@@ -714,15 +754,126 @@ declare class Query;
+ denormalize(input: {}, delegate: IDenormalizeDelegate): ReturnType;
}
type ProcessParameters : Par & SchemaArgs : SchemaArgs;
+interface ScalarOptions {
+ /**
+ * Selects the lens value from Endpoint args.
+ *
+ * The returned value is part of the stored cell key, so it must be stable
+ * for a given lens selection.
+ */
+ lens: (args: readonly any[]) => string | undefined;
+ /**
+ * Unique namespace for this Scalar's internal entity table.
+ */
+ key: string;
+ /**
+ * Entity class this Scalar stores cells for.
+ *
+ * Optional when used as a field on `Entity.schema`, where the parent Entity
+ * is inferred. Required for standalone usage such as `schema.Values`.
+ */
+ entity?: {
+ key: string;
+ pk?: (...args: any[]) => string | number | undefined;
+ };
+}
+/**
+ * Represents lens-dependent scalar fields on entities.
+ *
+ * Scalar stores values that belong to an Entity but vary by Endpoint args,
+ * such as portfolio-, currency-, or locale-specific columns. Use it as an
+ * `Entity.schema` field, or bind `entity` when using it standalone in
+ * `schema.Values`.
+ *
+ * @see https://dataclient.io/rest/api/Scalar
+ */
+declare class Scalar implements Mergeable {
+ readonly key: string;
+ readonly lensSelector: (args: readonly any[]) => string | undefined;
+ readonly entity: ScalarOptions['entity'];
+ readonly entityKey: string | undefined;
+ /**
+ * Allow normalize to receive primitive field values.
+ *
+ * Scalar stores per-cell values like `0.5`, so the visit walker must not
+ * apply its primitive short-circuit before dispatching to `normalize()`.
+ */
+ readonly acceptsPrimitives = true;
+ /**
+ * Represents lens-dependent scalar fields on entities.
+ *
+ * @see https://dataclient.io/rest/api/Scalar
+ */
+ constructor(options: ScalarOptions);
+ /**
+ * 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(...)`.
+ *
+ * @see https://dataclient.io/rest/api/Scalar#entityPk
+ * @param [input] the scalar cell input
+ * @param [parent] When normalizing, the object which included the cell
+ * @param [key] When normalizing, the surrounding map key (if any)
+ * @param [args] ...args sent to Endpoint
+ */
+ entityPk(input: any, parent: any, key: string | undefined, args: readonly any[]): string | number | undefined;
+ createIfValid(props: any): any;
+ merge(existing: any, incoming: any): any;
+ /**
+ * Determines whether an incoming write is older than the stored cell.
+ *
+ * Defaults to comparing `fetchedAt`, matching Entity behavior so older
+ * responses do not overwrite newer values.
+ */
+ shouldReorder(existingMeta: {
+ date: number;
+ fetchedAt: number;
+ }, incomingMeta: {
+ date: number;
+ fetchedAt: number;
+ }, existing: any, incoming: any): boolean;
+ mergeWithStore(existingMeta: {
+ date: number;
+ fetchedAt: number;
+ }, incomingMeta: {
+ date: number;
+ fetchedAt: number;
+ }, existing: any, incoming: any): any;
+ mergeMetaWithStore(existingMeta: {
+ fetchedAt: number;
+ date: number;
+ expiresAt: number;
+ }, incomingMeta: {
+ fetchedAt: number;
+ date: number;
+ expiresAt: number;
+ }, existing: any, incoming: any): {
+ fetchedAt: number;
+ date: number;
+ expiresAt: number;
+ };
+ normalize(input: any, parent: any, key: any, args: any[], visit: Visit, delegate: INormalizeDelegate, parentEntity: any): any;
+ denormalize(input: any, delegate: IDenormalizeDelegate): any;
+ /**
+ * Returns the cpks of cells matching the current lens, or undefined.
+ *
+ * Only consulted when `Scalar` is an endpoint's top-level schema; field
+ * usage resolves through the parent entity. Relies on `lens` not
+ * containing the cpk delimiter `|`.
+ */
+ queryKey(args: readonly any[], unvisit: any, delegate: IQueryDelegate): string[] | undefined;
+}
+
type CollectionOptions implements SchemaClass {
denormalize(
input: {},
- args: readonly any[],
- unvisit: (schema: any, input: any) => any,
+ delegate: IDenormalizeDelegate,
): (S extends EntityMap)[];
queryKey(
@@ -980,8 +1130,7 @@ declare class All<
denormalize(
input: {},
- args: readonly any[],
- unvisit: (schema: any, input: any) => any,
+ delegate: IDenormalizeDelegate,
): (S extends EntityMap)[];
queryKey(
@@ -1019,11 +1168,7 @@ declare class Object$1<
_denormalizeNullable(): DenormalizeNullableObject, ...args: any) => any> = Query;
declare const schema_d_Query: typeof Query;
+type schema_d_Scalar = Scalar;
+declare const schema_d_Scalar: typeof Scalar;
type schema_d_SchemaAttributeFunction = SchemaAttributeFunction;
type schema_d_SchemaClass implements SchemaSimple {
*/
constructor(schema: S);
normalize(input: any, parent: any, key: any, args: any[], visit: (...args: any) => any, _delegate: any): any;
- denormalize(input: {}, _args: readonly any[], _unvisit: any): any;
+ denormalize(input: {}, _delegate: IDenormalizeDelegate): any;
queryKey(_args: readonly any[], _unvisit: (...args: any) => any, _delegate: any): undefined;
/** Queryable schema for use with useQuery() to resolve lazy relationships */
get query(): LazyQuery;
private _query;
- _denormalizeNullable: (input: {}, args: readonly any[], unvisit: (schema: any, input: any) => any) => any;
+ _denormalizeNullable: (input: {}, delegate: IDenormalizeDelegate) => any;
_normalizeNullable: () => NormalizeNullable;
}
/**
@@ -693,12 +733,12 @@ declare class Lazy implements SchemaSimple {
declare class LazyQuery> {
schema: S;
constructor(schema: S);
- denormalize(input: {}, args: readonly any[], unvisit: (schema: any, input: any) => any): Denormalize;
+ denormalize(input: {}, delegate: IDenormalizeDelegate): Denormalize;
queryKey(args: Args, unvisit: (...args: any) => any, delegate: {
getEntity: any;
getIndex: any;
}): any;
- _denormalizeNullable: (input: {}, args: readonly any[], unvisit: (schema: any, input: any) => any) => DenormalizeNullable;
+ _denormalizeNullable: (input: {}, delegate: IDenormalizeDelegate) => DenormalizeNullable;
}
/**
@@ -718,15 +758,126 @@ declare class Query;
+ denormalize(input: {}, delegate: IDenormalizeDelegate): ReturnType;
}
type ProcessParameters : Par & SchemaArgs : SchemaArgs;
+interface ScalarOptions {
+ /**
+ * Selects the lens value from Endpoint args.
+ *
+ * The returned value is part of the stored cell key, so it must be stable
+ * for a given lens selection.
+ */
+ lens: (args: readonly any[]) => string | undefined;
+ /**
+ * Unique namespace for this Scalar's internal entity table.
+ */
+ key: string;
+ /**
+ * Entity class this Scalar stores cells for.
+ *
+ * Optional when used as a field on `Entity.schema`, where the parent Entity
+ * is inferred. Required for standalone usage such as `schema.Values`.
+ */
+ entity?: {
+ key: string;
+ pk?: (...args: any[]) => string | number | undefined;
+ };
+}
+/**
+ * Represents lens-dependent scalar fields on entities.
+ *
+ * Scalar stores values that belong to an Entity but vary by Endpoint args,
+ * such as portfolio-, currency-, or locale-specific columns. Use it as an
+ * `Entity.schema` field, or bind `entity` when using it standalone in
+ * `schema.Values`.
+ *
+ * @see https://dataclient.io/rest/api/Scalar
+ */
+declare class Scalar implements Mergeable {
+ readonly key: string;
+ readonly lensSelector: (args: readonly any[]) => string | undefined;
+ readonly entity: ScalarOptions['entity'];
+ readonly entityKey: string | undefined;
+ /**
+ * Allow normalize to receive primitive field values.
+ *
+ * Scalar stores per-cell values like `0.5`, so the visit walker must not
+ * apply its primitive short-circuit before dispatching to `normalize()`.
+ */
+ readonly acceptsPrimitives = true;
+ /**
+ * Represents lens-dependent scalar fields on entities.
+ *
+ * @see https://dataclient.io/rest/api/Scalar
+ */
+ constructor(options: ScalarOptions);
+ /**
+ * 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(...)`.
+ *
+ * @see https://dataclient.io/rest/api/Scalar#entityPk
+ * @param [input] the scalar cell input
+ * @param [parent] When normalizing, the object which included the cell
+ * @param [key] When normalizing, the surrounding map key (if any)
+ * @param [args] ...args sent to Endpoint
+ */
+ entityPk(input: any, parent: any, key: string | undefined, args: readonly any[]): string | number | undefined;
+ createIfValid(props: any): any;
+ merge(existing: any, incoming: any): any;
+ /**
+ * Determines whether an incoming write is older than the stored cell.
+ *
+ * Defaults to comparing `fetchedAt`, matching Entity behavior so older
+ * responses do not overwrite newer values.
+ */
+ shouldReorder(existingMeta: {
+ date: number;
+ fetchedAt: number;
+ }, incomingMeta: {
+ date: number;
+ fetchedAt: number;
+ }, existing: any, incoming: any): boolean;
+ mergeWithStore(existingMeta: {
+ date: number;
+ fetchedAt: number;
+ }, incomingMeta: {
+ date: number;
+ fetchedAt: number;
+ }, existing: any, incoming: any): any;
+ mergeMetaWithStore(existingMeta: {
+ fetchedAt: number;
+ date: number;
+ expiresAt: number;
+ }, incomingMeta: {
+ fetchedAt: number;
+ date: number;
+ expiresAt: number;
+ }, existing: any, incoming: any): {
+ fetchedAt: number;
+ date: number;
+ expiresAt: number;
+ };
+ normalize(input: any, parent: any, key: any, args: any[], visit: Visit, delegate: INormalizeDelegate, parentEntity: any): any;
+ denormalize(input: any, delegate: IDenormalizeDelegate): any;
+ /**
+ * Returns the cpks of cells matching the current lens, or undefined.
+ *
+ * Only consulted when `Scalar` is an endpoint's top-level schema; field
+ * usage resolves through the parent entity. Relies on `lens` not
+ * containing the cpk delimiter `|`.
+ */
+ queryKey(args: readonly any[], unvisit: any, delegate: IQueryDelegate): string[] | undefined;
+}
+
type CollectionOptions implements SchemaClass {
denormalize(
input: {},
- args: readonly any[],
- unvisit: (schema: any, input: any) => any,
+ delegate: IDenormalizeDelegate,
): (S extends EntityMap)[];
queryKey(
@@ -984,8 +1134,7 @@ declare class All<
denormalize(
input: {},
- args: readonly any[],
- unvisit: (schema: any, input: any) => any,
+ delegate: IDenormalizeDelegate,
): (S extends EntityMap)[];
queryKey(
@@ -1023,11 +1172,7 @@ declare class Object$1<
_denormalizeNullable(): DenormalizeNullableObject, ...args: any) => any> = Query;
declare const schema_d_Query: typeof Query;
+type schema_d_Scalar = Scalar;
+declare const schema_d_Scalar: typeof Scalar;
type schema_d_SchemaAttributeFunction = SchemaAttributeFunction;
type schema_d_SchemaClass