@substrat-run/model-emit 0.5.3 → 0.7.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.
@@ -0,0 +1,107 @@
1
+ /**
2
+ * A vertical's browser client, rendered from its model.
3
+ *
4
+ * The pure half of `tools/client-emit.mts` — everything from "a Zod schema" to "a
5
+ * string of TypeScript", with no filesystem and no `process.exit` in it. It lives here
6
+ * rather than in the tool for one reason: a Zod→TypeScript printer is the kind of code
7
+ * that is wrong in ways a drift check cannot see. `--check` re-emits and compares, so it
8
+ * catches a client that fell behind its model; it cannot catch a client that has been
9
+ * confidently mis-spelling `z.union([...])` inside an array since the day it was written.
10
+ * That needs a test with the schema on one side and the expected string on the other,
11
+ * and a test needs something importable.
12
+ *
13
+ * Its home is this package because that is already this package's job: build-time
14
+ * tooling over a Substrat model. `emitTables` turns entities into DDL; this turns
15
+ * entities and operations into a typed fetch client.
16
+ *
17
+ * ## What it emits
18
+ *
19
+ * Standalone TypeScript with no imports at all. A vertical's SPA is a separate Vite
20
+ * package that depends on neither `@substrat-run/contracts` nor zod and must keep
21
+ * depending on neither — and a checked-in artifact that re-exports its meaning from
22
+ * another package is not reviewable in a diff.
23
+ *
24
+ * ## What it refuses
25
+ *
26
+ * A schema it cannot spell raises `ClientEmitError` naming the operation and the field.
27
+ * Never `unknown`, never a skipped method: a generated client that quietly degrades to
28
+ * `any` is more dangerous than the hand-written one it replaced, because the green light
29
+ * is now mechanical.
30
+ */
31
+ /** Raised for anything the caller must fix. The CLI turns it into exit 2 and a remedy. */
32
+ export declare class ClientEmitError extends Error {
33
+ constructor(message: string);
34
+ }
35
+ /**
36
+ * How a vertical declares its client, from `package.json` under `substrat.client`.
37
+ */
38
+ export interface ClientConfig {
39
+ /**
40
+ * The module(s) the model is declared in.
41
+ *
42
+ * A list because a vertical is free to split it: todo keeps entities and operations
43
+ * in one `spec/model.ts`, callout has `src/entities.ts` and `src/operations.ts`, and
44
+ * an entry that is not a path is a package specifier — which is how a composed
45
+ * engine's published schemas are reached. Resolving the names across them is the
46
+ * caller's job (`tools/client-emit.mts`); this module is handed the values.
47
+ */
48
+ readonly model: string | readonly string[];
49
+ /**
50
+ * The export(s) holding entity declarations — `{ name: { fields, table } }`.
51
+ *
52
+ * A list because a vertical that composes engines wants the ENGINE's entities named
53
+ * too: without them `protocolGet`'s return type inlines four hundred characters of
54
+ * protocol row on one line, which is a diff nobody reads.
55
+ */
56
+ readonly entities: string | readonly string[];
57
+ /**
58
+ * Extra schemas to name, as `InterfaceName` → export name.
59
+ *
60
+ * An engine publishes types that are NOT its stored rows — `workOrder` is the
61
+ * engine's published shape, with the two `facility_*` columns folded into one
62
+ * `EntityRef`, and no entity's `fields` is that object. Those have nowhere else to
63
+ * be declared, so they are named here.
64
+ */
65
+ readonly schemas?: Readonly<Record<string, string>>;
66
+ /**
67
+ * The export(s) holding this vertical's operations.
68
+ *
69
+ * An array because a vertical that COMPOSES engines has more than one: callout
70
+ * declares its own six and then binds three engines' operations to its own URLs
71
+ * with `defineEngineRoutes`, which returns the same operation objects with `http`
72
+ * attached. Those are as much part of the app's surface as the vertical's own —
73
+ * the SPA calls `/workorders/{id}/complete` without caring whose operation it is —
74
+ * so a generator that read only the first bag would emit a third of the client and
75
+ * look complete doing it.
76
+ */
77
+ readonly operations: string | readonly string[];
78
+ readonly out: string;
79
+ readonly name: string;
80
+ /**
81
+ * Hand-stated types for schemas the printer cannot spell, keyed by the path the
82
+ * refusal prints (`<operation>.input.<field>`). Every entry must be reached — a
83
+ * stale escape hatch is worse than none, so an unused one is exit 2.
84
+ */
85
+ readonly types?: Readonly<Record<string, string>>;
86
+ }
87
+ /**
88
+ * One schema as TypeScript, with no operation around it.
89
+ *
90
+ * The printer's entry point for a test. `named` is the identity map an entity's
91
+ * `fields` would be in — pass one to check that a reference prints as its interface
92
+ * name rather than as an inline shape.
93
+ */
94
+ export declare function tsTypeOf(schema: unknown, named?: Map<unknown, string>): string;
95
+ /**
96
+ * `todo/create-list` → `createList`; `protocol/get` → `protocolGet`.
97
+ *
98
+ * The vertical's OWN prefix is dropped — it is the client's identity and repeating it
99
+ * on every method says nothing (`api.callout.calloutCreateOrder()`). A composed
100
+ * engine's prefix stays, and has to: callout binds `workorder/get`, `protocol/get` and
101
+ * `invoicing/get` at three URLs, and a client with one `get()` would reach whichever
102
+ * bag was read last. Renaming an engine's operation to suit a vertical's client is not
103
+ * a thing a vertical may do, so the disambiguation belongs here.
104
+ */
105
+ export declare const methodName: (operation: string, ownPrefix: string) => string;
106
+ export declare function renderClient(vertical: string, config: ClientConfig, source: string, entities: Record<string, unknown>, operations: Record<string, unknown>, extras: Record<string, unknown>): string;
107
+ //# sourceMappingURL=emit-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"emit-client.d.ts","sourceRoot":"","sources":["../src/emit-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,0FAA0F;AAC1F,qBAAa,eAAgB,SAAQ,KAAK;IACxC,YAAY,OAAO,EAAE,MAAM,EAG1B;CACF;AAMD;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B;;;;;;;;OAQG;IACH,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAAC;IAC3C;;;;;;OAMG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAAC;IAC9C;;;;;;;OAOG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACpD;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAAC;IAChD,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;CACnD;AAmLD;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,MAAM,EAAE,OAAO,EAAE,KAAK,GAAE,GAAG,CAAC,OAAO,EAAE,MAAM,CAAa,GAAG,MAAM,CAEzF;AAkCD;;;;;;;;;GASG;AACH,eAAO,MAAM,UAAU,cAAe,MAAM,aAAa,MAAM,WAI9D,CAAC;AAsGF,wBAAgB,YAAY,CAC1B,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,YAAY,EACpB,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACjC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACnC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC9B,MAAM,CAwSR"}
@@ -0,0 +1,566 @@
1
+ /**
2
+ * A vertical's browser client, rendered from its model.
3
+ *
4
+ * The pure half of `tools/client-emit.mts` — everything from "a Zod schema" to "a
5
+ * string of TypeScript", with no filesystem and no `process.exit` in it. It lives here
6
+ * rather than in the tool for one reason: a Zod→TypeScript printer is the kind of code
7
+ * that is wrong in ways a drift check cannot see. `--check` re-emits and compares, so it
8
+ * catches a client that fell behind its model; it cannot catch a client that has been
9
+ * confidently mis-spelling `z.union([...])` inside an array since the day it was written.
10
+ * That needs a test with the schema on one side and the expected string on the other,
11
+ * and a test needs something importable.
12
+ *
13
+ * Its home is this package because that is already this package's job: build-time
14
+ * tooling over a Substrat model. `emitTables` turns entities into DDL; this turns
15
+ * entities and operations into a typed fetch client.
16
+ *
17
+ * ## What it emits
18
+ *
19
+ * Standalone TypeScript with no imports at all. A vertical's SPA is a separate Vite
20
+ * package that depends on neither `@substrat-run/contracts` nor zod and must keep
21
+ * depending on neither — and a checked-in artifact that re-exports its meaning from
22
+ * another package is not reviewable in a diff.
23
+ *
24
+ * ## What it refuses
25
+ *
26
+ * A schema it cannot spell raises `ClientEmitError` naming the operation and the field.
27
+ * Never `unknown`, never a skipped method: a generated client that quietly degrades to
28
+ * `any` is more dangerous than the hand-written one it replaced, because the green light
29
+ * is now mechanical.
30
+ */
31
+ /** Raised for anything the caller must fix. The CLI turns it into exit 2 and a remedy. */
32
+ export class ClientEmitError extends Error {
33
+ constructor(message) {
34
+ super(message);
35
+ this.name = 'ClientEmitError';
36
+ }
37
+ }
38
+ function fail(message) {
39
+ throw new ClientEmitError(message);
40
+ }
41
+ function defOf(schema) {
42
+ return (schema?._zod?.def ??
43
+ schema?._def);
44
+ }
45
+ /** The wrappers that do not change a type, only its optionality or its parsing. */
46
+ const TRANSPARENT = new Set(['default', 'prefault', 'catch', 'readonly', 'nonoptional', 'pipe']);
47
+ /** A field is optional when its own outermost wrapper says so — `a?: T`, not `a: T | undefined`. */
48
+ function isOptional(schema) {
49
+ const def = defOf(schema);
50
+ if (def?.type === 'optional' || def?.type === 'nullish')
51
+ return true;
52
+ if (def && TRANSPARENT.has(def.type ?? '')) {
53
+ // A defaulted field is optional TO THE CALLER — the server fills it in.
54
+ return def.type === 'default' || def.type === 'prefault' || isOptional(def.innerType ?? def.in);
55
+ }
56
+ return false;
57
+ }
58
+ /**
59
+ * The type a `?` property carries, with the optionality itself taken off.
60
+ *
61
+ * `limit?: number | undefined` says the same thing twice and reads as though the
62
+ * two halves might differ. A `nullish` field keeps its `| null`, because that half
63
+ * is a value the server can actually send.
64
+ */
65
+ function unwrapOptional(schema) {
66
+ const def = defOf(schema);
67
+ if (def?.type === 'optional')
68
+ return unwrapOptional(def.innerType);
69
+ if (def?.type === 'default' || def?.type === 'prefault')
70
+ return unwrapOptional(def.innerType);
71
+ return schema;
72
+ }
73
+ function literalOf(value) {
74
+ if (typeof value === 'string')
75
+ return JSON.stringify(value);
76
+ if (typeof value === 'number' || typeof value === 'boolean')
77
+ return String(value);
78
+ if (value === null)
79
+ return 'null';
80
+ return fail(`a literal of type ${typeof value} has no TypeScript spelling here`);
81
+ }
82
+ /**
83
+ * A schema as a TypeScript type.
84
+ *
85
+ * An entity's `fields` object is matched by IDENTITY, not by shape: `spec/model.ts`
86
+ * writes `output: todoEntities.item.fields`, so the very same object arrives here
87
+ * and prints as `Item`. Two entities that happen to share a shape stay two names,
88
+ * and an inline object that happens to match an entity stays inline — which is
89
+ * what the model actually said in both cases.
90
+ */
91
+ function tsType(schema, ctx) {
92
+ const override = ctx.overrides.get(ctx.where);
93
+ if (override !== undefined) {
94
+ ctx.used.add(ctx.where);
95
+ return override;
96
+ }
97
+ const named = ctx.named.get(schema);
98
+ if (named)
99
+ return named;
100
+ const def = defOf(schema);
101
+ switch (def?.type) {
102
+ case 'string':
103
+ return 'string';
104
+ case 'number':
105
+ return 'number';
106
+ case 'boolean':
107
+ return 'boolean';
108
+ case 'bigint':
109
+ return 'bigint';
110
+ case 'unknown':
111
+ case 'any':
112
+ return 'unknown';
113
+ case 'null':
114
+ return 'null';
115
+ case 'literal': {
116
+ const values = def.values ?? [];
117
+ if (values.length === 0)
118
+ return fail(`${ctx.where}: a literal with no values`);
119
+ return values.map(literalOf).join(' | ');
120
+ }
121
+ case 'enum': {
122
+ const values = Object.values(def.entries ?? {});
123
+ if (values.length === 0)
124
+ return fail(`${ctx.where}: an enum with no members`);
125
+ return values.map(literalOf).join(' | ');
126
+ }
127
+ case 'array':
128
+ return `${maybeParen(tsType(def.element, ctx), ctx)}[]`;
129
+ case 'object':
130
+ return tsObject(def.shape ?? {}, ctx);
131
+ case 'record':
132
+ return `Record<${tsType(def.keyType, ctx)}, ${tsType(def.valueType, ctx)}>`;
133
+ case 'union': {
134
+ const options = def.options ?? [];
135
+ if (options.length === 0)
136
+ return fail(`${ctx.where}: a union with no options`);
137
+ return [...new Set(options.map((o) => tsType(o, ctx)))].join(' | ');
138
+ }
139
+ case 'optional':
140
+ // Reached only where optionality has no key to hang off — inside an array,
141
+ // say. As a property it is spelled `a?: T` by `tsObject` instead.
142
+ return `${tsType(def.innerType, ctx)} | undefined`;
143
+ case 'nullable':
144
+ return `${tsType(def.innerType, ctx)} | null`;
145
+ case 'nullish':
146
+ return `${tsType(def.innerType, ctx)} | null | undefined`;
147
+ case 'default':
148
+ case 'prefault':
149
+ case 'catch':
150
+ case 'readonly':
151
+ case 'nonoptional':
152
+ return tsType(def.innerType, ctx);
153
+ case 'pipe':
154
+ // `z.coerce.number()` is a pipe; the caller supplies its INPUT side.
155
+ return tsType(def.in, ctx);
156
+ default:
157
+ return fail(`${ctx.where}: no TypeScript spelling for a Zod '${def?.type ?? 'unrecognised'}' schema.\n` +
158
+ ` A generator that answered 'unknown' here would hand the app a green light over a\n` +
159
+ ` type it never checked.\n` +
160
+ ` Remedy, in order of preference: teach tsType() this case; declare the field with a\n` +
161
+ ` shape the client can carry; or, when the schema genuinely has no inferable input\n` +
162
+ ` type (a preprocess does not), state the type yourself in package.json —\n` +
163
+ ` "substrat": { "client": { "types": { ${JSON.stringify(ctx.where.split('.').slice(1).join('.'))}: "unknown" } } }`);
164
+ }
165
+ }
166
+ /** Parenthesise a union before `[]`, so `(A | B)[]` never prints as `A | B[]`. */
167
+ function maybeParen(printed, _ctx) {
168
+ return printed.includes('|') && !printed.startsWith('(') ? `(${printed})` : printed;
169
+ }
170
+ /**
171
+ * One schema as TypeScript, with no operation around it.
172
+ *
173
+ * The printer's entry point for a test. `named` is the identity map an entity's
174
+ * `fields` would be in — pass one to check that a reference prints as its interface
175
+ * name rather than as an inline shape.
176
+ */
177
+ export function tsTypeOf(schema, named = new Map()) {
178
+ return tsType(schema, { named, where: 'schema', overrides: new Map(), used: new Set() });
179
+ }
180
+ function tsObject(shape, ctx) {
181
+ const keys = Object.keys(shape);
182
+ if (keys.length === 0)
183
+ return 'Record<string, never>';
184
+ const fields = keys.map((key) => {
185
+ const optional = isOptional(shape[key]);
186
+ const value = optional ? unwrapOptional(shape[key]) : shape[key];
187
+ return `${key}${optional ? '?' : ''}: ${tsType(value, { ...ctx, where: `${ctx.where}.${key}` })}`;
188
+ });
189
+ return `{ ${fields.join('; ')} }`;
190
+ }
191
+ /** An entity's fields printed as a multi-line interface body. */
192
+ function interfaceBody(shape, ctx) {
193
+ return Object.keys(shape)
194
+ .map((key) => {
195
+ const optional = isOptional(shape[key]);
196
+ const value = optional ? unwrapOptional(shape[key]) : shape[key];
197
+ return ` ${key}${optional ? '?' : ''}: ${tsType(value, { ...ctx, where: `${ctx.where}.${key}` })};`;
198
+ })
199
+ .join('\n');
200
+ }
201
+ // ---------------------------------------------------------------------------
202
+ // Names.
203
+ // ---------------------------------------------------------------------------
204
+ const pascal = (s) => s.replace(/(^|[-_/])(\w)/g, (_, __, c) => c.toUpperCase());
205
+ const camel = (s) => {
206
+ const p = pascal(s);
207
+ return p.charAt(0).toLowerCase() + p.slice(1);
208
+ };
209
+ /**
210
+ * `todo/create-list` → `createList`; `protocol/get` → `protocolGet`.
211
+ *
212
+ * The vertical's OWN prefix is dropped — it is the client's identity and repeating it
213
+ * on every method says nothing (`api.callout.calloutCreateOrder()`). A composed
214
+ * engine's prefix stays, and has to: callout binds `workorder/get`, `protocol/get` and
215
+ * `invoicing/get` at three URLs, and a client with one `get()` would reach whichever
216
+ * bag was read last. Renaming an engine's operation to suit a vertical's client is not
217
+ * a thing a vertical may do, so the disambiguation belongs here.
218
+ */
219
+ export const methodName = (operation, ownPrefix) => {
220
+ const [prefix, ...rest] = operation.split('/');
221
+ const tail = rest.join('/');
222
+ return prefix === ownPrefix ? camel(tail) : camel(`${prefix}/${tail}`);
223
+ };
224
+ const pathParams = (path) => [...path.matchAll(/\{(\w+)\}/g)].map((m) => m[1]);
225
+ /**
226
+ * The fixed runtime, which is a template rather than a derivation — but it is
227
+ * emitted rather than shipped as a package for the same reason the types are:
228
+ * the app depends on nothing, and one file is one thing to review.
229
+ */
230
+ function preamble(name, source, anyPaged, stated) {
231
+ return `// GENERATED by tools/client-emit.mts from ${source} — do not edit by hand.${stated.length
232
+ ? `
233
+ //
234
+ // HAND-STATED TYPES (substrat.client.types). These schemas have no inferable input
235
+ // type — a \`z.preprocess\` infers \`unknown\` by construction — so the type below was
236
+ // decided by a person, not derived, and is not checked against the schema:
237
+ ${stated.map((line) => `// ${line}`).join('\n')}`
238
+ : ''}
239
+ //
240
+ // Re-emit with \`pnpm lint:client\`; CI runs \`pnpm lint:client --check\` and fails on
241
+ // drift, so the client cannot fall behind the model the way a hand-written one did.
242
+ //
243
+ // Standalone on purpose: no imports, so the browser bundle carries no schema library
244
+ // and \`tools/declared-deps.mjs\` has nothing to object to.
245
+
246
+ /** A failed request. \`status\` is the interesting field — 403 is the permission model answering. */
247
+ export class ApiError extends Error {
248
+ constructor(
249
+ readonly status: number,
250
+ message: string,
251
+ readonly body: unknown,
252
+ ) {
253
+ super(message);
254
+ this.name = 'ApiError';
255
+ }
256
+ }
257
+ ${anyPaged
258
+ ? `
259
+ /**
260
+ * One page of a paged read, reassembled from the response.
261
+ *
262
+ * The BODY is the entries and always was; the walk rides in headers (#829), which is
263
+ * what let paging be adopted without renaming a live endpoint's response. This puts
264
+ * the two back together so a caller sees a page rather than a header protocol.
265
+ *
266
+ * \`next\` is an absolute URL to FOLLOW (RFC 8288), not a cursor to reassemble — this
267
+ * request's filters and page size travel with it. Its absence is how the walk ends.
268
+ */
269
+ export interface Paged<T> {
270
+ entries: T[];
271
+ /** Hand back to \`follow()\`. \`null\` when there is no next page. */
272
+ next: string | null;
273
+ /** From \`X-Total-Count\`, and only for a read that declares \`total\`. */
274
+ total: number | null;
275
+ }
276
+ `
277
+ : ''}
278
+ export interface ClientOptions {
279
+ /** Prefixed to every declared path. Matches \`mountOperations\`' own default. */
280
+ baseUrl?: string;
281
+ /** Per-request headers — an auth token, or this demo's \`x-principal\` persona seam. */
282
+ headers?: () => Record<string, string>;
283
+ /** Injectable so the client works under a test harness, not only in a browser. */
284
+ fetch?: typeof globalThis.fetch;
285
+ /**
286
+ * Pull a message out of an error body.
287
+ *
288
+ * The one thing here the model does NOT declare: a vertical picks its error
289
+ * envelope in its own \`app.onError\`, so this is a best effort over the shapes in
290
+ * use — \`{ error }\`, and problem+json's \`detail\`/\`title\` — and an app that
291
+ * answers something else says so here rather than being guessed at.
292
+ */
293
+ errorMessage?: (body: unknown, response: Response) => string | undefined;
294
+ }
295
+
296
+ function defaultErrorMessage(body: unknown): string | undefined {
297
+ if (typeof body !== 'object' || body === null) return undefined;
298
+ const b = body as Record<string, unknown>;
299
+ for (const key of ['error', 'detail', 'title', 'message']) {
300
+ if (typeof b[key] === 'string') return b[key] as string;
301
+ }
302
+ return undefined;
303
+ }
304
+ `;
305
+ }
306
+ export function renderClient(vertical, config, source, entities, operations, extras) {
307
+ const named = new Map();
308
+ const claimedBy = new Map();
309
+ const claim = (schema, name, origin) => {
310
+ const already = claimedBy.get(name);
311
+ if (already !== undefined && named.get(schema) !== name) {
312
+ fail(`${vertical}: two different schemas both want the interface name \`${name}\` ` +
313
+ `(${already}, ${origin}).\n` +
314
+ ` Remedy: rename one in substrat.client.schemas.`);
315
+ }
316
+ claimedBy.set(name, origin);
317
+ named.set(schema, name);
318
+ };
319
+ for (const [key, def] of Object.entries(entities)) {
320
+ const fields = def?.fields;
321
+ if (!fields)
322
+ fail(`${vertical}: entity '${key}' has no \`fields\` schema`);
323
+ claim(fields, pascal(key), `entity ${key}`);
324
+ }
325
+ for (const [name, exportName] of Object.entries(config.schemas ?? {})) {
326
+ const schema = extras[exportName];
327
+ if (schema === undefined)
328
+ fail(`${vertical}: substrat.client.schemas names \`${exportName}\`, which nothing exports`);
329
+ claim(schema, name, `schema ${exportName}`);
330
+ }
331
+ // Keys are declared relative to the operation, so the vertical's own name is not
332
+ // repeated in every entry; `where` carries the prefix, so add it here.
333
+ const overrides = new Map(Object.entries(config.types ?? {}).map(([k, v]) => [`${vertical}.${k}`, v]));
334
+ const used = new Set();
335
+ const ctx = { named, where: vertical, overrides, used };
336
+ // Every named type gets a block: an entity's `fields`, and each extra schema. A name
337
+ // in the identity map with no interface behind it is a file that references a type it
338
+ // never declares — which typechecks nowhere and would only be found by building.
339
+ const blocks = [
340
+ ...Object.entries(entities).map(([key, def]) => ({
341
+ name: pascal(key),
342
+ note: `\`${def.table ?? key}\` — declared in ${source}`,
343
+ schema: def.fields,
344
+ where: `${vertical}.${key}`,
345
+ })),
346
+ ...Object.entries(config.schemas ?? {}).map(([name, exportName]) => ({
347
+ name,
348
+ note: `\`${exportName}\` — a published schema, not a stored row`,
349
+ schema: extras[exportName],
350
+ where: `${vertical}.${exportName}`,
351
+ })),
352
+ ];
353
+ const entityBlocks = blocks.map(({ name, note, schema, where }) => {
354
+ const def = defOf(schema);
355
+ if (def?.type !== 'object') {
356
+ fail(`${vertical}: \`${name}\` is not an object schema, so it cannot be an interface.\n` +
357
+ ` Remedy: drop it from substrat.client.schemas — a union or an array is spelled\n` +
358
+ ` inline where it is used.`);
359
+ }
360
+ // Printed with this schema temporarily UNNAMED, or the body would be `Foo` referring
361
+ // to itself. Every other reference to it still resolves to the name.
362
+ const named2 = new Map(ctx.named);
363
+ named2.delete(schema);
364
+ return `/** ${note}. */\nexport interface ${name} {\n${interfaceBody(def.shape ?? {}, { ...ctx, named: named2, where })}\n}`;
365
+ });
366
+ // Then one method per operation that declares an HTTP binding. An operation
367
+ // without one is not part of this app's surface — a composed engine's, say.
368
+ const declared = Object.entries(operations)
369
+ .map(([operation, op]) => [operation, op])
370
+ .filter((entry) => Boolean(entry[1]?.http))
371
+ .sort(([a], [b]) => a.localeCompare(b));
372
+ if (declared.length === 0)
373
+ fail(`${vertical}: no operation declares an \`http\` binding — a client over nothing`);
374
+ // The first named bag is the vertical's own `defineOperations`; the rest are engine
375
+ // route bindings. Taken from INSERTION order (the bags are merged in the order
376
+ // package.json names them), never from `declared`, which is sorted for a stable
377
+ // artifact — reading it there would make the answer depend on the alphabet, and it
378
+ // happens to be right for callout, which is the worst way for it to be wrong.
379
+ const ownPrefix = Object.keys(operations)[0]?.split('/')[0] ?? '';
380
+ const seen = new Map();
381
+ const methods = [];
382
+ const impls = [];
383
+ let anyPaged = false;
384
+ for (const [operation, op] of declared) {
385
+ const method = methodName(operation, ownPrefix);
386
+ const clash = seen.get(method);
387
+ if (clash) {
388
+ fail(`${vertical}: '${clash}' and '${operation}' both name the client method \`${method}\`.\n` +
389
+ ` Two methods with one name is a call site that silently reaches the wrong\n` +
390
+ ` endpoint. A composed engine's prefix is kept precisely to avoid this, so a\n` +
391
+ ` collision here means two operations of the SAME module share a name.\n` +
392
+ ` Remedy: rename one of them.`);
393
+ }
394
+ seen.set(method, operation);
395
+ const where = `${vertical}.${operation}`;
396
+ const params = pathParams(op.http.path);
397
+ const inputShape = (defOf(op.input)?.shape ?? {});
398
+ const hasInput = op.input !== undefined || params.length > 0;
399
+ // The parameter type is the input schema WHOLE — path parameters included.
400
+ // `mountOperations` reads them off the URL, but they are declared input fields
401
+ // and the compile-checked `{var}` join says so, so the caller passes one object.
402
+ const inputType = op.input ? tsObject(inputShape, { ...ctx, where: `${where}.input` }) : '{}';
403
+ const entryType = tsType(op.output, { ...ctx, where: `${where}.output` });
404
+ const returnType = op.paged ? `Paged<${maybeParen(entryType, ctx)}>` : entryType;
405
+ if (op.paged)
406
+ anyPaged = true;
407
+ const signature = hasInput ? `input: ${inputType}` : '';
408
+ const doc = [
409
+ ' /**',
410
+ ` * ${op.summary ?? operation}`,
411
+ ' *',
412
+ ` * \`${op.http.method} ${op.http.path}\` — \`${operation}\``,
413
+ ...(op.paged ? [' *', ' * Paged: walk it with `follow(page.next)` until `next` is `null`.'] : []),
414
+ ' */',
415
+ ].join('\n');
416
+ methods.push(`${doc}\n ${method}(${signature}): Promise<${returnType}>;`);
417
+ // Path parameters are spliced out of the payload; what is left is a body for a
418
+ // writing method and a query string for a read — exactly what mountOperations
419
+ // expects on the other side, which is the only reason this can be derived.
420
+ const takesBody = ['POST', 'PUT', 'PATCH'].includes(op.http.method);
421
+ const pathExpr = params.length
422
+ ? '`' + op.http.path.replace(/\{(\w+)\}/g, (_, p) => `\${encodeURIComponent(String(input.${p}))}`) + '`'
423
+ : JSON.stringify(op.http.path);
424
+ const rest = params.length ? `omit(input, ${JSON.stringify(params)})` : hasInput ? 'input' : 'undefined';
425
+ const call = op.paged ? 'page' : 'send';
426
+ impls.push(` ${method}: (${hasInput ? 'input: Args' : ''}) =>\n` +
427
+ ` ${call}(${pathExpr}, ${JSON.stringify(op.http.method)}, ${takesBody ? `${rest}, undefined` : `undefined, ${rest}`}),`);
428
+ }
429
+ const runtime = `
430
+ /**
431
+ * The plumbing below is deliberately untyped against the interface above, and cast
432
+ * once at the end.
433
+ *
434
+ * \`send\` cannot know an operation's return type — it has a \`Response\`, not a schema
435
+ * — so every method would otherwise need its own \`as\` and the file would carry
436
+ * fourteen casts instead of one. The interface is what a caller sees, and it IS
437
+ * derived from the model; this is the seam where a runtime that speaks \`unknown\`
438
+ * meets it.
439
+ */
440
+ type Args = Record<string, unknown>;
441
+
442
+ const omit = (input: Record<string, unknown>, keys: string[]): Record<string, unknown> => {
443
+ const out: Record<string, unknown> = {};
444
+ for (const [k, v] of Object.entries(input ?? {})) if (!keys.includes(k)) out[k] = v;
445
+ return out;
446
+ };
447
+
448
+ /** Declared fields become a query string; \`undefined\` is absent rather than the string "undefined". */
449
+ const query = (values: Record<string, unknown> | undefined): string => {
450
+ if (!values) return '';
451
+ const params = new URLSearchParams();
452
+ for (const [k, v] of Object.entries(values)) {
453
+ if (v === undefined || v === null) continue;
454
+ params.set(k, String(v));
455
+ }
456
+ const s = params.toString();
457
+ return s ? \`?\${s}\` : '';
458
+ };
459
+ ${anyPaged
460
+ ? `
461
+ /**
462
+ * The \`rel="next"\` URL out of an RFC 8288 \`Link\` header, or null.
463
+ *
464
+ * Only \`next\` is looked for because only \`next\` is sent: keyset paging walks one
465
+ * way and does not know its own offset, so there is no \`prev\`, \`first\` or \`last\`
466
+ * to honour. A header naming some other relation is not this walk's.
467
+ */
468
+ const nextFrom = (header: string | null): string | null => {
469
+ if (!header) return null;
470
+ for (const part of header.split(',')) {
471
+ const match = /^\\s*<([^>]+)>\\s*;\\s*(.+)$/.exec(part);
472
+ if (match && /rel\\s*=\\s*"?next"?/.test(match[2] as string)) return match[1] as string;
473
+ }
474
+ return null;
475
+ };
476
+ `
477
+ : ''}
478
+ export function createClient(options: ClientOptions = {}): ${config.name}Client {
479
+ const baseUrl = options.baseUrl ?? '/api';
480
+ const doFetch = options.fetch ?? globalThis.fetch.bind(globalThis);
481
+ const readMessage = options.errorMessage ?? defaultErrorMessage;
482
+
483
+ /** One request, against a path that is ALREADY prefixed and query-stringed. */
484
+ const raw = async (fullPath: string, method: string, body: unknown): Promise<Response> => {
485
+ const hasBody = body !== undefined && method !== 'GET' && method !== 'DELETE';
486
+ return await doFetch(fullPath, {
487
+ method,
488
+ headers: {
489
+ ...(hasBody ? { 'content-type': 'application/json' } : {}),
490
+ ...(options.headers?.() ?? {}),
491
+ },
492
+ ...(hasBody ? { body: JSON.stringify(body) } : {}),
493
+ });
494
+ };
495
+
496
+ const parse = async (res: Response): Promise<unknown> => {
497
+ const text = await res.text();
498
+ const body: unknown = text ? JSON.parse(text) : null;
499
+ if (!res.ok) throw new ApiError(res.status, readMessage(body, res) ?? res.statusText, body);
500
+ return body;
501
+ };
502
+
503
+ const send = async (path: string, method: string, body: unknown, params: unknown): Promise<unknown> =>
504
+ await parse(await raw(\`\${baseUrl}\${path}\${query(params as Record<string, unknown>)}\`, method, body));
505
+ ${anyPaged
506
+ ? `
507
+ /**
508
+ * A paged read: the entries come from the body, the walk from the headers.
509
+ *
510
+ * A cross-origin caller reads neither header without \`Access-Control-Expose-Headers\`,
511
+ * and the symptom is not an error — it is a list that looks like it has one page. Under
512
+ * a same-origin dev proxy (this demo) or a same-origin deployment, both are readable.
513
+ */
514
+ const readPage = async (res: Response): Promise<Paged<unknown>> => {
515
+ const entries = (await parse(res)) as unknown[];
516
+ const total = res.headers.get('X-Total-Count');
517
+ return { entries, next: nextFrom(res.headers.get('Link')), total: total === null ? null : Number(total) };
518
+ };
519
+
520
+ const page = async (path: string, method: string, body: unknown, params: unknown): Promise<Paged<unknown>> =>
521
+ await readPage(await raw(\`\${baseUrl}\${path}\${query(params as Record<string, unknown>)}\`, method, body));
522
+ `
523
+ : ''}
524
+ return {
525
+ ${impls.join('\n')}${anyPaged
526
+ ? `
527
+ follow: async (next: string) => {
528
+ // The link names the API's OWN origin, which under a dev proxy is not the origin
529
+ // this page was served from. So the path is kept and the origin is taken from
530
+ // whatever this client was configured to talk to: relative for a browser, which
531
+ // keeps the request same-origin, absolute for a harness that has no page.
532
+ const link = new URL(next, 'http://substrat.invalid');
533
+ const target = /^https?:\\/\\//.test(baseUrl)
534
+ ? new URL(link.pathname + link.search, baseUrl).href
535
+ : link.pathname + link.search;
536
+ return await readPage(await raw(target, 'GET', undefined));
537
+ },`
538
+ : ''}
539
+ } as unknown as ${config.name}Client;
540
+ }
541
+ `;
542
+ const followSignature = anyPaged
543
+ ? `
544
+ /**
545
+ * Fetch the next page of any paged read, given a previous page's \`next\`.
546
+ *
547
+ * One method for every paged read rather than one per read: \`next\` is a URL that
548
+ * already carries the filters, so there is nothing left for a caller to restate.
549
+ */
550
+ follow<T>(next: string): Promise<Paged<T>>;
551
+ `
552
+ : '';
553
+ const unused = [...overrides.keys()].filter((k) => !used.has(k));
554
+ if (unused.length) {
555
+ fail(`${vertical}: substrat.client.types names ${unused.map((k) => k.slice(vertical.length + 1)).join(', ')}, ` +
556
+ `which the printer never reached.\n` +
557
+ ` A stale escape hatch reads as a live decision. Remedy: remove the entry.`);
558
+ }
559
+ return [
560
+ preamble(config.name, source, anyPaged, [...used].sort().map((k) => `${k.slice(vertical.length + 1)} → ${overrides.get(k)}`)),
561
+ entityBlocks.join('\n\n'),
562
+ `/** Every operation this vertical binds to HTTP, one method each. */\nexport interface ${config.name}Client {\n${methods.join('\n\n')}\n${followSignature}}`,
563
+ runtime.trimStart(),
564
+ ].join('\n\n');
565
+ }
566
+ //# sourceMappingURL=emit-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"emit-client.js","sourceRoot":"","sources":["../src/emit-client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,72 @@
1
+ /**
2
+ * The declared lifecycle, rendered as an XState v5 machine config (#844).
3
+ *
4
+ * One direction, and that is the design. XState's statechart *semantics* are the
5
+ * right vocabulary — nested states are K-17's substates exactly, and `setup()`
6
+ * splitting implementations out of the config is why a guard can be referenced
7
+ * by name here without it being a hack. Its *actor runtime* is not wanted: the
8
+ * state lives in a column in a scope DO, the transition happens inside the
9
+ * operation's transaction, and a second thing deciding transitions would be the
10
+ * duplication the declaration exists to delete.
11
+ *
12
+ * So this emits, and nothing here parses. The conversion is TOTAL — every valid
13
+ * lifecycle produces a valid machine — which is exactly what a narrow source
14
+ * format buys: it can refuse `invoke`, `context`, `after` and parallel regions,
15
+ * so the emitter never meets one.
16
+ *
17
+ * ## What it is for
18
+ *
19
+ * A **diagram** in an engine's docs page, and a **test oracle**: `xstate`'s own
20
+ * `machine.transition()` is a pure function, so it can stand beside
21
+ * `assertTransition` in a test and confirm the two agree on every state/event
22
+ * pair, with `xstate` never leaving devDependencies.
23
+ *
24
+ * ## What it must never become
25
+ *
26
+ * A round-trip. Editing the emitted machine in a visual editor and reading it
27
+ * back would make that editor a second authoring surface — the visual-builder
28
+ * tarpit arriving through the back door, without anyone deciding to open it. Any
29
+ * checked-in output of this belongs behind a `--check` re-emit, so an edit to it
30
+ * goes red rather than becoming the source.
31
+ */
32
+ import type { EmittedLifecycle } from '@substrat-run/contracts';
33
+ /** A guard wired in a manifest: `{ before, predicate }`, where `before` is an operation id. */
34
+ export interface GuardWiring {
35
+ readonly before: string;
36
+ readonly predicate: string;
37
+ }
38
+ export interface XStateTransition {
39
+ readonly target: string;
40
+ readonly guard?: string;
41
+ readonly meta?: Record<string, unknown>;
42
+ }
43
+ export interface XStateNode {
44
+ readonly type?: 'final';
45
+ readonly on?: Record<string, XStateTransition | string>;
46
+ readonly meta?: Record<string, unknown>;
47
+ }
48
+ export interface XStateMachine {
49
+ readonly id: string;
50
+ readonly initial: string;
51
+ readonly states: Record<string, XStateNode>;
52
+ }
53
+ export interface EmitXStateOptions {
54
+ /**
55
+ * Manifest guard wirings, joined onto edges by operation id.
56
+ *
57
+ * Joined rather than declared: K-38 already puts guards in the manifest and has
58
+ * the kernel evaluate them before the guarded operation. Every edge names its
59
+ * operation, so the join key exists and a second declaration would only be a
60
+ * second thing to disagree.
61
+ */
62
+ readonly guards?: readonly GuardWiring[];
63
+ }
64
+ /**
65
+ * Event names are the operation ids verbatim — `workorder/complete`, not
66
+ * `COMPLETE`.
67
+ *
68
+ * XState imposes no naming convention, and SCREAMING_SNAKE would be a lossy
69
+ * rename: the diagram would stop naming the operation a reader can go and read.
70
+ */
71
+ export declare function emitXState(entity: string, lifecycle: EmittedLifecycle, options?: EmitXStateOptions): XStateMachine;
72
+ //# sourceMappingURL=emit-xstate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"emit-xstate.d.ts","sourceRoot":"","sources":["../src/emit-xstate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAEhE,+FAA+F;AAC/F,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACzC;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,GAAG,MAAM,CAAC,CAAC;IACxD,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACzC;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;CAC7C;AAED,MAAM,WAAW,iBAAiB;IAChC;;;;;;;OAOG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,WAAW,EAAE,CAAC;CAC1C;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CACxB,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,gBAAgB,EAC3B,OAAO,GAAE,iBAAsB,GAC9B,aAAa,CAgCf"}
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Event names are the operation ids verbatim — `workorder/complete`, not
3
+ * `COMPLETE`.
4
+ *
5
+ * XState imposes no naming convention, and SCREAMING_SNAKE would be a lossy
6
+ * rename: the diagram would stop naming the operation a reader can go and read.
7
+ */
8
+ export function emitXState(entity, lifecycle, options = {}) {
9
+ const byOperation = new Map();
10
+ for (const g of options.guards ?? [])
11
+ byOperation.set(g.before, g.predicate);
12
+ const states = {};
13
+ for (const name of Object.keys(lifecycle.states).sort()) {
14
+ const state = lifecycle.states[name];
15
+ if (!state)
16
+ continue;
17
+ const on = {};
18
+ for (const op of Object.keys(state.on ?? {}).sort()) {
19
+ const target = state.on?.[op];
20
+ if (target === undefined)
21
+ continue;
22
+ const guard = byOperation.get(op);
23
+ on[op] = guard ? { target, guard } : target;
24
+ }
25
+ /**
26
+ * `allow` deliberately does NOT become a self-transition.
27
+ *
28
+ * A self-loop in XState re-enters the state and fires its entry actions. The
29
+ * declaration says the opposite — this operation is legal here and changes
30
+ * nothing. Drawing it as an edge would put a loop on the diagram for every
31
+ * note a technician can add to a work order, which is both wrong and
32
+ * unreadable. It rides in `meta`, where a renderer can list it as what it is.
33
+ */
34
+ const meta = state.allow?.length ? { allow: [...state.allow] } : undefined;
35
+ states[name] = {
36
+ ...(state.terminal ? { type: 'final' } : {}),
37
+ ...(Object.keys(on).length ? { on } : {}),
38
+ ...(meta ? { meta } : {}),
39
+ };
40
+ }
41
+ return { id: entity, initial: lifecycle.initial, states };
42
+ }
43
+ //# sourceMappingURL=emit-xstate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"emit-xstate.js","sourceRoot":"","sources":["../src/emit-xstate.ts"],"names":[],"mappings":"AAqEA;;;;;;GAMG;AACH,MAAM,UAAU,UAAU,CACxB,MAAc,EACd,SAA2B,EAC3B,OAAO,GAAsB,EAAE;IAE/B,MAAM,WAAW,GAAG,IAAI,GAAG,EAAkB,CAAC;IAC9C,KAAK,MAAM,CAAC,IAAI,OAAO,CAAC,MAAM,IAAI,EAAE;QAAE,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,SAAS,CAAC,CAAC;IAE7E,MAAM,MAAM,GAA+B,EAAE,CAAC;IAC9C,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC;QACxD,MAAM,KAAK,GAAG,SAAS,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QACrC,IAAI,CAAC,KAAK;YAAE,SAAS;QACrB,MAAM,EAAE,GAA8C,EAAE,CAAC;QACzD,KAAK,MAAM,EAAE,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC;YACpD,MAAM,MAAM,GAAG,KAAK,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC;YAC9B,IAAI,MAAM,KAAK,SAAS;gBAAE,SAAS;YACnC,MAAM,KAAK,GAAG,WAAW,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;YAClC,EAAE,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC;QAC9C,CAAC;QACD;;;;;;;;WAQG;QACH,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;QAC3E,MAAM,CAAC,IAAI,CAAC,GAAG;YACb,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,OAAgB,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACrD,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACzC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAC1B,CAAC;IACJ,CAAC;IACD,OAAO,EAAE,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,CAAC;AAC5D,CAAC"}
package/dist/index.d.ts CHANGED
@@ -2,4 +2,6 @@ export { emitTables, columnsOf, primaryKeyConstraint, uniqueConstraints, type Em
2
2
  export { journalColumns, journalUniques, journalPrimaryKeys } from './journal.js';
3
3
  export { readSchema, statements, type TableSchema } from './replay.js';
4
4
  export { planMigration, parseJournal, type Journal, type JournalEntry, type MigrationPlan, } from './plan.js';
5
+ export { renderClient, ClientEmitError, tsTypeOf, methodName, type ClientConfig, } from './emit-client.js';
6
+ export { emitXState, type EmitXStateOptions, type GuardWiring, type XStateMachine, type XStateNode, type XStateTransition, } from './emit-xstate.js';
5
7
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,UAAU,EACV,SAAS,EACT,oBAAoB,EACpB,iBAAiB,EACjB,KAAK,cAAc,EACnB,KAAK,aAAa,GACnB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAClF,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,KAAK,WAAW,EAAE,MAAM,aAAa,CAAC;AACvE,OAAO,EACL,aAAa,EACb,YAAY,EACZ,KAAK,OAAO,EACZ,KAAK,YAAY,EACjB,KAAK,aAAa,GACnB,MAAM,WAAW,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,UAAU,EACV,SAAS,EACT,oBAAoB,EACpB,iBAAiB,EACjB,KAAK,cAAc,EACnB,KAAK,aAAa,GACnB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAClF,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,KAAK,WAAW,EAAE,MAAM,aAAa,CAAC;AACvE,OAAO,EACL,aAAa,EACb,YAAY,EACZ,KAAK,OAAO,EACZ,KAAK,YAAY,EACjB,KAAK,aAAa,GACnB,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,YAAY,EACZ,eAAe,EACf,QAAQ,EACR,UAAU,EACV,KAAK,YAAY,GAClB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,UAAU,EACV,KAAK,iBAAiB,EACtB,KAAK,WAAW,EAChB,KAAK,aAAa,EAClB,KAAK,UAAU,EACf,KAAK,gBAAgB,GACtB,MAAM,kBAAkB,CAAC"}
package/dist/index.js CHANGED
@@ -2,4 +2,6 @@ export { emitTables, columnsOf, primaryKeyConstraint, uniqueConstraints, } from
2
2
  export { journalColumns, journalUniques, journalPrimaryKeys } from './journal.js';
3
3
  export { readSchema, statements } from './replay.js';
4
4
  export { planMigration, parseJournal, } from './plan.js';
5
+ export { renderClient, ClientEmitError, tsTypeOf, methodName, } from './emit-client.js';
6
+ export { emitXState, } from './emit-xstate.js';
5
7
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,UAAU,EACV,SAAS,EACT,oBAAoB,EACpB,iBAAiB,GAGlB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAClF,OAAO,EAAE,UAAU,EAAE,UAAU,EAAoB,MAAM,aAAa,CAAC;AACvE,OAAO,EACL,aAAa,EACb,YAAY,GAIb,MAAM,WAAW,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,UAAU,EACV,SAAS,EACT,oBAAoB,EACpB,iBAAiB,GAGlB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAClF,OAAO,EAAE,UAAU,EAAE,UAAU,EAAoB,MAAM,aAAa,CAAC;AACvE,OAAO,EACL,aAAa,EACb,YAAY,GAIb,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,YAAY,EACZ,eAAe,EACf,QAAQ,EACR,UAAU,GAEX,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,UAAU,GAMX,MAAM,kBAAkB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@substrat-run/model-emit",
3
- "version": "0.5.3",
3
+ "version": "0.7.0",
4
4
  "description": "Build-time tooling over a Substrat model — DDL emitted from the entity registry, and the journal reader that holds it honest",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -22,7 +22,7 @@
22
22
  "dist"
23
23
  ],
24
24
  "dependencies": {
25
- "@substrat-run/contracts": "0.82.0"
25
+ "@substrat-run/contracts": "0.84.0"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@types/node": "^22.0.0",