@tsofist/schema-forge 3.2.0 → 3.3.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.
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generator.d.ts","sourceRoot":"","sources":["../../src/dbml-generator/generator.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"generator.d.ts","sourceRoot":"","sources":["../../src/dbml-generator/generator.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AACpE,OAAO,EAIH,oBAAoB,EAGpB,gBAAgB,EACnB,MAAM,SAAS,CAAC;AAEjB,wBAAgB,gBAAgB,CAC5B,cAAc,EAAE,mBAAmB,EACnC,MAAM,EAAE,gBAAgB,EAAE,EAC1B,OAAO,GAAE,oBAAyB,UAsGrC"}
|
|
@@ -9,17 +9,21 @@ const snake_1 = require("@tsofist/stem/lib/string/case/snake");
|
|
|
9
9
|
const compare_1 = require("@tsofist/stem/lib/string/compare");
|
|
10
10
|
const substr_1 = require("@tsofist/stem/lib/string/substr");
|
|
11
11
|
const text_builder_1 = require("@tsofist/stem/lib/string/text-builder");
|
|
12
|
+
const cache_1 = require("../schema-dereference/cache");
|
|
13
|
+
const dereference_1 = require("../schema-dereference/dereference");
|
|
12
14
|
function generateDBMLSpec(schemaRegistry, scopes, options = {}) {
|
|
13
15
|
const text = new text_builder_1.TextBuilder();
|
|
14
16
|
const tables = new Map();
|
|
15
17
|
const groups = new Map();
|
|
16
18
|
const sources = new Map();
|
|
19
|
+
const dereferencedRootSchemas = new Map();
|
|
17
20
|
if (options.meta?.comment) {
|
|
18
21
|
text.push(`// ${options.meta?.comment}`);
|
|
19
22
|
}
|
|
20
23
|
text.push(`Project ${options.meta?.name ?? 'Scratch'} {`);
|
|
21
24
|
text.push(`database_type: 'PostgreSQL'`, 1);
|
|
22
25
|
if (options.meta?.note) {
|
|
26
|
+
text.push('');
|
|
23
27
|
text.push(buildNote(options.meta.note), 1);
|
|
24
28
|
}
|
|
25
29
|
text.push('}');
|
|
@@ -34,9 +38,21 @@ function generateDBMLSpec(schemaRegistry, scopes, options = {}) {
|
|
|
34
38
|
sources.set(scope.scopeName ?? DefaultScopeName, scope);
|
|
35
39
|
}
|
|
36
40
|
}
|
|
37
|
-
for (const { definitions, scopeName = DefaultScopeName } of sources.values()) {
|
|
41
|
+
for (const { definitions, scopeName = DefaultScopeName, schemaId } of sources.values()) {
|
|
42
|
+
let dereferencedRootSchema = dereferencedRootSchemas.get(schemaId);
|
|
43
|
+
if (!dereferencedRootSchemas.has(schemaId)) {
|
|
44
|
+
const root = schemaRegistry.getRootSchema(schemaId) ||
|
|
45
|
+
(0, error_1.raise)(`Root schema ${schemaId} not found`);
|
|
46
|
+
dereferencedRootSchema =
|
|
47
|
+
(0, dereference_1.dereferenceSchema)(root, {
|
|
48
|
+
throwOnDereferenceFailure: true,
|
|
49
|
+
sharedCacheStorage: DereferenceSchemaCache ||
|
|
50
|
+
(DereferenceSchemaCache = (0, cache_1.createSchemaDereferenceSharedCache)()),
|
|
51
|
+
}) || (0, error_1.raise)(`Failed to dereference root schema for ${schemaId}`);
|
|
52
|
+
dereferencedRootSchemas.set(schemaId, dereferencedRootSchema);
|
|
53
|
+
}
|
|
38
54
|
for (const definition of definitions) {
|
|
39
|
-
const tableSpec = generateTable(definition,
|
|
55
|
+
const tableSpec = generateTable(definition, options, dereferencedRootSchema);
|
|
40
56
|
if (tableSpec) {
|
|
41
57
|
if (tables.has(tableSpec.name)) {
|
|
42
58
|
(0, error_1.raise)(`Definitions contain duplicate table name: ${tableSpec.name}`);
|
|
@@ -76,18 +92,20 @@ function generateDBMLSpec(schemaRegistry, scopes, options = {}) {
|
|
|
76
92
|
}
|
|
77
93
|
return text.stringify();
|
|
78
94
|
}
|
|
79
|
-
function generateTable(info,
|
|
80
|
-
const includeNotes = options
|
|
95
|
+
function generateTable(info, options, dereferencedRootSchema) {
|
|
96
|
+
const includeNotes = options.includeNotes ?? false;
|
|
81
97
|
const entityTypeName = info.type;
|
|
82
|
-
const entitySchema =
|
|
98
|
+
const entitySchema = (dereferencedRootSchema.definitions || dereferencedRootSchema.$defs)?.[entityTypeName];
|
|
83
99
|
if (!entitySchema)
|
|
84
100
|
return;
|
|
101
|
+
if (typeof entitySchema !== 'object')
|
|
102
|
+
(0, error_1.raise)(`Invalid definition type for table: ${typeof entitySchema} (expected object)`);
|
|
85
103
|
const tableName = readDBEntityName(entitySchema);
|
|
86
104
|
if (!tableName)
|
|
87
105
|
return;
|
|
88
106
|
const { properties, required } = listProperties(entitySchema);
|
|
89
|
-
const columns = generateColumns(properties, required, includeNotes,
|
|
90
|
-
const indexes = generateIndexes(tableName, properties, readDBEntityIndexes(entitySchema), includeNotes,
|
|
107
|
+
const columns = generateColumns(properties, required, includeNotes, info.schemaId, tableName);
|
|
108
|
+
const indexes = generateIndexes(tableName, properties, readDBEntityIndexes(entitySchema), includeNotes, info.schemaId);
|
|
91
109
|
const text = new text_builder_1.TextBuilder();
|
|
92
110
|
if (entitySchema.$comment) {
|
|
93
111
|
text.push(stringifyComment(entitySchema.$comment));
|
|
@@ -110,7 +128,7 @@ function generateTable(info, registry, options) {
|
|
|
110
128
|
value: text.stringify(),
|
|
111
129
|
};
|
|
112
130
|
}
|
|
113
|
-
function generateColumns(properties, requiredFields, notes,
|
|
131
|
+
function generateColumns(properties, requiredFields, notes, schemaId, tableName) {
|
|
114
132
|
const text = new text_builder_1.TextBuilder();
|
|
115
133
|
let count = 0;
|
|
116
134
|
for (const [key, property] of (0, entries_1.entries)(properties).sort(([keyA], [keyB]) => {
|
|
@@ -128,16 +146,18 @@ function generateColumns(properties, requiredFields, notes, registry, schemaId)
|
|
|
128
146
|
continue;
|
|
129
147
|
const isRequired = requiredFields.includes(key);
|
|
130
148
|
const isColumnNullable = isNullable(property);
|
|
131
|
-
const columnType =
|
|
149
|
+
const columnType = getDBType(property, schemaId);
|
|
132
150
|
const attributes = generateColumnAttributes(key, property, isRequired, isColumnNullable, notes);
|
|
133
151
|
if (property.$comment)
|
|
134
152
|
text.push(stringifyComment(property.$comment));
|
|
135
|
-
|
|
153
|
+
const attributesText = attributes.stringify(', ');
|
|
154
|
+
const hasLineBreak = attributesText.includes('\n');
|
|
155
|
+
text.push(`${(0, snake_1.snakeCase)(key)} ${columnType} [${attributesText}${hasLineBreak ? '\n' : ']'}`);
|
|
156
|
+
hasLineBreak && text.push(']');
|
|
136
157
|
count++;
|
|
137
158
|
}
|
|
138
159
|
if (count === 0) {
|
|
139
|
-
|
|
140
|
-
// raise(`Table ${schemaId} has no columns`);
|
|
160
|
+
(0, error_1.raise)(`Table ${tableName} (${schemaId}) does not have any columns defined`);
|
|
141
161
|
}
|
|
142
162
|
return text;
|
|
143
163
|
}
|
|
@@ -169,11 +189,11 @@ function generateColumnAttributes(_key, property, isRequired, isColumnNullable,
|
|
|
169
189
|
result.push(`default: ${String(v)}`);
|
|
170
190
|
}
|
|
171
191
|
if (notes && property.description) {
|
|
172
|
-
result.push(buildNote(property.description).stringify());
|
|
192
|
+
result.push(buildNote(property.description, 2).stringify());
|
|
173
193
|
}
|
|
174
194
|
return result;
|
|
175
195
|
}
|
|
176
|
-
function generateIndexes(tableName, schemaProperties, entityIndexes, includeNotes,
|
|
196
|
+
function generateIndexes(tableName, schemaProperties, entityIndexes, includeNotes, schemaId) {
|
|
177
197
|
const text = new text_builder_1.TextBuilder();
|
|
178
198
|
const idx = {};
|
|
179
199
|
const normalizedTableName = tableName.replace(/\./g, '_');
|
|
@@ -211,7 +231,7 @@ function generateIndexes(tableName, schemaProperties, entityIndexes, includeNote
|
|
|
211
231
|
const field = (0, substr_1.substr)(rawKey, '.');
|
|
212
232
|
const index = (idx[indexName] ||= {
|
|
213
233
|
...(typeof item === 'object' ? item : {}),
|
|
214
|
-
columnType:
|
|
234
|
+
columnType: getDBType(schemaProperties[column], schemaId),
|
|
215
235
|
columns: [],
|
|
216
236
|
fields: [],
|
|
217
237
|
});
|
|
@@ -241,9 +261,9 @@ function generateIndexes(tableName, schemaProperties, entityIndexes, includeNote
|
|
|
241
261
|
continue;
|
|
242
262
|
const { columns, unique, pk, comment, note } = index;
|
|
243
263
|
if (columns.length !== new Set(columns).size) {
|
|
244
|
-
console.
|
|
264
|
+
console.warn(`Duplicate columns in index ${indexName} for table ${tableName}`);
|
|
245
265
|
}
|
|
246
|
-
const options =
|
|
266
|
+
const options = new text_builder_1.TextBuilder(`name: "${indexName}"`);
|
|
247
267
|
const type = index.type || index.columnType === 'jsonb' || index.columnType.endsWith('[]')
|
|
248
268
|
? 'gin'
|
|
249
269
|
: undefined;
|
|
@@ -253,15 +273,15 @@ function generateIndexes(tableName, schemaProperties, entityIndexes, includeNote
|
|
|
253
273
|
options.push(`pk`);
|
|
254
274
|
if (!pk && unique)
|
|
255
275
|
options.push('unique');
|
|
256
|
-
// todo
|
|
276
|
+
// todo?
|
|
257
277
|
// if (includeNotes && fields.length) {
|
|
258
278
|
// options.push(`note: 'Fields: ${fields.join(', ')};'`);
|
|
259
279
|
// }
|
|
260
280
|
if (includeNotes && note)
|
|
261
|
-
options.push(buildNote(note).stringify());
|
|
281
|
+
options.push(buildNote(note, 3).stringify());
|
|
262
282
|
if (comment)
|
|
263
283
|
text.push(`// ${comment}`);
|
|
264
|
-
text.push(`(${columns.map(snake_1.snakeCase).join(', ')}) [${options.
|
|
284
|
+
text.push(`(${columns.map(snake_1.snakeCase).join(', ')}) [${options.stringify(', ')}]`);
|
|
265
285
|
}
|
|
266
286
|
return text;
|
|
267
287
|
}
|
|
@@ -285,12 +305,12 @@ function stringifyComment(value) {
|
|
|
285
305
|
return undefined;
|
|
286
306
|
return value.split('\n').map((item) => `// ${item.trim()}`);
|
|
287
307
|
}
|
|
288
|
-
function buildNote(value) {
|
|
308
|
+
function buildNote(value, mlLevel = 1) {
|
|
289
309
|
const result = new text_builder_1.TextBuilder();
|
|
290
310
|
if (value) {
|
|
291
311
|
if (value.includes('\n')) {
|
|
292
312
|
result.push(`note:`);
|
|
293
|
-
result.push([`'''`, ...value.split('\n'), `'''`],
|
|
313
|
+
result.push([`'''`, ...value.split('\n'), `'''`], mlLevel);
|
|
294
314
|
}
|
|
295
315
|
else {
|
|
296
316
|
result.push(`note: "${value}"`);
|
|
@@ -301,35 +321,26 @@ function buildNote(value) {
|
|
|
301
321
|
function isNullable(property) {
|
|
302
322
|
return (property.anyOf?.some((item) => typeof item !== 'boolean' && item?.type !== 'null') ?? false);
|
|
303
323
|
}
|
|
304
|
-
|
|
324
|
+
let DereferenceSchemaCache;
|
|
325
|
+
function getDBType(property, schemaId) {
|
|
305
326
|
if (property.dbColumn?.type)
|
|
306
327
|
return property.dbColumn.type;
|
|
307
328
|
let type = property.type;
|
|
308
|
-
if (property.$ref) {
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
if (refType && refType in {}) {
|
|
317
|
-
// @1ts-expect-error It's OK
|
|
318
|
-
// return TypeMappings[refType];
|
|
319
|
-
return '-'; // TODO!
|
|
320
|
-
}
|
|
321
|
-
else if (schema) {
|
|
322
|
-
property = schema;
|
|
323
|
-
}
|
|
329
|
+
if (!type && property.$ref) {
|
|
330
|
+
(0, error_1.raise)(`Type ${JSON.stringify(property)} (${schemaId}) was not dereferenced`);
|
|
331
|
+
}
|
|
332
|
+
if (type && Array.isArray(type)) {
|
|
333
|
+
type = type.filter((item) => item !== 'null')[0];
|
|
334
|
+
}
|
|
335
|
+
if (type && typeof type !== 'string') {
|
|
336
|
+
(0, error_1.raise)(`Invalid SchemaTypeName value type (${typeof type}) (expected string)`);
|
|
324
337
|
}
|
|
325
|
-
if (typeof type !== 'string')
|
|
326
|
-
(0, error_1.raise)(`Invalid schema type ${typeof type} (expected string)`);
|
|
327
338
|
const variants = property.anyOf || property.oneOf;
|
|
328
339
|
if (variants) {
|
|
329
340
|
const nonNullType = variants.find((item) => typeof item !== 'boolean' && item?.type !== 'null');
|
|
330
341
|
if (nonNullType) {
|
|
331
342
|
return typeof nonNullType === 'object'
|
|
332
|
-
?
|
|
343
|
+
? getDBType(nonNullType, schemaId)
|
|
333
344
|
: nonNullType.toString();
|
|
334
345
|
}
|
|
335
346
|
}
|
|
@@ -352,7 +363,10 @@ function listProperties(schema) {
|
|
|
352
363
|
required: schema.required || [],
|
|
353
364
|
};
|
|
354
365
|
}
|
|
355
|
-
|
|
366
|
+
if (schema.oneOf) {
|
|
367
|
+
// todo oneOf
|
|
368
|
+
(0, error_1.raise)(`Schema ${JSON.stringify(schema)} has oneOf, but not implemented yet`);
|
|
369
|
+
}
|
|
356
370
|
if (!schema.anyOf) {
|
|
357
371
|
return {
|
|
358
372
|
type: 'object',
|
|
@@ -380,11 +394,11 @@ function listProperties(schema) {
|
|
|
380
394
|
}
|
|
381
395
|
else {
|
|
382
396
|
if (!(0, equal_keys_1.isEqualKeys)(propertiesMap, item.properties)) {
|
|
383
|
-
console.
|
|
397
|
+
console.warn('Properties are not identical across all members of anyOf');
|
|
384
398
|
}
|
|
385
399
|
for (const name of item.required || []) {
|
|
386
400
|
if (!requiredSet.has(name)) {
|
|
387
|
-
console.
|
|
401
|
+
console.warn('Required fields are not identical across all members of anyOf');
|
|
388
402
|
}
|
|
389
403
|
}
|
|
390
404
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tsofist/schema-forge",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.1",
|
|
4
4
|
"description": "Generate JSON schema from TypeScript types",
|
|
5
5
|
"author": "Andrew Berdnikov <tsofistgudmen@gmail.com>",
|
|
6
6
|
"license": "LGPL-3.0",
|
|
@@ -20,9 +20,9 @@
|
|
|
20
20
|
"test:watch": "jest --watch"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@dbml/core": "~3.13.
|
|
24
|
-
"@faker-js/faker": "^9.
|
|
25
|
-
"@tsofist/stem": "^5.1
|
|
23
|
+
"@dbml/core": "~3.13.7",
|
|
24
|
+
"@faker-js/faker": "^9.9.0",
|
|
25
|
+
"@tsofist/stem": "^5.2.1",
|
|
26
26
|
"@ungap/structured-clone": "^1.3.0",
|
|
27
27
|
"ajv": "^8.17.1",
|
|
28
28
|
"ajv-formats": "^3.0.1",
|
|
@@ -34,13 +34,13 @@
|
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"@tsofist/web-buddy": "^1.21.0",
|
|
37
|
-
"@types/jest": "^
|
|
37
|
+
"@types/jest": "^30.0.0",
|
|
38
38
|
"@types/node": "^20.19.0",
|
|
39
39
|
"@types/supertest": "^6.0.3",
|
|
40
40
|
"@types/ungap__structured-clone": "^1.2.0",
|
|
41
|
-
"jest": "~
|
|
41
|
+
"jest": "~30.0.4",
|
|
42
42
|
"rimraf": "^6.0.1",
|
|
43
|
-
"supertest": "^7.1.
|
|
43
|
+
"supertest": "^7.1.3",
|
|
44
44
|
"ts-jest": "~29.4.0",
|
|
45
45
|
"typescript": "~5.8.3"
|
|
46
46
|
},
|