@zmdb/schema 1.0.0-beta.1
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 +674 -0
- package/README.md +30 -0
- package/dist/custom-types/index.d.ts +41 -0
- package/dist/custom-types/index.d.ts.map +1 -0
- package/dist/custom-types/index.js +32 -0
- package/dist/custom-types/index.js.map +1 -0
- package/dist/derive/index.d.ts +122 -0
- package/dist/derive/index.d.ts.map +1 -0
- package/dist/derive/index.js +13 -0
- package/dist/derive/index.js.map +1 -0
- package/dist/derive/query.d.ts +62 -0
- package/dist/derive/query.d.ts.map +1 -0
- package/dist/derive/query.js +18 -0
- package/dist/derive/query.js.map +1 -0
- package/dist/dto/index.d.ts +224 -0
- package/dist/dto/index.d.ts.map +1 -0
- package/dist/dto/index.js +118 -0
- package/dist/dto/index.js.map +1 -0
- package/dist/entity-modeling/index.d.ts +12 -0
- package/dist/entity-modeling/index.d.ts.map +1 -0
- package/dist/entity-modeling/index.js +28 -0
- package/dist/entity-modeling/index.js.map +1 -0
- package/dist/index.d.ts +151 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +84 -0
- package/dist/index.js.map +1 -0
- package/dist/ir/index.d.ts +374 -0
- package/dist/ir/index.d.ts.map +1 -0
- package/dist/ir/index.js +735 -0
- package/dist/ir/index.js.map +1 -0
- package/dist/ir/validation-shape.d.ts +46 -0
- package/dist/ir/validation-shape.d.ts.map +1 -0
- package/dist/ir/validation-shape.js +130 -0
- package/dist/ir/validation-shape.js.map +1 -0
- package/dist/ir/vocabulary.d.ts +54 -0
- package/dist/ir/vocabulary.d.ts.map +1 -0
- package/dist/ir/vocabulary.js +51 -0
- package/dist/ir/vocabulary.js.map +1 -0
- package/dist/naming/index.d.ts +26 -0
- package/dist/naming/index.d.ts.map +1 -0
- package/dist/naming/index.js +147 -0
- package/dist/naming/index.js.map +1 -0
- package/dist/openapi/index.d.ts +57 -0
- package/dist/openapi/index.d.ts.map +1 -0
- package/dist/openapi/index.js +98 -0
- package/dist/openapi/index.js.map +1 -0
- package/dist/relations/index.d.ts +23 -0
- package/dist/relations/index.d.ts.map +1 -0
- package/dist/relations/index.js +98 -0
- package/dist/relations/index.js.map +1 -0
- package/dist/tags/index.d.ts +261 -0
- package/dist/tags/index.d.ts.map +1 -0
- package/dist/tags/index.js +64 -0
- package/dist/tags/index.js.map +1 -0
- package/package.json +82 -0
- package/src/custom-types/index.ts +59 -0
- package/src/derive/index.ts +224 -0
- package/src/derive/query.ts +128 -0
- package/src/dto/index.ts +395 -0
- package/src/entity-modeling/index.ts +33 -0
- package/src/index.ts +263 -0
- package/src/ir/index.ts +1085 -0
- package/src/ir/validation-shape.ts +145 -0
- package/src/ir/vocabulary.ts +56 -0
- package/src/naming/index.ts +159 -0
- package/src/openapi/index.ts +133 -0
- package/src/relations/index.ts +134 -0
- package/src/tags/index.ts +284 -0
package/dist/ir/index.js
ADDED
|
@@ -0,0 +1,735 @@
|
|
|
1
|
+
// @zmdb/schema/ir — the intermediate representation every walker consumes.
|
|
2
|
+
//
|
|
3
|
+
// This module exists because the repo grew four independent walkers over the same
|
|
4
|
+
// column metadata (`PLAN-type-first.md` §1): the AOT's `emitCheck`, its runtime
|
|
5
|
+
// `matches`/`collectIssues`, `openapi`'s `scalarSchema`, and the repository's
|
|
6
|
+
// `valueMatchesColumn`. Each had its own vocabulary and its own gaps, and they
|
|
7
|
+
// disagreed with each other. Adding a fifth for tagged types would have made it
|
|
8
|
+
// worse, so the tags land on top of one IR instead.
|
|
9
|
+
//
|
|
10
|
+
// FRONT-END IR BACK-ENDS
|
|
11
|
+
// ┌── predicate JS (is)
|
|
12
|
+
// tagged type ────▶ SchemaIR / TypeIR ─────┼── JSON Schema (openapi/llm/web)
|
|
13
|
+
// (pure data) ├── runtime walker (fallback, repository)
|
|
14
|
+
// ├── schema value (schemaFromIR)
|
|
15
|
+
// └── SQL / DDL (query-compiler)
|
|
16
|
+
//
|
|
17
|
+
// There used to be a second front-end — `irFromSchema`, which read the IR back out of a
|
|
18
|
+
// `defineSchema` value — and its whole purpose was to be the thing the tagged front-end
|
|
19
|
+
// was proved equal to. It went when `defineSchema` did. `schemaFromIR` is what remains,
|
|
20
|
+
// and it points the other way: the value is now a projection of the IR, not a source of it.
|
|
21
|
+
//
|
|
22
|
+
// Two hard constraints on everything below:
|
|
23
|
+
//
|
|
24
|
+
// 1. **The IR is serialisable JSON.** No symbols, no functions, no class
|
|
25
|
+
// instances. That is what lets the codegen CLI write it to disk, lets golden
|
|
26
|
+
// tests snapshot it, and keeps `typescript` out of every runtime bundle.
|
|
27
|
+
// 2. **`sql` stays abstract.** A `timestamp` column carries `'timestamp'`, never
|
|
28
|
+
// `'timestamptz'`. A column has three types — wire, app and db — and each
|
|
29
|
+
// layer renders the one it owns (plan D3). Rendering a dialect's spelling is
|
|
30
|
+
// the dialect's job; baking one in here would force every other back-end to
|
|
31
|
+
// parse it back out.
|
|
32
|
+
import {} from '../index.js';
|
|
33
|
+
import { KNOWN_CONSTRAINT_KINDS } from './vocabulary.js';
|
|
34
|
+
export { KNOWN_CONSTRAINT_KINDS, TAG_NAMES } from './vocabulary.js';
|
|
35
|
+
/**
|
|
36
|
+
* The four cardinalities, as data so a reader can check a string against them.
|
|
37
|
+
*
|
|
38
|
+
* Written this way round — the list first, the type derived — because `../tags` fixes
|
|
39
|
+
* `kind` to a literal per tag, but the reflection reads it back off the checker as a
|
|
40
|
+
* `string`. Deriving the type from the list is what lets that read be a check rather than
|
|
41
|
+
* an assertion, and keeps the two from drifting.
|
|
42
|
+
*/
|
|
43
|
+
export const RELATION_KINDS = ['manyToOne', 'oneToMany', 'oneToOne', 'manyToMany'];
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// Vocabulary coverage (REQ-TF-1)
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
/** Every `SqlType`, as data. `sql-types.type-test.ts` asserts exhaustiveness. */
|
|
48
|
+
export const SQL_TYPES = [
|
|
49
|
+
'serial',
|
|
50
|
+
'integer',
|
|
51
|
+
'bigint',
|
|
52
|
+
'numeric',
|
|
53
|
+
'text',
|
|
54
|
+
'varchar',
|
|
55
|
+
'boolean',
|
|
56
|
+
'timestamp',
|
|
57
|
+
'json',
|
|
58
|
+
'jsonEnum',
|
|
59
|
+
];
|
|
60
|
+
/** The closed protobuf scalar vocabulary carried by {@link ScalarIR}. */
|
|
61
|
+
export const PROTO_SCALARS = [
|
|
62
|
+
'int32',
|
|
63
|
+
'int64',
|
|
64
|
+
'uint32',
|
|
65
|
+
'uint64',
|
|
66
|
+
'sint32',
|
|
67
|
+
'sint64',
|
|
68
|
+
'fixed32',
|
|
69
|
+
'fixed64',
|
|
70
|
+
'sfixed32',
|
|
71
|
+
'sfixed64',
|
|
72
|
+
'float',
|
|
73
|
+
'double',
|
|
74
|
+
'bool',
|
|
75
|
+
'string',
|
|
76
|
+
'bytes',
|
|
77
|
+
];
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
// Back-end: IR → schema value (REQ-TF-10)
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
/**
|
|
82
|
+
* The constraints, in the spelling `ColumnMeta.validation` uses.
|
|
83
|
+
*
|
|
84
|
+
* Emitted in `KNOWN_CONSTRAINT_KINDS` order rather than any order they arrived in: the
|
|
85
|
+
* IR holds them in a record, which has none to preserve, so a fixed order is the only
|
|
86
|
+
* one that makes the output a function of the input. Named custom rules keep their name
|
|
87
|
+
* and lose their arguments, because `ColumnIR.rules` only ever held the name.
|
|
88
|
+
*/
|
|
89
|
+
function validationFromIR(col) {
|
|
90
|
+
const rules = [];
|
|
91
|
+
for (const kind of KNOWN_CONSTRAINT_KINDS) {
|
|
92
|
+
const value = col.constraints[kind];
|
|
93
|
+
if (value !== undefined)
|
|
94
|
+
rules.push({ kind, value });
|
|
95
|
+
}
|
|
96
|
+
for (const kind of col.rules)
|
|
97
|
+
rules.push({ kind });
|
|
98
|
+
return rules;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* A column's metadata. Flags are written only when set — a plain `text` column gives
|
|
102
|
+
* `{ nullable: false }` and nothing else — so the generated literal stays as small as
|
|
103
|
+
* the declaration it came from.
|
|
104
|
+
*/
|
|
105
|
+
function columnMetaFromIR(col) {
|
|
106
|
+
const validation = validationFromIR(col);
|
|
107
|
+
return {
|
|
108
|
+
type: col.sql,
|
|
109
|
+
flags: {
|
|
110
|
+
nullable: col.nullable,
|
|
111
|
+
...(col.primaryKey ? { primaryKey: true } : {}),
|
|
112
|
+
...(col.serial ? { autoIncrement: true } : {}),
|
|
113
|
+
...(col.unique ? { unique: true } : {}),
|
|
114
|
+
...(col.hasDefault ? { hasDefault: true } : {}),
|
|
115
|
+
...(col.sensitive ? { sensitive: true } : {}),
|
|
116
|
+
...(col.length === undefined ? {} : { length: col.length }),
|
|
117
|
+
...(col.enum === undefined ? {} : { enum: col.enum }),
|
|
118
|
+
},
|
|
119
|
+
...(col.default === undefined ? {} : { default: col.default }),
|
|
120
|
+
...(col.references === undefined ? {} : { references: { target: col.references } }),
|
|
121
|
+
...(validation.length === 0 ? {} : { validation }),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* The schema value, from the IR — and the only way to get one (REQ-TF-10).
|
|
126
|
+
*
|
|
127
|
+
* The query compiler wants the table name and the column types as *data*, and this is
|
|
128
|
+
* data. `@zmdb/compiler` emits the result of this function as a frozen literal, so
|
|
129
|
+
* `schemaOf<T>()` costs nothing at runtime and the tagged type stays the only place the
|
|
130
|
+
* schema is written.
|
|
131
|
+
*
|
|
132
|
+
* The IR itself is carried through on `ir` rather than left behind. Three things a
|
|
133
|
+
* `ColumnMeta` has no field for — `Numeric<P, S>` precision, a `Codec<'Name'>`, a `json`
|
|
134
|
+
* payload shape — used to be dropped here and were unrecoverable afterwards, because the
|
|
135
|
+
* only way back to an IR was to walk the flags. Keeping the IR makes the value a superset
|
|
136
|
+
* of what it projects rather than a lossy copy, and it is what let `irFromSchema` go: no
|
|
137
|
+
* consumer has to reconstruct from `columns` what the declaration already said.
|
|
138
|
+
*
|
|
139
|
+
* Nothing is registered. A generated literal is not a call and has nowhere to do that,
|
|
140
|
+
* and a global "which schema was that?" lookup is for code that has lost track of its own
|
|
141
|
+
* schema — which type-first code, by construction, has not.
|
|
142
|
+
*/
|
|
143
|
+
export function schemaFromIR(ir) {
|
|
144
|
+
const key = new Set(ir.primaryKey);
|
|
145
|
+
const columnNames = new Set(ir.columns.map(column => column.name));
|
|
146
|
+
const missing = ir.primaryKey.filter(column => !columnNames.has(column));
|
|
147
|
+
if (missing.length > 0) {
|
|
148
|
+
throw new Error(`${ir.table}: primary key names ${missing.map(column => `"${column}"`).join(', ')}, ` +
|
|
149
|
+
`${missing.length === 1 ? 'a column' : 'columns'} the table does not have`);
|
|
150
|
+
}
|
|
151
|
+
const normalizedColumns = ir.columns.map(column => {
|
|
152
|
+
const primaryKey = key.has(column.name);
|
|
153
|
+
return column.primaryKey === primaryKey ? column : { ...column, primaryKey };
|
|
154
|
+
});
|
|
155
|
+
const normalizedIr = normalizedColumns.every((column, index) => column === ir.columns[index])
|
|
156
|
+
? ir
|
|
157
|
+
: { ...ir, columns: normalizedColumns };
|
|
158
|
+
const physicalNames = new Map();
|
|
159
|
+
for (const column of normalizedIr.columns) {
|
|
160
|
+
const previous = physicalNames.get(column.physicalName);
|
|
161
|
+
if (previous !== undefined) {
|
|
162
|
+
throw new Error(`${normalizedIr.table}: \`${previous}\` and \`${column.name}\` both map to the column ` +
|
|
163
|
+
`\`${column.physicalName}\``);
|
|
164
|
+
}
|
|
165
|
+
physicalNames.set(column.physicalName, column.name);
|
|
166
|
+
}
|
|
167
|
+
const physicalByProperty = new Map(normalizedIr.columns.map(column => [column.name, column.physicalName]));
|
|
168
|
+
const columns = {};
|
|
169
|
+
for (const col of normalizedIr.columns)
|
|
170
|
+
columns[col.physicalName] = columnMetaFromIR(col);
|
|
171
|
+
return {
|
|
172
|
+
table: normalizedIr.physicalTable,
|
|
173
|
+
columns,
|
|
174
|
+
primaryKey: normalizedIr.primaryKey.map(column => physicalByProperty.get(column) ?? column),
|
|
175
|
+
references: normalizedIr.columns.flatMap(col => col.references === undefined ? [] : [{ column: col.physicalName, target: col.references }]),
|
|
176
|
+
...(normalizedIr.ftsTable === undefined ? {} : { ftsTable: normalizedIr.ftsTable }),
|
|
177
|
+
ir: normalizedIr,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
181
|
+
// The three types of a column (plan D3 / REQ-TF-13)
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
function withNull(type, nullable) {
|
|
184
|
+
return nullable ? { kind: 'union', members: [type, { kind: 'null' }] } : type;
|
|
185
|
+
}
|
|
186
|
+
function constrained(scalar, col, format) {
|
|
187
|
+
const withLength = col.length !== undefined && col.constraints.maxLength === undefined
|
|
188
|
+
? { ...col.constraints, maxLength: col.length }
|
|
189
|
+
: col.constraints;
|
|
190
|
+
const derived = format === undefined ? undefined : FORMAT_PATTERNS[format];
|
|
191
|
+
const constraints = derived !== undefined && withLength.pattern === undefined ? { ...withLength, pattern: derived } : withLength;
|
|
192
|
+
return {
|
|
193
|
+
kind: 'scalar',
|
|
194
|
+
scalar,
|
|
195
|
+
...(format === undefined ? {} : { format }),
|
|
196
|
+
...(Object.keys(constraints).length === 0 ? {} : { constraints }),
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
function extensionDimension(type) {
|
|
200
|
+
if (type.name !== 'vector')
|
|
201
|
+
return undefined;
|
|
202
|
+
const dimension = type.args?.[0];
|
|
203
|
+
return typeof dimension === 'number' && Number.isInteger(dimension) && dimension >= 0 ? dimension : undefined;
|
|
204
|
+
}
|
|
205
|
+
function extensionAppBase(col, type) {
|
|
206
|
+
if (type.name === 'vector') {
|
|
207
|
+
const dimension = extensionDimension(type);
|
|
208
|
+
return {
|
|
209
|
+
kind: 'array',
|
|
210
|
+
element: { kind: 'scalar', scalar: 'number' },
|
|
211
|
+
...(dimension === undefined ? {} : { constraints: { minLength: dimension, maxLength: dimension } }),
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
if (type.name === 'citext')
|
|
215
|
+
return constrained('string', col);
|
|
216
|
+
return {
|
|
217
|
+
kind: 'unsupported',
|
|
218
|
+
reason: `extension type "${type.name}" on column "${col.name}" needs its declared application shape ` +
|
|
219
|
+
'carried in the IR',
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* The **app** type: what handler code sees. A `timestamp` is a `Date` here, and
|
|
224
|
+
* a `bigint` is a `bigint`.
|
|
225
|
+
*/
|
|
226
|
+
export function appTypeOf(col) {
|
|
227
|
+
return withNull(appBaseOf(col), col.nullable);
|
|
228
|
+
}
|
|
229
|
+
function appBaseOf(col) {
|
|
230
|
+
if (typeof col.sql !== 'string') {
|
|
231
|
+
if (col.sql.name === 'vector' || col.sql.name === 'citext')
|
|
232
|
+
return extensionAppBase(col, col.sql);
|
|
233
|
+
if (col.payload !== undefined)
|
|
234
|
+
return col.payload;
|
|
235
|
+
return extensionAppBase(col, col.sql);
|
|
236
|
+
}
|
|
237
|
+
// A declared app type wins over the SQL type it is stored as. `amount: Money &
|
|
238
|
+
// Sql<'integer'> & Codec<'Money'>` is an integer in the database and a `Money` in the
|
|
239
|
+
// app, and a validator that checked `integer` here would reject every valid value.
|
|
240
|
+
if (col.payload !== undefined)
|
|
241
|
+
return col.payload;
|
|
242
|
+
switch (col.sql) {
|
|
243
|
+
case 'serial':
|
|
244
|
+
case 'integer':
|
|
245
|
+
return constrained('integer', col);
|
|
246
|
+
case 'bigint':
|
|
247
|
+
return constrained('bigint', col);
|
|
248
|
+
case 'numeric':
|
|
249
|
+
return constrained('number', col);
|
|
250
|
+
case 'text':
|
|
251
|
+
case 'varchar':
|
|
252
|
+
return constrained('string', col);
|
|
253
|
+
case 'boolean':
|
|
254
|
+
return { kind: 'scalar', scalar: 'boolean' };
|
|
255
|
+
case 'timestamp':
|
|
256
|
+
return { kind: 'scalar', scalar: 'date' };
|
|
257
|
+
case 'jsonEnum':
|
|
258
|
+
return col.enum === undefined || col.enum.length === 0
|
|
259
|
+
? constrained('string', col)
|
|
260
|
+
: { kind: 'union', members: col.enum.map(value => ({ kind: 'literal', value })) };
|
|
261
|
+
case 'json':
|
|
262
|
+
return JSON_CONTAINER;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* A `json` column whose payload shape is not known: anything JSON puts in a column, and
|
|
267
|
+
* nothing else. An object with no declared properties accepts any record, and an array of
|
|
268
|
+
* `unknown` accepts any array, so together they are "not a primitive".
|
|
269
|
+
*
|
|
270
|
+
* Not `{ kind: 'unknown' }`, which accepts `123`. A declaration that says what the payload
|
|
271
|
+
* is — `lines: Line[] & Sql<'json'>` — gets that type instead, via `ColumnIR.payload`; this
|
|
272
|
+
* is the answer for a bare `object & Sql<'json'>`, which really does permit any record. It
|
|
273
|
+
* is the weakest true statement rather than no statement, which is the difference between a
|
|
274
|
+
* validator that rejects `settings: 123` and one that does not.
|
|
275
|
+
*/
|
|
276
|
+
const JSON_CONTAINER = {
|
|
277
|
+
kind: 'union',
|
|
278
|
+
members: [
|
|
279
|
+
{ kind: 'object', properties: [] },
|
|
280
|
+
{ kind: 'array', element: { kind: 'unknown' } },
|
|
281
|
+
],
|
|
282
|
+
};
|
|
283
|
+
/**
|
|
284
|
+
* The assertion behind a `format`, as a `pattern`.
|
|
285
|
+
*
|
|
286
|
+
* `format` is an annotation in JSON Schema, not an assertion — a document may say
|
|
287
|
+
* `date-time` and a conforming validator may check nothing. Neither the runtime walk nor the
|
|
288
|
+
* emitter reads `format` at all, so a wire type that only said `{scalar:'string',
|
|
289
|
+
* format:'date-time'}` accepted `"tomorrow"`, and plan D3's claim that a `Wire<T>` validator
|
|
290
|
+
* checks the ISO string was not true of any validator. Lowering it to a `pattern` makes it
|
|
291
|
+
* true through machinery that already exists in both walks, which is the reason it is spelled
|
|
292
|
+
* this way rather than as a sixth constraint kind: a new keyword would need the emitter, the
|
|
293
|
+
* walker and their equivalence test, and would then check exactly what a pattern checks.
|
|
294
|
+
*
|
|
295
|
+
* `date-time` is RFC 3339, so the offset is **required**. `2020-01-01T00:00:00` is a valid
|
|
296
|
+
* ISO-8601 string and `new Date()` reads it as local time, which is the same lost-offset bug
|
|
297
|
+
* `TIMESTAMPTZ` exists to prevent — the wire is where that has to be refused, because by the
|
|
298
|
+
* time it is a `Date` the offset it was read at is gone.
|
|
299
|
+
*
|
|
300
|
+
* `int64` is `asBigInt`'s own `DECIMAL`, so what the wire validator accepts and what the
|
|
301
|
+
* decoder can convert are one expression rather than two that agree today.
|
|
302
|
+
*/
|
|
303
|
+
/** What `asBigInt` will convert. Declared here so the wire pattern is not a second copy. */
|
|
304
|
+
const DECIMAL = /^-?\d+$/;
|
|
305
|
+
const FORMAT_PATTERNS = {
|
|
306
|
+
'date-time': '^\\d{4}-\\d{2}-\\d{2}[Tt ]\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?(?:[Zz]|[+-]\\d{2}:\\d{2})$',
|
|
307
|
+
int64: DECIMAL.source,
|
|
308
|
+
};
|
|
309
|
+
/**
|
|
310
|
+
* The **wire** type: what a JSON body actually contains. A `timestamp` is an
|
|
311
|
+
* ISO-8601 string, because a `Date` cannot survive JSON, and a `bigint` is a
|
|
312
|
+
* string for the same reason. Anything else matches the app type.
|
|
313
|
+
*/
|
|
314
|
+
export function wireTypeOf(col) {
|
|
315
|
+
if (col.wire !== undefined)
|
|
316
|
+
return withNull(col.wire, col.nullable);
|
|
317
|
+
if (col.codec !== undefined) {
|
|
318
|
+
// A gap, and gaps are visible (plan D4). A codec exists because the app type is not
|
|
319
|
+
// the stored type; what it puts on the wire is a third choice that nothing but the
|
|
320
|
+
// declaration knows, and guessing "the same as the app type" is how a `Money`
|
|
321
|
+
// instance reaches `JSON.stringify`.
|
|
322
|
+
return {
|
|
323
|
+
kind: 'unsupported',
|
|
324
|
+
reason: `the codec "${col.codec}" does not say what column "${col.name}" looks like on the wire; add WireAs<…> to the declaration`,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
if (typeof col.sql !== 'string')
|
|
328
|
+
return appTypeOf(col);
|
|
329
|
+
if (col.sql === 'timestamp')
|
|
330
|
+
return withNull(constrained('string', col, 'date-time'), col.nullable);
|
|
331
|
+
if (col.sql === 'bigint')
|
|
332
|
+
return withNull(constrained('string', col, 'int64'), col.nullable);
|
|
333
|
+
return appTypeOf(col);
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* A single column's JSON Schema. Emitted from the **wire** type, which is why a
|
|
337
|
+
* `timestamp` becomes `{type:'string',format:'date-time'}` here and a `Date` in
|
|
338
|
+
* `Entity<T>` — one column, two correct answers, each in its own layer.
|
|
339
|
+
*/
|
|
340
|
+
export function jsonSchemaForColumn(col) {
|
|
341
|
+
const base = {};
|
|
342
|
+
const declared = declaredWireKeywords(col);
|
|
343
|
+
if (declared)
|
|
344
|
+
return nullableType(declared, col.nullable);
|
|
345
|
+
if (typeof col.sql !== 'string')
|
|
346
|
+
return nullableType(extensionJsonSchema(col, col.sql), col.nullable);
|
|
347
|
+
switch (col.sql) {
|
|
348
|
+
case 'serial':
|
|
349
|
+
case 'integer':
|
|
350
|
+
base.type = 'integer';
|
|
351
|
+
break;
|
|
352
|
+
case 'bigint':
|
|
353
|
+
base.type = 'integer';
|
|
354
|
+
base.format = 'int64';
|
|
355
|
+
break;
|
|
356
|
+
case 'numeric':
|
|
357
|
+
base.type = 'number';
|
|
358
|
+
break;
|
|
359
|
+
case 'text':
|
|
360
|
+
case 'varchar':
|
|
361
|
+
base.type = 'string';
|
|
362
|
+
if (col.length !== undefined)
|
|
363
|
+
base.maxLength = col.length;
|
|
364
|
+
break;
|
|
365
|
+
case 'boolean':
|
|
366
|
+
base.type = 'boolean';
|
|
367
|
+
break;
|
|
368
|
+
case 'timestamp':
|
|
369
|
+
base.type = 'string';
|
|
370
|
+
base.format = 'date-time';
|
|
371
|
+
break;
|
|
372
|
+
case 'jsonEnum':
|
|
373
|
+
base.type = 'string';
|
|
374
|
+
base.enum = [...(col.enum ?? [])];
|
|
375
|
+
break;
|
|
376
|
+
case 'json':
|
|
377
|
+
break;
|
|
378
|
+
}
|
|
379
|
+
const c = col.constraints;
|
|
380
|
+
if (c.minimum !== undefined)
|
|
381
|
+
base.minimum = c.minimum;
|
|
382
|
+
if (c.maximum !== undefined)
|
|
383
|
+
base.maximum = c.maximum;
|
|
384
|
+
if (c.minLength !== undefined)
|
|
385
|
+
base.minLength = c.minLength;
|
|
386
|
+
if (c.maxLength !== undefined)
|
|
387
|
+
base.maxLength = c.maxLength;
|
|
388
|
+
if (c.pattern !== undefined)
|
|
389
|
+
base.pattern = c.pattern;
|
|
390
|
+
// Nullable widens the `type` keyword. A `json` column has no `type` to widen,
|
|
391
|
+
// which is the pre-existing behaviour and is preserved deliberately.
|
|
392
|
+
return nullableType(base, col.nullable);
|
|
393
|
+
}
|
|
394
|
+
function extensionJsonSchema(col, type) {
|
|
395
|
+
if (type.name === 'vector') {
|
|
396
|
+
const dimension = extensionDimension(type);
|
|
397
|
+
return {
|
|
398
|
+
type: 'array',
|
|
399
|
+
items: { type: 'number' },
|
|
400
|
+
...(dimension === undefined ? {} : { minItems: dimension, maxItems: dimension }),
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
if (type.name === 'citext')
|
|
404
|
+
return jsonSchemaFromTypeIR(constrained('string', col));
|
|
405
|
+
return col.payload === undefined ? {} : jsonSchemaFromTypeIR(col.payload);
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* The JSON Schema projection of one structural type.
|
|
409
|
+
*
|
|
410
|
+
* HTTP contracts, validators and other build-time consumers call this after the
|
|
411
|
+
* existing reflector has produced TypeIR. Keeping the projection here prevents a
|
|
412
|
+
* second consumer from interpreting TypeScript types or re-walking schema metadata.
|
|
413
|
+
*/
|
|
414
|
+
export function jsonSchemaFromTypeIR(type) {
|
|
415
|
+
switch (type.kind) {
|
|
416
|
+
case 'scalar': {
|
|
417
|
+
const scalar = JSON_SCALAR_TYPES[type.scalar];
|
|
418
|
+
if (scalar === undefined)
|
|
419
|
+
return {};
|
|
420
|
+
return {
|
|
421
|
+
type: scalar,
|
|
422
|
+
...(type.format === undefined ? {} : { format: type.format }),
|
|
423
|
+
...(type.constraints?.minimum === undefined ? {} : { minimum: type.constraints.minimum }),
|
|
424
|
+
...(type.constraints?.maximum === undefined ? {} : { maximum: type.constraints.maximum }),
|
|
425
|
+
...(type.constraints?.minLength === undefined ? {} : { minLength: type.constraints.minLength }),
|
|
426
|
+
...(type.constraints?.maxLength === undefined ? {} : { maxLength: type.constraints.maxLength }),
|
|
427
|
+
...(type.constraints?.pattern === undefined ? {} : { pattern: type.constraints.pattern }),
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
case 'literal':
|
|
431
|
+
return { const: type.value };
|
|
432
|
+
case 'null':
|
|
433
|
+
return { type: 'null' };
|
|
434
|
+
case 'array':
|
|
435
|
+
return {
|
|
436
|
+
type: 'array',
|
|
437
|
+
items: jsonSchemaFromTypeIR(type.element),
|
|
438
|
+
...(type.constraints?.minLength === undefined ? {} : { minItems: type.constraints.minLength }),
|
|
439
|
+
...(type.constraints?.maxLength === undefined ? {} : { maxItems: type.constraints.maxLength }),
|
|
440
|
+
};
|
|
441
|
+
case 'tuple':
|
|
442
|
+
return {
|
|
443
|
+
type: 'array',
|
|
444
|
+
prefixItems: type.elements.map(jsonSchemaFromTypeIR),
|
|
445
|
+
minItems: type.elements.length,
|
|
446
|
+
maxItems: type.elements.length,
|
|
447
|
+
};
|
|
448
|
+
case 'object': {
|
|
449
|
+
const properties = {};
|
|
450
|
+
const required = [];
|
|
451
|
+
for (const property of type.properties) {
|
|
452
|
+
properties[property.name] = jsonSchemaFromTypeIR(property.type);
|
|
453
|
+
if (!property.optional)
|
|
454
|
+
required.push(property.name);
|
|
455
|
+
}
|
|
456
|
+
return { type: 'object', properties, required };
|
|
457
|
+
}
|
|
458
|
+
case 'union':
|
|
459
|
+
return { anyOf: type.members.map(jsonSchemaFromTypeIR) };
|
|
460
|
+
case 'undefined':
|
|
461
|
+
case 'unknown':
|
|
462
|
+
case 'ref':
|
|
463
|
+
case 'unsupported':
|
|
464
|
+
return {};
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
function nullableType(schema, nullable) {
|
|
468
|
+
if (nullable && typeof schema.type === 'string')
|
|
469
|
+
return { ...schema, type: [schema.type, 'null'] };
|
|
470
|
+
return schema;
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* The keywords for a `WireAs<W>` column, when `W` is something JSON Schema has keywords
|
|
474
|
+
* for. A scalar and a union of string literals cover every wire form seen so far — cents
|
|
475
|
+
* as a decimal string, a UUID, an enum.
|
|
476
|
+
*
|
|
477
|
+
* Anything richer (a tuple, an object) gets no `type` keyword rather than a wrong one:
|
|
478
|
+
* the same "widest true statement" a `json` column has always produced, for the same
|
|
479
|
+
* reason. It is a smaller document, not a false one.
|
|
480
|
+
*/
|
|
481
|
+
function declaredWireKeywords(col) {
|
|
482
|
+
if (col.wire === undefined)
|
|
483
|
+
return undefined;
|
|
484
|
+
const node = col.wire;
|
|
485
|
+
if (node.kind === 'scalar') {
|
|
486
|
+
const type = JSON_SCALAR_TYPES[node.scalar];
|
|
487
|
+
if (type === undefined)
|
|
488
|
+
return {};
|
|
489
|
+
return { type, ...(node.format === undefined ? {} : { format: node.format }) };
|
|
490
|
+
}
|
|
491
|
+
if (node.kind === 'union' && node.members.every(member => member.kind === 'literal')) {
|
|
492
|
+
// boundary: `every` proves the predicate for every member but returns a `boolean`, so
|
|
493
|
+
// the narrowing does not reach the `map` that follows. Re-testing `kind` inside the map
|
|
494
|
+
// would be the same check twice for a branch that cannot be taken.
|
|
495
|
+
return { enum: node.members.map(member => member.value) };
|
|
496
|
+
}
|
|
497
|
+
return {};
|
|
498
|
+
}
|
|
499
|
+
/** The JSON Schema `type` for a scalar the wire can carry. `date` and `bigint` cannot. */
|
|
500
|
+
const JSON_SCALAR_TYPES = {
|
|
501
|
+
string: 'string',
|
|
502
|
+
number: 'number',
|
|
503
|
+
integer: 'integer',
|
|
504
|
+
boolean: 'boolean',
|
|
505
|
+
};
|
|
506
|
+
/**
|
|
507
|
+
* A variant, rewritten as a shape.
|
|
508
|
+
*
|
|
509
|
+
* The three rules the variants used to spell out inline: an input variant has no
|
|
510
|
+
* database-generated columns at all, a patch requires nothing, and an input column with
|
|
511
|
+
* a default may be left out. Each is exactly what the corresponding derived type does to
|
|
512
|
+
* `Entity<T>`, which is the reason this translation exists rather than a coincidence.
|
|
513
|
+
*
|
|
514
|
+
* `update` also drops the primary key, which is what `UpdateDTO<T>` does and what this
|
|
515
|
+
* function did not: a patch body identifies its row in the URL, so a key in the body is
|
|
516
|
+
* either redundant or an attempt to move the row. It only ever showed for a *non-serial*
|
|
517
|
+
* key, since a serial one was already gone, which is why no existing document changes.
|
|
518
|
+
*/
|
|
519
|
+
export function shapeOfVariant(ir, variant) {
|
|
520
|
+
const isResponse = variant === 'entity' || variant === 'get' || variant === 'list' || variant === 'search';
|
|
521
|
+
return ir.columns
|
|
522
|
+
.filter(col => isResponse || (!col.serial && col.name !== ir.softDelete?.column))
|
|
523
|
+
.filter(col => variant !== 'update' || !col.primaryKey)
|
|
524
|
+
.map(col => ({
|
|
525
|
+
column: col,
|
|
526
|
+
// Nullable is optional on the way in, for the reason `CreateDTO`'s comment gives:
|
|
527
|
+
// omitting the key inserts `NULL`, so requiring it buys nothing. On the way out it
|
|
528
|
+
// is not — a row that came back has every column, `null` included.
|
|
529
|
+
optional: variant === 'update' || (!isResponse && (col.hasDefault || col.nullable)),
|
|
530
|
+
}));
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* The document for a shape.
|
|
534
|
+
*
|
|
535
|
+
* `required` is "not optional and not nullable", which is the single rule the three
|
|
536
|
+
* variants were three cases of. A nullable column is never required because the value
|
|
537
|
+
* `null` is admissible for it, so demanding the key adds nothing a validator can act on
|
|
538
|
+
* — that is pre-existing behaviour, preserved deliberately.
|
|
539
|
+
*
|
|
540
|
+
* Sensitive columns are dropped here, in the emitter, and not in the shape. A generated
|
|
541
|
+
* document is published, `Sensitive` means "must not be", and putting the filter at the
|
|
542
|
+
* last step is what makes that unconditional (REQ-TF-6): no variant, and no derived type
|
|
543
|
+
* a caller invents, can route around it. `CreateDTO<User>` deliberately *keeps* a
|
|
544
|
+
* sensitive column — you have to be able to send a password — and its document still
|
|
545
|
+
* must not name it.
|
|
546
|
+
*/
|
|
547
|
+
export function jsonSchemaFromShape(shape) {
|
|
548
|
+
const visible = shape
|
|
549
|
+
.filter(entry => !entry.column.sensitive)
|
|
550
|
+
.toSorted((a, b) => a.column.name.localeCompare(b.column.name));
|
|
551
|
+
const properties = {};
|
|
552
|
+
const required = [];
|
|
553
|
+
for (const { column, optional } of visible) {
|
|
554
|
+
properties[column.name] = jsonSchemaForColumn(column);
|
|
555
|
+
if (!optional && !column.nullable)
|
|
556
|
+
required.push(column.name);
|
|
557
|
+
}
|
|
558
|
+
return { type: 'object', properties, required: required.toSorted() };
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* The document for a variant. Byte-for-byte the contract `toJsonSchema` already
|
|
562
|
+
* publishes; the point is that it is a pure function of IR, so naming a variant and
|
|
563
|
+
* naming a derived type cannot produce different documents (REQ-TF-7). That AC stops
|
|
564
|
+
* being a test to chase and becomes the only thing the code can do.
|
|
565
|
+
*/
|
|
566
|
+
export function jsonSchemaFromIR(ir, variant = 'entity') {
|
|
567
|
+
return jsonSchemaFromShape(shapeOfVariant(ir, variant));
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* A shape as the object type a validator checks against.
|
|
571
|
+
*
|
|
572
|
+
* Unlike the JSON Schema back-end this keeps sensitive columns: a payload validator that
|
|
573
|
+
* silently ignored `passwordHash` would reject every legitimate `create`. REQ-TF-6 is
|
|
574
|
+
* about what gets *published*, and nothing here is published.
|
|
575
|
+
*
|
|
576
|
+
* Column order is preserved rather than sorted, because a `TypeIR` is not a contract
|
|
577
|
+
* anybody serialises — the JSON Schema back-end sorts because a document is published
|
|
578
|
+
* and key order is part of it.
|
|
579
|
+
*/
|
|
580
|
+
export function objectTypeFromShape(shape, layer = 'app') {
|
|
581
|
+
return {
|
|
582
|
+
kind: 'object',
|
|
583
|
+
properties: shape.map(({ column, optional }) => ({
|
|
584
|
+
name: column.name,
|
|
585
|
+
type: layer === 'wire' ? wireTypeOf(column) : appTypeOf(column),
|
|
586
|
+
optional,
|
|
587
|
+
// A DTO is a plain object the caller just built, so nothing about it is readonly.
|
|
588
|
+
// `Entity<T>` is `-readonly` for the same reason.
|
|
589
|
+
readonly: false,
|
|
590
|
+
})),
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
/** The object type of one variant of one table, at one layer. */
|
|
594
|
+
export function objectTypeFromIR(ir, variant = 'entity', layer = 'app') {
|
|
595
|
+
return objectTypeFromShape(shapeOfVariant(ir, variant), layer);
|
|
596
|
+
}
|
|
597
|
+
/** An ISO-8601 string, if that is what this is and it parses. */
|
|
598
|
+
function asDate(value) {
|
|
599
|
+
if (typeof value !== 'string')
|
|
600
|
+
return value;
|
|
601
|
+
const parsed = new Date(value);
|
|
602
|
+
return Number.isNaN(parsed.getTime()) ? value : parsed;
|
|
603
|
+
}
|
|
604
|
+
/**
|
|
605
|
+
* A decimal string as a `bigint`.
|
|
606
|
+
*
|
|
607
|
+
* `DECIMAL` rather than a bare `BigInt()` call in a `try`: `BigInt('0x10')` is 16 and
|
|
608
|
+
* `BigInt('')` is 0, neither of which is something a caller meant to send.
|
|
609
|
+
*/
|
|
610
|
+
function asBigInt(value) {
|
|
611
|
+
return typeof value === 'string' && DECIMAL.test(value) ? BigInt(value) : value;
|
|
612
|
+
}
|
|
613
|
+
const VECTOR_COMPONENT = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/;
|
|
614
|
+
function asVector(value) {
|
|
615
|
+
if (Array.isArray(value) || typeof value !== 'string' || !value.startsWith('[') || !value.endsWith(']')) {
|
|
616
|
+
return value;
|
|
617
|
+
}
|
|
618
|
+
const body = value.slice(1, -1);
|
|
619
|
+
if (body === '')
|
|
620
|
+
return [];
|
|
621
|
+
const fields = body.split(',');
|
|
622
|
+
if (fields.some(field => !VECTOR_COMPONENT.test(field.trim())))
|
|
623
|
+
return value;
|
|
624
|
+
const vector = fields.map(field => Number(field.trim()));
|
|
625
|
+
return vector.every(Number.isFinite) ? vector : value;
|
|
626
|
+
}
|
|
627
|
+
function codecFor(col, codecs) {
|
|
628
|
+
if (col.codec === undefined)
|
|
629
|
+
return undefined;
|
|
630
|
+
const codec = codecs[col.codec];
|
|
631
|
+
if (!codec) {
|
|
632
|
+
// A named codec with nothing behind it is a gap, and a gap has to be visible (plan
|
|
633
|
+
// D4). Silently passing the value through would store whatever JSON happened to
|
|
634
|
+
// carry in a column whose whole point is that it needs converting.
|
|
635
|
+
throw new Error(`column "${col.name}" names the codec "${col.codec}", which is not in the registry`);
|
|
636
|
+
}
|
|
637
|
+
return codec;
|
|
638
|
+
}
|
|
639
|
+
/** One column's value, as the app layer holds it. */
|
|
640
|
+
export function decodeWireValue(col, value, codecs = {}) {
|
|
641
|
+
if (value === null || value === undefined)
|
|
642
|
+
return value;
|
|
643
|
+
const codec = codecFor(col, codecs);
|
|
644
|
+
if (codec)
|
|
645
|
+
return codec.decode(value);
|
|
646
|
+
if (col.sql === 'timestamp')
|
|
647
|
+
return asDate(value);
|
|
648
|
+
if (col.sql === 'bigint')
|
|
649
|
+
return asBigInt(value);
|
|
650
|
+
return value;
|
|
651
|
+
}
|
|
652
|
+
/** One column's value, as JSON can carry it. */
|
|
653
|
+
export function encodeWireValue(col, value, codecs = {}) {
|
|
654
|
+
if (value === null || value === undefined)
|
|
655
|
+
return value;
|
|
656
|
+
const codec = codecFor(col, codecs);
|
|
657
|
+
if (codec)
|
|
658
|
+
return codec.encode(value);
|
|
659
|
+
if (col.sql === 'timestamp' && value instanceof Date) {
|
|
660
|
+
return Number.isNaN(value.getTime()) ? value : value.toISOString();
|
|
661
|
+
}
|
|
662
|
+
if (col.sql === 'bigint' && typeof value === 'bigint')
|
|
663
|
+
return value.toString();
|
|
664
|
+
return value;
|
|
665
|
+
}
|
|
666
|
+
/**
|
|
667
|
+
* A JSON body as an app-layer payload: the wire→app decode, once, at the boundary.
|
|
668
|
+
*
|
|
669
|
+
* Keys the variant does not have are copied through rather than dropped, because dropping
|
|
670
|
+
* them here would hide them from the repository's excess check — the decoder's job is to
|
|
671
|
+
* convert, and deciding what a payload may contain belongs to exactly one place.
|
|
672
|
+
*/
|
|
673
|
+
export function decodeWire(ir, variant, body, codecs = {}) {
|
|
674
|
+
const columns = new Map(shapeOfVariant(ir, variant).map(({ column }) => [column.name, column]));
|
|
675
|
+
const out = {};
|
|
676
|
+
for (const key of Object.keys(body)) {
|
|
677
|
+
const column = columns.get(key);
|
|
678
|
+
out[key] = column ? decodeWireValue(column, body[key], codecs) : body[key];
|
|
679
|
+
}
|
|
680
|
+
return out;
|
|
681
|
+
}
|
|
682
|
+
/**
|
|
683
|
+
* A value that came out of a database, as the app layer holds it — the third layer's
|
|
684
|
+
* crossing (plan D3).
|
|
685
|
+
*
|
|
686
|
+
* Written in terms of what *arrived* rather than in terms of the dialect, and that is the
|
|
687
|
+
* whole design: `pg` hands back a `Date` for a `timestamptz` and a string for an `int8`,
|
|
688
|
+
* SQLite hands back the `TEXT` it stored and a `number` for an `INTEGER`, and a third
|
|
689
|
+
* driver will do something else again. Asking "is this already the app value?" answers all
|
|
690
|
+
* of them, and keeps this function out of the dialect's business — which is the constraint
|
|
691
|
+
* the whole IR is written under.
|
|
692
|
+
*
|
|
693
|
+
* `timestamp` and `bigint` are the only core types whose app values need a distinct JSON
|
|
694
|
+
* wire form. The db crossing also handles extension vectors: their app and wire forms are
|
|
695
|
+
* both number arrays, but a driver without pgvector's parser can return the database text
|
|
696
|
+
* form instead.
|
|
697
|
+
*/
|
|
698
|
+
export function decodeDbValue(col, value) {
|
|
699
|
+
if (value === null || value === undefined)
|
|
700
|
+
return value;
|
|
701
|
+
if (typeof col.sql !== 'string')
|
|
702
|
+
return col.sql.name === 'vector' ? asVector(value) : value;
|
|
703
|
+
if (col.sql === 'timestamp')
|
|
704
|
+
return asDate(value);
|
|
705
|
+
if (col.sql === 'bigint') {
|
|
706
|
+
// A driver that read an 8-byte integer into a `number`. Safe integers only: past 2^53
|
|
707
|
+
// the number has already lost digits, and `BigInt(9007199254740993)` would state a
|
|
708
|
+
// value the database never held — better to hand back the number the driver gave and
|
|
709
|
+
// let the validator say the app type is not what arrived.
|
|
710
|
+
if (typeof value === 'number')
|
|
711
|
+
return Number.isSafeInteger(value) ? BigInt(value) : value;
|
|
712
|
+
return asBigInt(value);
|
|
713
|
+
}
|
|
714
|
+
return value;
|
|
715
|
+
}
|
|
716
|
+
/** Which columns `decodeDbValue` can change — so a read path can skip the walk entirely. */
|
|
717
|
+
export function dbDecodedColumns(ir) {
|
|
718
|
+
return ir.columns.filter(col => {
|
|
719
|
+
if (typeof col.sql === 'string')
|
|
720
|
+
return col.sql === 'timestamp' || col.sql === 'bigint';
|
|
721
|
+
return col.sql.name === 'vector';
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
/** A row as a JSON body: the app→wire encode, for a response. */
|
|
725
|
+
export function encodeWire(ir, row, codecs = {}) {
|
|
726
|
+
const columns = new Map(ir.columns.map(column => [column.name, column]));
|
|
727
|
+
const out = {};
|
|
728
|
+
for (const key of Object.keys(row)) {
|
|
729
|
+
const column = columns.get(key);
|
|
730
|
+
out[key] = column ? encodeWireValue(column, row[key], codecs) : row[key];
|
|
731
|
+
}
|
|
732
|
+
return out;
|
|
733
|
+
}
|
|
734
|
+
export { discriminantOf, expectedForConstraint, expectedForDiscriminant, expectedOf, hasExcessCheck, messageFor, } from './validation-shape.js';
|
|
735
|
+
//# sourceMappingURL=index.js.map
|