@johnhenry/graceholder 0.0.0-stage → 0.1.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
@@ -0,0 +1,12 @@
1
+ # Vocabulary provenance
2
+
3
+ `en.js` is hand-written for this project. Names (including a few accented ones that exercise ASCII folding) are common given names and surnames of
4
+ mixed origin; none is a list copied from a third-party dataset. Places are real, generic
5
+ town names with state/province, ISO-style code and a three-character postal prefix; they
6
+ are not addresses. Lorem words are the classical public-domain ipsum list. Domains use the
7
+ reserved `example.*` names. Everything here is contributed under the repository's MIT
8
+ licence and is original or public domain.
9
+
10
+ The tables are order-frozen: values are selected by index, so any edit, reorder or
11
+ append is an output change (see docs/design.md sections 7.1 and 11). `test/vocab.test.js`
12
+ asserts a SHA-256 checksum of every table.
@@ -0,0 +1,106 @@
1
+ // Vendored "en" vocabulary. FROZEN at 1.0.0: tables are indexed by position, so
2
+ // any edit, reorder or append changes generated output (docs/design.md 7.1, 11).
3
+ // All content is original or public domain; see PROVENANCE.md.
4
+
5
+ const freeze = (o) => {
6
+ if (o !== null && typeof o === "object") for (const v of Object.values(o)) freeze(v);
7
+ return Object.freeze(o);
8
+ };
9
+
10
+ export const en = freeze({
11
+ firstNames: [
12
+ "Ada", "Alan", "Amara", "Andre", "Anika", "Arjun", "Beatrix", "Bruno", "Camila", "Carlos",
13
+ "Chiara", "Dana", "Darius", "Elena", "Emeka", "Farah", "Felix", "Fiona", "Gabriel", "Grace",
14
+ "Hana", "Hector", "Ines", "Isaac", "Jamal", "Jasmine", "Joaquin", "Julia", "Kai", "Keiko",
15
+ "Lena", "Leo", "Linnea", "Lucas", "Maya", "Mateo", "Mei", "Miles", "Nadia", "Noah",
16
+ "Olga", "Omar", "Priya", "Quentin", "Rafael", "Rosa", "Sana", "Samuel", "Tessa", "Theo",
17
+ "Uma", "Victor", "Wanda", "Wei", "Xavier", "Yara", "Yusuf", "Zara", "Zoe", "Ibrahim",
18
+ "Harriet", "Ngozi", "Tomas", "Sofia",
19
+ "Jos\u00e9", "Zo\u00eb", "S\u00f8ren", "Ren\u00e9e", "\u00c5sa", "Chlo\u00e9",
20
+ ],
21
+ lastNames: [
22
+ "Abbott", "Adeyemi", "Alvarez", "Andersson", "Bauer", "Bennett", "Brooks", "Castillo", "Chen", "Cohen",
23
+ "Dalton", "Dubois", "Eriksen", "Fischer", "Flores", "Garcia", "Gupta", "Hansen", "Haruna", "Hughes",
24
+ "Ibarra", "Jensen", "Kapoor", "Keller", "Kim", "Larsen", "Lopez", "Marsh", "Mbeki", "Moreau",
25
+ "Nakamura", "Novak", "Okafor", "Olsen", "Ortega", "Patel", "Petrov", "Quinn", "Ramos", "Reyes",
26
+ "Rossi", "Saito", "Santos", "Schmidt", "Silva", "Singh", "Tanaka", "Torres", "Underwood", "Vega",
27
+ "Wagner", "Whitfield", "Wright", "Xu", "Yamamoto", "Young", "Zhang", "Ziegler", "Hartley", "Lindqvist",
28
+ "Okonkwo", "Marchetti", "Delacroix", "Barnes",
29
+ "M\u00fcller", "\u00c7elik", "N\u00fa\u00f1ez", "\u00d8stergaard", "B\u00e9ranger", "\u0141ukasik",
30
+ ],
31
+ emailDomains: ["example.com", "example.org", "example.net", "mail.example.com", "inbox.example.org"],
32
+ tlds: ["com", "org", "net", "io", "dev"],
33
+ places: [
34
+ { city: "Springfield", state: "Illinois", code: "IL", country: "United States", zip: "627" },
35
+ { city: "Riverton", state: "Wyoming", code: "WY", country: "United States", zip: "825" },
36
+ { city: "Lakeside", state: "California", code: "CA", country: "United States", zip: "920" },
37
+ { city: "Fairview", state: "Oregon", code: "OR", country: "United States", zip: "970" },
38
+ { city: "Georgetown", state: "Texas", code: "TX", country: "United States", zip: "786" },
39
+ { city: "Clinton", state: "Iowa", code: "IA", country: "United States", zip: "527" },
40
+ { city: "Madison", state: "Wisconsin", code: "WI", country: "United States", zip: "537" },
41
+ { city: "Franklin", state: "Tennessee", code: "TN", country: "United States", zip: "370" },
42
+ { city: "Salem", state: "Massachusetts", code: "MA", country: "United States", zip: "019" },
43
+ { city: "Bristol", state: "Connecticut", code: "CT", country: "United States", zip: "060" },
44
+ { city: "Oxford", state: "Ohio", code: "OH", country: "United States", zip: "450" },
45
+ { city: "Ashland", state: "Kentucky", code: "KY", country: "United States", zip: "411" },
46
+ { city: "Kingston", state: "New York", code: "NY", country: "United States", zip: "124" },
47
+ { city: "Dover", state: "Delaware", code: "DE", country: "United States", zip: "199" },
48
+ { city: "Burlington", state: "Vermont", code: "VT", country: "United States", zip: "054" },
49
+ { city: "Milton", state: "Georgia", code: "GA", country: "United States", zip: "300" },
50
+ { city: "Greenville", state: "South Carolina", code: "SC", country: "United States", zip: "296" },
51
+ { city: "Auburn", state: "Alabama", code: "AL", country: "United States", zip: "368" },
52
+ { city: "Centerville", state: "Utah", code: "UT", country: "United States", zip: "840" },
53
+ { city: "Arlington", state: "Virginia", code: "VA", country: "United States", zip: "222" },
54
+ { city: "Hamilton", state: "Ontario", code: "ON", country: "Canada", zip: "L8P" },
55
+ { city: "Kelowna", state: "British Columbia", code: "BC", country: "Canada", zip: "V1Y" },
56
+ { city: "Brandon", state: "Manitoba", code: "MB", country: "Canada", zip: "R7A" },
57
+ { city: "Fredericton", state: "New Brunswick", code: "NB", country: "Canada", zip: "E3B" },
58
+ ],
59
+ streets: [
60
+ "Main", "Oak", "Maple", "Cedar", "Elm", "Pine", "Washington", "Lake", "Hill", "Park",
61
+ "Walnut", "Sunset", "Railroad", "Church", "Willow", "River", "Meadow", "Highland", "Spring", "Chestnut",
62
+ ],
63
+ streetSuffixes: ["St", "Ave", "Rd", "Ln", "Blvd", "Way", "Ct", "Dr"],
64
+ jobTitles: [
65
+ "Software Engineer", "Product Manager", "Data Analyst", "Designer", "Accountant", "Nurse", "Electrician",
66
+ "Teacher", "Marketing Lead", "Operations Manager", "Support Specialist", "Chef", "Architect", "Librarian",
67
+ "Mechanic", "Researcher", "Editor", "Pharmacist", "Logistics Coordinator", "Sales Representative",
68
+ ],
69
+ industries: [
70
+ "Software", "Healthcare", "Logistics", "Education", "Retail", "Manufacturing", "Finance", "Energy",
71
+ "Media", "Hospitality", "Agriculture", "Construction",
72
+ ],
73
+ companyHeads: [
74
+ "Harbor", "Summit", "Juniper", "Northwind", "Bluebird", "Ironwood", "Lantern", "Meridian", "Cobalt", "Willow",
75
+ "Granite", "Evergreen", "Beacon", "Foxglove", "Kestrel", "Pinnacle", "Redwood", "Silverline", "Tidewater", "Brightside",
76
+ ],
77
+ companySuffixes: ["Labs", "Industries", "Group", "Partners", "Systems", "Works", "Holdings", "Collective", "Co", "Studio"],
78
+ productAdjectives: [
79
+ "Compact", "Classic", "Deluxe", "Portable", "Rustic", "Smart", "Sturdy", "Sleek", "Organic", "Vintage",
80
+ "Ergonomic", "Everyday", "Premium", "Lightweight", "Handmade", "Modular",
81
+ ],
82
+ productNouns: [
83
+ "Lamp", "Backpack", "Kettle", "Notebook", "Chair", "Mug", "Speaker", "Blanket", "Planter", "Wallet",
84
+ "Bottle", "Clock", "Toolkit", "Candle", "Headphones", "Keyboard", "Umbrella", "Tote",
85
+ ],
86
+ words: [
87
+ "lorem", "ipsum", "dolor", "sit", "amet", "consectetur", "adipiscing", "elit", "sed", "do",
88
+ "eiusmod", "tempor", "incididunt", "ut", "labore", "et", "dolore", "magna", "aliqua", "enim",
89
+ "ad", "minim", "veniam", "quis", "nostrud", "exercitation", "ullamco", "laboris", "nisi", "aliquip",
90
+ "ex", "ea", "commodo", "consequat", "duis", "aute", "irure", "in", "reprehenderit", "voluptate",
91
+ "velit", "esse", "cillum", "eu", "fugiat", "nulla", "pariatur", "excepteur", "sint", "occaecat",
92
+ "cupidatat", "non", "proident", "sunt", "culpa", "qui", "officia", "deserunt", "mollit", "anim",
93
+ "id", "est", "laborum", "at", "vero", "eos", "accusamus", "iusto", "odio", "dignissimos",
94
+ "ducimus", "blanditiis", "praesentium", "voluptatum", "deleniti", "atque", "corrupti", "quos", "dolores", "quas",
95
+ "molestias", "excepturi", "occaecati", "cupiditate", "provident", "similique", "mollitia", "animi", "perspiciatis", "unde",
96
+ "omnis", "iste", "natus", "error", "voluptatem", "accusantium", "doloremque", "laudantium", "totam", "rem",
97
+ ],
98
+ status: ["draft", "pending", "active", "published", "archived", "closed", "open", "resolved"],
99
+ role: ["admin", "editor", "member", "viewer", "owner", "moderator", "guest"],
100
+ currency: ["USD", "EUR", "GBP", "JPY", "CAD", "AUD", "CHF", "SEK"],
101
+ locale: ["en-US", "en-GB", "fr-FR", "de-DE", "es-ES", "pt-BR", "ja-JP", "sv-SE"],
102
+ language: ["English", "French", "German", "Spanish", "Portuguese", "Japanese", "Swedish", "Italian"],
103
+ colors: ["red", "orange", "yellow", "green", "teal", "blue", "indigo", "violet", "pink", "brown", "black", "white", "gray", "silver"],
104
+ });
105
+
106
+ export const LOCALES = Object.freeze({ en });
package/src/world.js ADDED
@@ -0,0 +1,461 @@
1
+ // A shared, persistent world: pinned entities and deterministic mutations (docs/design.md section 16, World).
2
+ // State is an append-only log of effects; replaying the log rebuilds the identical state.
3
+ import { Kind, valueFromASTUntyped, visit } from "graphql";
4
+ import { GraceholderError } from "./errors.js";
5
+ import { toDocument } from "./infer.js";
6
+ import { compileSpec } from "./spec.js";
7
+ import { keyStream } from "./key.js";
8
+ import { tokenize, typeNameFor } from "./naming.js";
9
+ import { ID_ARGS, entityKeyFromArgs } from "./args.js";
10
+ import { streamWorld } from "./bulk/stream.js";
11
+ import { globalScenarios, errorRateOf } from "./scenario.js";
12
+ import { explainWith, parsePath } from "./explain.js";
13
+ import { anchorMs, checkLocale, generateResult, realismOptions, DEFAULT_ANCHOR, DEFAULT_SEED } from "./generate.js";
14
+
15
+ const DEFAULT_VERBS = {
16
+ create: "create", add: "create", insert: "create",
17
+ update: "update", edit: "update", set: "update",
18
+ delete: "delete", remove: "delete",
19
+ };
20
+ const INPUT_ARGS = ["input", "data", "patch"];
21
+ const RELATION = /^(.+?)(?:Id|_id)$/;
22
+
23
+ const isObject = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
24
+ const bad = (msg, extra) => new GraceholderError("bad-options", msg, extra);
25
+ const recKey = (type, key) => `${type}\u0000${key}`;
26
+ const lowerFirst = (s) => s[0].toLowerCase() + s.slice(1);
27
+
28
+ function parseRef(ref) {
29
+ const i = typeof ref === "string" ? ref.indexOf(":") : -1;
30
+ const type = i > 0 ? ref.slice(0, i) : "";
31
+ const key = i > 0 ? ref.slice(i + 1) : "";
32
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(type) || key === "") {
33
+ throw bad(`an entity reference looks like "User:42", got ${JSON.stringify(ref)}`);
34
+ }
35
+ return { type, key };
36
+ }
37
+
38
+ /** The state the executor reads (see execute.js `ws`), rebuilt from the log. */
39
+ class WorldState {
40
+ constructor() {
41
+ /** @type {Map<string, { fields: Record<string, any>, created: boolean, updated: boolean }>} "Type\0key" -> record */
42
+ this.recs = new Map();
43
+ /** @type {Map<string, Set<string>>} Type -> deleted keys */
44
+ this.deleted = new Map();
45
+ /** @type {Map<string, string[]>} Type -> keys in creation order (may include deleted keys) */
46
+ this.created = new Map();
47
+ /** @type {Map<string, Array<{ ownerType: string, ownerKey: string }>>} */
48
+ this.rels = new Map();
49
+ /** @type {"start"|"end"|undefined} */
50
+ this.position = undefined;
51
+ /** @type {Set<string>} "Type.field" of every field set to null, which must be nullable in the SDL */
52
+ this.nulls = new Set();
53
+ }
54
+
55
+ clear() {
56
+ this.recs.clear();
57
+ this.deleted.clear();
58
+ this.created.clear();
59
+ this.rels.clear();
60
+ this.nulls.clear();
61
+ this.position = undefined;
62
+ }
63
+
64
+ entity(type, key) {
65
+ return this.recs.get(recKey(type, key));
66
+ }
67
+
68
+ isDeleted(type, key) {
69
+ return this.deleted.get(type)?.has(key) ?? false;
70
+ }
71
+
72
+ nullFields() {
73
+ return this.nulls.size ? this.nulls : undefined;
74
+ }
75
+
76
+ deletedKeys(type) {
77
+ return this.deleted.get(type) ?? [];
78
+ }
79
+
80
+ deletedTypes() {
81
+ const out = new Set();
82
+ for (const [t, set] of this.deleted) if (set.size) out.add(t);
83
+ return out.size ? out : undefined;
84
+ }
85
+
86
+ createdKeys(type) {
87
+ return (this.created.get(type) ?? []).filter((k) => !this.isDeleted(type, k));
88
+ }
89
+
90
+ relatedKeys(type, ownerType, ownerKey) {
91
+ return this.createdKeys(type).filter((k) => (this.rels.get(recKey(type, k)) ?? []).some((r) => r.ownerType === ownerType && r.ownerKey === ownerKey));
92
+ }
93
+
94
+ touches(type) {
95
+ return (this.deleted.get(type)?.size ?? 0) > 0 || (this.created.get(type)?.length ?? 0) > 0;
96
+ }
97
+
98
+ taken(type, key) {
99
+ return this.isDeleted(type, key) || (this.created.get(type) ?? []).includes(key);
100
+ }
101
+
102
+ /**
103
+ * Apply one log entry.
104
+ * @param {any} e
105
+ */
106
+ apply(e) {
107
+ const rk = recKey(e.type, e.key);
108
+ if (e.op === "pin" || e.op === "update") {
109
+ const rec = this.recs.get(rk) ?? { fields: {}, created: false, updated: false };
110
+ this.recs.set(rk, rec);
111
+ Object.assign(rec.fields, e.fields);
112
+ for (const [k, v] of Object.entries(e.fields)) if (v === null) this.nulls.add(`${e.type}.${k}`);
113
+ if (e.op === "update") rec.updated = true;
114
+ if (e.rel) this.rels.set(rk, e.rel);
115
+ } else if (e.op === "create") {
116
+ this.deleted.get(e.type)?.delete(e.key);
117
+ const list = this.created.get(e.type) ?? [];
118
+ this.created.set(e.type, [...list.filter((k) => k !== e.key), e.key]);
119
+ this.recs.set(rk, { fields: { ...e.fields }, created: true, updated: false });
120
+ for (const [k, v] of Object.entries(e.fields)) if (v === null) this.nulls.add(`${e.type}.${k}`);
121
+ if (e.rel?.length) this.rels.set(rk, e.rel);
122
+ else this.rels.delete(rk);
123
+ } else if (e.op === "delete") {
124
+ if (!this.deleted.has(e.type)) this.deleted.set(e.type, new Set());
125
+ /** @type {Set<string>} */ (this.deleted.get(e.type)).add(e.key);
126
+ this.recs.delete(rk);
127
+ this.rels.delete(rk);
128
+ }
129
+ }
130
+ }
131
+
132
+ const OPS = new Set(["pin", "create", "update", "delete"]);
133
+
134
+ function checkEntry(e, i) {
135
+ const fail = (m) => bad(`log entry ${i}: ${m}`);
136
+ if (!isObject(e) || !OPS.has(e.op)) throw fail("op must be one of pin, create, update, delete");
137
+ if (typeof e.type !== "string" || typeof e.key !== "string" || e.key === "") throw fail("type and key must be strings");
138
+ if (e.op !== "delete" && !isObject(e.fields)) throw fail("fields must be an object");
139
+ if (e.rel !== undefined && !Array.isArray(e.rel)) throw fail("rel must be an array");
140
+ }
141
+
142
+ function plainErrors(errors) {
143
+ return errors.map((e) => {
144
+ const out = { message: e.message };
145
+ if (e.locations) out.locations = e.locations.map((l) => ({ line: l.line, column: l.column }));
146
+ if (e.path) out.path = [...e.path];
147
+ const ext = { ...(e.extensions ?? {}) };
148
+ if (e.originalError instanceof GraceholderError) ext.code = e.originalError.code;
149
+ if (Object.keys(ext).length) out.extensions = ext;
150
+ return out;
151
+ });
152
+ }
153
+
154
+ function thrownErrors(e) {
155
+ if (!(e instanceof GraceholderError)) throw e;
156
+ if (e.code === "graphql" && e.errors) return plainErrors(e.errors);
157
+ const ext = { code: e.code };
158
+ if (e.path !== undefined) ext.path = e.path;
159
+ if (e.hint !== undefined) ext.hint = e.hint;
160
+ return [{ message: e.message, extensions: ext }];
161
+ }
162
+
163
+ function selectOperation(doc, operationName) {
164
+ const ops = doc.definitions.filter((d) => d.kind === Kind.OPERATION_DEFINITION);
165
+ let op;
166
+ if (operationName !== undefined && operationName !== null) {
167
+ op = ops.find((o) => o.name?.value === operationName);
168
+ if (!op) throw new GraceholderError("bad-operation", `no operation named ${JSON.stringify(operationName)}`);
169
+ } else if (ops.length === 1) {
170
+ op = ops[0];
171
+ } else {
172
+ throw new GraceholderError("bad-operation", ops.length === 0 ? "the document has no operation" : "the document has several operations; pass operationName");
173
+ }
174
+ if (op.operation === "subscription") throw new GraceholderError("bad-operation", "subscription operations are not supported");
175
+ return op;
176
+ }
177
+
178
+ /** @skip / @include on a root field. */
179
+ function skipped(node, vars) {
180
+ for (const d of node.directives ?? []) {
181
+ const name = d.name.value;
182
+ if (name !== "skip" && name !== "include") continue;
183
+ const arg = d.arguments?.find((a) => a.name.value === "if");
184
+ const v = arg ? valueFromASTUntyped(arg.value, vars) : undefined;
185
+ if (name === "skip" && v === true) return true;
186
+ if (name === "include" && v === false) return true;
187
+ }
188
+ return false;
189
+ }
190
+
191
+ /** Root fields of a mutation, looking through fragments on `Mutation`. */
192
+ function rootFields(selSet, fragments, vars, out = []) {
193
+ for (const s of selSet.selections) {
194
+ if (skipped(s, vars)) continue;
195
+ if (s.kind === Kind.FIELD) out.push(s);
196
+ else if (s.kind === Kind.INLINE_FRAGMENT) rootFields(s.selectionSet, fragments, vars, out);
197
+ else if (s.kind === Kind.FRAGMENT_SPREAD) {
198
+ const def = fragments.get(s.name.value);
199
+ if (!def) throw new GraceholderError("graphql", `Unknown fragment "${s.name.value}".`);
200
+ rootFields(def.selectionSet, fragments, vars, out);
201
+ }
202
+ }
203
+ return out;
204
+ }
205
+
206
+ /**
207
+ * Mutation inference (docs/design.md section 16, World): the verb and entity type of a root mutation field.
208
+ * @returns {{ op: "create"|"update"|"delete", type: string, read: string, cfg: any }}
209
+ */
210
+ export function classifyMutation(fieldName, mutations) {
211
+ const cfg = mutations.fields[fieldName];
212
+ const tokens = tokenize(fieldName);
213
+ const op = cfg?.op ?? mutations.verbs[tokens[0]] ?? DEFAULT_VERBS[tokens[0]];
214
+ if (!op) {
215
+ throw new GraceholderError("bad-mutation", `cannot tell what the mutation "${fieldName}" does: "${tokens[0]}" is not a known verb`, {
216
+ hint: `known verbs: ${Object.keys({ ...DEFAULT_VERBS, ...mutations.verbs }).join(", ")}; declare others in the spec: { "$mutations": { "verbs": { "${tokens[0]}": "update" } } } or { "fields": { "${fieldName}": { "op": "update", "type": "..." } } }`,
217
+ });
218
+ }
219
+ const noun = tokens.slice(1).join("_");
220
+ if (!cfg?.type && noun === "") {
221
+ throw new GraceholderError("bad-mutation", `the mutation "${fieldName}" names no entity type`, { hint: `name it like ${tokens[0]}User or set { "$mutations": { "fields": { "${fieldName}": { "op": "${op}", "type": "User" } } } }` });
222
+ }
223
+ const type = cfg?.type ?? typeNameFor(noun);
224
+ return { op, type, read: cfg?.read ?? lowerFirst(type), cfg };
225
+ }
226
+
227
+ /**
228
+ * @param {import("./index.js").WorldOptions & { log?: any[] }} [options]
229
+ */
230
+ export function createWorld(options = {}) {
231
+ if (!isObject(options)) throw bad("createWorld expects an options object");
232
+ const seed = options.seed ?? DEFAULT_SEED;
233
+ const locale = options.locale ?? "en";
234
+ checkLocale(locale);
235
+ const anchorDate = options.anchorDate ?? DEFAULT_ANCHOR;
236
+ const anchorIso = new Date(anchorMs(anchorDate)).toISOString();
237
+ const spec = options.spec === undefined ? undefined : structuredClone(options.spec);
238
+ const mutations = compileSpec(spec).mutations;
239
+ // realism options (docs/design.md section 16, Realism) apply to every query of the world; fail early on a bad one
240
+ realismOptions(options);
241
+ const scenario = globalScenarios(options.scenario);
242
+ errorRateOf(options.scenarioOptions);
243
+ const realism = {
244
+ ...(options.packs ? { packs: options.packs } : {}),
245
+ ...(options.images !== undefined ? { images: options.images } : {}),
246
+ ...(options.distributions !== undefined ? { distributions: options.distributions } : {}),
247
+ ...(scenario.length ? { scenario } : {}),
248
+ ...(options.scenarioOptions !== undefined ? { scenarioOptions: { ...options.scenarioOptions } } : {}),
249
+ };
250
+ const base = { seed, spec, anchorDate: anchorIso, locale, strict: options.strict, ...realism, schema: options.schema };
251
+
252
+ const state = new WorldState();
253
+ state.position = mutations.position;
254
+ /** @type {any[]} */
255
+ let log = [];
256
+ const append = (entry) => {
257
+ state.apply(entry);
258
+ log.push(entry);
259
+ };
260
+ const replay = (entries) => {
261
+ if (!Array.isArray(entries)) throw bad("log must be an array");
262
+ entries.forEach(checkEntry);
263
+ for (const e of entries) append(structuredClone(e));
264
+ };
265
+ if (options.log !== undefined) replay(options.log);
266
+
267
+ const view = state;
268
+ const run = (query, opts) => generateResult(query, { ...base, ...opts }, view);
269
+
270
+ /** Variable values: declared defaults, then the provided ones. */
271
+ const varsOf = (op, provided) => {
272
+ const out = {};
273
+ for (const def of op.variableDefinitions ?? []) if (def.defaultValue) out[def.variable.name.value] = valueFromASTUntyped(def.defaultValue);
274
+ return { ...out, ...(provided ?? {}) };
275
+ };
276
+
277
+ const notFound = (type, key) => new GraceholderError("not-found", `${type} ${JSON.stringify(key)} does not exist (it was deleted)`);
278
+
279
+ /** Decide the effect of one mutation field and return it (nothing is written). */
280
+ function effectOf(node, vars) {
281
+ const name = node.name.value;
282
+ const cls = classifyMutation(name, mutations);
283
+ const args = {};
284
+ for (const a of node.arguments ?? []) args[a.name.value] = valueFromASTUntyped(a.value, vars);
285
+ const inputName = cls.cfg?.input;
286
+ let input;
287
+ if (inputName === "args") input = undefined;
288
+ else if (inputName) input = isObject(args[inputName]) ? args[inputName] : undefined;
289
+ else input = INPUT_ARGS.map((n) => args[n]).find(isObject);
290
+
291
+ const idNames = cls.op === "create" ? [cls.cfg?.idArg ?? "id"] : cls.cfg?.idArg ? [cls.cfg.idArg] : ID_ARGS;
292
+ const idName = idNames.find((n) => args[n] != null) ?? idNames.find((n) => input?.[n] != null) ?? idNames.find((n) => isObject(args.where) && args.where[n] != null);
293
+ const idValue = idName === undefined ? undefined : (args[idName] ?? input?.[idName] ?? args.where?.[idName]);
294
+ const key = idName === undefined ? undefined : entityKeyFromArgs({ id: idValue });
295
+
296
+ let fields;
297
+ if (input) {
298
+ fields = { ...input };
299
+ } else {
300
+ fields = {};
301
+ for (const [k, v] of Object.entries(args)) if (k !== "where" || !isObject(v)) fields[k] = v;
302
+ }
303
+ if (idName !== undefined) delete fields[idName];
304
+ for (const k of Object.keys(fields)) if (fields[k] === undefined) delete fields[k];
305
+
306
+ const rel = [];
307
+ for (const [fname, value] of Object.entries(fields)) {
308
+ const m = RELATION.exec(fname);
309
+ if (!m || value === null || typeof value === "object") continue;
310
+ const ownerType = mutations.relations[`${cls.type}.${fname}`] ?? typeNameFor(m[1]);
311
+ rel.push({ ownerType, ownerKey: entityKeyFromArgs({ id: value }) });
312
+ }
313
+ return { name, ...cls, key, fields, rel };
314
+ }
315
+
316
+ /** Evaluate the selection of a mutation field as the query field for the affected entity. */
317
+ function selectionOf(node, doc, op, vars, cls, key, errorsOut) {
318
+ const alias = node.alias?.value ?? node.name.value;
319
+ const field = {
320
+ kind: Kind.FIELD,
321
+ alias: { kind: Kind.NAME, value: alias },
322
+ name: { kind: Kind.NAME, value: cls.read },
323
+ arguments: [{ kind: Kind.ARGUMENT, name: { kind: Kind.NAME, value: "id" }, value: { kind: Kind.STRING, value: key } }],
324
+ directives: [],
325
+ selectionSet: node.selectionSet,
326
+ };
327
+ const fragments = doc.definitions.filter((d) => d.kind === Kind.FRAGMENT_DEFINITION);
328
+ const used = new Set();
329
+ for (const part of [field, ...fragments]) visit(part, { Variable: (v) => void used.add(v.name.value) });
330
+ const query = {
331
+ kind: Kind.DOCUMENT,
332
+ definitions: [
333
+ {
334
+ kind: Kind.OPERATION_DEFINITION,
335
+ operation: "query",
336
+ variableDefinitions: (op.variableDefinitions ?? []).filter((d) => used.has(d.variable.name.value)),
337
+ selectionSet: { kind: Kind.SELECTION_SET, selections: [field] },
338
+ },
339
+ ...fragments,
340
+ ],
341
+ };
342
+ /** @type {any} */
343
+ const mutSpec = { ...(spec ?? {}) };
344
+ mutSpec[cls.read] = { ...(mutSpec[cls.read] ?? {}) };
345
+ if (mutSpec[cls.read].list === undefined) mutSpec[cls.read].one = true;
346
+ if (typeNameFor(cls.read) !== cls.type && mutSpec[cls.read].type === undefined) mutSpec[cls.read].type = cls.type;
347
+ const res = run(query, { variables: vars, spec: mutSpec, strict: false });
348
+ if (res.errors?.length) errorsOut.push(...plainErrors(res.errors).map((e) => ({ ...e, path: [alias, ...(e.path ?? []).slice(1)] })));
349
+ return res.data?.[alias] ?? null;
350
+ }
351
+
352
+ function mutate(doc, op, provided) {
353
+ const vars = varsOf(op, provided);
354
+ const fragments = new Map(doc.definitions.filter((d) => d.kind === Kind.FRAGMENT_DEFINITION).map((d) => [d.name.value, d]));
355
+ const data = {};
356
+ const errors = [];
357
+ for (const node of rootFields(op.selectionSet, fragments, vars)) {
358
+ const alias = node.alias?.value ?? node.name.value;
359
+ try {
360
+ if (node.name.value === "__typename") {
361
+ data[alias] = "Mutation";
362
+ continue;
363
+ }
364
+ const eff = effectOf(node, vars);
365
+ const { op: kind, type } = eff;
366
+ let key = eff.key;
367
+ if (kind === "create") {
368
+ const seq = log.length + 1;
369
+ if (key === undefined) {
370
+ let n = keyStream({ seed, scope: "a", typeName: type, entityKey: "world", fieldName: "create", subKeys: [seq] }).int(1000000, 9999999);
371
+ while (state.taken(type, String(n))) n++;
372
+ key = String(n);
373
+ } else if (state.createdKeys(type).includes(key)) {
374
+ throw new GraceholderError("bad-mutation", `${type} ${JSON.stringify(key)} already exists`);
375
+ }
376
+ // a decimal key is its own id; any other key needs the id field pinned to it
377
+ if (!/^[0-9]+$/.test(key) && eff.fields.id === undefined) eff.fields = { id: key, ...eff.fields };
378
+ append({ op: "create", type, key, fields: eff.fields, ...(eff.rel.length ? { rel: eff.rel } : {}), mutation: eff.name });
379
+ } else {
380
+ if (key === undefined) {
381
+ throw new GraceholderError("bad-mutation", `the mutation "${eff.name}" needs an identifying argument (${ID_ARGS.join(", ")}), in its arguments or its input`);
382
+ }
383
+ if (state.isDeleted(type, key)) throw notFound(type, key);
384
+ if (kind === "update") {
385
+ append({ op: "update", type, key, fields: eff.fields, ...(eff.rel.length ? { rel: eff.rel } : {}), mutation: eff.name });
386
+ }
387
+ }
388
+ if (!node.selectionSet) {
389
+ data[alias] = kind === "delete" ? true : key;
390
+ } else {
391
+ data[alias] = selectionOf(node, doc, op, vars, eff, key, errors);
392
+ }
393
+ // a delete returns the entity as it was; the write comes after the read
394
+ if (kind === "delete") append({ op: "delete", type, key, mutation: eff.name });
395
+ } catch (e) {
396
+ if (!(e instanceof GraceholderError)) throw e;
397
+ data[alias] = null;
398
+ errors.push({ ...thrownErrors(e)[0], path: [alias] });
399
+ }
400
+ }
401
+ return errors.length ? { data, errors } : { data };
402
+ }
403
+
404
+ return {
405
+ /**
406
+ * Run a query or a mutation against the world. Returns a GraphQL result: `{ data }`, or with
407
+ * `errors` (parse, validation, argument and mutation errors never throw here).
408
+ */
409
+ query(query, opts = {}) {
410
+ try {
411
+ const doc = toDocument(query);
412
+ const op = selectOperation(doc, opts.operationName);
413
+ if (op.operation === "mutation") return mutate(doc, op, opts.variables);
414
+ const res = run(doc, { variables: opts.variables, operationName: opts.operationName });
415
+ if (res.errors?.length) return res.data === undefined ? { errors: plainErrors(res.errors) } : { data: res.data, errors: plainErrors(res.errors) };
416
+ return { data: res.data };
417
+ } catch (e) {
418
+ return { errors: thrownErrors(e) };
419
+ }
420
+ },
421
+
422
+ /**
423
+ * The root collection of a query as a lazy iterable (see `stream`), reflecting the pins and
424
+ * mutations at the time each window is generated: consume it before mutating the world.
425
+ */
426
+ stream(query, opts = {}) {
427
+ return streamWorld(query, { ...base, ...opts }, view);
428
+ },
429
+
430
+ /** Why the value at `path` looks the way it does in this world, pins and mutations included (see `explain`). */
431
+ explain(query, path, opts = {}) {
432
+ return explainWith(query, parsePath(path), { ...base, ...opts }, view);
433
+ },
434
+
435
+ /** Override fields of one entity ("User:42"); unpinned fields stay generated. Pins are logged. */
436
+ pin(ref, fields) {
437
+ const { type, key } = parseRef(ref);
438
+ if (!isObject(fields)) throw bad("pin expects an object of field values");
439
+ if (state.isDeleted(type, key)) throw notFound(type, key);
440
+ const clean = {};
441
+ for (const [k, v] of Object.entries(fields)) if (v !== undefined) clean[k] = v;
442
+ append(structuredClone({ op: "pin", type, key, fields: clean }));
443
+ return this;
444
+ },
445
+
446
+ /** `{ seed, spec, anchorDate, locale, log }`: pass it to createWorld to replay to the same state. */
447
+ export() {
448
+ // pack objects are exported by name: register them (registerPack) before replaying
449
+ const extra = { ...realism, ...(realism.packs ? { packs: realism.packs.map((p) => (typeof p === "string" ? p : p.name)) } : {}) };
450
+ return structuredClone({ seed, spec, anchorDate: anchorIso, locale, ...extra, log });
451
+ },
452
+
453
+ /** Forget every pin and mutation. */
454
+ reset() {
455
+ state.clear();
456
+ state.position = mutations.position;
457
+ log = [];
458
+ return this;
459
+ },
460
+ };
461
+ }
package/src/xxh64.js ADDED
@@ -0,0 +1,87 @@
1
+ // XXH64 as published (https://github.com/Cyan4973/xxHash, XXH64 spec v0.8).
2
+ // BigInt implementation: synchronous, dependency free, identical in Node and browsers.
3
+ // FROZEN: any deviation from the published algorithm is a bug (docs/design.md 2.1).
4
+
5
+ const M = 0xffffffffffffffffn;
6
+ const P1 = 0x9e3779b185ebca87n;
7
+ const P2 = 0xc2b2ae3d27d4eb4fn;
8
+ const P3 = 0x165667b19e3779f9n;
9
+ const P4 = 0x85ebca77c2b2ae63n;
10
+ const P5 = 0x27d4eb2f165667c5n;
11
+
12
+ const rotl = (x, r) => ((x << BigInt(r)) | (x >> BigInt(64 - r))) & M;
13
+
14
+ function read64(b, i) {
15
+ let v = 0n;
16
+ for (let k = 7; k >= 0; k--) v = (v << 8n) | BigInt(b[i + k]);
17
+ return v;
18
+ }
19
+
20
+ function read32(b, i) {
21
+ return BigInt((b[i] | (b[i + 1] << 8) | (b[i + 2] << 16) | (b[i + 3] << 24)) >>> 0);
22
+ }
23
+
24
+ function round(acc, input) {
25
+ acc = (acc + input * P2) & M;
26
+ acc = rotl(acc, 31);
27
+ return (acc * P1) & M;
28
+ }
29
+
30
+ function mergeRound(acc, val) {
31
+ acc ^= round(0n, val);
32
+ return (acc * P1 + P4) & M;
33
+ }
34
+
35
+ /**
36
+ * @param {Uint8Array} bytes
37
+ * @param {bigint} [seed]
38
+ * @returns {bigint} unsigned 64-bit hash
39
+ */
40
+ export function xxh64(bytes, seed = 0n) {
41
+ const len = bytes.length;
42
+ let i = 0;
43
+ let h;
44
+ if (len >= 32) {
45
+ let v1 = (seed + P1 + P2) & M;
46
+ let v2 = (seed + P2) & M;
47
+ let v3 = seed & M;
48
+ let v4 = (seed - P1) & M;
49
+ const limit = len - 32;
50
+ do {
51
+ v1 = round(v1, read64(bytes, i));
52
+ v2 = round(v2, read64(bytes, i + 8));
53
+ v3 = round(v3, read64(bytes, i + 16));
54
+ v4 = round(v4, read64(bytes, i + 24));
55
+ i += 32;
56
+ } while (i <= limit);
57
+ h = (rotl(v1, 1) + rotl(v2, 7) + rotl(v3, 12) + rotl(v4, 18)) & M;
58
+ h = mergeRound(h, v1);
59
+ h = mergeRound(h, v2);
60
+ h = mergeRound(h, v3);
61
+ h = mergeRound(h, v4);
62
+ } else {
63
+ h = (seed + P5) & M;
64
+ }
65
+ h = (h + BigInt(len)) & M;
66
+ while (i + 8 <= len) {
67
+ h ^= round(0n, read64(bytes, i));
68
+ h = (rotl(h, 27) * P1 + P4) & M;
69
+ i += 8;
70
+ }
71
+ if (i + 4 <= len) {
72
+ h ^= (read32(bytes, i) * P1) & M;
73
+ h = (rotl(h, 23) * P2 + P3) & M;
74
+ i += 4;
75
+ }
76
+ while (i < len) {
77
+ h ^= (BigInt(bytes[i]) * P5) & M;
78
+ h = (rotl(h, 11) * P1) & M;
79
+ i++;
80
+ }
81
+ h ^= h >> 33n;
82
+ h = (h * P2) & M;
83
+ h ^= h >> 29n;
84
+ h = (h * P3) & M;
85
+ h ^= h >> 32n;
86
+ return h;
87
+ }