@typed-sql/schema 2.0.0 → 2.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/CHANGELOG.md +62 -0
- package/dist/packages/schema/src/generator.d.ts +29 -1
- package/dist/packages/schema/src/generator.d.ts.map +1 -1
- package/dist/packages/schema/src/generator.js +104 -19
- package/dist/packages/schema/src/generator.js.map +1 -1
- package/dist/packages/schema/src/index.d.ts +6 -5
- package/dist/packages/schema/src/index.d.ts.map +1 -1
- package/dist/packages/schema/src/index.js +4 -3
- package/dist/packages/schema/src/index.js.map +1 -1
- package/dist/packages/schema/src/loader.d.ts +4 -4
- package/dist/packages/schema/src/loader.d.ts.map +1 -1
- package/dist/packages/schema/src/loader.js +26 -148
- package/dist/packages/schema/src/loader.js.map +1 -1
- package/dist/packages/schema/src/model.d.ts +13 -44
- package/dist/packages/schema/src/model.d.ts.map +1 -1
- package/dist/packages/schema/src/model.js +2 -1
- package/dist/packages/schema/src/model.js.map +1 -1
- package/dist/packages/schema/src/v1/codec.d.ts +3 -0
- package/dist/packages/schema/src/v1/codec.d.ts.map +1 -0
- package/dist/packages/schema/src/v1/codec.js +151 -0
- package/dist/packages/schema/src/v1/codec.js.map +1 -0
- package/dist/packages/schema/src/v1/model.d.ts +44 -0
- package/dist/packages/schema/src/v1/model.d.ts.map +1 -0
- package/dist/packages/schema/src/v1/model.js +2 -0
- package/dist/packages/schema/src/v1/model.js.map +1 -0
- package/dist/packages/schema/src/v1/upgrade.d.ts +5 -0
- package/dist/packages/schema/src/v1/upgrade.d.ts.map +1 -0
- package/dist/packages/schema/src/v1/upgrade.js +141 -0
- package/dist/packages/schema/src/v1/upgrade.js.map +1 -0
- package/dist/packages/schema/src/v2/codec.d.ts +8 -0
- package/dist/packages/schema/src/v2/codec.d.ts.map +1 -0
- package/dist/packages/schema/src/v2/codec.js +659 -0
- package/dist/packages/schema/src/v2/codec.js.map +1 -0
- package/dist/packages/schema/src/v2/model.d.ts +231 -0
- package/dist/packages/schema/src/v2/model.d.ts.map +1 -0
- package/dist/packages/schema/src/v2/model.js +2 -0
- package/dist/packages/schema/src/v2/model.js.map +1 -0
- package/package.json +4 -2
|
@@ -0,0 +1,659 @@
|
|
|
1
|
+
import { parseDialectServerEvidence } from "@typed-sql/core";
|
|
2
|
+
import { SCHEMA_FORMAT_VERSION } from "./model.js";
|
|
3
|
+
const secretKeyPattern = /(?:authorization|connection|string|credential|databaseurl|dsn|error|password|path|permission|sample|secret|statistic|token|url|user)/iu;
|
|
4
|
+
const sensitiveValuePattern = /(?:[a-z][a-z0-9+.-]*:\/\/|(?:password|secret|token)\s*[=:]|(?:^|[\\/])Users[\\/])/iu;
|
|
5
|
+
const fingerprintPattern = /^sha256:[a-f\d]{64}$/u;
|
|
6
|
+
function record(value, path) {
|
|
7
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
8
|
+
throw new TypeError(`${path} must be an object`);
|
|
9
|
+
return value;
|
|
10
|
+
}
|
|
11
|
+
function allowed(value, keys, path) {
|
|
12
|
+
const unknown = Object.keys(value)
|
|
13
|
+
.filter((key) => !keys.includes(key))
|
|
14
|
+
.sort();
|
|
15
|
+
if (unknown.length > 0)
|
|
16
|
+
throw new TypeError(`${path} contains unknown properties: ${unknown.join(", ")}`);
|
|
17
|
+
}
|
|
18
|
+
function text(value, path) {
|
|
19
|
+
if (typeof value !== "string" || value.length === 0)
|
|
20
|
+
throw new TypeError(`${path} must be a non-empty string`);
|
|
21
|
+
if (value.length > 2_000 || [...value].some((item) => item.charCodeAt(0) < 32 || item.charCodeAt(0) === 127)) {
|
|
22
|
+
throw new TypeError(`${path} is not safe artifact text`);
|
|
23
|
+
}
|
|
24
|
+
if (sensitiveValuePattern.test(value))
|
|
25
|
+
throw new TypeError(`${path} appears to contain secret material`);
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
function optionalText(value, path) {
|
|
29
|
+
return value === undefined ? undefined : text(value, path);
|
|
30
|
+
}
|
|
31
|
+
function parseMetadata(value, path) {
|
|
32
|
+
if (value === undefined)
|
|
33
|
+
return undefined;
|
|
34
|
+
const source = record(value, path);
|
|
35
|
+
allowed(source, ["generatorVersion", "schemaHash", "typePolicyHash", "schemaFormat"], path);
|
|
36
|
+
const schemaFormat = source.schemaFormat;
|
|
37
|
+
if (schemaFormat !== undefined && schemaFormat !== 1 && schemaFormat !== 2) {
|
|
38
|
+
throw new TypeError(`${path}.schemaFormat must be 1 or 2`);
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
generatorVersion: text(source.generatorVersion, `${path}.generatorVersion`),
|
|
42
|
+
schemaHash: text(source.schemaHash, `${path}.schemaHash`),
|
|
43
|
+
typePolicyHash: text(source.typePolicyHash, `${path}.typePolicyHash`),
|
|
44
|
+
...(schemaFormat === undefined ? {} : { schemaFormat }),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function bool(value, path) {
|
|
48
|
+
if (typeof value !== "boolean")
|
|
49
|
+
throw new TypeError(`${path} must be a boolean`);
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
function integer(value, path) {
|
|
53
|
+
if (!Number.isSafeInteger(value) || value < 0)
|
|
54
|
+
throw new TypeError(`${path} must be a non-negative integer`);
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
57
|
+
function oneOf(value, values, path) {
|
|
58
|
+
if (typeof value !== "string" || !values.includes(value)) {
|
|
59
|
+
throw new TypeError(`${path} must be one of: ${values.join(", ")}`);
|
|
60
|
+
}
|
|
61
|
+
return value;
|
|
62
|
+
}
|
|
63
|
+
function booleanOrUnknown(value, path) {
|
|
64
|
+
if (value === "unknown" || typeof value === "boolean")
|
|
65
|
+
return value;
|
|
66
|
+
throw new TypeError(`${path} must be a boolean or unknown`);
|
|
67
|
+
}
|
|
68
|
+
function stringArray(value, path) {
|
|
69
|
+
if (!Array.isArray(value))
|
|
70
|
+
throw new TypeError(`${path} must be an array`);
|
|
71
|
+
return value.map((item, index) => text(item, `${path}[${index}]`));
|
|
72
|
+
}
|
|
73
|
+
function fingerprint(value, path) {
|
|
74
|
+
if (typeof value !== "string" || !fingerprintPattern.test(value)) {
|
|
75
|
+
throw new TypeError(`${path} must be a sha256 fingerprint`);
|
|
76
|
+
}
|
|
77
|
+
return value;
|
|
78
|
+
}
|
|
79
|
+
function extensionValue(value, path, key) {
|
|
80
|
+
if (key !== undefined && secretKeyPattern.test(key.replaceAll(/[^A-Za-z]/gu, ""))) {
|
|
81
|
+
throw new TypeError(`${path} is not allowed in schema extension evidence`);
|
|
82
|
+
}
|
|
83
|
+
if (value === null || typeof value === "boolean")
|
|
84
|
+
return value;
|
|
85
|
+
if (typeof value === "number") {
|
|
86
|
+
if (!Number.isFinite(value))
|
|
87
|
+
throw new TypeError(`${path} must be finite`);
|
|
88
|
+
return value;
|
|
89
|
+
}
|
|
90
|
+
if (typeof value === "string")
|
|
91
|
+
return value.length === 0 ? value : text(value, path);
|
|
92
|
+
if (Array.isArray(value))
|
|
93
|
+
return value.map((item, index) => extensionValue(item, `${path}[${index}]`));
|
|
94
|
+
const source = record(value, path);
|
|
95
|
+
return Object.fromEntries(Object.keys(source)
|
|
96
|
+
.sort()
|
|
97
|
+
.map((item) => [item, extensionValue(source[item], `${path}.${item}`, item)]));
|
|
98
|
+
}
|
|
99
|
+
function parseExtension(value, path) {
|
|
100
|
+
if (value === undefined)
|
|
101
|
+
return undefined;
|
|
102
|
+
const source = record(value, path);
|
|
103
|
+
allowed(source, ["version", "attributes"], path);
|
|
104
|
+
const attributes = extensionValue(source.attributes, `${path}.attributes`);
|
|
105
|
+
if (typeof attributes !== "object" || attributes === null || Array.isArray(attributes)) {
|
|
106
|
+
throw new TypeError(`${path}.attributes must be an object`);
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
version: text(source.version, `${path}.version`),
|
|
110
|
+
attributes: attributes,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
function objectMap(value, path, parser) {
|
|
114
|
+
const source = record(value, path);
|
|
115
|
+
return Object.fromEntries(Object.keys(source)
|
|
116
|
+
.sort()
|
|
117
|
+
.map((key) => [key, parser(source[key], `${path}.${key}`)]));
|
|
118
|
+
}
|
|
119
|
+
function parseColumn(value, path) {
|
|
120
|
+
const source = record(value, path);
|
|
121
|
+
allowed(source, [
|
|
122
|
+
"name",
|
|
123
|
+
"position",
|
|
124
|
+
"databaseType",
|
|
125
|
+
"typeIdentity",
|
|
126
|
+
"tsType",
|
|
127
|
+
"nullable",
|
|
128
|
+
"nullabilitySource",
|
|
129
|
+
"default",
|
|
130
|
+
"defaultExpressionHash",
|
|
131
|
+
"generated",
|
|
132
|
+
"generatedExpressionHash",
|
|
133
|
+
"identity",
|
|
134
|
+
"collation",
|
|
135
|
+
"characterSet",
|
|
136
|
+
"dimensions",
|
|
137
|
+
"classification",
|
|
138
|
+
"insertable",
|
|
139
|
+
"updatable",
|
|
140
|
+
"extension",
|
|
141
|
+
], path);
|
|
142
|
+
const defaultExpressionHash = source.defaultExpressionHash === undefined
|
|
143
|
+
? undefined
|
|
144
|
+
: fingerprint(source.defaultExpressionHash, `${path}.defaultExpressionHash`);
|
|
145
|
+
const dimensions = source.dimensions === undefined
|
|
146
|
+
? undefined
|
|
147
|
+
: Array.isArray(source.dimensions)
|
|
148
|
+
? source.dimensions.map((item, index) => integer(item, `${path}.dimensions[${index}]`))
|
|
149
|
+
: (() => {
|
|
150
|
+
throw new TypeError(`${path}.dimensions must be an array`);
|
|
151
|
+
})();
|
|
152
|
+
const extension = parseExtension(source.extension, `${path}.extension`);
|
|
153
|
+
return {
|
|
154
|
+
name: text(source.name, `${path}.name`),
|
|
155
|
+
position: integer(source.position, `${path}.position`),
|
|
156
|
+
databaseType: text(source.databaseType, `${path}.databaseType`),
|
|
157
|
+
typeIdentity: text(source.typeIdentity, `${path}.typeIdentity`),
|
|
158
|
+
tsType: text(source.tsType, `${path}.tsType`),
|
|
159
|
+
nullable: bool(source.nullable, `${path}.nullable`),
|
|
160
|
+
nullabilitySource: oneOf(source.nullabilitySource, ["declared", "domain", "generated", "inferred", "unknown"], `${path}.nullabilitySource`),
|
|
161
|
+
default: oneOf(source.default, ["none", "present", "unknown"], `${path}.default`),
|
|
162
|
+
...(defaultExpressionHash === undefined ? {} : { defaultExpressionHash }),
|
|
163
|
+
generated: oneOf(source.generated, ["none", "virtual", "stored"], `${path}.generated`),
|
|
164
|
+
...(source.generatedExpressionHash === undefined
|
|
165
|
+
? {}
|
|
166
|
+
: { generatedExpressionHash: fingerprint(source.generatedExpressionHash, `${path}.generatedExpressionHash`) }),
|
|
167
|
+
identity: oneOf(source.identity, ["none", "always", "by-default", "unknown"], `${path}.identity`),
|
|
168
|
+
...(source.collation === undefined ? {} : { collation: text(source.collation, `${path}.collation`) }),
|
|
169
|
+
...(source.characterSet === undefined ? {} : { characterSet: text(source.characterSet, `${path}.characterSet`) }),
|
|
170
|
+
...(dimensions === undefined ? {} : { dimensions }),
|
|
171
|
+
classification: oneOf(source.classification, ["normal", "hidden", "system", "rowid"], `${path}.classification`),
|
|
172
|
+
insertable: booleanOrUnknown(source.insertable, `${path}.insertable`),
|
|
173
|
+
updatable: booleanOrUnknown(source.updatable, `${path}.updatable`),
|
|
174
|
+
...(extension === undefined ? {} : { extension }),
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
function constraintBase(source, path) {
|
|
178
|
+
const extension = parseExtension(source.extension, `${path}.extension`);
|
|
179
|
+
return {
|
|
180
|
+
...(source.name === undefined ? {} : { name: text(source.name, `${path}.name`) }),
|
|
181
|
+
identity: text(source.identity, `${path}.identity`),
|
|
182
|
+
columns: stringArray(source.columns, `${path}.columns`),
|
|
183
|
+
partial: booleanOrUnknown(source.partial, `${path}.partial`),
|
|
184
|
+
expressionBased: booleanOrUnknown(source.expressionBased, `${path}.expressionBased`),
|
|
185
|
+
deferrable: booleanOrUnknown(source.deferrable, `${path}.deferrable`),
|
|
186
|
+
initiallyDeferred: booleanOrUnknown(source.initiallyDeferred, `${path}.initiallyDeferred`),
|
|
187
|
+
...(extension === undefined ? {} : { extension }),
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
const constraintBaseKeys = [
|
|
191
|
+
"kind",
|
|
192
|
+
"name",
|
|
193
|
+
"identity",
|
|
194
|
+
"columns",
|
|
195
|
+
"partial",
|
|
196
|
+
"expressionBased",
|
|
197
|
+
"deferrable",
|
|
198
|
+
"initiallyDeferred",
|
|
199
|
+
"extension",
|
|
200
|
+
];
|
|
201
|
+
function parseExclusionElement(value, path) {
|
|
202
|
+
const source = record(value, path);
|
|
203
|
+
allowed(source, ["column", "expressionHash", "operator", "operatorClass", "collation"], path);
|
|
204
|
+
const column = optionalText(source.column, `${path}.column`);
|
|
205
|
+
const expressionHash = source.expressionHash === undefined ? undefined : fingerprint(source.expressionHash, `${path}.expressionHash`);
|
|
206
|
+
if ((column === undefined) === (expressionHash === undefined)) {
|
|
207
|
+
throw new TypeError(`${path} must contain exactly one of column or expressionHash`);
|
|
208
|
+
}
|
|
209
|
+
return {
|
|
210
|
+
...(column === undefined ? {} : { column }),
|
|
211
|
+
...(expressionHash === undefined ? {} : { expressionHash }),
|
|
212
|
+
operator: text(source.operator, `${path}.operator`),
|
|
213
|
+
...(source.operatorClass === undefined
|
|
214
|
+
? {}
|
|
215
|
+
: { operatorClass: text(source.operatorClass, `${path}.operatorClass`) }),
|
|
216
|
+
...(source.collation === undefined ? {} : { collation: text(source.collation, `${path}.collation`) }),
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
function parseConstraint(value, path) {
|
|
220
|
+
const source = record(value, path);
|
|
221
|
+
const kind = oneOf(source.kind, ["primary-key", "unique", "foreign-key", "check", "exclusion"], `${path}.kind`);
|
|
222
|
+
const base = constraintBase(source, path);
|
|
223
|
+
if (kind === "primary-key") {
|
|
224
|
+
allowed(source, [...constraintBaseKeys, "nullsDistinct"], path);
|
|
225
|
+
if (source.nullsDistinct !== false)
|
|
226
|
+
throw new TypeError(`${path}.nullsDistinct must be false`);
|
|
227
|
+
return { kind, ...base, nullsDistinct: false };
|
|
228
|
+
}
|
|
229
|
+
if (kind === "unique") {
|
|
230
|
+
allowed(source, [...constraintBaseKeys, "nullsDistinct"], path);
|
|
231
|
+
return { kind, ...base, nullsDistinct: booleanOrUnknown(source.nullsDistinct, `${path}.nullsDistinct`) };
|
|
232
|
+
}
|
|
233
|
+
if (kind === "foreign-key") {
|
|
234
|
+
allowed(source, [...constraintBaseKeys, "referencedRelation", "referencedColumns", "match", "onUpdate", "onDelete"], path);
|
|
235
|
+
return {
|
|
236
|
+
kind,
|
|
237
|
+
...base,
|
|
238
|
+
referencedRelation: text(source.referencedRelation, `${path}.referencedRelation`),
|
|
239
|
+
referencedColumns: stringArray(source.referencedColumns, `${path}.referencedColumns`),
|
|
240
|
+
match: oneOf(source.match, ["simple", "full", "partial", "unknown"], `${path}.match`),
|
|
241
|
+
onUpdate: oneOf(source.onUpdate, ["no-action", "restrict", "cascade", "set-null", "set-default", "unknown"], `${path}.onUpdate`),
|
|
242
|
+
onDelete: oneOf(source.onDelete, ["no-action", "restrict", "cascade", "set-null", "set-default", "unknown"], `${path}.onDelete`),
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
if (kind === "check") {
|
|
246
|
+
allowed(source, [...constraintBaseKeys, "predicate", "predicateHash"], path);
|
|
247
|
+
const predicateHash = source.predicateHash === undefined ? undefined : fingerprint(source.predicateHash, `${path}.predicateHash`);
|
|
248
|
+
const result = {
|
|
249
|
+
kind,
|
|
250
|
+
...base,
|
|
251
|
+
predicate: oneOf(source.predicate, ["present", "unknown"], `${path}.predicate`),
|
|
252
|
+
...(predicateHash === undefined ? {} : { predicateHash }),
|
|
253
|
+
};
|
|
254
|
+
return result;
|
|
255
|
+
}
|
|
256
|
+
allowed(source, [...constraintBaseKeys, "elements", "predicateHash"], path);
|
|
257
|
+
if (!Array.isArray(source.elements))
|
|
258
|
+
throw new TypeError(`${path}.elements must be an array`);
|
|
259
|
+
const predicateHash = source.predicateHash === undefined ? undefined : fingerprint(source.predicateHash, `${path}.predicateHash`);
|
|
260
|
+
return {
|
|
261
|
+
kind,
|
|
262
|
+
...base,
|
|
263
|
+
elements: source.elements.map((item, index) => parseExclusionElement(item, `${path}.elements[${index}]`)),
|
|
264
|
+
...(predicateHash === undefined ? {} : { predicateHash }),
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
function parseIndexColumn(value, path) {
|
|
268
|
+
const source = record(value, path);
|
|
269
|
+
allowed(source, ["column", "expressionHash", "descending", "nulls", "operatorClass", "collation"], path);
|
|
270
|
+
const column = optionalText(source.column, `${path}.column`);
|
|
271
|
+
const expressionHash = source.expressionHash === undefined ? undefined : fingerprint(source.expressionHash, `${path}.expressionHash`);
|
|
272
|
+
if ((column === undefined) === (expressionHash === undefined)) {
|
|
273
|
+
throw new TypeError(`${path} must contain exactly one of column or expressionHash`);
|
|
274
|
+
}
|
|
275
|
+
return {
|
|
276
|
+
...(column === undefined ? {} : { column }),
|
|
277
|
+
...(expressionHash === undefined ? {} : { expressionHash }),
|
|
278
|
+
...(source.descending === undefined ? {} : { descending: bool(source.descending, `${path}.descending`) }),
|
|
279
|
+
...(source.nulls === undefined ? {} : { nulls: oneOf(source.nulls, ["first", "last"], `${path}.nulls`) }),
|
|
280
|
+
...(source.operatorClass === undefined
|
|
281
|
+
? {}
|
|
282
|
+
: { operatorClass: text(source.operatorClass, `${path}.operatorClass`) }),
|
|
283
|
+
...(source.collation === undefined ? {} : { collation: text(source.collation, `${path}.collation`) }),
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
function parseIndex(value, path) {
|
|
287
|
+
const source = record(value, path);
|
|
288
|
+
allowed(source, [
|
|
289
|
+
"name",
|
|
290
|
+
"identity",
|
|
291
|
+
"unique",
|
|
292
|
+
"method",
|
|
293
|
+
"columns",
|
|
294
|
+
"includedColumns",
|
|
295
|
+
"predicate",
|
|
296
|
+
"predicateHash",
|
|
297
|
+
"valid",
|
|
298
|
+
"extension",
|
|
299
|
+
], path);
|
|
300
|
+
if (!Array.isArray(source.columns))
|
|
301
|
+
throw new TypeError(`${path}.columns must be an array`);
|
|
302
|
+
const extension = parseExtension(source.extension, `${path}.extension`);
|
|
303
|
+
return {
|
|
304
|
+
name: text(source.name, `${path}.name`),
|
|
305
|
+
identity: text(source.identity, `${path}.identity`),
|
|
306
|
+
unique: bool(source.unique, `${path}.unique`),
|
|
307
|
+
...(source.method === undefined ? {} : { method: text(source.method, `${path}.method`) }),
|
|
308
|
+
columns: source.columns.map((item, index) => parseIndexColumn(item, `${path}.columns[${index}]`)),
|
|
309
|
+
...(source.includedColumns === undefined
|
|
310
|
+
? {}
|
|
311
|
+
: { includedColumns: stringArray(source.includedColumns, `${path}.includedColumns`) }),
|
|
312
|
+
predicate: oneOf(source.predicate, ["none", "present", "unknown"], `${path}.predicate`),
|
|
313
|
+
...(source.predicateHash === undefined
|
|
314
|
+
? {}
|
|
315
|
+
: { predicateHash: fingerprint(source.predicateHash, `${path}.predicateHash`) }),
|
|
316
|
+
valid: booleanOrUnknown(source.valid, `${path}.valid`),
|
|
317
|
+
...(extension === undefined ? {} : { extension }),
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
function parseRelation(value, path) {
|
|
321
|
+
const source = record(value, path);
|
|
322
|
+
allowed(source, ["schema", "name", "kind", "columns", "constraints", "indexes", "capabilities", "extension"], path);
|
|
323
|
+
const columns = objectMap(source.columns, `${path}.columns`, parseColumn);
|
|
324
|
+
const positions = Object.values(columns).map(({ position }) => position);
|
|
325
|
+
if (new Set(positions).size !== positions.length)
|
|
326
|
+
throw new TypeError(`${path}.columns positions must be unique`);
|
|
327
|
+
if (!Array.isArray(source.constraints))
|
|
328
|
+
throw new TypeError(`${path}.constraints must be an array`);
|
|
329
|
+
if (!Array.isArray(source.indexes))
|
|
330
|
+
throw new TypeError(`${path}.indexes must be an array`);
|
|
331
|
+
const constraints = source.constraints
|
|
332
|
+
.map((item, index) => parseConstraint(item, `${path}.constraints[${index}]`))
|
|
333
|
+
.sort((left, right) => left.identity.localeCompare(right.identity));
|
|
334
|
+
const indexes = source.indexes
|
|
335
|
+
.map((item, index) => parseIndex(item, `${path}.indexes[${index}]`))
|
|
336
|
+
.sort((left, right) => left.identity.localeCompare(right.identity));
|
|
337
|
+
const capabilities = source.capabilities === undefined
|
|
338
|
+
? undefined
|
|
339
|
+
: Object.fromEntries(Object.entries(record(source.capabilities, `${path}.capabilities`))
|
|
340
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
341
|
+
.map(([key, item]) => {
|
|
342
|
+
if (typeof item !== "string" &&
|
|
343
|
+
typeof item !== "boolean" &&
|
|
344
|
+
(typeof item !== "number" || !Number.isFinite(item))) {
|
|
345
|
+
throw new TypeError(`${path}.capabilities.${key} must be a string, boolean, or finite number`);
|
|
346
|
+
}
|
|
347
|
+
return [key, item];
|
|
348
|
+
}));
|
|
349
|
+
const extension = parseExtension(source.extension, `${path}.extension`);
|
|
350
|
+
return {
|
|
351
|
+
...(source.schema === undefined ? {} : { schema: text(source.schema, `${path}.schema`) }),
|
|
352
|
+
name: text(source.name, `${path}.name`),
|
|
353
|
+
kind: oneOf(source.kind, ["table", "view", "materialized-view", "foreign-table", "virtual-table"], `${path}.kind`),
|
|
354
|
+
columns,
|
|
355
|
+
constraints,
|
|
356
|
+
indexes,
|
|
357
|
+
...(capabilities === undefined ? {} : { capabilities }),
|
|
358
|
+
...(extension === undefined ? {} : { extension }),
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
function typeBase(source, path) {
|
|
362
|
+
const extension = parseExtension(source.extension, `${path}.extension`);
|
|
363
|
+
return {
|
|
364
|
+
name: text(source.name, `${path}.name`),
|
|
365
|
+
...(source.schema === undefined ? {} : { schema: text(source.schema, `${path}.schema`) }),
|
|
366
|
+
identity: text(source.identity, `${path}.identity`),
|
|
367
|
+
databaseType: text(source.databaseType, `${path}.databaseType`),
|
|
368
|
+
tsType: text(source.tsType, `${path}.tsType`),
|
|
369
|
+
...(extension === undefined ? {} : { extension }),
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
const typeBaseKeys = ["kind", "name", "schema", "identity", "databaseType", "tsType", "extension"];
|
|
373
|
+
function parseCompositeField(value, path) {
|
|
374
|
+
const source = record(value, path);
|
|
375
|
+
allowed(source, ["name", "typeIdentity", "databaseType", "tsType", "nullable"], path);
|
|
376
|
+
return {
|
|
377
|
+
name: text(source.name, `${path}.name`),
|
|
378
|
+
typeIdentity: text(source.typeIdentity, `${path}.typeIdentity`),
|
|
379
|
+
databaseType: text(source.databaseType, `${path}.databaseType`),
|
|
380
|
+
tsType: text(source.tsType, `${path}.tsType`),
|
|
381
|
+
nullable: bool(source.nullable, `${path}.nullable`),
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
function parseType(value, path) {
|
|
385
|
+
const source = record(value, path);
|
|
386
|
+
const kind = oneOf(source.kind, ["scalar", "enum", "domain", "composite", "range", "multirange", "collection", "opaque"], `${path}.kind`);
|
|
387
|
+
const base = typeBase(source, path);
|
|
388
|
+
if (kind === "scalar") {
|
|
389
|
+
allowed(source, typeBaseKeys, path);
|
|
390
|
+
return { kind, ...base };
|
|
391
|
+
}
|
|
392
|
+
if (kind === "enum") {
|
|
393
|
+
allowed(source, [...typeBaseKeys, "labels"], path);
|
|
394
|
+
return { kind, ...base, labels: stringArray(source.labels, `${path}.labels`) };
|
|
395
|
+
}
|
|
396
|
+
if (kind === "domain") {
|
|
397
|
+
allowed(source, [...typeBaseKeys, "baseTypeIdentity", "nullable", "checks"], path);
|
|
398
|
+
return {
|
|
399
|
+
kind,
|
|
400
|
+
...base,
|
|
401
|
+
baseTypeIdentity: text(source.baseTypeIdentity, `${path}.baseTypeIdentity`),
|
|
402
|
+
nullable: bool(source.nullable, `${path}.nullable`),
|
|
403
|
+
checks: stringArray(source.checks, `${path}.checks`).map((item, index) => fingerprint(item, `${path}.checks[${index}]`)),
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
if (kind === "composite") {
|
|
407
|
+
allowed(source, [...typeBaseKeys, "fields"], path);
|
|
408
|
+
if (!Array.isArray(source.fields))
|
|
409
|
+
throw new TypeError(`${path}.fields must be an array`);
|
|
410
|
+
return {
|
|
411
|
+
kind,
|
|
412
|
+
...base,
|
|
413
|
+
fields: source.fields.map((item, index) => parseCompositeField(item, `${path}.fields[${index}]`)),
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
if (kind === "range" || kind === "multirange") {
|
|
417
|
+
allowed(source, [...typeBaseKeys, "subtypeIdentity"], path);
|
|
418
|
+
return { kind, ...base, subtypeIdentity: text(source.subtypeIdentity, `${path}.subtypeIdentity`) };
|
|
419
|
+
}
|
|
420
|
+
if (kind === "collection") {
|
|
421
|
+
allowed(source, [...typeBaseKeys, "elementTypeIdentity", "dimensions"], path);
|
|
422
|
+
const dimensions = source.dimensions === undefined
|
|
423
|
+
? undefined
|
|
424
|
+
: Array.isArray(source.dimensions)
|
|
425
|
+
? source.dimensions.map((item, index) => integer(item, `${path}.dimensions[${index}]`))
|
|
426
|
+
: (() => {
|
|
427
|
+
throw new TypeError(`${path}.dimensions must be an array`);
|
|
428
|
+
})();
|
|
429
|
+
const output = {
|
|
430
|
+
kind,
|
|
431
|
+
...base,
|
|
432
|
+
elementTypeIdentity: text(source.elementTypeIdentity, `${path}.elementTypeIdentity`),
|
|
433
|
+
...(dimensions === undefined ? {} : { dimensions }),
|
|
434
|
+
};
|
|
435
|
+
return output;
|
|
436
|
+
}
|
|
437
|
+
allowed(source, [...typeBaseKeys, "reason"], path);
|
|
438
|
+
return { kind, ...base, reason: text(source.reason, `${path}.reason`) };
|
|
439
|
+
}
|
|
440
|
+
function parseRoutineArgument(value, path) {
|
|
441
|
+
const source = record(value, path);
|
|
442
|
+
allowed(source, ["name", "mode", "typeIdentity", "databaseType", "tsType", "default"], path);
|
|
443
|
+
return {
|
|
444
|
+
...(source.name === undefined ? {} : { name: text(source.name, `${path}.name`) }),
|
|
445
|
+
mode: oneOf(source.mode, ["in", "out", "inout", "variadic"], `${path}.mode`),
|
|
446
|
+
typeIdentity: text(source.typeIdentity, `${path}.typeIdentity`),
|
|
447
|
+
databaseType: text(source.databaseType, `${path}.databaseType`),
|
|
448
|
+
tsType: text(source.tsType, `${path}.tsType`),
|
|
449
|
+
default: oneOf(source.default, ["none", "present", "unknown"], `${path}.default`),
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
function parseRoutineResult(value, path) {
|
|
453
|
+
const source = record(value, path);
|
|
454
|
+
const kind = oneOf(source.kind, ["scalar", "set", "record", "table", "void", "command"], `${path}.kind`);
|
|
455
|
+
if (kind === "scalar" || kind === "set") {
|
|
456
|
+
allowed(source, ["kind", "typeIdentity", "databaseType", "tsType", "nullable"], path);
|
|
457
|
+
return {
|
|
458
|
+
kind,
|
|
459
|
+
typeIdentity: text(source.typeIdentity, `${path}.typeIdentity`),
|
|
460
|
+
databaseType: text(source.databaseType, `${path}.databaseType`),
|
|
461
|
+
tsType: text(source.tsType, `${path}.tsType`),
|
|
462
|
+
nullable: bool(source.nullable, `${path}.nullable`),
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
if (kind === "record" || kind === "table") {
|
|
466
|
+
allowed(source, ["kind", "columns"], path);
|
|
467
|
+
return { kind, columns: objectMap(source.columns, `${path}.columns`, parseColumn) };
|
|
468
|
+
}
|
|
469
|
+
allowed(source, ["kind"], path);
|
|
470
|
+
return { kind };
|
|
471
|
+
}
|
|
472
|
+
function parseRoutine(value, path) {
|
|
473
|
+
const source = record(value, path);
|
|
474
|
+
allowed(source, [
|
|
475
|
+
"name",
|
|
476
|
+
"schema",
|
|
477
|
+
"identity",
|
|
478
|
+
"kind",
|
|
479
|
+
"arguments",
|
|
480
|
+
"result",
|
|
481
|
+
"volatility",
|
|
482
|
+
"deterministic",
|
|
483
|
+
"dataAccess",
|
|
484
|
+
"nullInput",
|
|
485
|
+
"availableSince",
|
|
486
|
+
"availableUntil",
|
|
487
|
+
"polymorphicFamily",
|
|
488
|
+
"extension",
|
|
489
|
+
], path);
|
|
490
|
+
if (!Array.isArray(source.arguments))
|
|
491
|
+
throw new TypeError(`${path}.arguments must be an array`);
|
|
492
|
+
const extension = parseExtension(source.extension, `${path}.extension`);
|
|
493
|
+
return {
|
|
494
|
+
name: text(source.name, `${path}.name`),
|
|
495
|
+
...(source.schema === undefined ? {} : { schema: text(source.schema, `${path}.schema`) }),
|
|
496
|
+
identity: text(source.identity, `${path}.identity`),
|
|
497
|
+
kind: oneOf(source.kind, ["function", "procedure", "aggregate", "window"], `${path}.kind`),
|
|
498
|
+
arguments: source.arguments.map((item, index) => parseRoutineArgument(item, `${path}.arguments[${index}]`)),
|
|
499
|
+
result: parseRoutineResult(source.result, `${path}.result`),
|
|
500
|
+
volatility: oneOf(source.volatility, ["immutable", "stable", "volatile", "unknown"], `${path}.volatility`),
|
|
501
|
+
deterministic: booleanOrUnknown(source.deterministic, `${path}.deterministic`),
|
|
502
|
+
dataAccess: oneOf(source.dataAccess, ["none", "contains-sql", "reads-sql", "modifies-sql", "unknown"], `${path}.dataAccess`),
|
|
503
|
+
nullInput: oneOf(source.nullInput, ["strict", "called", "unknown"], `${path}.nullInput`),
|
|
504
|
+
...(source.availableSince === undefined
|
|
505
|
+
? {}
|
|
506
|
+
: { availableSince: text(source.availableSince, `${path}.availableSince`) }),
|
|
507
|
+
...(source.availableUntil === undefined
|
|
508
|
+
? {}
|
|
509
|
+
: { availableUntil: text(source.availableUntil, `${path}.availableUntil`) }),
|
|
510
|
+
...(source.polymorphicFamily === undefined
|
|
511
|
+
? {}
|
|
512
|
+
: { polymorphicFamily: text(source.polymorphicFamily, `${path}.polymorphicFamily`) }),
|
|
513
|
+
...(extension === undefined ? {} : { extension }),
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
function parseNamespace(value, path) {
|
|
517
|
+
const source = record(value, path);
|
|
518
|
+
allowed(source, ["name", "kind", "extension"], path);
|
|
519
|
+
const extension = parseExtension(source.extension, `${path}.extension`);
|
|
520
|
+
return {
|
|
521
|
+
name: text(source.name, `${path}.name`),
|
|
522
|
+
kind: oneOf(source.kind, ["catalog", "database", "schema"], `${path}.kind`),
|
|
523
|
+
...(extension === undefined ? {} : { extension }),
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
function legacyColumn(column) {
|
|
527
|
+
return {
|
|
528
|
+
name: column.name,
|
|
529
|
+
databaseType: column.databaseType,
|
|
530
|
+
tsType: column.tsType,
|
|
531
|
+
nullable: column.nullable,
|
|
532
|
+
...(column.dimensions === undefined ? {} : { array: true }),
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
function legacyViews(relations, types, routines) {
|
|
536
|
+
const tables = {};
|
|
537
|
+
for (const [key, relation] of Object.entries(relations)) {
|
|
538
|
+
tables[key] = {
|
|
539
|
+
name: relation.name,
|
|
540
|
+
...(relation.schema === undefined ? {} : { schema: relation.schema }),
|
|
541
|
+
columns: Object.fromEntries(Object.entries(relation.columns)
|
|
542
|
+
.sort(([, left], [, right]) => left.position - right.position)
|
|
543
|
+
.map(([columnKey, column]) => [columnKey, legacyColumn(column)])),
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
const enums = {};
|
|
547
|
+
const domains = {};
|
|
548
|
+
for (const [key, type] of Object.entries(types)) {
|
|
549
|
+
if (type.kind === "enum")
|
|
550
|
+
enums[key] = type.labels;
|
|
551
|
+
if (type.kind === "domain") {
|
|
552
|
+
domains[key] = { name: type.name, databaseType: type.databaseType, tsType: type.tsType, nullable: type.nullable };
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
const functions = {};
|
|
556
|
+
for (const overloads of Object.values(routines)) {
|
|
557
|
+
for (const routine of overloads) {
|
|
558
|
+
if (routine.kind !== "function" && routine.kind !== "aggregate" && routine.kind !== "window")
|
|
559
|
+
continue;
|
|
560
|
+
const inputArguments = routine.arguments.filter(({ mode }) => mode === "in" || mode === "inout" || mode === "variadic");
|
|
561
|
+
const result = routine.result;
|
|
562
|
+
const databaseReturnType = result.kind === "scalar" || result.kind === "set" ? result.databaseType : result.kind;
|
|
563
|
+
const returnType = result.kind === "scalar" || result.kind === "set" ? result.tsType : "unknown";
|
|
564
|
+
const nullable = result.kind === "scalar" || result.kind === "set" ? result.nullable : true;
|
|
565
|
+
const key = `${routine.schema === undefined ? "" : `${routine.schema}.`}${routine.name}(${inputArguments
|
|
566
|
+
.map(({ databaseType }) => databaseType)
|
|
567
|
+
.join(",")})`;
|
|
568
|
+
functions[key] = {
|
|
569
|
+
name: routine.name,
|
|
570
|
+
...(routine.schema === undefined ? {} : { schema: routine.schema }),
|
|
571
|
+
argumentTypes: inputArguments.map(({ databaseType }) => databaseType),
|
|
572
|
+
databaseReturnType,
|
|
573
|
+
returnType,
|
|
574
|
+
nullable,
|
|
575
|
+
...(result.kind === "set" ? { setReturning: true } : {}),
|
|
576
|
+
...(routine.volatility === "unknown" ? {} : { volatility: routine.volatility }),
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
return {
|
|
581
|
+
tables,
|
|
582
|
+
...(Object.keys(enums).length === 0 ? {} : { enums }),
|
|
583
|
+
...(Object.keys(domains).length === 0 ? {} : { domains }),
|
|
584
|
+
...(Object.keys(functions).length === 0 ? {} : { functions }),
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
/** Parses a strict canonical v2 envelope and adds transitional v1 resolver views in memory. */
|
|
588
|
+
export function parseSchemaSnapshotV2(value) {
|
|
589
|
+
const source = record(value, "schema");
|
|
590
|
+
allowed(source, [
|
|
591
|
+
"formatVersion",
|
|
592
|
+
"dialect",
|
|
593
|
+
"dialectVersion",
|
|
594
|
+
"server",
|
|
595
|
+
"namespaces",
|
|
596
|
+
"types",
|
|
597
|
+
"relations",
|
|
598
|
+
"routines",
|
|
599
|
+
"metadata",
|
|
600
|
+
"extension",
|
|
601
|
+
], "schema");
|
|
602
|
+
if (source.formatVersion !== SCHEMA_FORMAT_VERSION) {
|
|
603
|
+
throw new TypeError(`schema.formatVersion must be ${SCHEMA_FORMAT_VERSION}`);
|
|
604
|
+
}
|
|
605
|
+
const namespaces = objectMap(source.namespaces, "schema.namespaces", parseNamespace);
|
|
606
|
+
const types = objectMap(source.types, "schema.types", parseType);
|
|
607
|
+
const relations = objectMap(source.relations, "schema.relations", parseRelation);
|
|
608
|
+
const routineSource = record(source.routines, "schema.routines");
|
|
609
|
+
const routines = Object.fromEntries(Object.keys(routineSource)
|
|
610
|
+
.sort()
|
|
611
|
+
.map((key) => {
|
|
612
|
+
const overloads = routineSource[key];
|
|
613
|
+
if (!Array.isArray(overloads))
|
|
614
|
+
throw new TypeError(`schema.routines.${key} must be an array`);
|
|
615
|
+
return [
|
|
616
|
+
key,
|
|
617
|
+
overloads
|
|
618
|
+
.map((item, index) => parseRoutine(item, `schema.routines.${key}[${index}]`))
|
|
619
|
+
.sort((left, right) => left.identity.localeCompare(right.identity)),
|
|
620
|
+
];
|
|
621
|
+
}));
|
|
622
|
+
const extension = parseExtension(source.extension, "schema.extension");
|
|
623
|
+
const metadata = parseMetadata(source.metadata, "schema.metadata");
|
|
624
|
+
const server = parseDialectServerEvidence(source.server);
|
|
625
|
+
return {
|
|
626
|
+
formatVersion: SCHEMA_FORMAT_VERSION,
|
|
627
|
+
dialect: text(source.dialect, "schema.dialect"),
|
|
628
|
+
dialectVersion: text(source.dialectVersion, "schema.dialectVersion"),
|
|
629
|
+
server,
|
|
630
|
+
namespaces,
|
|
631
|
+
types,
|
|
632
|
+
relations,
|
|
633
|
+
routines,
|
|
634
|
+
...(metadata === undefined ? {} : { metadata }),
|
|
635
|
+
...(extension === undefined ? {} : { extension }),
|
|
636
|
+
version: server.version,
|
|
637
|
+
...legacyViews(relations, types, routines),
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
/** Validates and canonicalizes a grammar-produced v2 envelope. */
|
|
641
|
+
export function defineSchemaSnapshotV2(value) {
|
|
642
|
+
return parseSchemaSnapshotV2(value);
|
|
643
|
+
}
|
|
644
|
+
/** Removes transitional projections so v2 has one serialized source of truth. */
|
|
645
|
+
export function schemaSnapshotV2Envelope(snapshot) {
|
|
646
|
+
return {
|
|
647
|
+
formatVersion: snapshot.formatVersion,
|
|
648
|
+
dialect: snapshot.dialect,
|
|
649
|
+
dialectVersion: snapshot.dialectVersion,
|
|
650
|
+
server: snapshot.server,
|
|
651
|
+
namespaces: snapshot.namespaces,
|
|
652
|
+
types: snapshot.types,
|
|
653
|
+
relations: snapshot.relations,
|
|
654
|
+
routines: snapshot.routines,
|
|
655
|
+
...(snapshot.metadata === undefined ? {} : { metadata: snapshot.metadata }),
|
|
656
|
+
...(snapshot.extension === undefined ? {} : { extension: snapshot.extension }),
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
//# sourceMappingURL=codec.js.map
|