@johnhenry/graceholder 0.0.0-stage → 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +492 -2
  3. package/STABILITY.md +24 -0
  4. package/package.json +126 -4
  5. package/src/adapters/apollo.d.ts +13 -0
  6. package/src/adapters/apollo.js +48 -0
  7. package/src/adapters/fetch.d.ts +15 -0
  8. package/src/adapters/fetch.js +150 -0
  9. package/src/adapters/shared.d.ts +14 -0
  10. package/src/adapters/shared.js +65 -0
  11. package/src/adapters/urql.d.ts +8 -0
  12. package/src/adapters/urql.js +53 -0
  13. package/src/anchor.js +13 -0
  14. package/src/args.js +41 -0
  15. package/src/bulk/csv.js +118 -0
  16. package/src/bulk/ndjson.js +10 -0
  17. package/src/bulk/plan.js +195 -0
  18. package/src/bulk/shape.js +67 -0
  19. package/src/bulk/source.js +24 -0
  20. package/src/bulk/sql.js +366 -0
  21. package/src/bulk/stream.js +55 -0
  22. package/src/cursor.js +25 -0
  23. package/src/dbg.d.ts +720 -0
  24. package/src/detmath.js +74 -0
  25. package/src/directives.js +113 -0
  26. package/src/dist.js +90 -0
  27. package/src/errors.js +17 -0
  28. package/src/execute.js +577 -0
  29. package/src/explain.js +473 -0
  30. package/src/fast-check.d.ts +31 -0
  31. package/src/fast-check.js +58 -0
  32. package/src/generate.js +84 -0
  33. package/src/generator-types.d.ts +90 -0
  34. package/src/generators.js +361 -0
  35. package/src/image.js +55 -0
  36. package/src/index.d.ts +507 -0
  37. package/src/index.js +62 -0
  38. package/src/infer.js +888 -0
  39. package/src/key.js +30 -0
  40. package/src/learn.js +397 -0
  41. package/src/naming.js +74 -0
  42. package/src/pack-registry.js +59 -0
  43. package/src/packs/PROVENANCE.md +26 -0
  44. package/src/packs/commerce.d.ts +4 -0
  45. package/src/packs/commerce.js +83 -0
  46. package/src/packs/finance.d.ts +4 -0
  47. package/src/packs/finance.js +180 -0
  48. package/src/packs/geo.d.ts +4 -0
  49. package/src/packs/geo.js +227 -0
  50. package/src/packs/health.d.ts +4 -0
  51. package/src/packs/health.js +76 -0
  52. package/src/packs/index.d.ts +7 -0
  53. package/src/packs/index.js +9 -0
  54. package/src/rng.js +89 -0
  55. package/src/rules.js +228 -0
  56. package/src/scenario.js +120 -0
  57. package/src/schema.js +148 -0
  58. package/src/spec.js +235 -0
  59. package/src/suggest.js +138 -0
  60. package/src/typed.d.ts +723 -0
  61. package/src/vocab/PROVENANCE.md +12 -0
  62. package/src/vocab/en.js +106 -0
  63. package/src/world.js +461 -0
  64. package/src/xxh64.js +87 -0
package/src/execute.js ADDED
@@ -0,0 +1,577 @@
1
+ // Execution: graphql-js `execute` over the inferred schema with keyed resolvers
2
+ // (docs/design.md section 8; key rules in sections 2 and 3).
3
+ import { buildSchema, execute, validate, isNonNullType } from "graphql";
4
+ import { GraceholderError } from "./errors.js";
5
+ import { keyStream, KEY_SCHEME } from "./key.js";
6
+ import { xxh64 } from "./xxh64.js";
7
+ import { generate, asciiFold } from "./generators.js";
8
+ import { encodeCursor, decodeCursor } from "./cursor.js";
9
+ import { argsKeyOf, entityKeyFromArgs } from "./args.js";
10
+ import { classifyScalar, isSeriesType } from "./rules.js";
11
+ import { tokenize, typeNameFor } from "./naming.js";
12
+ import { ScenarioError, hugeSize, HUGE_TOTAL, isTextGen, unicodeValue, boundaryValue, DEFAULT_ERROR_RATE } from "./scenario.js";
13
+
14
+ export const POOL_SIZE = 100;
15
+ export const ENDLESS_TOTAL = 1000;
16
+ const MAX_WINDOW = 10000;
17
+ const encoder = new TextEncoder();
18
+
19
+ const bad = (msg, path) => new GraceholderError("bad-arguments", msg, { path });
20
+
21
+ function nonNegInt(args, name, min = 0) {
22
+ const v = args[name];
23
+ if (v === undefined || v === null) return undefined;
24
+ if (!Number.isInteger(v) || v < min) throw bad(`argument ${name} must be an integer >= ${min}, got ${JSON.stringify(v)}`);
25
+ return v;
26
+ }
27
+
28
+ /**
29
+ * Window selection from paging arguments (docs/design.md 3.4).
30
+ * `declared` means the collection has a declared total `n` (@count or spec), so it has a real end.
31
+ * `defaultSize` is the page size when no size argument is given (default: `n`).
32
+ * @param {Record<string, any>} args
33
+ * @param {number} n
34
+ * @param {{ connection?: any, declared?: boolean, endlessTotal?: number, defaultSize?: number }} opts
35
+ * @returns {{ start: number, count: number, total: number|null, endless: boolean }}
36
+ * `start` is 1-based; `total` is the finite size of the collection, if it has one.
37
+ */
38
+ export function windowOf(args, n, { connection, declared = false, endlessTotal = ENDLESS_TOTAL, defaultSize }) {
39
+ const dflt = defaultSize ?? n; // the page size when none is given
40
+ const has = (k) => args[k] !== undefined && args[k] !== null;
41
+ const cap = (count) => {
42
+ if (count > MAX_WINDOW) throw bad(`a window of ${count} items exceeds the limit of ${MAX_WINDOW}`);
43
+ return count;
44
+ };
45
+ const finish = (w) => {
46
+ if (!declared) return w;
47
+ const end = Math.min(w.start + w.count - 1, n);
48
+ return { start: w.start, count: Math.max(0, end - w.start + 1), total: n, endless: false };
49
+ };
50
+ const paged = ["first", "last", "after", "before", "offset", "limit", "skip", "take", "page", "pageSize", "perPage"].some(has);
51
+ const total = connection ? endlessTotal : null;
52
+ if (!paged) return finish({ start: 1, count: connection ? dflt : n, total: connection ? total : n, endless: Boolean(connection) });
53
+
54
+ // Exactly one size argument, one offset argument, and one style (page, offset or relay).
55
+ const sizeNames = ["first", "limit", "take", "pageSize", "perPage"].filter(has);
56
+ const offsetNames = ["offset", "skip"].filter(has);
57
+ const conflict = (why) => bad(`conflicting paging arguments (${why}): ${["first", "last", "after", "before", "offset", "skip", "limit", "take", "page", "pageSize", "perPage"].filter(has).join(", ")}`);
58
+ if (sizeNames.length > 1) throw conflict("one size argument only");
59
+ if (offsetNames.length > 1) throw conflict("one of offset or skip");
60
+ const pageStyle = has("page") || has("pageSize") || has("perPage");
61
+ if (pageStyle) {
62
+ if (!has("page")) throw conflict("pageSize/perPage need page");
63
+ if (offsetNames.length || has("last") || has("after") || has("before") || ["first", "limit", "take"].some(has)) throw conflict("page cannot be combined with other styles");
64
+ } else if (offsetNames.length) {
65
+ if (has("last") || has("after") || has("before")) throw conflict("offset/skip cannot be combined with cursors");
66
+ } else if (has("last") && sizeNames.length) {
67
+ throw conflict("last cannot be combined with a size");
68
+ } else if (!has("last") && (has("limit") || has("take")) && (has("after") || has("before"))) {
69
+ throw conflict("limit/take cannot be combined with cursors; use first");
70
+ }
71
+ const size = sizeNames.length ? nonNegInt(args, sizeNames[0]) : undefined;
72
+
73
+ if (pageStyle) {
74
+ const page = nonNegInt(args, "page", 1);
75
+ const count = size ?? dflt;
76
+ return finish({ start: (page - 1) * count + 1, count: cap(count), total, endless: true });
77
+ }
78
+ if (offsetNames.length || ((has("limit") || has("take")) && !has("last"))) {
79
+ const off = offsetNames.length ? nonNegInt(args, offsetNames[0]) : 0;
80
+ return finish({ start: off + 1, count: cap(size ?? dflt), total, endless: true });
81
+ }
82
+ const first = size;
83
+ const last = nonNegInt(args, "last");
84
+ const after = has("after") ? decodeCursor(args.after) : undefined;
85
+ const before = has("before") ? decodeCursor(args.before) : undefined;
86
+ if (first === undefined && last !== undefined) {
87
+ if (before !== undefined) {
88
+ const end = before - 1;
89
+ const start = Math.max(1, end - last + 1);
90
+ return finish({ start, count: cap(Math.max(0, end - start + 1)), total, endless: true });
91
+ }
92
+ // the last items of a finite collection
93
+ const start = Math.max(1, n - last + 1);
94
+ return { start, count: cap(n - start + 1), total: n, endless: false };
95
+ }
96
+ const start = (after ?? 0) + 1;
97
+ let count = first ?? dflt;
98
+ if (before !== undefined) count = Math.min(count, Math.max(0, before - start));
99
+ return finish({ start, count: cap(count), total, endless: true });
100
+ }
101
+
102
+ const NO_VALUE = Object.freeze({ has: false, value: undefined });
103
+ const PAGING_KEYS = ["first", "last", "after", "before", "offset", "limit", "skip", "take", "page", "pageSize", "perPage"];
104
+ const CREATED_FIELDS = new Set(["createdAt", "created_at", "createdOn", "created"]);
105
+ const UPDATED_FIELDS = new Set(["updatedAt", "updated_at", "updatedOn", "modifiedAt", "modified_at", "updated"]);
106
+
107
+ const slugify = (v) => String(v).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
108
+
109
+ /**
110
+ * @param {{ sdl: string, document: any, model: Map<string, Map<string, any>>, abstracts: Map<string, any>, opSeed?: string, schema?: import("graphql").GraphQLSchema, rootType?: string }} inferred
111
+ * @param {{ seed: string|number, anchorMs: number, locale?: string, variables?: Record<string, unknown>, poolSize?: number, endlessTotal?: number, errorRate?: number, world?: any, trace?: (path: any, value: any) => void, images?: "url"|"svg", packs?: any[], realistic?: boolean, probe?: { target: Array<string|number>, events: any[], notes: any[] } }} opts
112
+ * @returns {import("graphql").ExecutionResult}
113
+ */
114
+ export function run(inferred, opts) {
115
+ const schema = inferred.schema ?? buildSchema(inferred.sdl);
116
+ const invalid = validate(schema, inferred.document);
117
+ if (invalid.length) return { errors: invalid };
118
+ const packs = opts.packs ?? [];
119
+ const realistic = Boolean(opts.realistic);
120
+ /** geo-style types (`address`, `location`) share their parent's persona so the parent's and the address's city agree */
121
+ const inherits = (typeName) => packs.some((p) => p.inherit?.(typeName));
122
+ const ROOT = inferred.rootType ?? "Query";
123
+ const poolSize = opts.poolSize ?? POOL_SIZE;
124
+ const endlessTotal = opts.endlessTotal ?? ENDLESS_TOTAL;
125
+ const rootSeed = inferred.opSeed ?? opts.seed;
126
+ const ws = opts.world;
127
+ const anchorIso = new Date(opts.anchorMs).toISOString();
128
+ const errorRate = opts.errorRate ?? DEFAULT_ERROR_RATE;
129
+ /** how many lists enclose the field being resolved (resolvers run one at a time, so this is safe to share) */
130
+ let curDepth = 0;
131
+ // explain() (src/explain.js) asks for a trace of how one value came about. `probe` is absent in normal runs,
132
+ // and every `note` call below only records: it never changes a value, a key or the order of any draw.
133
+ const probe = opts.probe;
134
+ /** @type {any} */
135
+ let note = null;
136
+ let noteDepth = 0;
137
+
138
+ const K = (seed, scope, typeName, entityKey, fieldName, subKeys) => ({ seed, scope, typeName, entityKey, fieldName, subKeys });
139
+ const sub = (argsKey) => (argsKey ? [argsKey] : undefined);
140
+ const fieldOf = (type, name) => inferred.model.get(type)?.get(name);
141
+ /** The keys of a graphql-js response path, outermost first. */
142
+ const pathKeys = (path) => {
143
+ const out = [];
144
+ for (let p = path; p; p = p.prev) out.unshift(p.key);
145
+ return out;
146
+ };
147
+ /** The part of a field's model entry that explain() reports. */
148
+ const probeField = (f) => ({
149
+ kind: f.kind, list: Boolean(f.list), connection: Boolean(f.connection), typeName: f.typeName, sdl: f.sdl, gen: f.gen, genArgs: f.genArgs, nullRate: f.nullRate,
150
+ count: f.count, scenario: f.scenario, own: f.own, shared: f.shared, keyArgs: f.keyArgs, special: f.special, abstract: Boolean(f.abstract), derive: f.derive, seed: f.seed, nullable: f.nullable, name: f.name,
151
+ });
152
+ /** Are the members of a collection shared (drawn from the pool of their type) or owned by their parent? */
153
+ const sharedMembers = (f, parent) => (f.own ? false : f.shared ?? parent === ROOT);
154
+
155
+ /** Collection size: a declared @count/spec count, else a keyed 3 to 10. */
156
+ const count = (seed, parent, entityKey, fieldName, argsKey) => {
157
+ const declared = fieldOf(parent, fieldName)?.count;
158
+ const sc = fieldOf(parent, fieldName)?.scenario;
159
+ if (sc?.includes("empty")) return 0;
160
+ if (sc?.includes("huge")) return hugeSize(curDepth);
161
+ const stream = keyStream(K(seed, "l", parent, entityKey, fieldName, sub(argsKey)));
162
+ if (declared?.n !== undefined) return declared.n;
163
+ if (declared) return stream.int(declared.min, declared.max);
164
+ // a time series reads better with some history
165
+ return realistic && fieldOf(parent, fieldName)?.kind === "object" && isSeriesType(fieldOf(parent, fieldName).typeName) ? stream.int(14, 60) : stream.int(3, 10);
166
+ };
167
+
168
+ /** Generate one scalar; `personaKey` is the entity whose persona slots the generator reads. */
169
+ const scalar = (seed, f, parent, entityKey, fieldName, subKeys, personaKey = entityKey, extra = {}) => {
170
+ if (fieldName === "qty") {
171
+ fieldName = "quantity"; // one latent quantity per item, whichever name is selected
172
+ note?.({ step: "latent-quantity", depth: noteDepth });
173
+ }
174
+ if (ws) {
175
+ const o = worldValue(parent, entityKey, fieldName);
176
+ if (o.has) {
177
+ note?.({ step: "world-value", depth: noteDepth, type: parent, key: entityKey, field: fieldName, value: o.value, kind: o.kind });
178
+ return o.value;
179
+ }
180
+ }
181
+ if (f.derive !== undefined) {
182
+ note?.({ step: "derive", depth: noteDepth, from: f.derive, gen: f.gen, type: parent, key: entityKey });
183
+ return derived(seed, f, parent, entityKey, fieldName, subKeys, personaKey, extra);
184
+ }
185
+ if (f.gen === "count.of") {
186
+ const size = siblingView(seed, parent, entityKey, f.genArgs.of, fieldOf(parent, f.genArgs.of)).size;
187
+ note?.({ step: "count.of", depth: noteDepth, of: f.genArgs.of, size });
188
+ return size;
189
+ }
190
+ if (f.gen.startsWith("order.")) {
191
+ note?.({ step: "order", depth: noteDepth, gen: f.gen });
192
+ return orderMoney(seed, f.gen, parent, entityKey);
193
+ }
194
+ const ctx = genCtx(seed, f, parent, entityKey, fieldName, subKeys, personaKey, extra);
195
+ const out = generate(f.gen, ctx);
196
+ note?.({ step: "generate", depth: noteDepth, gen: f.gen, args: f.genArgs, key: K(seed, "e", parent, entityKey, fieldName, subKeys), personaKey, personaType: extra.personaType ?? parent, pinned: ctx.pinned });
197
+ return out;
198
+ };
199
+
200
+ /** A pinned or mutated value of one field of one entity (docs/design.md section 16, World). */
201
+ const worldValue = (parent, entityKey, fieldName) => {
202
+ const rec = ws.entity(parent, entityKey);
203
+ if (!rec) return NO_VALUE;
204
+ const name = fieldName === "quantity" || fieldName === "qty" ? "quantity" : fieldName;
205
+ if (Object.hasOwn(rec.fields, name)) return { has: true, value: rec.fields[name], kind: "pin" };
206
+ if (rec.created && CREATED_FIELDS.has(name)) return { has: true, value: anchorIso, kind: "created" };
207
+ if ((rec.created || rec.updated) && UPDATED_FIELDS.has(name)) return { has: true, value: anchorIso, kind: "updated" };
208
+ return NO_VALUE;
209
+ };
210
+
211
+ /** Pinned first/last names of a persona, from a pinned first/last/full-name field. */
212
+ const personaCache = new Map();
213
+ const personaPins = (parent, entityKey) => {
214
+ const ck = `${parent}\u0000${entityKey}`;
215
+ if (personaCache.has(ck)) return personaCache.get(ck);
216
+ let out;
217
+ const rec = ws.entity(parent, entityKey);
218
+ if (rec) {
219
+ let first;
220
+ let last;
221
+ let full;
222
+ for (const [name, value] of Object.entries(rec.fields)) {
223
+ if (typeof value !== "string" || value.trim() === "") continue;
224
+ const gen = fieldOrDefault(parent, name).gen;
225
+ if (gen === "person.first") first = value.trim();
226
+ else if (gen === "person.last") last = value.trim();
227
+ else if (gen === "person.full") full = value.trim();
228
+ }
229
+ if (full !== undefined) {
230
+ const parts = full.split(/\s+/);
231
+ first ??= parts[0];
232
+ if (parts.length > 1) last ??= parts[parts.length - 1];
233
+ }
234
+ if (first !== undefined || last !== undefined) out = { first, last };
235
+ }
236
+ personaCache.set(ck, out);
237
+ return out;
238
+ };
239
+
240
+ const genCtx = (seed, f, parent, entityKey, fieldName, subKeys, personaKey, extra = {}) => ({
241
+ pinned: ws ? personaPins(extra.personaType ?? parent, personaKey) : undefined,
242
+ stream: keyStream(K(seed, "e", parent, entityKey, fieldName, subKeys)),
243
+ slot: note
244
+ ? (name) => {
245
+ const key = K(seed, "p", extra.personaType ?? parent, personaKey, name);
246
+ note({ step: "slot", name, key });
247
+ return keyStream(key);
248
+ }
249
+ : (name) => keyStream(K(seed, "p", extra.personaType ?? parent, personaKey, name)),
250
+ series: extra.series && { i: extra.series.i, n: extra.series.n, slot: (name) => keyStream(K(seed, "p", parent, extra.series.key, name)) },
251
+ images: opts.images,
252
+ packs,
253
+ anchorMs: opts.anchorMs,
254
+ args: f.genArgs,
255
+ typeName: parent,
256
+ entityKey,
257
+ fieldName,
258
+ locale: opts.locale,
259
+ });
260
+
261
+ const fieldOrDefault = (type, name, alt) => {
262
+ const fields = inferred.model.get(type);
263
+ for (const n of [name, alt]) if (n && fields?.has(n)) return fields.get(n);
264
+ const c = classifyScalar(name, { typeName: type, list: false, packs, realistic });
265
+ return { kind: "scalar", list: false, gen: c.gen, genArgs: c.args };
266
+ };
267
+
268
+ /** @derive: the source field's value of the same entity, shaped by this field's own generator kind. */
269
+ const derived = (seed, f, parent, entityKey, fieldName, subKeys, personaKey, extra) => {
270
+ const srcInfo = fieldOrDefault(parent, f.derive);
271
+ noteDepth++;
272
+ const value = scalar(seed, srcInfo, parent, entityKey, f.derive, undefined, personaKey, extra);
273
+ noteDepth--;
274
+ if (value === null || value === undefined) return value;
275
+ if (f.gen === "slug") return slugify(value);
276
+ if (f.gen === "person.username") return asciiFold(String(value));
277
+ if (f.gen === "person.email") {
278
+ const domain = String(generate("person.email", genCtx(seed, f, parent, entityKey, fieldName, subKeys, personaKey, extra))).split("@")[1];
279
+ return `${asciiFold(String(value))}@${domain}`;
280
+ }
281
+ return value;
282
+ };
283
+
284
+ /** subtotal = sum(items price * latent quantity) over the whole collection; total = subtotal + tax (integer cents). */
285
+ const orderMoney = (seed, gen, parent, entityKey) => {
286
+ const itemsField = [...(inferred.model.get(parent)?.values() ?? [])].find(
287
+ (x) => x.kind === "object" && x.list && !x.synthetic && ["items", "lines"].includes(tokenize(x.name).at(-1)),
288
+ );
289
+ // without a selected items field, assume the conventional `items` collection of `Item`
290
+ const itemsName = itemsField?.name ?? "items";
291
+ const itemType = itemsField?.typeName ?? "Item";
292
+ const view = siblingView(seed, parent, entityKey, itemsName, itemsField);
293
+ const n = view.size;
294
+ const keyOf = view.keyAt;
295
+ const price = fieldOrDefault(itemType, "price");
296
+ // every item has a latent quantity keyed by the item (the same value whether `quantity`, `qty`
297
+ // or nothing is selected), so aggregates never depend on the selection
298
+ const qty = fieldOrDefault(itemType, "quantity", "qty");
299
+ let subCents = 0;
300
+ const outer = note;
301
+ note = null; // the items are an input, not the subject: only a summary is noted
302
+ for (let i = 1; i <= n; i++) {
303
+ const member = keyOf(i);
304
+ subCents += Math.round(scalar(seed, price, itemType, member, "price") * 100) * scalar(seed, qty, itemType, member, "quantity");
305
+ }
306
+ note = outer;
307
+ const taxPercent = keyStream(K(seed, "p", parent, entityKey, "taxRate")).int(0, 12);
308
+ const taxCents = Math.round((subCents * taxPercent) / 100);
309
+ note?.({ step: "order-detail", depth: noteDepth, items: n, itemsField: itemsName, itemType, taxPercent, taxKey: K(seed, "p", parent, entityKey, "taxRate"), subtotalCents: subCents });
310
+ if (gen === "order.subtotal") return subCents / 100;
311
+ if (gen === "order.tax") return taxCents / 100;
312
+ return (subCents + taxCents) / 100;
313
+ };
314
+
315
+ /** Keys of the members of a collection: shared pool at the top level, owned elsewhere. */
316
+ const memberKeyFn = (parent, entityKey, fieldName, argsKey, f) => {
317
+ const prefix = argsKey ? `f${xxh64(encoder.encode(`${KEY_SCHEME}\x1f${argsKey}`)).toString(16).padStart(16, "0").slice(0, 8)}.` : "";
318
+ const shared = f.own ? false : (f.shared ?? parent === ROOT);
319
+ return shared ? (i) => `${prefix}${i}` : (i) => `${entityKey}.${fieldName}.${prefix}${i}`;
320
+ };
321
+
322
+ /**
323
+ * The effective members of a collection once the world has created or deleted entities of its
324
+ * item type (docs/design.md section 16, World): base members minus deleted ones, created ones added at the
325
+ * end (finite collections) or the start (endless ones). null when the world does not touch it.
326
+ * `baseSize` is the size of the base sequence.
327
+ */
328
+ const overlay = (f, parent, entityKey, argsKey, memberKey, baseSize, endless) => {
329
+ if (!ws || !f || f.kind !== "object" || f.abstract) return null;
330
+ const itemType = f.connection ? f.connection.item : f.typeName;
331
+ if (!ws.touches(itemType)) return null;
332
+ const shared = f.own ? false : (f.shared ?? parent === ROOT);
333
+ const prefix = memberKey(0).slice(0, -1);
334
+ const dead = [];
335
+ for (const key of ws.deletedKeys(itemType)) {
336
+ if (!key.startsWith(prefix)) continue;
337
+ const rest = key.slice(prefix.length);
338
+ if (/^[1-9][0-9]*$/.test(rest) && Number(rest) <= baseSize) dead.push(Number(rest));
339
+ }
340
+ dead.sort((a, b) => a - b);
341
+ const extra = argsKey ? [] : shared ? ws.createdKeys(itemType) : ws.relatedKeys(itemType, parent, entityKey);
342
+ const atStart = (ws.position ?? (endless ? "start" : "end")) === "start";
343
+ const added = atStart ? [...extra].reverse() : extra;
344
+ const live = baseSize - dead.length;
345
+ const baseAt = (i) => {
346
+ let j = i;
347
+ for (const d of dead) {
348
+ if (d <= j) j++;
349
+ else break;
350
+ }
351
+ return memberKey(j);
352
+ };
353
+ const keyAt = atStart
354
+ ? (i) => (i <= added.length ? added[i - 1] : baseAt(i - added.length))
355
+ : (i) => (i > live ? added[i - live - 1] : baseAt(i));
356
+ return { size: live + added.length, keyAt };
357
+ };
358
+
359
+ /** Unpaged, unfiltered view of the sibling collection `of` of an entity (counts, order items). */
360
+ const siblingView = (seed, parent, entityKey, name, info) => {
361
+ const n = count(seed, parent, entityKey, name, "");
362
+ const memberKey = memberKeyFn(parent, entityKey, name, "", info ?? {});
363
+ const f = info ?? { kind: "object", list: true, typeName: typeNameFor(name) };
364
+ if (n === 0 && info?.scenario?.includes("empty")) return { size: 0, keyAt: memberKey };
365
+ const ov = overlay(f, parent, entityKey, "", memberKey, n, false);
366
+ return ov ?? { size: n, keyAt: memberKey };
367
+ };
368
+
369
+ /** Resolve a (possibly paged) collection field to its window of member keys. */
370
+ const collection = (seed, f, parent, entityKey, fieldName, args) => {
371
+ if (f.scenario?.includes("empty")) return { start: 1, count: 0, total: 0, endless: false, memberKey: () => "" };
372
+ const endlessTotal = f.scenario?.includes("huge") ? HUGE_TOTAL : opts.endlessTotal ?? ENDLESS_TOTAL;
373
+ const idsArg = Array.isArray(args.ids) ? args.ids : undefined;
374
+ if (idsArg && !f.connection) {
375
+ const keys = idsArg.map((v) => (typeof v === "string" ? v : JSON.stringify(v)));
376
+ return { start: 1, count: keys.length, total: keys.length, endless: false, memberKey: (i) => keys[i - 1] };
377
+ }
378
+ const argsKey = argsKeyOf(args);
379
+ const n = count(seed, parent, entityKey, fieldName, argsKey);
380
+ const memberKey = memberKeyFn(parent, entityKey, fieldName, argsKey, f);
381
+ if (ws) {
382
+ // with a world that touches the item type the collection gets a real end: its base size
383
+ // (n, or the endless total for paged lists and connections) plus the created, minus the deleted
384
+ const declared = Boolean(f.count);
385
+ const paged = PAGING_KEYS.some((k) => args[k] !== undefined && args[k] !== null);
386
+ const endless = !declared && (Boolean(f.connection) || paged);
387
+ const ov = overlay(f, parent, entityKey, argsKey, memberKey, endless ? endlessTotal : n, endless);
388
+ if (ov) {
389
+ const win = windowOf(args, ov.size, { connection: Boolean(f.connection), declared: true, endlessTotal, defaultSize: declared ? ov.size : n });
390
+ return { ...win, memberKey: ov.keyAt };
391
+ }
392
+ }
393
+ const win = windowOf(args, n, { connection: Boolean(f.connection), declared: Boolean(f.count), endlessTotal });
394
+ return { ...win, memberKey };
395
+ };
396
+
397
+ /** The unicode and boundary scenarios: swap a generated scalar for an edge-case value. */
398
+ const edge = (f, value, seed, parent, entityKey, fieldName, index) => {
399
+ const sc = f.scenario;
400
+ if (!sc || value === null || value === undefined || (!sc.includes("unicode") && !sc.includes("boundary"))) return value;
401
+ const text = f.sdl === "String" && isTextGen(f.gen);
402
+ const wantUnicode = sc.includes("unicode") && text;
403
+ const wantBoundary = sc.includes("boundary") && !f.gen?.startsWith("id.") && (f.sdl === "Int" || f.sdl === "Float" || f.sdl === "DateTime" || text);
404
+ if (!wantUnicode && !wantBoundary) return value;
405
+ const key = K(seed, "e", parent, entityKey, fieldName, ["~scenario", `edge${index}`]);
406
+ const stream = keyStream(key);
407
+ const unicode = wantUnicode && (!wantBoundary || stream.bool(0.5));
408
+ const out = unicode ? unicodeValue(stream) : boundaryValue(f.sdl, stream);
409
+ note?.({ step: "scenario-edge", mode: unicode ? "unicode" : "boundary", key, replaced: value });
410
+ return out;
411
+ };
412
+
413
+ const fieldResolver = (source, args, _ctx, info) => {
414
+ const parent = info.parentType.name;
415
+ const fieldName = info.fieldName;
416
+ const f = fieldOf(parent, fieldName);
417
+ if (!f) return undefined;
418
+ curDepth = 0;
419
+ for (let p = info.path; p; p = p.prev) if (typeof p.key === "number") curDepth++;
420
+ const entityKey = source.entityKey;
421
+ const seed = f.seed ?? source.seed ?? rootSeed;
422
+ // explain(): where is this field relative to the value being explained?
423
+ note = null;
424
+ let pr = 0; // 1 an ancestor of the target, 2 the target, 3 the target is an element of this scalar list
425
+ let probeIndex;
426
+ if (probe) {
427
+ const at = pathKeys(info.path);
428
+ const t = probe.target;
429
+ const same = (n) => at.slice(0, n).every((k, i) => String(k) === String(t[i]));
430
+ if (at.length === t.length && same(at.length)) pr = 2;
431
+ else if (at.length < t.length && same(at.length)) pr = 1;
432
+ if (pr === 1 && f.kind === "scalar" && f.list && at.length === t.length - 1) pr = 3;
433
+ if (pr) {
434
+ probeIndex = Number.isInteger(Number(t[at.length])) ? Number(t[at.length]) : undefined;
435
+ probe.events.push({ type: "field", rel: ["", "ancestor", "self", "element"][pr], path: at, parent, fieldName, entityKey, seed, rootSeed, persona: source.persona ?? null, f: probeField(f), args });
436
+ if (pr === 2) note = (e) => probe.notes.push(e);
437
+ // what an ancestor decided about itself (a scenario error, a nullRate hit, ...) explains why the target is missing
438
+ else if (pr === 1) note = (e) => probe.events.push({ type: "ancestor-note", path: at, ...e });
439
+ }
440
+ }
441
+ const mark = (e) => { if (pr && probe) probe.events.push(e); };
442
+ const ref = (typeName, key, extra) => ({ typeName, entityKey: key, seed, ...extra });
443
+ // a geo-style child shares the persona of the entity that owns it (see `inherits`)
444
+ const personaOf = source.persona ?? { type: parent, key: entityKey };
445
+ const personaFor = (typeName) => (inherits(typeName) ? { persona: personaOf } : undefined);
446
+
447
+ switch (f.special) {
448
+ case "edges": {
449
+ const c = source.conn;
450
+ mark({ type: "collection", style: "edges", start: c.start, count: c.count, total: c.total, endless: c.endless, childKey: probeIndex === undefined ? undefined : c.memberKey(c.start + probeIndex) });
451
+ return Array.from({ length: c.count }, (_, k) => ({ typeName: f.typeName, entityKey, seed, index: c.start + k, itemKey: c.memberKey(c.start + k) }));
452
+ }
453
+ case "nodes": {
454
+ const c = source.conn;
455
+ mark({ type: "collection", style: "nodes", start: c.start, count: c.count, total: c.total, endless: c.endless, childKey: probeIndex === undefined ? undefined : c.memberKey(c.start + probeIndex) });
456
+ return Array.from({ length: c.count }, (_, k) => ref(f.typeName, c.memberKey(c.start + k)));
457
+ }
458
+ case "pageInfo": return { typeName: f.typeName, entityKey, seed, conn: source.conn };
459
+ case "totalCount": return source.conn.total ?? endlessTotal;
460
+ case "cursor": return encodeCursor(source.index);
461
+ case "edgeNode":
462
+ mark({ type: "entity", mode: "connection-member", key: source.itemKey });
463
+ return ref(f.typeName, source.itemKey);
464
+ case "pageInfoField": {
465
+ const c = source.conn;
466
+ const end = c.start + c.count - 1;
467
+ if (fieldName === "hasNextPage") return c.endless || (c.total !== null && end < c.total);
468
+ if (fieldName === "hasPreviousPage") return c.start > 1;
469
+ if (c.count === 0) return null;
470
+ return encodeCursor(fieldName === "startCursor" ? c.start : end);
471
+ }
472
+ default:
473
+ }
474
+
475
+ const sc = f.scenario;
476
+ if (sc?.includes("errors") && f.name !== "id") {
477
+ const key = K(seed, "e", parent, entityKey, fieldName, ["~scenario", "error"]);
478
+ const stream = keyStream(key);
479
+ const draw = stream.float();
480
+ note?.({ step: "scenario-errors", key, draw, rate: errorRate, hit: draw < errorRate });
481
+ if (draw < errorRate) throw new ScenarioError(stream);
482
+ }
483
+ if (sc?.includes("nulls") && !isNonNullType(info.returnType)) {
484
+ note?.({ step: "scenario-nulls" });
485
+ return null;
486
+ }
487
+ if (ws && f.kind === "scalar") {
488
+ const o = worldValue(parent, entityKey, fieldName);
489
+ if (o.has) {
490
+ note?.({ step: "world-value", depth: 0, type: parent, key: entityKey, field: fieldName, value: o.value, kind: o.kind });
491
+ return o.value;
492
+ }
493
+ }
494
+ if (f.nullRate !== undefined) {
495
+ const key = K(seed, "e", parent, entityKey, fieldName, ["null"]);
496
+ const draw = keyStream(key).float();
497
+ note?.({ step: "nullRate", key, draw, rate: f.nullRate, hit: draw < f.nullRate });
498
+ if (draw < f.nullRate) return null;
499
+ }
500
+
501
+ const argsKey = argsKeyOf(args);
502
+ if (f.kind === "scalar") {
503
+ const own = source.persona ? { personaType: source.persona.type } : {};
504
+ if (!f.list) return edge(f, scalar(seed, f, parent, entityKey, fieldName, sub(argsKey), source.persona?.key ?? entityKey, { ...own, series: source.series && { ...source.series } }), seed, parent, entityKey, fieldName, "");
505
+ const n = count(seed, parent, entityKey, fieldName, argsKey);
506
+ mark({ type: "scalar-list", count: n, countKey: K(seed, "l", parent, entityKey, fieldName, sub(argsKey)), declared: f.count ?? null });
507
+ return Array.from({ length: n }, (_, i) => {
508
+ if (pr === 3 && i === probeIndex && probe) note = (e) => probe.notes.push(e);
509
+ try {
510
+ return edge(f, scalar(seed, f, parent, entityKey, fieldName, argsKey ? [argsKey, i] : [i], `${entityKey}.${fieldName}.${i + 1}`), seed, parent, entityKey, fieldName, i);
511
+ } finally {
512
+ if (pr === 3) note = null;
513
+ }
514
+ });
515
+ }
516
+ if (f.connection) {
517
+ const conn = collection(seed, f, parent, entityKey, fieldName, args);
518
+ mark({ type: "collection", style: "connection", start: conn.start, count: conn.count, total: conn.total, endless: conn.endless, declared: f.count ?? null, countKey: K(seed, "l", parent, entityKey, fieldName, sub(argsKey)), shared: sharedMembers(f, parent), argsKey });
519
+ return { typeName: f.typeName, entityKey: `${entityKey}.${fieldName}`, seed, conn };
520
+ }
521
+ if (f.list) {
522
+ const c = collection(seed, f, parent, entityKey, fieldName, args);
523
+ mark({ type: "collection", style: "list", start: c.start, count: c.count, total: c.total, endless: c.endless, declared: f.count ?? null, countKey: K(seed, "l", parent, entityKey, fieldName, sub(argsKey)), shared: sharedMembers(f, parent), argsKey, childKey: probeIndex === undefined ? undefined : c.memberKey(c.start + probeIndex), index: probeIndex });
524
+ if (realistic && isSeriesType(f.typeName)) {
525
+ // one series per owning entity, field and filter; each point knows its position and the series length
526
+ const n = count(seed, parent, entityKey, fieldName, argsKey);
527
+ const key = `${entityKey}.${fieldName}${argsKey ? `.${argsKey}` : ""}`;
528
+ return Array.from({ length: c.count }, (_, k) => ref(f.typeName, c.memberKey(c.start + k), { series: { i: c.start + k, n, key } }));
529
+ }
530
+ return Array.from({ length: c.count }, (_, k) => ref(f.typeName, c.memberKey(c.start + k)));
531
+ }
532
+ // singular: identifying arguments select the entity; top-level fields are entity 1;
533
+ // otherwise a stable reference into the pool of the type (E2), or an owned entity
534
+ const selected = entityKeyFromArgs(args, f.keyArgs);
535
+ const child = personaFor(f.typeName);
536
+ const k =
537
+ selected ??
538
+ (f.own || (child && parent !== ROOT)
539
+ ? `${entityKey}.${fieldName}`
540
+ : parent === ROOT ? "1" : String(keyStream(K(seed, "l", parent, entityKey, fieldName, sub(argsKey))).int(1, poolSize)));
541
+ if (pr) {
542
+ const mode = selected !== undefined ? "args" : f.own ? "owned" : child && parent !== ROOT ? "inherited" : parent === ROOT ? "root" : "pool";
543
+ mark({ type: "entity", mode, key: k, keyArgs: f.keyArgs ?? null, args, poolSize, poolKey: mode === "pool" ? K(seed, "l", parent, entityKey, fieldName, sub(argsKey)) : undefined, deleted: Boolean(ws && !f.abstract && ws.isDeleted(f.typeName, k)) });
544
+ }
545
+ if (ws && !f.abstract && ws.isDeleted(f.typeName, k)) return null;
546
+ return ref(f.typeName, k, selected === undefined && parent !== ROOT ? child : undefined);
547
+ };
548
+
549
+ const typeResolver = (value, _ctx, _info, abstractType) => {
550
+ const a = inferred.abstracts.get(abstractType.name);
551
+ return keyStream(K(value.seed ?? rootSeed, "e", abstractType.name, value.entityKey, "__typename")).pick(a.possible);
552
+ };
553
+
554
+ // bulk SQL needs the entity key of every object in the result, which the plain data does not carry
555
+ const tracer = opts.trace;
556
+ const resolve = tracer
557
+ ? (source, args, ctx, info) => {
558
+ const out = fieldResolver(source, args, ctx, info);
559
+ if (out !== null && typeof out === "object") tracer(info.path, out);
560
+ return out;
561
+ }
562
+ : fieldResolver;
563
+
564
+ /** @type {any} */
565
+ const result = execute({
566
+ schema,
567
+ document: inferred.document,
568
+ rootValue: { typeName: ROOT, entityKey: ROOT, seed: rootSeed },
569
+ variableValues: opts.variables,
570
+ fieldResolver: resolve,
571
+ typeResolver,
572
+ });
573
+ if (typeof result?.then === "function") {
574
+ throw new GraceholderError("internal", "graceholder resolvers must be synchronous");
575
+ }
576
+ return result;
577
+ }