ormbridge 0.7.0 → 0.8.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.
Files changed (39) hide show
  1. package/README.md +1 -0
  2. package/dist/cliRunner.js +7 -5
  3. package/dist/cliRunner.js.map +1 -1
  4. package/dist/config.d.ts +6 -3
  5. package/dist/config.js +3 -3
  6. package/dist/config.js.map +1 -1
  7. package/dist/emitters/sequelize.d.ts +35 -0
  8. package/dist/emitters/sequelize.js +1276 -0
  9. package/dist/emitters/sequelize.js.map +1 -0
  10. package/dist/formats.d.ts +6 -5
  11. package/dist/formats.js +38 -4
  12. package/dist/formats.js.map +1 -1
  13. package/dist/index.d.ts +6 -0
  14. package/dist/index.js +3 -0
  15. package/dist/index.js.map +1 -1
  16. package/dist/io.js +29 -1
  17. package/dist/io.js.map +1 -1
  18. package/dist/parsers/sequelize.d.ts +15 -0
  19. package/dist/parsers/sequelize.js +906 -0
  20. package/dist/parsers/sequelize.js.map +1 -0
  21. package/dist/parsers/sequelizeAssociations.d.ts +45 -0
  22. package/dist/parsers/sequelizeAssociations.js +449 -0
  23. package/dist/parsers/sequelizeAssociations.js.map +1 -0
  24. package/dist/parsers/sequelizeCollect.d.ts +33 -0
  25. package/dist/parsers/sequelizeCollect.js +1298 -0
  26. package/dist/parsers/sequelizeCollect.js.map +1 -0
  27. package/dist/parsers/sequelizeInflection.d.ts +14 -0
  28. package/dist/parsers/sequelizeInflection.js +451 -0
  29. package/dist/parsers/sequelizeInflection.js.map +1 -0
  30. package/dist/parsers/sequelizeModel.d.ts +185 -0
  31. package/dist/parsers/sequelizeModel.js +2 -0
  32. package/dist/parsers/sequelizeModel.js.map +1 -0
  33. package/dist/parsers/sequelizeTypes.d.ts +24 -0
  34. package/dist/parsers/sequelizeTypes.js +248 -0
  35. package/dist/parsers/sequelizeTypes.js.map +1 -0
  36. package/dist/styles.d.ts +17 -0
  37. package/dist/styles.js +40 -0
  38. package/dist/styles.js.map +1 -0
  39. package/package.json +2 -1
@@ -0,0 +1,906 @@
1
+ import { toPascalCase, toSnakeCase } from '../naming.js';
2
+ import { describeThrown, err, ok } from '../result.js';
3
+ import { linkAssociations, } from './sequelizeAssociations.js';
4
+ import { collectFile, describeFile, mergeNamedIndexes, readIndexes, readModelOptions, } from './sequelizeCollect.js';
5
+ import { pluralize, singularize, underscore } from './sequelizeInflection.js';
6
+ import { resolveDataType } from './sequelizeTypes.js';
7
+ import { getTypeScriptParser } from './typescriptSyntax.js';
8
+ // ---------------------------------------------------------------------------
9
+ // Static tables
10
+ // ---------------------------------------------------------------------------
11
+ /** A file that mentions neither Sequelize nor its data types nor @Table cannot define a model. */
12
+ const SEQUELIZE_HINT = /sequelize|DataTypes?\b|@Table\b/i;
13
+ const ON_ACTION_MAP = {
14
+ CASCADE: 'cascade',
15
+ 'SET NULL': 'setNull',
16
+ RESTRICT: 'restrict',
17
+ 'NO ACTION': 'noAction',
18
+ 'SET DEFAULT': 'setDefault',
19
+ };
20
+ /** Index access methods the IR spells the way Prisma does. */
21
+ const INDEX_METHODS = {
22
+ btree: 'BTree',
23
+ hash: 'Hash',
24
+ gin: 'Gin',
25
+ gist: 'Gist',
26
+ spgist: 'SpGist',
27
+ brin: 'BRIN',
28
+ };
29
+ /** TypeScript property types that sequelize-typescript turns into a column type. */
30
+ const TS_TYPES = {
31
+ string: { type: 'string', maxLength: 255 },
32
+ number: { type: 'int' },
33
+ bigint: { type: 'bigInt' },
34
+ boolean: { type: 'boolean' },
35
+ Date: { type: 'dateTime' },
36
+ Buffer: { type: 'bytes' },
37
+ };
38
+ const NOW_SQL = /^\(?(now\(\)|current_timestamp(\(\d*\))?|localtimestamp(\(\d*\))?|transaction_timestamp\(\)|sysdatetime\(\)|getdate\(\)|datetime\('now'\)|strftime\(.*'now'.*\))\)?$/i;
39
+ const UUID_SQL = /^(uuid_generate_v[14]\(\)|gen_random_uuid\(\)|uuid\(\)|newid\(\))$/i;
40
+ const QUOTED_SQL = /^'((?:[^']|'')*)'(::[\w\s]+)?$/;
41
+ // ---------------------------------------------------------------------------
42
+ // Public entry point
43
+ // ---------------------------------------------------------------------------
44
+ /**
45
+ * Parses Sequelize 6 models into the shared IR using tree-sitter. The source is only read: nothing is
46
+ * imported or run, so models built in loops or from run-time values are not seen.
47
+ */
48
+ export async function parseSequelize(sources, options) {
49
+ const parserResult = await getTypeScriptParser();
50
+ if (!parserResult.ok) {
51
+ return parserResult;
52
+ }
53
+ const parser = parserResult.value;
54
+ const project = {
55
+ files: [],
56
+ models: [],
57
+ associations: [],
58
+ sharedConsts: new Map(),
59
+ defaults: {},
60
+ warnings: [],
61
+ };
62
+ // The trees are kept alive until the schema is built: the syntax nodes below point into them.
63
+ const trees = [];
64
+ try {
65
+ for (const source of sources) {
66
+ if (!SEQUELIZE_HINT.test(source.text)) {
67
+ continue;
68
+ }
69
+ const tree = parser.parse(source.text);
70
+ trees.push(tree);
71
+ const file = describeFile(source.path, tree.rootNode);
72
+ project.files.push(file);
73
+ if (tree.rootNode.hasError) {
74
+ project.warnings.push(`${file.label}: the file contains syntax errors; some models or attributes may be missing from the output.`);
75
+ }
76
+ for (const [name, node] of file.consts) {
77
+ const shared = project.sharedConsts.get(name) ?? [];
78
+ shared.push({ file, node });
79
+ project.sharedConsts.set(name, shared);
80
+ }
81
+ }
82
+ for (const file of project.files) {
83
+ collectFile(file, project);
84
+ }
85
+ if (project.models.length === 0) {
86
+ const checkedPaths = sources
87
+ .map((source) => source.path)
88
+ .join(', ');
89
+ return err('NO_MODELS_FOUND', `No Sequelize models were found in: ${checkedPaths}. A model is \`sequelize.define('Name', { ... })\`, ` +
90
+ `\`class Name extends Model\` with \`Name.init({ ... }, { sequelize })\`, or a sequelize-typescript class with @Table.`);
91
+ }
92
+ return ok(buildSchema(project, options));
93
+ }
94
+ catch (thrown) {
95
+ return err('PARSE_FAILED', `Failed to read the Sequelize models: ${describeThrown(thrown)}`);
96
+ }
97
+ finally {
98
+ for (const tree of trees) {
99
+ tree.delete();
100
+ }
101
+ }
102
+ }
103
+ // ---------------------------------------------------------------------------
104
+ // Naming
105
+ // ---------------------------------------------------------------------------
106
+ /** The IR model name: the Sequelize model name, in PascalCase when it is written as `user` or `blog_post`. */
107
+ function irModelName(modelName) {
108
+ return /^[A-Z][A-Za-z0-9]*$/.test(modelName)
109
+ ? modelName
110
+ : toPascalCase(modelName) || modelName;
111
+ }
112
+ function tableNameOf(modelName, options) {
113
+ if (options.tableName !== undefined) {
114
+ return options.tableName;
115
+ }
116
+ if (options.freezeTableName) {
117
+ return modelName;
118
+ }
119
+ const plural = pluralize(modelName);
120
+ return options.underscored ? underscore(plural) : plural;
121
+ }
122
+ function columnName(options, attribute) {
123
+ return (attribute.field ??
124
+ (options.underscored ? underscore(attribute.name) : attribute.name));
125
+ }
126
+ function lowerFirst(text) {
127
+ return text.charAt(0).toLowerCase() + text.slice(1);
128
+ }
129
+ function describeNode(value) {
130
+ const text = value.kind === 'name'
131
+ ? value.value
132
+ : value.kind === 'other'
133
+ ? value.text
134
+ : value.kind;
135
+ const collapsed = text.replace(/\s+/g, ' ');
136
+ return collapsed.length > 60 ? `${collapsed.slice(0, 57)}...` : collapsed;
137
+ }
138
+ // ---------------------------------------------------------------------------
139
+ // Preparation: options, names, the attributes Sequelize adds itself
140
+ // ---------------------------------------------------------------------------
141
+ const DATE_TYPE = { kind: 'name', value: 'DataTypes.DATE' };
142
+ const INTEGER_TYPE = { kind: 'name', value: 'DataTypes.INTEGER' };
143
+ /** Adds the `id` and timestamp columns that `Model.init` adds when the model does not declare them. */
144
+ function addDefaultAttributes(model, options) {
145
+ const attributes = model.attributes;
146
+ if (!attributes.some((attribute) => attribute.primaryKey)) {
147
+ if (attributes.some((attribute) => attribute.name === 'id')) {
148
+ model.unsupported.push('an "id" column that is not a primary key (Sequelize refuses such a model)');
149
+ }
150
+ else {
151
+ attributes.unshift({
152
+ name: 'id',
153
+ origin: 'id',
154
+ type: INTEGER_TYPE,
155
+ allowNull: false,
156
+ primaryKey: true,
157
+ autoIncrement: true,
158
+ });
159
+ }
160
+ }
161
+ if (options.timestamps) {
162
+ const timestamps = [
163
+ ['createdAt', options.createdAt, false],
164
+ ['updatedAt', options.updatedAt, false],
165
+ ['deletedAt', options.paranoid ? options.deletedAt : false, true],
166
+ ];
167
+ for (const [kind, name, nullable] of timestamps) {
168
+ if (name === false) {
169
+ continue;
170
+ }
171
+ const existing = attributes.find((attribute) => attribute.name === name);
172
+ if (existing !== undefined) {
173
+ existing.timestamp = kind;
174
+ }
175
+ else {
176
+ attributes.push({
177
+ name,
178
+ origin: 'timestamp',
179
+ type: DATE_TYPE,
180
+ timestamp: kind,
181
+ ...(nullable ? {} : { allowNull: false }),
182
+ });
183
+ }
184
+ }
185
+ }
186
+ if (options.version !== undefined &&
187
+ !attributes.some((attribute) => attribute.name === options.version)) {
188
+ attributes.push({
189
+ name: options.version,
190
+ origin: 'declared',
191
+ type: INTEGER_TYPE,
192
+ allowNull: false,
193
+ defaultValue: { kind: 'number', value: 0 },
194
+ });
195
+ }
196
+ }
197
+ function prepareModels(project) {
198
+ const prepared = [];
199
+ const modelNames = new Set();
200
+ const irNames = new Set();
201
+ for (const model of project.models) {
202
+ if (modelNames.has(model.modelName)) {
203
+ project.warnings.push(`${model.location}: another model with the same name was already read; this one was skipped.`);
204
+ continue;
205
+ }
206
+ const irName = irModelName(model.modelName);
207
+ if (irNames.has(irName)) {
208
+ project.warnings.push(`${model.location}: the name "${irName}" is already taken by another model; this one was skipped.`);
209
+ continue;
210
+ }
211
+ modelNames.add(model.modelName);
212
+ irNames.add(irName);
213
+ const options = readModelOptions(model, project);
214
+ model.indexes = mergeNamedIndexes(readIndexes(model, project));
215
+ addDefaultAttributes(model, options);
216
+ prepared.push({
217
+ raw: model,
218
+ options,
219
+ irName,
220
+ tableName: tableNameOf(model.modelName, options),
221
+ });
222
+ }
223
+ return prepared;
224
+ }
225
+ // ---------------------------------------------------------------------------
226
+ // Foreign keys declared with `references`
227
+ // ---------------------------------------------------------------------------
228
+ /** The model or table a `references.model` value names. */
229
+ function referencedName(value) {
230
+ if (value === undefined) {
231
+ return undefined;
232
+ }
233
+ switch (value.kind) {
234
+ case 'string':
235
+ return value.value;
236
+ case 'name':
237
+ return value.value.split('.').pop();
238
+ case 'call':
239
+ // `User.getTableName()`
240
+ return value.callee.split('.').slice(-2, -1)[0];
241
+ case 'object': {
242
+ const table = value.properties['tableName'];
243
+ return table !== undefined && table.kind === 'string'
244
+ ? table.value
245
+ : undefined;
246
+ }
247
+ default:
248
+ return undefined;
249
+ }
250
+ }
251
+ function findReferencedModel(name, prepared) {
252
+ const lower = name.toLowerCase();
253
+ return (prepared.find((model) => model.tableName.toLowerCase() === lower) ??
254
+ prepared.find((model) => model.raw.modelName === name) ??
255
+ prepared.find((model) => model.raw.aliases.includes(name)));
256
+ }
257
+ /** Turns `references: { model, key }` on an attribute into a foreign key link. */
258
+ function resolveReferences(prepared, project) {
259
+ for (const model of prepared) {
260
+ for (const attribute of model.raw.attributes) {
261
+ if (attribute.references === undefined || attribute.link !== undefined) {
262
+ continue;
263
+ }
264
+ const name = referencedName(attribute.references.model);
265
+ if (name === undefined) {
266
+ project.warnings.push(`${model.raw.location}, field "${attribute.name}": the model in references could not be read statically, so no relation was created.`);
267
+ continue;
268
+ }
269
+ const target = findReferencedModel(name, prepared);
270
+ const key = attribute.references.key;
271
+ const keyAttribute = target === undefined
272
+ ? undefined
273
+ : target.raw.attributes.find((candidate) => candidate.name === key ||
274
+ (key !== undefined &&
275
+ columnName(target.options, candidate) === key));
276
+ const link = {
277
+ target: target?.raw,
278
+ targetName: target?.irName ?? irModelName(toPascalCase(singularize(name))),
279
+ keyAttribute: keyAttribute?.name,
280
+ ...(key === undefined || keyAttribute !== undefined
281
+ ? {}
282
+ : { keyColumn: key }),
283
+ ...(attribute.onDelete === undefined
284
+ ? {}
285
+ : { onDelete: attribute.onDelete }),
286
+ ...(attribute.onUpdate === undefined
287
+ ? {}
288
+ : { onUpdate: attribute.onUpdate }),
289
+ constraint: true,
290
+ };
291
+ attribute.link = link;
292
+ }
293
+ }
294
+ }
295
+ // ---------------------------------------------------------------------------
296
+ // Types, defaults, enums
297
+ // ---------------------------------------------------------------------------
298
+ function enumMemberName(value) {
299
+ const snake = toSnakeCase(value).toUpperCase();
300
+ if (snake === '') {
301
+ return 'EMPTY';
302
+ }
303
+ return /^[0-9]/.test(snake) ? `V_${snake}` : snake;
304
+ }
305
+ function enumValuesOf(values) {
306
+ const used = new Set();
307
+ return values.map((dbValue) => {
308
+ let name = enumMemberName(dbValue);
309
+ while (used.has(name)) {
310
+ name = `${name}_`;
311
+ }
312
+ used.add(name);
313
+ return { name, dbValue };
314
+ });
315
+ }
316
+ function resolveAttributeType(model, attribute, project) {
317
+ const label = `${model.raw.location}, field "${attribute.name}"`;
318
+ if (attribute.type !== undefined) {
319
+ const resolved = resolveDataType(attribute.type);
320
+ if (resolved === undefined) {
321
+ project.warnings.push(`${label}: the data type (${describeNode(attribute.type)}) is not a Sequelize data type that could be read statically; the field was read as unsupported.`);
322
+ return { type: 'unsupported', unsupportedType: 'unknown' };
323
+ }
324
+ if (resolved.enumValues !== undefined &&
325
+ resolved.enumValues.length === 0 &&
326
+ attribute.enumValues !== undefined) {
327
+ return { ...resolved, enumValues: attribute.enumValues };
328
+ }
329
+ return resolved;
330
+ }
331
+ if (attribute.tsType !== undefined) {
332
+ const inferred = TS_TYPES[attribute.tsType];
333
+ if (inferred !== undefined) {
334
+ return inferred;
335
+ }
336
+ project.warnings.push(`${label}: sequelize-typescript cannot infer a column type from the TypeScript type "${attribute.tsType}"; name it in @Column(DataType.X). The field was read as unsupported.`);
337
+ return { type: 'unsupported', unsupportedType: attribute.tsType };
338
+ }
339
+ project.warnings.push(`${label}: the attribute has no data type; the field was read as unsupported.`);
340
+ return { type: 'unsupported', unsupportedType: 'unknown' };
341
+ }
342
+ function jsonText(value) {
343
+ switch (value.kind) {
344
+ case 'string':
345
+ return JSON.stringify(value.value);
346
+ case 'number':
347
+ return String(value.value);
348
+ case 'bool':
349
+ return value.value ? 'true' : 'false';
350
+ case 'null':
351
+ return 'null';
352
+ case 'array': {
353
+ const items = value.items.map(jsonText);
354
+ return items.every((item) => item !== undefined)
355
+ ? `[${items.join(',')}]`
356
+ : undefined;
357
+ }
358
+ case 'object': {
359
+ const parts = [];
360
+ for (const [key, item] of Object.entries(value.properties)) {
361
+ const text = jsonText(item);
362
+ if (text === undefined) {
363
+ return undefined;
364
+ }
365
+ parts.push(`${JSON.stringify(key)}:${text}`);
366
+ }
367
+ return `{${parts.join(',')}}`;
368
+ }
369
+ default:
370
+ return undefined;
371
+ }
372
+ }
373
+ function convertSqlDefault(sql, field, enumeration) {
374
+ const trimmed = sql.trim();
375
+ if (NOW_SQL.test(trimmed)) {
376
+ return { kind: 'now' };
377
+ }
378
+ if (UUID_SQL.test(trimmed)) {
379
+ return { kind: 'uuid' };
380
+ }
381
+ if (/^null$/i.test(trimmed)) {
382
+ return undefined;
383
+ }
384
+ if (/^(true|false)$/i.test(trimmed)) {
385
+ return { kind: 'literal', value: trimmed.toLowerCase() === 'true' };
386
+ }
387
+ if (/^-?\d+(\.\d+)?$/.test(trimmed)) {
388
+ return field.type === 'string' || field.type === 'text'
389
+ ? { kind: 'literal', value: trimmed }
390
+ : { kind: 'literal', value: Number(trimmed) };
391
+ }
392
+ const quoted = QUOTED_SQL.exec(trimmed);
393
+ if (quoted !== null) {
394
+ const text = (quoted[1] ?? '').replace(/''/g, "'");
395
+ const member = enumeration?.values.find((candidate) => candidate.dbValue === text);
396
+ return member === undefined
397
+ ? { kind: 'literal', value: text }
398
+ : { kind: 'enumValue', value: member.name };
399
+ }
400
+ return { kind: 'dbExpression', expression: trimmed };
401
+ }
402
+ /** `Sequelize.fn('now')` as SQL text: `now()`. Arguments must be literals. */
403
+ function callText(callee, args) {
404
+ const rendered = [];
405
+ for (const arg of args.slice(1)) {
406
+ if (arg.kind === 'string') {
407
+ rendered.push(`'${arg.value.replace(/'/g, "''")}'`);
408
+ }
409
+ else if (arg.kind === 'number') {
410
+ rendered.push(String(arg.value));
411
+ }
412
+ else {
413
+ return undefined;
414
+ }
415
+ }
416
+ const first = args[0];
417
+ return first !== undefined && first.kind === 'string' && callee !== ''
418
+ ? `${first.value}(${rendered.join(', ')})`
419
+ : undefined;
420
+ }
421
+ function convertDefault(value, field, enumeration, label, project) {
422
+ switch (value.kind) {
423
+ case 'null':
424
+ return undefined;
425
+ case 'bool':
426
+ case 'number':
427
+ return { kind: 'literal', value: value.value };
428
+ case 'string': {
429
+ const member = enumeration?.values.find((candidate) => candidate.dbValue === value.value);
430
+ if (member !== undefined) {
431
+ return { kind: 'enumValue', value: member.name };
432
+ }
433
+ if (/^-?\d+(\.\d+)?$/.test(value.value) &&
434
+ (field.type === 'decimal' ||
435
+ field.type === 'int' ||
436
+ field.type === 'bigInt' ||
437
+ field.type === 'float')) {
438
+ return { kind: 'literal', value: Number(value.value) };
439
+ }
440
+ return { kind: 'literal', value: value.value };
441
+ }
442
+ case 'name':
443
+ case 'call': {
444
+ const callee = value.kind === 'name' ? value.value : value.callee;
445
+ if (/(^|\.)NOW$/.test(callee)) {
446
+ return { kind: 'now' };
447
+ }
448
+ const version = /(^|\.)UUIDV([14])$/.exec(callee);
449
+ if (version !== null) {
450
+ // Version 4 is what an unversioned IR default means.
451
+ return version[2] === '4'
452
+ ? { kind: 'uuid' }
453
+ : { kind: 'uuid', version: Number(version[2]) };
454
+ }
455
+ if (value.kind === 'call' && /(^|\.)literal$/.test(callee)) {
456
+ const sql = value.args[0];
457
+ if (sql !== undefined && sql.kind === 'string') {
458
+ return convertSqlDefault(sql.value, field, enumeration);
459
+ }
460
+ }
461
+ if (value.kind === 'call' && /(^|\.)fn$/.test(callee)) {
462
+ const text = callText(callee, value.args);
463
+ if (text !== undefined) {
464
+ return convertSqlDefault(text, field, enumeration);
465
+ }
466
+ }
467
+ break;
468
+ }
469
+ case 'object':
470
+ case 'array': {
471
+ const text = jsonText(value);
472
+ if (text !== undefined &&
473
+ field.type === 'json' &&
474
+ field.arrayDepth === undefined) {
475
+ return { kind: 'literal', value: text };
476
+ }
477
+ break;
478
+ }
479
+ default:
480
+ break;
481
+ }
482
+ project.warnings.push(`${label}: the default value (${describeNode(value)}) is computed at run time or cannot be read statically and was dropped.`);
483
+ return undefined;
484
+ }
485
+ // ---------------------------------------------------------------------------
486
+ // Fields
487
+ // ---------------------------------------------------------------------------
488
+ function registerEnum(context, model, attribute, values) {
489
+ let name = `${model.irName}${toPascalCase(attribute.name)}`;
490
+ while (context.enums.some((item) => item.name === name)) {
491
+ name = `${name}Enum`;
492
+ }
493
+ const created = {
494
+ name,
495
+ values: enumValuesOf(values),
496
+ ...(model.options.schema === undefined
497
+ ? {}
498
+ : { schema: model.options.schema }),
499
+ };
500
+ context.enums.push(created);
501
+ return created;
502
+ }
503
+ function buildField(context, model, attribute) {
504
+ const label = `${model.raw.location}, field "${attribute.name}"`;
505
+ const resolved = resolveAttributeType(model, attribute, context.project);
506
+ if (resolved.isVirtual === true) {
507
+ context.project.warnings.push(`${label}: DataTypes.VIRTUAL has no database column, so the field was skipped.`);
508
+ return undefined;
509
+ }
510
+ if (resolved.note !== undefined) {
511
+ context.project.warnings.push(`${label}: ${resolved.note}.`);
512
+ }
513
+ const isPrimaryKey = attribute.primaryKey === true;
514
+ if (attribute.tsNullable === true && attribute.allowNull === false) {
515
+ context.project.warnings.push(`${label}: the TypeScript type allows null (or the property is optional) but the column is NOT NULL (allowNull: false); the column was read as NOT NULL.`);
516
+ }
517
+ const field = {
518
+ name: attribute.name,
519
+ columnName: columnName(model.options, attribute),
520
+ type: resolved.type,
521
+ isPrimaryKey,
522
+ isUnique: false,
523
+ isNullable: !isPrimaryKey && attribute.allowNull !== false,
524
+ isAutoUpdated: attribute.timestamp === 'updatedAt',
525
+ ...(resolved.maxLength === undefined
526
+ ? {}
527
+ : { maxLength: resolved.maxLength }),
528
+ ...(resolved.maxDigits === undefined
529
+ ? {}
530
+ : { maxDigits: resolved.maxDigits }),
531
+ ...(resolved.decimalPlaces === undefined
532
+ ? {}
533
+ : { decimalPlaces: resolved.decimalPlaces }),
534
+ ...(resolved.arrayDepth === undefined
535
+ ? {}
536
+ : { arrayDepth: resolved.arrayDepth }),
537
+ ...(resolved.rangeOf === undefined ? {} : { rangeOf: resolved.rangeOf }),
538
+ ...(resolved.unsupportedType === undefined
539
+ ? {}
540
+ : { unsupportedType: resolved.unsupportedType }),
541
+ };
542
+ let enumeration;
543
+ if (resolved.enumValues !== undefined) {
544
+ if (resolved.enumValues.length === 0) {
545
+ context.project.warnings.push(`${label}: an ENUM column without values was read as a plain string.`);
546
+ field.maxLength = 255;
547
+ }
548
+ else {
549
+ enumeration = registerEnum(context, model, attribute, resolved.enumValues);
550
+ field.enumName = enumeration.name;
551
+ }
552
+ }
553
+ if (attribute.autoIncrement === true) {
554
+ field.default = { kind: 'autoIncrement' };
555
+ }
556
+ else if (attribute.defaultValue !== undefined) {
557
+ const converted = convertDefault(attribute.defaultValue, field, enumeration, label, context.project);
558
+ if (converted !== undefined) {
559
+ field.default = converted;
560
+ }
561
+ }
562
+ return field;
563
+ }
564
+ // ---------------------------------------------------------------------------
565
+ // Models
566
+ // ---------------------------------------------------------------------------
567
+ function mapAction(value, label, what, project) {
568
+ if (value === undefined) {
569
+ return undefined;
570
+ }
571
+ const mapped = ON_ACTION_MAP[value.toUpperCase()];
572
+ if (mapped === undefined) {
573
+ project.warnings.push(`${label}: ${what} "${value}" has no equivalent and was read as NO ACTION.`);
574
+ return 'noAction';
575
+ }
576
+ return mapped;
577
+ }
578
+ /** The accessor name for a foreign key without a `belongsTo`: `authorId` becomes `author`. */
579
+ function derivedRelationName(attribute, taken) {
580
+ const stripped = lowerFirst(attribute.name.replace(/(_id|Id|ID)$/, ''));
581
+ return stripped === '' || stripped === attribute.name || taken.has(stripped)
582
+ ? attribute.name
583
+ : stripped;
584
+ }
585
+ function primaryKeyAttributes(attributes) {
586
+ return attributes.filter((attribute) => attribute.primaryKey === true);
587
+ }
588
+ function indexMethod(using, label, project) {
589
+ if (using === undefined) {
590
+ return undefined;
591
+ }
592
+ const method = INDEX_METHODS[using.toLowerCase()];
593
+ if (method === undefined) {
594
+ project.warnings.push(`${label}: the index method "${using}" has no equivalent and was dropped.`);
595
+ }
596
+ return method;
597
+ }
598
+ function buildModel(context, model) {
599
+ const { project, links } = context;
600
+ const label = model.raw.location;
601
+ const attributes = model.raw.attributes;
602
+ // Shared `unique: 'name'` values make a composite unique constraint. Sequelize creates the tables with
603
+ // the model's uniqueKeys option when there is one, and then ignores the names given on attributes.
604
+ const uniqueGroups = new Map();
605
+ const ignoredUniqueNames = [];
606
+ for (const attribute of attributes) {
607
+ const name = attribute.unique?.name;
608
+ if (name === undefined) {
609
+ continue;
610
+ }
611
+ if (model.raw.optionEntries.has('uniqueKeys')) {
612
+ ignoredUniqueNames.push(name);
613
+ delete attribute.unique;
614
+ }
615
+ else {
616
+ uniqueGroups.set(name, [...(uniqueGroups.get(name) ?? []), attribute]);
617
+ }
618
+ }
619
+ if (ignoredUniqueNames.length > 0) {
620
+ project.warnings.push(`${label}: the uniqueKeys option replaces the unique constraints named on attributes (${[...new Set(ignoredUniqueNames)].join(', ')}), so Sequelize does not create them; they were ignored.`);
621
+ }
622
+ const isGrouped = (attribute) => {
623
+ const name = attribute.unique?.name;
624
+ return name !== undefined && (uniqueGroups.get(name)?.length ?? 0) > 1;
625
+ };
626
+ // A single-field unique index without a name is the same as `unique: true`.
627
+ const uniqueByIndex = new Set();
628
+ const modelIndexes = [];
629
+ for (const index of model.raw.indexes) {
630
+ const only = index.fields[0];
631
+ if (index.unique &&
632
+ index.name === undefined &&
633
+ index.fields.length === 1 &&
634
+ only !== undefined &&
635
+ !index.isPartial &&
636
+ !index.hasExpression &&
637
+ index.type === undefined &&
638
+ index.using === undefined) {
639
+ uniqueByIndex.add(only.name);
640
+ }
641
+ else {
642
+ modelIndexes.push(index);
643
+ }
644
+ }
645
+ const isUniqueAttribute = (attribute) => (attribute.unique !== undefined && !isGrouped(attribute)) ||
646
+ uniqueByIndex.has(attribute.name) ||
647
+ uniqueByIndex.has(columnName(model.options, attribute));
648
+ const fields = [];
649
+ const relations = [];
650
+ /** Attribute name and column name -> the name the IR uses (a relation for foreign keys). */
651
+ const irNames = new Map();
652
+ const takenNames = new Set();
653
+ const foreignKeys = [];
654
+ for (const attribute of attributes) {
655
+ if (attribute.link?.constraint === true) {
656
+ foreignKeys.push(attribute);
657
+ }
658
+ else {
659
+ takenNames.add(attribute.name);
660
+ }
661
+ }
662
+ for (const attribute of attributes) {
663
+ if (attribute.link?.constraint === true) {
664
+ continue;
665
+ }
666
+ const field = buildField(context, model, attribute);
667
+ if (field === undefined) {
668
+ continue;
669
+ }
670
+ if (isUniqueAttribute(attribute) && !field.isPrimaryKey) {
671
+ field.isUnique = true;
672
+ if (attribute.unique?.name !== undefined) {
673
+ field.uniqueName = attribute.unique.name;
674
+ }
675
+ }
676
+ fields.push(field);
677
+ irNames.set(attribute.name, field.name);
678
+ irNames.set(field.columnName, field.name);
679
+ }
680
+ const pkAttributes = primaryKeyAttributes(attributes);
681
+ for (const attribute of foreignKeys) {
682
+ const link = attribute.link;
683
+ if (link === undefined) {
684
+ continue;
685
+ }
686
+ const alias = links.aliases.get(attribute);
687
+ const reverse = links.reverses.get(attribute);
688
+ let name = alias ?? derivedRelationName(attribute, takenNames);
689
+ if (takenNames.has(name)) {
690
+ name = attribute.name;
691
+ }
692
+ takenNames.add(name);
693
+ const isPrimary = pkAttributes.length === 1 && pkAttributes[0] === attribute;
694
+ const isOneToOne = isPrimary || isUniqueAttribute(attribute);
695
+ if (reverse?.kind === 'hasOne' && !isOneToOne) {
696
+ project.warnings.push(`${label}: ${reverse.label} does not make "${attribute.name}" unique, so the relation was read as a foreign key; add unique: true to the column for a one-to-one relation.`);
697
+ }
698
+ const target = link.target;
699
+ const targetPrepared = context.prepared.find((candidate) => candidate.raw === target);
700
+ const targetPrimary = primaryKeyAttributes(target?.attributes ?? [])[0];
701
+ const onDelete = mapAction(link.onDelete, `${label}, field "${attribute.name}"`, 'onDelete', project);
702
+ const onUpdate = mapAction(link.onUpdate, `${label}, field "${attribute.name}"`, 'onUpdate', project);
703
+ const isNullable = attribute.primaryKey !== true && attribute.allowNull !== false;
704
+ relations.push({
705
+ name,
706
+ kind: isOneToOne ? 'oneToOne' : 'foreignKey',
707
+ targetModel: targetPrepared?.irName ?? link.targetName,
708
+ columnName: columnName(model.options, attribute),
709
+ isNullable,
710
+ onDelete: onDelete ?? 'noAction',
711
+ ...(reverse === undefined ? {} : { relatedName: reverse.name }),
712
+ ...(link.keyAttribute === undefined ||
713
+ link.keyAttribute === targetPrimary?.name ||
714
+ target === undefined
715
+ ? {}
716
+ : { toField: link.keyAttribute }),
717
+ ...(isPrimary ? { isPrimaryKey: true } : {}),
718
+ // ON UPDATE CASCADE is what Sequelize adds to every key; only a different action is worth recording.
719
+ ...(onUpdate === undefined || onUpdate === 'cascade' ? {} : { onUpdate }),
720
+ });
721
+ irNames.set(attribute.name, name);
722
+ irNames.set(columnName(model.options, attribute), name);
723
+ }
724
+ for (const link of links.manyToMany) {
725
+ if (link.owner !== model) {
726
+ continue;
727
+ }
728
+ relations.push(manyToManyRelation(link, takenNames, project));
729
+ }
730
+ // Indexes ----------------------------------------------------------------
731
+ const indexes = [];
732
+ for (const [name, group] of uniqueGroups) {
733
+ if (group.length < 2) {
734
+ continue;
735
+ }
736
+ indexes.push({
737
+ fields: group.map((attribute) => irNames.get(attribute.name) ?? attribute.name),
738
+ isUnique: true,
739
+ name,
740
+ });
741
+ }
742
+ for (const raw of modelIndexes) {
743
+ const names = raw.fields.map((field) => irNames.get(field.name) ?? field.name);
744
+ const known = new Set([
745
+ ...fields.map((field) => field.name),
746
+ ...relations.map((relation) => relation.name),
747
+ ]);
748
+ const missing = names.filter((name) => !known.has(name));
749
+ if (raw.hasExpression || raw.fields.length === 0) {
750
+ project.warnings.push(`${label}: an index on an expression (Sequelize.literal) cannot be represented and was skipped.`);
751
+ continue;
752
+ }
753
+ if (missing.length > 0) {
754
+ project.warnings.push(`${label}: the index on (${raw.fields.map((field) => field.name).join(', ')}) refers to "${missing.join('", "')}", which is not a column of the model; it was skipped.`);
755
+ continue;
756
+ }
757
+ if (raw.isPartial) {
758
+ project.warnings.push(`${label}: the where condition of the index on (${names.join(', ')}) cannot be represented and was dropped.`);
759
+ }
760
+ if (raw.type === 'SPATIAL') {
761
+ project.warnings.push(`${label}: the SPATIAL index on (${names.join(', ')}) was read as an ordinary index.`);
762
+ }
763
+ const method = indexMethod(raw.using, label, project);
764
+ const fieldOptions = {};
765
+ for (const field of raw.fields) {
766
+ const options = {
767
+ ...(field.order === undefined ? {} : { sort: field.order }),
768
+ ...(field.length === undefined ? {} : { length: field.length }),
769
+ ...(field.operator === undefined ? {} : { ops: field.operator }),
770
+ };
771
+ if (Object.keys(options).length > 0) {
772
+ fieldOptions[irNames.get(field.name) ?? field.name] = options;
773
+ }
774
+ }
775
+ indexes.push({
776
+ fields: names,
777
+ isUnique: raw.unique,
778
+ ...(raw.name === undefined ? {} : { name: raw.name }),
779
+ ...(raw.type === 'FULLTEXT' && !raw.unique ? { kind: 'fulltext' } : {}),
780
+ ...(method === undefined ? {} : { method }),
781
+ ...(Object.keys(fieldOptions).length === 0 ? {} : { fieldOptions }),
782
+ });
783
+ }
784
+ // Primary key ------------------------------------------------------------
785
+ let compositePrimaryKey;
786
+ if (pkAttributes.length === 0) {
787
+ project.warnings.push(`${label}: the model has no primary key; most ORMs need one, so add primaryKey: true to a column.`);
788
+ }
789
+ else if (pkAttributes.length > 1) {
790
+ compositePrimaryKey = pkAttributes.map((attribute) => irNames.get(attribute.name) ?? attribute.name);
791
+ for (const field of fields) {
792
+ if (compositePrimaryKey.includes(field.name)) {
793
+ field.isPrimaryKey = false;
794
+ field.isUnique = false;
795
+ }
796
+ }
797
+ for (const relation of relations) {
798
+ delete relation.isPrimaryKey;
799
+ }
800
+ }
801
+ // Behaviour that is not schema --------------------------------------------
802
+ const accessors = attributes
803
+ .filter((attribute) => attribute.hasAccessor === true)
804
+ .map((attribute) => attribute.name);
805
+ if (accessors.length > 0) {
806
+ project.warnings.push(`${label}: custom getters or setters on ${accessors.join(', ')} are behaviour, not schema, and were ignored.`);
807
+ }
808
+ if (model.raw.unsupported.length > 0) {
809
+ project.warnings.push(`${label}: ${model.raw.unsupported.join(', ')} ${model.raw.unsupported.length === 1 ? 'is' : 'are'} not part of the database schema and ${model.raw.unsupported.length === 1 ? 'was' : 'were'} ignored.`);
810
+ }
811
+ return {
812
+ name: model.irName,
813
+ tableName: model.tableName,
814
+ appLabel: context.options.appLabel,
815
+ fields,
816
+ relations,
817
+ indexes,
818
+ ...(compositePrimaryKey === undefined ? {} : { compositePrimaryKey }),
819
+ ...(model.options.schema === undefined
820
+ ? {}
821
+ : { schema: model.options.schema }),
822
+ };
823
+ }
824
+ function manyToManyRelation(link, takenNames, project) {
825
+ let name = link.name;
826
+ while (takenNames.has(name)) {
827
+ name = `${name}_`;
828
+ }
829
+ takenNames.add(name);
830
+ // Other formats derive the join table from the two models; say what does not survive.
831
+ const isSelf = link.owner === link.target;
832
+ const expectedTable = `${link.owner.tableName}_${name}`;
833
+ const expectedOwner = `${isSelf ? 'from_' : ''}${toSnakeCase(link.owner.irName)}_id`;
834
+ const expectedTarget = `${isSelf ? 'to_' : ''}${toSnakeCase(link.target.irName)}_id`;
835
+ if (link.table !== expectedTable ||
836
+ link.ownerColumn !== expectedOwner ||
837
+ link.targetColumn !== expectedTarget) {
838
+ project.warnings.push(`${link.owner.raw.location}: the join table "${link.table}" (columns ${link.ownerColumn}, ${link.targetColumn}) of ${link.label} is not preserved by name; ` +
839
+ `formats that derive join tables will use "${expectedTable}" (columns ${expectedOwner}, ${expectedTarget}).`);
840
+ }
841
+ return {
842
+ name,
843
+ kind: 'manyToMany',
844
+ targetModel: link.target.irName,
845
+ columnName: `${toSnakeCase(name)}_id`,
846
+ isNullable: false,
847
+ onDelete: 'cascade',
848
+ ...(link.relatedName === undefined
849
+ ? {}
850
+ : { relatedName: link.relatedName }),
851
+ };
852
+ }
853
+ // ---------------------------------------------------------------------------
854
+ // Schema
855
+ // ---------------------------------------------------------------------------
856
+ function addStubModels(models, warnings, options) {
857
+ const known = new Set(models.map((model) => model.name));
858
+ const stubs = [];
859
+ for (const model of models) {
860
+ for (const relation of model.relations) {
861
+ const target = relation.targetModel;
862
+ if (known.has(target)) {
863
+ continue;
864
+ }
865
+ known.add(target);
866
+ stubs.push({
867
+ name: target,
868
+ tableName: toSnakeCase(target),
869
+ appLabel: options.appLabel,
870
+ fields: [
871
+ {
872
+ name: 'id',
873
+ columnName: 'id',
874
+ type: 'int',
875
+ isPrimaryKey: true,
876
+ isUnique: false,
877
+ isNullable: false,
878
+ isAutoUpdated: false,
879
+ default: { kind: 'autoIncrement' },
880
+ },
881
+ ],
882
+ relations: [],
883
+ indexes: [],
884
+ });
885
+ warnings.push(`${model.name}.${relation.name} references "${target}", which is not a model in the input. ` +
886
+ `A stub model with an auto-increment id was generated; replace it with the real definition.`);
887
+ }
888
+ }
889
+ models.push(...stubs);
890
+ }
891
+ function buildSchema(project, options) {
892
+ const prepared = prepareModels(project);
893
+ const links = linkAssociations(prepared, project);
894
+ resolveReferences(prepared, project);
895
+ const context = {
896
+ project,
897
+ options,
898
+ enums: [],
899
+ prepared,
900
+ links,
901
+ };
902
+ const models = prepared.map((model) => buildModel(context, model));
903
+ addStubModels(models, project.warnings, options);
904
+ return { models, enums: context.enums, warnings: project.warnings };
905
+ }
906
+ //# sourceMappingURL=sequelize.js.map