@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.
- package/LICENSE +21 -0
- package/README.md +492 -2
- package/STABILITY.md +24 -0
- package/package.json +126 -4
- package/src/adapters/apollo.d.ts +13 -0
- package/src/adapters/apollo.js +48 -0
- package/src/adapters/fetch.d.ts +15 -0
- package/src/adapters/fetch.js +150 -0
- package/src/adapters/shared.d.ts +14 -0
- package/src/adapters/shared.js +65 -0
- package/src/adapters/urql.d.ts +8 -0
- package/src/adapters/urql.js +53 -0
- package/src/anchor.js +13 -0
- package/src/args.js +41 -0
- package/src/bulk/csv.js +118 -0
- package/src/bulk/ndjson.js +10 -0
- package/src/bulk/plan.js +195 -0
- package/src/bulk/shape.js +67 -0
- package/src/bulk/source.js +24 -0
- package/src/bulk/sql.js +366 -0
- package/src/bulk/stream.js +55 -0
- package/src/cursor.js +25 -0
- package/src/dbg.d.ts +720 -0
- package/src/detmath.js +74 -0
- package/src/directives.js +113 -0
- package/src/dist.js +90 -0
- package/src/errors.js +17 -0
- package/src/execute.js +577 -0
- package/src/explain.js +473 -0
- package/src/fast-check.d.ts +31 -0
- package/src/fast-check.js +58 -0
- package/src/generate.js +84 -0
- package/src/generator-types.d.ts +90 -0
- package/src/generators.js +361 -0
- package/src/image.js +55 -0
- package/src/index.d.ts +507 -0
- package/src/index.js +62 -0
- package/src/infer.js +888 -0
- package/src/key.js +30 -0
- package/src/learn.js +397 -0
- package/src/naming.js +74 -0
- package/src/pack-registry.js +59 -0
- package/src/packs/PROVENANCE.md +26 -0
- package/src/packs/commerce.d.ts +4 -0
- package/src/packs/commerce.js +83 -0
- package/src/packs/finance.d.ts +4 -0
- package/src/packs/finance.js +180 -0
- package/src/packs/geo.d.ts +4 -0
- package/src/packs/geo.js +227 -0
- package/src/packs/health.d.ts +4 -0
- package/src/packs/health.js +76 -0
- package/src/packs/index.d.ts +7 -0
- package/src/packs/index.js +9 -0
- package/src/rng.js +89 -0
- package/src/rules.js +228 -0
- package/src/scenario.js +120 -0
- package/src/schema.js +148 -0
- package/src/spec.js +235 -0
- package/src/suggest.js +138 -0
- package/src/typed.d.ts +723 -0
- package/src/vocab/PROVENANCE.md +12 -0
- package/src/vocab/en.js +106 -0
- package/src/world.js +461 -0
- package/src/xxh64.js +87 -0
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
import type { DocumentNode, GraphQLError, GraphQLSchema, IntrospectionQuery } from "graphql";
|
|
2
|
+
import type { ResultOf, ItemOf } from "./typed.js";
|
|
3
|
+
|
|
4
|
+
export type { ResultOf, ItemOf };
|
|
5
|
+
|
|
6
|
+
/** Key-scheme version literal (frozen at 1.0.0). */
|
|
7
|
+
export declare const KEY_SCHEME: "gh1";
|
|
8
|
+
|
|
9
|
+
/** A schema for schema mode: SDL text, a parsed SDL document, an introspection result, or a graphql-js schema. */
|
|
10
|
+
export type SchemaInput = string | DocumentNode | GraphQLSchema | IntrospectionQuery | { data: IntrospectionQuery };
|
|
11
|
+
/** Edge-case modes (docs/design.md section 16, Scenarios). Combine them in an array. */
|
|
12
|
+
export type Scenario = "empty" | "huge" | "nulls" | "unicode" | "boundary" | "errors";
|
|
13
|
+
/** The scenario names, in the order a set is normalised to. */
|
|
14
|
+
export declare const SCENARIOS: readonly Scenario[];
|
|
15
|
+
|
|
16
|
+
export interface ScenarioOptions {
|
|
17
|
+
/** Share of eligible fields that fail under the `errors` scenario, from 0 to 1. Default 0.25. */
|
|
18
|
+
errorRate?: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** A GraphQL error in JSON form (what the `errors` scenario returns). */
|
|
22
|
+
export interface ResultError {
|
|
23
|
+
message: string;
|
|
24
|
+
locations?: Array<{ line: number; column: number }>;
|
|
25
|
+
path?: Array<string | number>;
|
|
26
|
+
extensions?: Record<string, unknown>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface Options {
|
|
30
|
+
/** Default `"graceholder"`. Numbers are stringified (`String(n)`). */
|
|
31
|
+
seed?: string | number;
|
|
32
|
+
/** Variables for the operation. */
|
|
33
|
+
variables?: Record<string, unknown>;
|
|
34
|
+
operationName?: string;
|
|
35
|
+
/** Default `"en"`, the only locale in this release; others throw `unknown-locale`. */
|
|
36
|
+
locale?: string;
|
|
37
|
+
/** Default constant `"2025-01-01T00:00:00.000Z"`; the clock is never read. */
|
|
38
|
+
anchorDate?: string | Date;
|
|
39
|
+
/** `"throw"` (default) throws a {@link GraceholderError}; `"result"` returns `{ data, errors }`. */
|
|
40
|
+
errors?: "throw" | "result";
|
|
41
|
+
/** Side-car spec keyed by path pattern (`users.name`, `*.status`, `**.createdAt`, `@User.email`). */
|
|
42
|
+
spec?: Spec;
|
|
43
|
+
/** Make a spec path that matches no field an error (`unused-spec-path`) instead of a reported decision. */
|
|
44
|
+
strict?: boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Vocabulary packs to enable, by registered name or as objects imported from
|
|
47
|
+
* `@johnhenry/graceholder/packs/<name>`. Default none. Packs add coherent geo, commerce, finance and
|
|
48
|
+
* (fictional) health data; see docs/api.md. Pass `["geo"]` only after `registerPack(geo)`.
|
|
49
|
+
*/
|
|
50
|
+
packs?: Array<string | Pack>;
|
|
51
|
+
/**
|
|
52
|
+
* `"svg"` makes image, avatar, thumbnail, picture, photo and logo fields deterministic inline SVG data
|
|
53
|
+
* URIs (initials avatars, geometric placeholders) instead of example URLs. Default `"url"`.
|
|
54
|
+
*/
|
|
55
|
+
images?: "url" | "svg";
|
|
56
|
+
/**
|
|
57
|
+
* `"realistic"` makes popularity-like fields (views, likes, followers, sales, ...) heavy-tailed and
|
|
58
|
+
* turns fields named like `salesHistory { date value }` into time series. Default `"uniform"` (the 0.x behaviour).
|
|
59
|
+
*/
|
|
60
|
+
distributions?: "uniform" | "realistic";
|
|
61
|
+
/**
|
|
62
|
+
* Schema mode: the query is validated against this schema, and enums, nullability, custom scalars,
|
|
63
|
+
* lists and unions/interfaces come from it instead of from name heuristics.
|
|
64
|
+
*/
|
|
65
|
+
schema?: SchemaInput;
|
|
66
|
+
/**
|
|
67
|
+
* Edge-case mode(s), one name or an array: `empty`, `huge`, `nulls`, `unicode`, `boundary`, `errors`.
|
|
68
|
+
* Applies to every field unless a spec entry sets its own `scenario`. Default none (output unchanged).
|
|
69
|
+
*/
|
|
70
|
+
scenario?: Scenario | Scenario[] | "none";
|
|
71
|
+
scenarioOptions?: ScenarioOptions;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Distribution names accepted by the `dist` argument of `int`, `float` and `money`. */
|
|
75
|
+
export type Distribution = "uniform" | "normal" | "lognormal" | "zipf" | "powerlaw";
|
|
76
|
+
|
|
77
|
+
/** A vocabulary pack: frozen tables plus the generators and name rules that use them. */
|
|
78
|
+
export interface Pack {
|
|
79
|
+
readonly name: string;
|
|
80
|
+
readonly version: number;
|
|
81
|
+
readonly vocab: Readonly<Record<string, readonly unknown[]>>;
|
|
82
|
+
/** SDL scalar type of generators whose output is not a String. */
|
|
83
|
+
readonly types?: Readonly<Record<string, "Int" | "Float" | "Boolean" | "DateTime" | "String">>;
|
|
84
|
+
generators(vocab: Record<string, any>, base: Record<string, (ctx: any) => unknown>): Record<string, (ctx: any) => unknown>;
|
|
85
|
+
rules?(ctx: { tokens: string[]; first: string; last: string; kind: string; typeName: string; list: boolean }): { sdl: string; gen: string; args?: Record<string, unknown>; nullable?: boolean } | null;
|
|
86
|
+
inherit?(typeName: string): boolean;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Make a pack addressable by name in `options.packs`. Registering the same pack twice is fine. */
|
|
90
|
+
export declare function registerPack(pack: Pack): Pack;
|
|
91
|
+
/** Names of the packs registered so far. */
|
|
92
|
+
export declare function registeredPacks(): string[];
|
|
93
|
+
|
|
94
|
+
export type Count = number | [number, number] | { min: number; max: number };
|
|
95
|
+
|
|
96
|
+
export interface FieldSpec {
|
|
97
|
+
list?: boolean;
|
|
98
|
+
one?: boolean;
|
|
99
|
+
count?: Count;
|
|
100
|
+
gen?: string | { name: string; args?: Record<string, unknown> };
|
|
101
|
+
pattern?: string;
|
|
102
|
+
pick?: Array<unknown | { value: unknown; weight: number }>;
|
|
103
|
+
nullRate?: number;
|
|
104
|
+
derive?: { from: string };
|
|
105
|
+
type?: string;
|
|
106
|
+
seed?: string | number;
|
|
107
|
+
/** Identifying argument name(s); several names form a composite key. */
|
|
108
|
+
key?: string | string[];
|
|
109
|
+
/** Draw members of a nested collection from the type pool. */
|
|
110
|
+
shared?: boolean;
|
|
111
|
+
/** Do not share the type pool: entities are owned by their parent. */
|
|
112
|
+
own?: boolean;
|
|
113
|
+
/** Scenario(s) for this field only (replacing the global ones); `"none"` or `[]` opts the field out. */
|
|
114
|
+
scenario?: Scenario | Scenario[] | "none";
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export type MutationOp = "create" | "update" | "delete";
|
|
118
|
+
|
|
119
|
+
/** How mutations are read (docs/design.md section 16, World); used by a world only. */
|
|
120
|
+
export interface MutationSpec {
|
|
121
|
+
/** Extra verbs: the first word of a mutation field name. */
|
|
122
|
+
verbs?: Record<string, MutationOp>;
|
|
123
|
+
/** Per mutation field: what it does, which type it affects, where its id and input are. */
|
|
124
|
+
fields?: Record<string, { op: MutationOp; type?: string; idArg?: string; input?: string; read?: string }>;
|
|
125
|
+
/** `"Post.authorId": "User"` makes a created Post belong to that User's `posts`-like collections. */
|
|
126
|
+
relations?: Record<string, string>;
|
|
127
|
+
/** Where created entities go: the end of finite collections and the start of endless ones by default. */
|
|
128
|
+
position?: "start" | "end";
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export type Spec = { [path: string]: FieldSpec | { poolSize?: number; endlessTotal?: number } | MutationSpec } & {
|
|
132
|
+
$options?: { poolSize?: number; endlessTotal?: number };
|
|
133
|
+
$mutations?: MutationSpec;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
export type Data = Record<string, unknown>;
|
|
137
|
+
|
|
138
|
+
export interface ResultMode<D = Data> {
|
|
139
|
+
data?: D;
|
|
140
|
+
errors?: readonly GraphQLError[] | ResultError[];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Generate data for a query. Synchronous; same inputs give byte-identical output.
|
|
145
|
+
*
|
|
146
|
+
* When `query` is a string literal the result is typed from the query text (see {@link ResultOf}:
|
|
147
|
+
* selection sets, aliases, lists, nested lists, fragments, unions, connections and scalar types by
|
|
148
|
+
* field name). A `string` that is not a literal, or a parsed document, gives `Record<string, unknown>`.
|
|
149
|
+
*/
|
|
150
|
+
export declare function graceholder<Q extends string>(query: Q, options?: Options & { errors?: "throw" }): ResultOf<Q>;
|
|
151
|
+
export declare function graceholder<Q extends string>(query: Q, options: Options & { errors: "result" }): ResultMode<ResultOf<Q>>;
|
|
152
|
+
export declare function graceholder(query: DocumentNode, options?: Options & { errors?: "throw" }): Data;
|
|
153
|
+
export declare function graceholder(query: DocumentNode, options: Options & { errors: "result" }): ResultMode;
|
|
154
|
+
export declare function graceholder(query: string | DocumentNode, options: Options): Data | ResultMode;
|
|
155
|
+
|
|
156
|
+
export interface Decision {
|
|
157
|
+
path: string;
|
|
158
|
+
field: string;
|
|
159
|
+
kind: "list" | "object" | "scalar" | "arg" | "type" | "count" | "null" | "key" | "seed" | "unused" | "nullable";
|
|
160
|
+
value: unknown;
|
|
161
|
+
source: "directive" | "spec" | "heuristic" | "default" | "schema";
|
|
162
|
+
rule?: string;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export interface InferResult {
|
|
166
|
+
sdl: string;
|
|
167
|
+
decisions: Decision[];
|
|
168
|
+
/** The selected operation and the fragments it uses. */
|
|
169
|
+
document: DocumentNode;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Infer the SDL (and the decisions behind it) from a query alone. */
|
|
173
|
+
export declare function inferSchema(
|
|
174
|
+
query: string | DocumentNode,
|
|
175
|
+
spec?: Spec,
|
|
176
|
+
options?: Pick<Options, "operationName" | "variables" | "strict" | "packs" | "images" | "distributions" | "schema" | "scenario">,
|
|
177
|
+
): InferResult;
|
|
178
|
+
|
|
179
|
+
export type ErrorCode =
|
|
180
|
+
| "unknown-directive"
|
|
181
|
+
| "unknown-generator"
|
|
182
|
+
| "bad-generator-args"
|
|
183
|
+
| "conflicting-shape"
|
|
184
|
+
| "bad-operation"
|
|
185
|
+
| "bad-options"
|
|
186
|
+
| "bad-cursor"
|
|
187
|
+
| "bad-arguments"
|
|
188
|
+
| "unknown-locale"
|
|
189
|
+
| "bad-spec"
|
|
190
|
+
| "bad-directive"
|
|
191
|
+
| "bad-derive"
|
|
192
|
+
| "conflicting-overrides"
|
|
193
|
+
| "unused-spec-path"
|
|
194
|
+
| "bad-suggestion"
|
|
195
|
+
| "bad-pack"
|
|
196
|
+
| "unknown-pack"
|
|
197
|
+
| "bad-schema"
|
|
198
|
+
| "schema-conflict"
|
|
199
|
+
| "bad-samples"
|
|
200
|
+
| "unsupported"
|
|
201
|
+
| "bad-mutation"
|
|
202
|
+
| "bad-path"
|
|
203
|
+
| "not-found"
|
|
204
|
+
| "graphql"
|
|
205
|
+
| "internal";
|
|
206
|
+
|
|
207
|
+
export declare class GraceholderError extends Error {
|
|
208
|
+
code: ErrorCode;
|
|
209
|
+
path?: string;
|
|
210
|
+
hint?: string;
|
|
211
|
+
/** For `graphql` errors, every underlying GraphQL error. */
|
|
212
|
+
errors?: readonly GraphQLError[] | ResultError[];
|
|
213
|
+
/** For `graphql` errors raised by the `errors` scenario, the partial data. */
|
|
214
|
+
data?: Data | null;
|
|
215
|
+
constructor(code: ErrorCode, message: string, extra?: { path?: string; hint?: string; cause?: unknown });
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Version tag at the top of the suggestSpec prompt. */
|
|
219
|
+
export declare const SUGGEST_PROMPT_VERSION: string;
|
|
220
|
+
|
|
221
|
+
/** The prompt `suggestSpec` sends (query, inferred SDL and decisions, spec grammar, generator names). */
|
|
222
|
+
export declare function buildSuggestPrompt(query: string, spec?: Spec, options?: Pick<Options, "packs" | "distributions" | "images">): string;
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Authoring-time helper: ask an injected model for a spec, validate it against the query, and offer
|
|
226
|
+
* one repair round. The core never calls it and imports no LLM SDK.
|
|
227
|
+
* @throws {GraceholderError} `bad-suggestion` when the reply is still invalid after the repair round
|
|
228
|
+
*/
|
|
229
|
+
export declare function suggestSpec(
|
|
230
|
+
query: string,
|
|
231
|
+
opts: { llm: (prompt: string) => Promise<string>; spec?: Spec } & Pick<Options, "packs" | "distributions" | "images">,
|
|
232
|
+
): Promise<Spec>;
|
|
233
|
+
|
|
234
|
+
export type WorldEntry =
|
|
235
|
+
| { op: "pin"; type: string; key: string; fields: Record<string, unknown> }
|
|
236
|
+
| { op: "create"; type: string; key: string; fields: Record<string, unknown>; rel?: Array<{ ownerType: string; ownerKey: string }>; mutation?: string }
|
|
237
|
+
| { op: "update"; type: string; key: string; fields: Record<string, unknown>; rel?: Array<{ ownerType: string; ownerKey: string }>; mutation?: string }
|
|
238
|
+
| { op: "delete"; type: string; key: string; mutation?: string };
|
|
239
|
+
|
|
240
|
+
export interface ExportedWorld {
|
|
241
|
+
seed: string | number;
|
|
242
|
+
spec?: Spec;
|
|
243
|
+
anchorDate: string;
|
|
244
|
+
locale: string;
|
|
245
|
+
/** Present only when the world was created with these options; packs are exported by name (use `registerPack` before replaying). */
|
|
246
|
+
packs?: string[];
|
|
247
|
+
images?: "url" | "svg";
|
|
248
|
+
distributions?: "uniform" | "realistic";
|
|
249
|
+
scenario?: Scenario[];
|
|
250
|
+
scenarioOptions?: ScenarioOptions;
|
|
251
|
+
log: WorldEntry[];
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export interface WorldOptions {
|
|
255
|
+
seed?: string | number;
|
|
256
|
+
spec?: Spec;
|
|
257
|
+
anchorDate?: string | Date;
|
|
258
|
+
locale?: string;
|
|
259
|
+
strict?: boolean;
|
|
260
|
+
/** Same meaning as in {@link Options}; they apply to every query of the world. */
|
|
261
|
+
packs?: Array<string | Pack>;
|
|
262
|
+
images?: "url" | "svg";
|
|
263
|
+
distributions?: "uniform" | "realistic";
|
|
264
|
+
/** Schema mode for the world's queries. */
|
|
265
|
+
schema?: SchemaInput;
|
|
266
|
+
scenario?: Scenario | Scenario[] | "none";
|
|
267
|
+
scenarioOptions?: ScenarioOptions;
|
|
268
|
+
/** A log from `world.export()`; replayed at creation. */
|
|
269
|
+
log?: WorldEntry[];
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export interface WorldResult<D = Data> {
|
|
273
|
+
data?: D | null;
|
|
274
|
+
errors?: Array<{ message: string; locations?: Array<{ line: number; column: number }>; path?: Array<string | number>; extensions?: Record<string, unknown> }>;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export interface World {
|
|
278
|
+
/** Run a query or a mutation. Never throws for a bad operation: failures come back as `errors`. */
|
|
279
|
+
query<Q extends string>(query: Q, options?: { variables?: Record<string, unknown>; operationName?: string }): WorldResult<ResultOf<Q>>;
|
|
280
|
+
query(query: DocumentNode, options?: { variables?: Record<string, unknown>; operationName?: string }): WorldResult;
|
|
281
|
+
/** The root collection of a query as a lazy iterable (see {@link stream}); reflects pins and mutations when each window is generated. */
|
|
282
|
+
stream<Q extends string, F extends string | undefined = undefined>(query: Q, options?: Omit<StreamOptions, keyof Options> & Pick<Options, "variables" | "operationName"> & { field?: F }): Stream<ItemOf<Q, F>>;
|
|
283
|
+
stream(query: string | DocumentNode, options?: Omit<StreamOptions, keyof Options> & Pick<Options, "variables" | "operationName">): Stream;
|
|
284
|
+
/** Override fields of the entity `"Type:key"`. */
|
|
285
|
+
pin(ref: string, fields: Record<string, unknown>): World;
|
|
286
|
+
export(): ExportedWorld;
|
|
287
|
+
reset(): World;
|
|
288
|
+
/** Like the top-level {@link explain}, for this world. */
|
|
289
|
+
explain(query: string | DocumentNode, path: string | Array<string | number>, options?: Omit<Options, "errors" | "seed" | "spec" | "anchorDate" | "locale">): Explanation;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** A persistent, deterministic world shared across queries. */
|
|
293
|
+
export declare function createWorld(options?: WorldOptions): World;
|
|
294
|
+
|
|
295
|
+
export interface SpecFromSamplesOptions {
|
|
296
|
+
/** Schema mode: enums (and their frequencies), nullability, scalar types and list-ness come from the schema. */
|
|
297
|
+
schema?: SchemaInput;
|
|
298
|
+
/**
|
|
299
|
+
* Allow enum-like sets of string values to be copied into the spec as `pick`. `true` allows every
|
|
300
|
+
* field; an array allows only the listed field paths. Default `false`: free text is never copied.
|
|
301
|
+
*/
|
|
302
|
+
allowValues?: boolean | string[];
|
|
303
|
+
/** Largest number of distinct values that counts as an enumeration (default 12). */
|
|
304
|
+
maxEnum?: number;
|
|
305
|
+
/** Dates are learned as days before this anchor (default: the library's default anchor). */
|
|
306
|
+
anchorDate?: string | Date;
|
|
307
|
+
operationName?: string;
|
|
308
|
+
variables?: Record<string, unknown>;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export interface SampleNote {
|
|
312
|
+
path: string;
|
|
313
|
+
kind: "withheld" | "kept-heuristic" | "skipped";
|
|
314
|
+
message: string;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Infer a side-car spec from one or more real responses to a query. Deterministic; the result is a plain
|
|
319
|
+
* object to commit. Never copies free text; see `allowValues`.
|
|
320
|
+
* @throws {GraceholderError} `bad-samples` when a sample does not fit the query (or the schema)
|
|
321
|
+
*/
|
|
322
|
+
export declare function specFromSamples(
|
|
323
|
+
query: string | DocumentNode,
|
|
324
|
+
samples: Data | { data: Data } | Array<Data | { data: Data }>,
|
|
325
|
+
opts?: SpecFromSamplesOptions,
|
|
326
|
+
): Spec;
|
|
327
|
+
|
|
328
|
+
/** Like {@link specFromSamples}, and also returns notes about what was withheld or left to the heuristics. */
|
|
329
|
+
export declare function analyzeSamples(
|
|
330
|
+
query: string | DocumentNode,
|
|
331
|
+
samples: Data | { data: Data } | Array<Data | { data: Data }>,
|
|
332
|
+
opts?: SpecFromSamplesOptions,
|
|
333
|
+
): { spec: Spec; notes: SampleNote[] };
|
|
334
|
+
|
|
335
|
+
// ---- bulk generation (docs/design.md section 19) -------------------------------------------------
|
|
336
|
+
|
|
337
|
+
export interface StreamOptions extends Omit<Options, "errors"> {
|
|
338
|
+
/** How many items to yield. Required unless the collection has a declared size (`@count`, or a spec `count`). */
|
|
339
|
+
count?: number;
|
|
340
|
+
/** Index of the first item (default 0): item k equals item k of `field(offset: k, first: 1)`. */
|
|
341
|
+
offset?: number;
|
|
342
|
+
/** Items generated per window (default 500, at most 10000). Never changes the output. */
|
|
343
|
+
chunkSize?: number;
|
|
344
|
+
/** Response key of the root collection to stream; default the first root list or connection. */
|
|
345
|
+
field?: string;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** A lazily generated collection: a sync and an async iterable, iterable any number of times. */
|
|
349
|
+
export interface Stream<T = Record<string, any>> extends Iterable<T>, AsyncIterable<T> {
|
|
350
|
+
/** Response key of the streamed root field. */
|
|
351
|
+
readonly field: string;
|
|
352
|
+
/** The items in windows of at most `chunkSize`. */
|
|
353
|
+
chunks(): Generator<T[]>;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/** The root collection of a query as a lazy iterable of items, with bounded memory. */
|
|
357
|
+
export declare function stream<Q extends string, F extends string | undefined = undefined>(query: Q, options?: StreamOptions & { field?: F }): Stream<ItemOf<Q, F>>;
|
|
358
|
+
export declare function stream<T = Record<string, any>>(query: string | DocumentNode, options?: StreamOptions): Stream<T>;
|
|
359
|
+
|
|
360
|
+
/** One compact JSON document per item, each followed by a newline. A sync source gives a sync generator. */
|
|
361
|
+
export declare function toNDJSON(source: Iterable<unknown>): Generator<string>;
|
|
362
|
+
export declare function toNDJSON(source: AsyncIterable<unknown>): AsyncGenerator<string>;
|
|
363
|
+
|
|
364
|
+
export interface CsvOptions {
|
|
365
|
+
/** One character, default `","`. */
|
|
366
|
+
delimiter?: string;
|
|
367
|
+
/** Line ending, default `"\r\n"` (RFC 4180). */
|
|
368
|
+
eol?: "\r\n" | "\n" | "\r";
|
|
369
|
+
/** Write the header line (default true). */
|
|
370
|
+
header?: boolean;
|
|
371
|
+
/** Column paths to write, in order (default: every flattened path). */
|
|
372
|
+
columns?: string[];
|
|
373
|
+
/** `"json"` (default) writes a list as one JSON cell; `"explode"` writes one row per element. */
|
|
374
|
+
lists?: "json" | "explode";
|
|
375
|
+
/** Explode only these list paths (implies exploding); other lists stay JSON. */
|
|
376
|
+
explode?: string[];
|
|
377
|
+
}
|
|
378
|
+
/** CSV with flattened column paths (`address.city`). */
|
|
379
|
+
export declare function toCSV(source: Iterable<unknown>, options?: CsvOptions): Generator<string>;
|
|
380
|
+
export declare function toCSV(source: AsyncIterable<unknown>, options?: CsvOptions): AsyncGenerator<string>;
|
|
381
|
+
|
|
382
|
+
export interface SqlOptions extends StreamOptions {
|
|
383
|
+
/** Default `"postgres"`. */
|
|
384
|
+
dialect?: "postgres" | "sqlite";
|
|
385
|
+
/** Write CREATE TABLE statements (default true). */
|
|
386
|
+
ddl?: boolean;
|
|
387
|
+
/** Write DROP TABLE IF EXISTS before the CREATE statements (default false). */
|
|
388
|
+
dropExisting?: boolean;
|
|
389
|
+
/** Wrap the script in BEGIN/COMMIT (default true). */
|
|
390
|
+
transaction?: boolean;
|
|
391
|
+
/** `"snake"` (default) turns `BlogPost`/`createdAt` into `blog_post`/`created_at`; `"preserve"` keeps the names. */
|
|
392
|
+
names?: "snake" | "preserve";
|
|
393
|
+
tablePrefix?: string;
|
|
394
|
+
/** Table name per entity type, overriding `names`. */
|
|
395
|
+
tableNames?: Record<string, string>;
|
|
396
|
+
/** Write the preamble (BEGIN and DDL); default true. */
|
|
397
|
+
header?: boolean;
|
|
398
|
+
/** Write the closing COMMIT; default true. */
|
|
399
|
+
footer?: boolean;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
export interface SqlTable {
|
|
403
|
+
type: string;
|
|
404
|
+
table: string;
|
|
405
|
+
primaryKey: string;
|
|
406
|
+
columns: Array<{ name: string; type: string; references?: string }>;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
export interface SqlWriter {
|
|
410
|
+
dialect: "postgres" | "sqlite";
|
|
411
|
+
/** BEGIN, optional DROPs, CREATE TABLEs and foreign keys. */
|
|
412
|
+
header(): string;
|
|
413
|
+
/** INSERTs for items `[from, to)` (absolute indexes); call in order, earlier items are replayed silently. */
|
|
414
|
+
rows(from?: number, to?: number): Generator<string>;
|
|
415
|
+
/** COMMIT (empty when `transaction` is false). */
|
|
416
|
+
footer(): string;
|
|
417
|
+
/** The planned tables, in creation order. */
|
|
418
|
+
tables(): SqlTable[];
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/** A resumable SQL writer for a query or a {@link Stream}. */
|
|
422
|
+
export declare function sqlWriter(source: string | DocumentNode | Stream, options?: SqlOptions): SqlWriter;
|
|
423
|
+
/** SQL (DDL, INSERTs, COMMIT) for the root collection of a query and everything under it, as statements. */
|
|
424
|
+
export declare function toSQL(source: string | DocumentNode | Stream, options?: SqlOptions): Generator<string>;
|
|
425
|
+
|
|
426
|
+
// ---- explain (docs/design.md section 20) ---------------------------------------------------------
|
|
427
|
+
|
|
428
|
+
/** A key as the library builds it for a value: the exact string (fields are separated by U+001F), its parts and its XXH64 hash. */
|
|
429
|
+
export interface ExplainedKey {
|
|
430
|
+
string: string;
|
|
431
|
+
parts: string[];
|
|
432
|
+
hash: string;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
export interface ExplainedStep {
|
|
436
|
+
path: string;
|
|
437
|
+
field: string;
|
|
438
|
+
/** `list`, `edges`, `nodes`, `connection` (a collection) or `entity` (a singular field). */
|
|
439
|
+
via: string;
|
|
440
|
+
mode?: "args" | "root" | "pool" | "owned" | "inherited" | "connection-member";
|
|
441
|
+
index?: number;
|
|
442
|
+
/** The entity key (a list member, or the entity a singular field refers to). */
|
|
443
|
+
key?: string;
|
|
444
|
+
/** Drawn from the shared pool of the type (true) or owned by its parent (false). */
|
|
445
|
+
shared?: boolean;
|
|
446
|
+
size?: number;
|
|
447
|
+
window?: { start: number; total: number | null; endless: boolean };
|
|
448
|
+
countKey?: string;
|
|
449
|
+
declaredCount?: unknown;
|
|
450
|
+
note?: string;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
export interface ExplainedCandidate {
|
|
454
|
+
source: "directive" | "spec" | "heuristic" | "schema";
|
|
455
|
+
/** For a spec candidate: the spec key and how it matched (`exact`, `*`, `**` or `@Type`). */
|
|
456
|
+
entry?: string;
|
|
457
|
+
matches?: string;
|
|
458
|
+
rule?: string;
|
|
459
|
+
value?: unknown;
|
|
460
|
+
description?: string;
|
|
461
|
+
/** True for the candidate that decided the property. */
|
|
462
|
+
wins: boolean;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/** What `explain` reports for a value (docs/api.md, explain). Every field is plain JSON. */
|
|
466
|
+
export interface Explanation {
|
|
467
|
+
path: string;
|
|
468
|
+
/** The value in the output (absent when the path is `unavailable` or the field failed). */
|
|
469
|
+
value?: unknown;
|
|
470
|
+
kind: "scalar" | "scalar-list" | "list-element" | "object" | "list" | "connection" | "item" | "connection-member" | "unavailable";
|
|
471
|
+
/** For `unavailable`: the first null or failed value on the way, and why. */
|
|
472
|
+
blockedAt?: string;
|
|
473
|
+
because?: string;
|
|
474
|
+
by?: Record<string, unknown>;
|
|
475
|
+
error?: { message: string; extensions?: Record<string, unknown> };
|
|
476
|
+
field?: { name: string; alias?: string; parentType: string; path: string; type: string; list: boolean; nullable: boolean };
|
|
477
|
+
entity?: { identity: string; type: string; key: string; shared?: boolean; how: ExplainedStep[]; note?: string };
|
|
478
|
+
collection?: { size: number; window?: { start: number; total: number | null; endless: boolean }; declaredCount?: unknown; countKey?: string; members?: string };
|
|
479
|
+
key?: ExplainedKey;
|
|
480
|
+
generator?: { name: string; args: Record<string, unknown>; outputType: string };
|
|
481
|
+
rule?: { id: string; source: string; description?: string; pack?: string };
|
|
482
|
+
listRule?: { id: string; source: string; description?: string; isList: boolean };
|
|
483
|
+
decisions?: Array<{ kind: string; value: unknown; source: string; rule?: string }>;
|
|
484
|
+
precedence?: Array<{ property: string; winner: string | undefined; candidates: ExplainedCandidate[] }>;
|
|
485
|
+
persona?: { type: string; key: string; inherited?: boolean; slots?: Array<{ name: string; key: string }>; pinned?: Record<string, string> };
|
|
486
|
+
derivation?: { kind: "derive" | "count.of" | "order"; [k: string]: unknown };
|
|
487
|
+
nullRate?: { rate: number; draw: number; key: string; hit: boolean };
|
|
488
|
+
scenario?: { names: Scenario[]; source: "option" | "spec"; effect: string };
|
|
489
|
+
pin?: { entity: string; field?: string; kind: "pin" | "created" | "updated" | "deleted"; value: unknown };
|
|
490
|
+
/** The seed in effect and where it came from (`option`, `default`, `directive`, `spec`, `operation directive`, or an enclosing field). */
|
|
491
|
+
seed?: { value: string; source: string; at?: string };
|
|
492
|
+
/** For Relay machinery (`totalCount`, `cursor`, paging info), which has no generator of its own. */
|
|
493
|
+
special?: { name: string; description: string };
|
|
494
|
+
summary: string;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Explain why the value at `path` looks the way it does: its key string, generator and arguments, the rule,
|
|
499
|
+
* spec entry or directive that decided it (with the precedence), the persona slots or derivation inputs,
|
|
500
|
+
* the entity it belongs to, the scenario effect and any pin. The query is run once with a read-only trace:
|
|
501
|
+
* explaining never changes an output.
|
|
502
|
+
*
|
|
503
|
+
* `path` uses response keys (aliases) and zero-based list indexes: `users.3.email` or `["users", 3, "email"]`.
|
|
504
|
+
* Pass `{ world }` to explain a value of a world (pins and mutations are taken into account).
|
|
505
|
+
* @throws {GraceholderError} `bad-path` when the path names nothing in the result
|
|
506
|
+
*/
|
|
507
|
+
export declare function explain(query: string | DocumentNode, path: string | Array<string | number>, options?: Omit<Options, "errors"> & { world?: World }): Explanation;
|
package/src/index.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// @johnhenry/graceholder: consistently repeatable generated data from a GraphQL query.
|
|
2
|
+
// Design: docs/design.md. Every value is a pure function of a key (section 2).
|
|
3
|
+
import { infer } from "./infer.js";
|
|
4
|
+
import { GraceholderError } from "./errors.js";
|
|
5
|
+
import { generateResult } from "./generate.js";
|
|
6
|
+
import { KEY_SCHEME } from "./key.js";
|
|
7
|
+
import { registerPack, registeredPacks } from "./pack-registry.js";
|
|
8
|
+
import { realismOptions } from "./generate.js";
|
|
9
|
+
import { ScenarioError, SCENARIOS, globalScenarios } from "./scenario.js";
|
|
10
|
+
|
|
11
|
+
export { GraceholderError, KEY_SCHEME, registerPack, registeredPacks, SCENARIOS };
|
|
12
|
+
export { createWorld } from "./world.js";
|
|
13
|
+
export { explain } from "./explain.js";
|
|
14
|
+
export { stream } from "./bulk/stream.js";
|
|
15
|
+
export { toNDJSON } from "./bulk/ndjson.js";
|
|
16
|
+
export { toCSV } from "./bulk/csv.js";
|
|
17
|
+
export { toSQL, sqlWriter } from "./bulk/sql.js";
|
|
18
|
+
export { suggestSpec, buildSuggestPrompt, SUGGEST_PROMPT_VERSION } from "./suggest.js";
|
|
19
|
+
export { specFromSamples, analyzeSamples } from "./learn.js";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Generate data for a query. Synchronous and deterministic.
|
|
23
|
+
* @param {string|import("graphql").DocumentNode} query
|
|
24
|
+
* @param {import("./index.js").Options} [options]
|
|
25
|
+
*/
|
|
26
|
+
export function graceholder(query, options = {}) {
|
|
27
|
+
const mode = options.errors ?? "throw";
|
|
28
|
+
try {
|
|
29
|
+
const result = generateResult(query, options);
|
|
30
|
+
if (result.errors?.length && result.errors.every((e) => e.originalError instanceof ScenarioError)) {
|
|
31
|
+
// the errors scenario: a partial result. Scenario errors are plain JSON, in the usual GraphQL response shape.
|
|
32
|
+
const errors = result.errors.map((e) => e.toJSON());
|
|
33
|
+
if (mode === "result") return { data: result.data ?? null, errors };
|
|
34
|
+
throw new GraceholderError("graphql", errors[0].message, { cause: result.errors[0], errors, data: result.data ?? null });
|
|
35
|
+
}
|
|
36
|
+
if (result.errors?.length) {
|
|
37
|
+
// an error raised by one of our resolvers (bad cursor, bad arguments) keeps its own code
|
|
38
|
+
const own = result.errors.find((e) => e.originalError instanceof GraceholderError);
|
|
39
|
+
if (own) throw own.originalError;
|
|
40
|
+
throw new GraceholderError("graphql", result.errors[0].message, { cause: result.errors[0], errors: result.errors });
|
|
41
|
+
}
|
|
42
|
+
const data = result.data;
|
|
43
|
+
return mode === "result" ? { data } : data;
|
|
44
|
+
} catch (e) {
|
|
45
|
+
if (mode === "result" && e instanceof GraceholderError && e.code === "graphql") {
|
|
46
|
+
return { errors: e.errors ?? [e.cause ?? e] };
|
|
47
|
+
}
|
|
48
|
+
throw e;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Infer the SDL and the decisions behind it, without generating data.
|
|
54
|
+
* @param {string|import("graphql").DocumentNode} query
|
|
55
|
+
* @param {Record<string, any>} [spec] side-car spec (section 5.2)
|
|
56
|
+
* @param {{ operationName?: string, variables?: Record<string, unknown>, strict?: boolean, distributions?: "uniform"|"realistic", packs?: any[], images?: "url"|"svg", schema?: any, scenario?: string|string[] }} [options]
|
|
57
|
+
*/
|
|
58
|
+
export function inferSchema(query, spec, options = {}) {
|
|
59
|
+
const realism = realismOptions(options);
|
|
60
|
+
const { sdl, decisions, document } = infer(query, { scenario: globalScenarios(options.scenario), operationName: options.operationName, variables: options.variables, strict: options.strict, spec, schema: options.schema, packs: realism.packs, realistic: realism.realistic });
|
|
61
|
+
return { sdl, decisions, document };
|
|
62
|
+
}
|