@tsofist/schema-forge 3.2.0 → 3.3.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.
@@ -1 +1 @@
1
- {"version":3,"file":"generator.d.ts","sourceRoot":"","sources":["../../src/dbml-generator/generator.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAEpE,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,UAqFrC"}
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, schemaRegistry, options);
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, registry, options) {
80
- const includeNotes = options?.includeNotes ?? false;
95
+ function generateTable(info, options, dereferencedRootSchema) {
96
+ const includeNotes = options.includeNotes ?? false;
81
97
  const entityTypeName = info.type;
82
- const entitySchema = registry.getSchema(`${info.schemaId}#/definitions/${entityTypeName}`);
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, registry, info.schemaId);
90
- const indexes = generateIndexes(tableName, properties, readDBEntityIndexes(entitySchema), includeNotes, registry, info.schemaId);
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, registry, schemaId) {
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 = getType(property, schemaId, registry);
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
- text.push(`${(0, snake_1.snakeCase)(key)} ${columnType} [${attributes.stringify(', ')}]`);
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
- // todo!
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, registry, schemaId) {
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: getType(schemaProperties[column], schemaId, registry),
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.error(`Duplicate columns in index ${indexName} for table ${tableName}`);
264
+ console.warn(`Duplicate columns in index ${indexName} for table ${tableName}`);
245
265
  }
246
- const options = [`name: "${indexName}"`];
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.join(', ')}]`);
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'), `'''`], 1);
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
- function getType(property, schemaId, registry) {
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
- const schema = registry.getSchema(`${schemaId}${property.$ref}`);
310
- if (schema && schema.type)
311
- type = schema.type;
312
- const ref = schema?.$ref || property.$ref;
313
- if (typeof ref !== 'string')
314
- (0, error_1.raise)(`Invalid schema reference type ${typeof ref} (expected string)`);
315
- const refType = ref.split('/').pop();
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
- ? getType(nonNullType, schemaId, registry)
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
- // todo oneOf
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.error('Properties are not identical across all members of anyOf');
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.error('Required fields are not identical across all members of anyOf');
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.2.0",
3
+ "version": "3.3.0",
4
4
  "description": "Generate JSON schema from TypeScript types",
5
5
  "author": "Andrew Berdnikov <tsofistgudmen@gmail.com>",
6
6
  "license": "LGPL-3.0",