@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
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 John Henry
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,493 @@
1
- # Temporary Holding Version
1
+ # graceholder
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Consistently repeatable generated data, in any shape you want, from a GraphQL query.
4
+
5
+ Give it a query and get data back. There is no schema to write and no random stream to keep in order: every value is a pure function of a key (`seed`, type, entity, field), so the same seed, query and options always produce byte-identical output, on Node and in the browser. Reordering fields, adding siblings or changing aliases never changes another value.
6
+
7
+ - **Query-only.** The schema is inferred from the query: shapes from field names (`posts` is a list of `Post`, `createdAt` a date, `email` an email), then executed with graphql-js.
8
+ - **A real entity graph.** `user(id: 5)` is the same person wherever it appears. Lists page consistently (`first`/`after`, `offset`/`limit`, `page`), Relay connections are inferred from the query, unions and interfaces work.
9
+ - **Coherent records.** Names, usernames and emails agree; `createdAt <= updatedAt <= anchorDate`; `total = subtotal + tax`; `commentCount` equals the size of `comments`.
10
+ - **Overrides when heuristics are wrong.** Inline directives (`@count`, `@pick`, `@pattern`, ...) or a side-car spec keyed by path, with documented precedence.
11
+ - **Library only.** ESM, TypeScript types, Node >= 22 and browsers. `graphql` is the only runtime dependency. No network, no clock, no `Math.random`.
12
+
13
+ > Status: pre-1.0 (0.x). Output can still change between minor versions; see [STABILITY.md](STABILITY.md) for what will be frozen at 1.0.0.
14
+
15
+ ## Install
16
+
17
+ ```sh
18
+ npm install @johnhenry/graceholder graphql
19
+ ```
20
+
21
+ ## Quick start
22
+
23
+ ```js
24
+ import { graceholder } from "@johnhenry/graceholder";
25
+
26
+ const data = graceholder("{ users(first: 2) { id fullName email createdAt } }", { seed: "demo" });
27
+ console.log(data);
28
+ ```
29
+
30
+ ```json
31
+ {
32
+ "users": [
33
+ {
34
+ "id": "1",
35
+ "fullName": "Ines Olsen",
36
+ "email": "inesolsen@example.org",
37
+ "createdAt": "2024-01-18T11:34:49.272Z"
38
+ },
39
+ {
40
+ "id": "2",
41
+ "fullName": "Hector Østergaard",
42
+ "email": "hector.ostergaard@example.net",
43
+ "createdAt": "2021-04-21T05:15:43.261Z"
44
+ }
45
+ ]
46
+ }
47
+ ```
48
+
49
+ `graceholder()` is synchronous and returns plain JSON-compatible objects. The default seed is `"graceholder"`.
50
+
51
+ ## Repeatable by construction
52
+
53
+ ```js
54
+ import { graceholder } from "@johnhenry/graceholder";
55
+
56
+ const plain = graceholder("{ posts { id title author { ... on User { name } } } }", { seed: 1 });
57
+ const fancy = graceholder(
58
+ "{ articles: posts { writer: author { ... on User { who: name } } headline: title key: id body tags } }",
59
+ { seed: 1 },
60
+ );
61
+ console.log({
62
+ sameCall: JSON.stringify(plain) === JSON.stringify(graceholder("{ posts { id title author { ... on User { name } } } }", { seed: 1 })),
63
+ aliasesReorderedAndExtraFields: plain.posts.every(
64
+ (p, i) => p.title === fancy.articles[i].headline && p.id === fancy.articles[i].key && p.author.name === fancy.articles[i].writer.who,
65
+ ),
66
+ otherSeed: JSON.stringify(plain) !== JSON.stringify(graceholder("{ posts { id title author { ... on User { name } } } }", { seed: 2 })),
67
+ });
68
+ ```
69
+
70
+ ```json
71
+ {
72
+ "sameCall": true,
73
+ "aliasesReorderedAndExtraFields": true,
74
+ "otherSeed": true
75
+ }
76
+ ```
77
+
78
+ ## Entities, arguments and pagination
79
+
80
+ Arguments such as `id`, `key`, `slug`, `email` select an entity: the same `Type:key` is identical everywhere. Paging arguments select a window of an endless collection, so page 2 of `first: 5` is items 6 to 10 of `first: 10`. Cursors are opaque and stable.
81
+
82
+ ```js
83
+ import { graceholder } from "@johnhenry/graceholder";
84
+
85
+ const query = `query Page($after: String) {
86
+ me: user(id: 5) { id fullName }
87
+ posts(first: 2, after: $after) {
88
+ edges { cursor node { id title } }
89
+ pageInfo { hasNextPage endCursor }
90
+ totalCount
91
+ }
92
+ }`;
93
+ const first = graceholder(query, { seed: "demo" });
94
+ const next = graceholder(query, { seed: "demo", variables: { after: first.posts.pageInfo.endCursor } });
95
+ console.log({
96
+ me: first.me,
97
+ firstPage: first.posts.edges.map((e) => e.node.id),
98
+ nextPage: next.posts.edges.map((e) => e.node.id),
99
+ totalCount: first.posts.totalCount,
100
+ hasNextPage: next.posts.pageInfo.hasNextPage,
101
+ });
102
+ ```
103
+
104
+ ```json
105
+ {
106
+ "me": {
107
+ "id": "5",
108
+ "fullName": "Keiko Petrov"
109
+ },
110
+ "firstPage": [
111
+ "1",
112
+ "2"
113
+ ],
114
+ "nextPage": [
115
+ "3",
116
+ "4"
117
+ ],
118
+ "totalCount": 1000,
119
+ "hasNextPage": true
120
+ }
121
+ ```
122
+
123
+ ## Overriding the heuristics
124
+
125
+ Inline directives are stripped before execution. The same properties can live in a side-car spec keyed by path (`users.name`, `*.status`, `**.createdAt`, `@User.email`). A directive beats a spec entry; an exact path beats `*`, which beats `**`.
126
+
127
+ ```js
128
+ import { graceholder } from "@johnhenry/graceholder";
129
+
130
+ const data = graceholder(
131
+ `{
132
+ orders @count(n: 2) {
133
+ code @pattern(p: "ORD-#####")
134
+ status @pick(values: [{ value: "new", weight: 3 }, { value: "paid", weight: 1 }])
135
+ note
136
+ total
137
+ }
138
+ }`,
139
+ { seed: "demo", spec: { "orders.note": { nullRate: 0.5 } } },
140
+ );
141
+ console.log(data);
142
+ ```
143
+
144
+ ```json
145
+ {
146
+ "orders": [
147
+ {
148
+ "code": "ORD-60441",
149
+ "status": "new",
150
+ "note": "natus eu",
151
+ "total": 5943.63
152
+ },
153
+ {
154
+ "code": "ORD-17295",
155
+ "status": "new",
156
+ "note": null,
157
+ "total": 4317.75
158
+ }
159
+ ]
160
+ }
161
+ ```
162
+
163
+ Directives: `@list`, `@one`, `@count`, `@gen`, `@pattern`, `@pick`, `@nullRate`, `@derive`, `@type`, `@seed`. Everything is documented in [docs/api.md](docs/api.md); recipes are in [docs/cookbook.md](docs/cookbook.md).
164
+
165
+ ## See what was inferred
166
+
167
+ ```js
168
+ import { inferSchema } from "@johnhenry/graceholder";
169
+
170
+ const { sdl, decisions } = inferSchema("{ posts(first: 3) { id title createdAt tags } }");
171
+ console.log({ sdl, titleDecision: decisions.find((d) => d.path === "posts.title" && d.kind === "scalar") });
172
+ ```
173
+
174
+ ```json
175
+ {
176
+ "sdl": "scalar DateTime\n\ntype Query {\n posts(first: Int): [Post!]!\n}\n\ntype Post {\n id: ID!\n title: String!\n createdAt: DateTime!\n tags: [String!]!\n}\n",
177
+ "titleDecision": {
178
+ "path": "posts.title",
179
+ "field": "title",
180
+ "kind": "scalar",
181
+ "value": "String",
182
+ "source": "heuristic",
183
+ "rule": "T11"
184
+ }
185
+ }
186
+ ```
187
+
188
+ ## Typed results
189
+
190
+ Pass the query as a string literal and the result is typed from it, the way gql.tada does it: selection sets, aliases, lists (by the same rules the runtime uses), nested lists, fragments, unions, Relay connections and scalar types by field name.
191
+
192
+ ```ts
193
+ import { graceholder, createWorld, stream, type ResultOf } from "@johnhenry/graceholder";
194
+
195
+ const data = graceholder("{ users(first: 3) { id name createdAt posts { title } } }");
196
+ data.users[0].posts[0].title; // string
197
+ data.users[0].email; // error: not in the query
198
+
199
+ type Users = ResultOf<"{ users { id age } }">; // { users: { id: string; age: number }[] }
200
+
201
+ createWorld().query("{ user(id: 3) { name } }").data?.user.name; // string
202
+ for (const u of stream("{ users { id email } }", { count: 1000 })) u.email; // string
203
+ ```
204
+
205
+ Anything the types cannot read falls back to `unknown` or `Record<string, unknown>`: schema mode, packs, scenarios and the spec are not seen, and a field's directives or arguments elsewhere in the query are not merged the way the runtime merges them. [The limits](docs/api.md#typed-results) are listed, and tests compare the types of fixture and random queries with the real output.
206
+
207
+ ## Explain a value
208
+
209
+ `explain(query, path, options)` says why a value looks the way it does: the key string, generator and arguments, the rule, spec entry or directive that decided it (with the precedence), the persona slots or derivation inputs, the entity (`User:4`, shared or owned), the scenario effect and any pin. It never changes an output.
210
+
211
+ ```js
212
+ import { explain } from "@johnhenry/graceholder";
213
+
214
+ const e = explain("{ users(first: 5) { id name email } }", "users.3.email", { seed: "demo" });
215
+ console.log({ summary: e.summary, entity: e.entity.identity, key: e.key.parts, rule: e.rule.id });
216
+ ```
217
+
218
+ ```json
219
+ {
220
+ "summary": "users.3.email = \"chloeortega@example.com\": person.email(), chosen by rule T3 (heuristic), on entity User:4 (shared).",
221
+ "entity": "User:4",
222
+ "key": [
223
+ "gh1",
224
+ "demo",
225
+ "e",
226
+ "User",
227
+ "4",
228
+ "email"
229
+ ],
230
+ "rule": "T3"
231
+ }
232
+ ```
233
+
234
+ See [docs/api.md](docs/api.md#explainquery-path-options) for the full format.
235
+
236
+ ## Playground
237
+
238
+ [`playground/index.html`](playground/index.html) is a single static page: edit the query, spec, seed, options and scenario, see the output and the inferred SDL, and click any value to explain it. It runs from `file://` (open the file) and from GitHub Pages, and loads nothing from the network. ![the playground](playground/screenshot.png)
239
+
240
+ ## Learn from real data
241
+
242
+ `specFromSamples(query, samples, options?)` reads one or more real responses and writes a spec: list sizes, null rates, number ranges, boolean ratios, date ranges, ID formats (`SKU-####`), string lengths and, only when you allow it, enum-like value sets. It needs no LLM and never copies free text. See [docs/api.md](docs/api.md#specfromsamplesquery-samples-options) for the rules and [docs/cookbook.md](docs/cookbook.md#learning-a-spec-from-real-responses) for a worked example.
243
+
244
+ ```js
245
+ import { specFromSamples } from "@johnhenry/graceholder";
246
+
247
+ const spec = specFromSamples("{ users { id age active } }", { users: [
248
+ { id: "7", age: 31, active: true },
249
+ { id: "9", age: 45, active: false },
250
+ { id: "12", age: 38, active: true },
251
+ ] });
252
+ console.log(spec);
253
+ ```
254
+
255
+ ```json
256
+ {
257
+ "users": {
258
+ "count": 3
259
+ },
260
+ "users.age": {
261
+ "gen": {
262
+ "name": "int",
263
+ "args": {
264
+ "min": 31,
265
+ "max": 45
266
+ }
267
+ }
268
+ },
269
+ "users.active": {
270
+ "gen": {
271
+ "name": "bool",
272
+ "args": {
273
+ "p": 0.667
274
+ }
275
+ }
276
+ }
277
+ }
278
+ ```
279
+
280
+ ## Use your real schema
281
+
282
+ Pass `schema` (SDL, an introspection result or a graphql-js schema) and the query is validated against it. Enums, nullability, lists, custom scalars, connections and unions/interfaces then come from the schema instead of from name heuristics; the entity graph and the determinism guarantee are unchanged. Without `schema`, nothing changes.
283
+
284
+ ```js
285
+ import { graceholder } from "@johnhenry/graceholder";
286
+
287
+ const schema = "enum Plan { FREE PRO } type Query { account: Account! } type Account { plan: Plan! seats: Int! }";
288
+ console.log(graceholder("{ account { plan seats } }", { schema, seed: "demo" }));
289
+ ```
290
+
291
+ ```json
292
+ {
293
+ "account": {
294
+ "plan": "PRO",
295
+ "seats": 44
296
+ }
297
+ }
298
+ ```
299
+
300
+ Directives and spec entries that contradict the schema throw `schema-conflict`. Details: [docs/api.md](docs/api.md#schema-mode).
301
+
302
+ ## Suggest a spec with an LLM (authoring time)
303
+
304
+ `suggestSpec` asks a model you provide to review the inference and return a spec. The core imports no LLM SDK: you pass a function `(prompt) => Promise<string>`. The reply is validated against the query; one repair round is offered; then `bad-suggestion`. Commit the result; generation itself never calls a model.
305
+
306
+ ```js
307
+ import { suggestSpec, graceholder } from "@johnhenry/graceholder";
308
+
309
+ // a stand-in for your model call
310
+ const llm = async () => JSON.stringify({ "orders.code": { pattern: "ORD-#####" } });
311
+
312
+ const query = "{ orders { code total } }";
313
+ const spec = await suggestSpec(query, { llm });
314
+ console.log({ spec, code: graceholder(query, { spec, seed: "demo" }).orders[0].code });
315
+ ```
316
+
317
+ ```json
318
+ {
319
+ "spec": {
320
+ "orders.code": {
321
+ "pattern": "ORD-#####"
322
+ }
323
+ },
324
+ "code": "ORD-60441"
325
+ }
326
+ ```
327
+
328
+ ## A world that remembers
329
+
330
+ `createWorld` gives a fake backend: pin specific entities, run mutations (`createUser`, `updateUser`, `deleteUser`, `addX`, `removeX`, ...) and later queries see them. Created ids, positions and counts are deterministic, and the state is an append-only log you can export and replay.
331
+
332
+ ```js
333
+ import { createWorld } from "@johnhenry/graceholder";
334
+
335
+ const world = createWorld({ seed: "readme" });
336
+ world.pin("User:1", { name: "Alice Admin" });
337
+ world.query(`mutation { deleteUser(id: 2) { id } }`);
338
+ const { data } = world.query("{ users { id name email } }");
339
+ console.log(data.users.slice(0, 2));
340
+ ```
341
+
342
+ ```json
343
+ [
344
+ {
345
+ "id": "1",
346
+ "name": "Alice Admin",
347
+ "email": "alice.admin@example.net"
348
+ },
349
+ {
350
+ "id": "3",
351
+ "name": "Ines Gupta",
352
+ "email": "ines.gupta@example.net"
353
+ }
354
+ ]
355
+ ```
356
+
357
+ See [docs/api.md](docs/api.md#createworldoptions) for the mutation rules.
358
+
359
+ ## More realistic data (opt-in)
360
+
361
+ Domain packs, heavy-tailed distributions, time series and SVG placeholders are all off by default, so existing output never moves. Packs are separate imports, so you only ship the ones you use.
362
+
363
+ ```js
364
+ import { graceholder } from "@johnhenry/graceholder";
365
+ import { geo } from "@johnhenry/graceholder/packs/geo";
366
+ import { commerce } from "@johnhenry/graceholder/packs/commerce";
367
+
368
+ const data = graceholder("{ user(id: 1) { fullName city country timezone phone avatar } product { name category price } }", {
369
+ packs: [geo, commerce],
370
+ images: "svg",
371
+ distributions: "realistic",
372
+ });
373
+ console.log({ ...data.user, avatar: data.user.avatar.slice(0, 40) + "...", product: data.product });
374
+ ```
375
+
376
+ ```json
377
+ {
378
+ "fullName": "Miles Tanaka",
379
+ "city": "Leeds",
380
+ "country": "United Kingdom",
381
+ "timezone": "Europe/London",
382
+ "phone": "+44 113 496 0314",
383
+ "avatar": "data:image/svg+xml;charset=utf-8,%3Csvg%...",
384
+ "product": {
385
+ "name": "Maplecrest Foldable Whiteboard",
386
+ "category": "Office",
387
+ "price": 165.55
388
+ }
389
+ }
390
+ ```
391
+
392
+ See [Packs, distributions and images](docs/api.md#packs) in the API reference and the [cookbook](docs/cookbook.md#realistic-data).
393
+
394
+ ## Seed a database, write fixtures files (bulk)
395
+
396
+ `stream` turns the root collection of a query into a lazy iterable (sync and async) with bounded memory, and `toNDJSON`, `toCSV` and `toSQL` write it out. Item `k` of the stream is exactly item `k` of the windowed query `users(offset: k, first: 1)`, so it works with `schema`, `packs`, `spec` and a world (`world.stream`), and chunked output (rows 0 to 999, then 1000 to 1999) is byte-identical to one run.
397
+
398
+ ```js
399
+ import { stream, toNDJSON, toSQL } from "@johnhenry/graceholder";
400
+
401
+ // stream() is lazy: 100,000 rows cost no more memory than 2
402
+ const query = "{ posts { id title author { id name } } }";
403
+ console.log([...toNDJSON(stream(query, { seed: "readme", count: 2 }))]);
404
+ console.log("run toSQL for a Postgres or SQLite script:", [...toSQL(query, { seed: "readme", count: 1, dialect: "sqlite" })].length > 0);
405
+ ```
406
+
407
+ SQL gets one table per entity type, nested lists as child tables with foreign keys, shared references written once, and Postgres or SQLite quoting. See [docs/api.md](docs/api.md#bulk-generation-stream-tondjson-tocsv-tosql) and the cookbook recipe [Seed a staging Postgres identically](docs/cookbook.md#seed-a-staging-postgres-identically).
408
+
409
+ ## Client adapters
410
+
411
+ Point an existing GraphQL client at a world instead of a backend. Each adapter is its own subpath export; the client libraries are optional peer dependencies, so the core still depends on `graphql` only.
412
+
413
+ | Import | Gives you | Peer dependencies |
414
+ | --- | --- | --- |
415
+ | `@johnhenry/graceholder/apollo` | `GraceholderLink` (an `ApolloLink`) | `@apollo/client` |
416
+ | `@johnhenry/graceholder/urql` | `graceholderExchange` | `@urql/core`, `wonka` |
417
+ | `@johnhenry/graceholder/fetch` | `createFetch` (a `fetch` replacement) | none |
418
+
419
+ Every adapter takes either a ready `world` or world options (`seed`, `spec`, ...), and an optional `latency` that is deterministic from the seed. Mutations go through the world, so a mutation followed by a query sees the new entity.
420
+
421
+ ```js
422
+ import { ApolloClient, InMemoryCache, gql } from "@apollo/client";
423
+ import { GraceholderLink } from "@johnhenry/graceholder/apollo";
424
+
425
+ const client = new ApolloClient({ link: new GraceholderLink({ seed: "readme", spec: { users: { count: 2 } } }), cache: new InMemoryCache() });
426
+ const { data } = await client.query({ query: gql`{ users { id name } }` });
427
+ console.log(data.users.map((u) => u.name));
428
+ ```
429
+
430
+ ```json
431
+ [
432
+ "Mateo Mbeki",
433
+ "Julia Wright"
434
+ ]
435
+ ```
436
+
437
+ See [docs/api.md](docs/api.md#client-adapters) for the options and [the cookbook](docs/cookbook.md#storybook-with-apollo) for Storybook and Playwright recipes.
438
+
439
+ ## Edge cases, repeatably
440
+
441
+ `scenario` swaps the output for an edge case, deterministically: `empty`, `huge`, `nulls`, `unicode`, `boundary` and `errors`, alone or combined, for the whole query or one spec path. Without it nothing changes.
442
+
443
+ ```js
444
+ import { graceholder } from "@johnhenry/graceholder";
445
+
446
+ const query = "{ users(first: 2) { id name posts { id } } }";
447
+ const spec = { "users.posts": { scenario: "empty" } };
448
+ console.log({
449
+ emptyPosts: graceholder(query, { spec }).users.map((u) => u.posts.length),
450
+ nulls: graceholder(query, { scenario: "nulls" }).users[0],
451
+ errors: graceholder("{ user(id: 1) { id name email } }", { scenario: "errors", scenarioOptions: { errorRate: 0.5 }, errors: "result" }).errors.map((e) => e.path.join(".")),
452
+ });
453
+ ```
454
+
455
+ ```json
456
+ {
457
+ "emptyPosts": [
458
+ 0,
459
+ 0
460
+ ],
461
+ "nulls": {
462
+ "id": "1",
463
+ "name": null,
464
+ "posts": null
465
+ },
466
+ "errors": [
467
+ "user.name"
468
+ ]
469
+ }
470
+ ```
471
+
472
+ `@johnhenry/graceholder/fast-check` turns the same idea into a property-testing arbitrary over seeds and scenario sets, with shrinking. See [Scenarios](docs/api.md#scenarios) and the [cookbook](docs/cookbook.md#test-your-ui-against-every-edge-case).
473
+
474
+ ## Determinism and stability
475
+
476
+ The key scheme, hash (XXH64), generator (SplitMix64), vocabulary and inference rules are frozen at 1.0.0; see [STABILITY.md](STABILITY.md). Golden files in `test/golden` pin the output, are checked on Node 22 and 24, and are reproduced in a browser-like VM and in headless Chromium.
477
+
478
+ ## Size
479
+
480
+ The whole library with `graphql` bundled, minified, is about 213 kB (about 62 kB gzipped). `graphql` is the bulk of it; graceholder itself is a small part. The vocabulary packs are separate modules (minified and gzipped: `geo` 5.0 kB, `commerce` 2.1 kB, `finance` 2.3 kB, `health` 1.4 kB) that are only included when imported.
481
+
482
+ ## Development
483
+
484
+ ```sh
485
+ npm ci
486
+ npm run check # banned-token lint, typecheck, tests (including the browser-like bundle test)
487
+ npm run test:browser # headless Chromium smoke test (needs a Chromium, see test/browser.test.js)
488
+ npm run golden:update # rewrite golden files: any change is an output change, review it
489
+ npm run docs:update # re-run the README and docs examples and refresh their output blocks
490
+ npm run playground:build # rebuild playground/graceholder.js after changing src/ (a test fails when it is stale)
491
+ ```
492
+
493
+ The design is in [docs/design.md](docs/design.md). MIT licensed.
package/STABILITY.md ADDED
@@ -0,0 +1,24 @@
1
+ # Stability policy
2
+
3
+ This file is generated from section 11 of [docs/design.md](docs/design.md) by `npm run stability`. Do not edit it by hand.
4
+
5
+ The key-scheme version literal is exported as `KEY_SCHEME` (currently `"gh1"`).
6
+
7
+ **Guarantee:** same `seed` + same query + same `spec` + same options + same package version => byte-identical output (`JSON.stringify` equality), on every supported runtime (Node >= 22, evergreen browsers). Across versions: within a major version, output for a given input never changes.
8
+
9
+ **What counts as an output change (needs a major bump):**
10
+ - the key scheme (`gh1` literal, separator, component set or order), hash, RNG, helper formulas (`int`, `pick`, etc.);
11
+ - any generator's value for the same draws; generator catalogue *semantics* changes;
12
+ - any vocabulary table edit, reorder or locale default change, including every table of every pack (section 17);
13
+ - inference rules: shape, type-name, count, scalar-type or generator selection for a previously inferred case; the singularizer; persona table; derivation formulas; paging and cursor formats; `anchorDate` default;
14
+ - changes to how directives/spec properties resolve (precedence).
15
+
16
+ **Opt-in realism (section 17):** `packs`, `images: "svg"` and `distributions: "realistic"` default to off, so a release that adds one cannot change existing output; once released, their output is frozen like everything else.
17
+
18
+ **Not output changes (minor/patch):** new generators, new directives or spec properties, new locales, error-message wording, performance, types, docs and the `suggestSpec` prompt. (newly recognised name heuristics are output changes, even for names that currently fall through to `T13`). Only strictly additive features that cannot affect existing inputs qualify.
19
+
20
+ **Enforcement:**
21
+ - Golden files: `test/golden/*.json` (input query + spec + options + expected output). CI runs them on Node 22 and the current LTS, and in a browser-like runner (Node with `--experimental-vm-modules` jsdom-free check is enough; plus a Playwright smoke in M5). A PR that changes a golden file requires the `breaking` label and a changeset marked major; a CI script fails otherwise.
22
+ - The XXH64/SplitMix64 vectors and vocabulary SHA-256 checksums are asserted separately.
23
+ - Dependency policy: the only runtime dependency is `graphql` (peer range `^16`, tested on 16 and 17-alpha if available). graphql-js affects execution semantics, not values, and the golden tests would catch any change. Everything affecting values (hash, RNG, vocab) is in-repo. `devDependencies` are free to float.
24
+ - A `STABILITY.md` generated from this section ships with the package; the key-scheme version literal `gh1` is exported as `KEY_SCHEME`.
package/package.json CHANGED
@@ -1,6 +1,128 @@
1
1
  {
2
2
  "name": "@johnhenry/graceholder",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Consistently repeatable generated data, in any shape you want, from a GraphQL query.",
5
+ "keywords": [
6
+ "graphql",
7
+ "mock",
8
+ "fake-data",
9
+ "deterministic",
10
+ "seeded",
11
+ "testing"
12
+ ],
13
+ "type": "module",
14
+ "main": "./src/index.js",
15
+ "types": "./src/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./src/index.d.ts",
19
+ "default": "./src/index.js"
20
+ },
21
+ "./packs": {
22
+ "types": "./src/packs/index.d.ts",
23
+ "default": "./src/packs/index.js"
24
+ },
25
+ "./packs/commerce": {
26
+ "types": "./src/packs/commerce.d.ts",
27
+ "default": "./src/packs/commerce.js"
28
+ },
29
+ "./packs/finance": {
30
+ "types": "./src/packs/finance.d.ts",
31
+ "default": "./src/packs/finance.js"
32
+ },
33
+ "./packs/geo": {
34
+ "types": "./src/packs/geo.d.ts",
35
+ "default": "./src/packs/geo.js"
36
+ },
37
+ "./packs/health": {
38
+ "types": "./src/packs/health.d.ts",
39
+ "default": "./src/packs/health.js"
40
+ },
41
+ "./apollo": {
42
+ "types": "./src/adapters/apollo.d.ts",
43
+ "default": "./src/adapters/apollo.js"
44
+ },
45
+ "./urql": {
46
+ "types": "./src/adapters/urql.d.ts",
47
+ "default": "./src/adapters/urql.js"
48
+ },
49
+ "./fetch": {
50
+ "types": "./src/adapters/fetch.d.ts",
51
+ "default": "./src/adapters/fetch.js"
52
+ },
53
+ "./fast-check": {
54
+ "types": "./src/fast-check.d.ts",
55
+ "default": "./src/fast-check.js"
56
+ },
57
+ "./package.json": "./package.json"
58
+ },
59
+ "files": [
60
+ "src/",
61
+ "STABILITY.md"
62
+ ],
63
+ "sideEffects": false,
64
+ "scripts": {
65
+ "lint": "node scripts/check-banned.mjs",
66
+ "typecheck": "tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig.dts.json",
67
+ "test": "node --test \"test/**/*.test.js\"",
68
+ "test:golden": "node --test test/golden.test.js",
69
+ "golden:update": "node scripts/update-golden.mjs",
70
+ "check": "npm run lint && npm run typecheck && npm test",
71
+ "test:browser": "GRACEHOLDER_BROWSER=1 node --test test/browser.test.js test/playground.test.js",
72
+ "stability": "node scripts/gen-stability.mjs",
73
+ "docs:update": "node scripts/docs.mjs --write",
74
+ "gen:types": "node scripts/gen-generator-types.mjs",
75
+ "playground:build": "node scripts/build-playground.mjs",
76
+ "playground:screenshot": "node scripts/playground-screenshot.mjs"
77
+ },
78
+ "author": "John Henry",
79
+ "license": "MIT",
80
+ "engines": {
81
+ "node": ">=22"
82
+ },
83
+ "publishConfig": {
84
+ "access": "public"
85
+ },
86
+ "dependencies": {
87
+ "graphql": "^16.0.0"
88
+ },
89
+ "devDependencies": {
90
+ "@apollo/client": "^4.3.5",
91
+ "@types/node": "^22.0.0",
92
+ "@urql/core": "^6.0.3",
93
+ "esbuild": "^0.28.2",
94
+ "fast-check": "^4.10.2",
95
+ "playwright-core": "^1.64.1",
96
+ "rxjs": "^7.8.2",
97
+ "typescript": "^5.6.0",
98
+ "wonka": "^6.3.6"
99
+ },
100
+ "repository": {
101
+ "type": "git",
102
+ "url": "git+https://github.com/johnhenry/graceholder.git"
103
+ },
104
+ "homepage": "https://github.com/johnhenry/graceholder#readme",
105
+ "bugs": {
106
+ "url": "https://github.com/johnhenry/graceholder/issues"
107
+ },
108
+ "peerDependencies": {
109
+ "@apollo/client": ">=3.8.0 <5",
110
+ "@urql/core": ">=5.0.0 <7",
111
+ "fast-check": ">=3.0.0",
112
+ "wonka": "^6.3.0"
113
+ },
114
+ "peerDependenciesMeta": {
115
+ "@apollo/client": {
116
+ "optional": true
117
+ },
118
+ "@urql/core": {
119
+ "optional": true
120
+ },
121
+ "fast-check": {
122
+ "optional": true
123
+ },
124
+ "wonka": {
125
+ "optional": true
126
+ }
127
+ }
128
+ }
@@ -0,0 +1,13 @@
1
+ import { ApolloLink } from "@apollo/client";
2
+ import type { World } from "../index.js";
3
+ import type { AdapterOptions } from "./shared.js";
4
+
5
+ export type GraceholderLinkOptions = AdapterOptions;
6
+
7
+ /** A terminating Apollo link that answers every operation from a graceholder world. */
8
+ export declare class GraceholderLink extends ApolloLink {
9
+ constructor(options?: GraceholderLinkOptions);
10
+ /** The world that answers; use it to pin entities or export the log. */
11
+ readonly world: World;
12
+ }
13
+ export default GraceholderLink;