ormbridge 0.6.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 +90 -597
  2. package/dist/cliRunner.js +21 -9
  3. package/dist/cliRunner.js.map +1 -1
  4. package/dist/config.d.ts +8 -3
  5. package/dist/config.js +11 -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 +11 -5
  11. package/dist/formats.js +44 -5
  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 +5 -2
@@ -0,0 +1,1298 @@
1
+ import { basename } from 'node:path';
2
+ import { evaluateNode, lastSegment, } from './typescriptSyntax.js';
3
+ const ASSOCIATION_METHODS = new Set([
4
+ 'hasMany',
5
+ 'hasOne',
6
+ 'belongsTo',
7
+ 'belongsToMany',
8
+ ]);
9
+ /** Decorators of sequelize-typescript that make a property a column. */
10
+ const COLUMN_DECORATORS = new Set([
11
+ 'Column',
12
+ 'PrimaryKey',
13
+ 'AutoIncrement',
14
+ 'AllowNull',
15
+ 'NotNull',
16
+ 'Unique',
17
+ 'Default',
18
+ 'CreatedAt',
19
+ 'UpdatedAt',
20
+ 'DeletedAt',
21
+ 'ForeignKey',
22
+ 'Comment',
23
+ 'Index',
24
+ ]);
25
+ const TIMESTAMP_DECORATORS = new Set([
26
+ 'CreatedAt',
27
+ 'UpdatedAt',
28
+ 'DeletedAt',
29
+ ]);
30
+ /** Generic wrappers that do not change the column type (`CreationOptional<number>`). */
31
+ const TRANSPARENT_TYPES = new Set([
32
+ 'CreationOptional',
33
+ 'NonAttribute',
34
+ 'ForeignKey',
35
+ 'Promise',
36
+ 'Awaited',
37
+ 'Relation',
38
+ 'Readonly',
39
+ 'Required',
40
+ ]);
41
+ const ASSOCIATION_DECORATORS = {
42
+ HasMany: 'hasMany',
43
+ HasOne: 'hasOne',
44
+ BelongsTo: 'belongsTo',
45
+ BelongsToMany: 'belongsToMany',
46
+ };
47
+ /** Model options that describe behaviour the IR cannot hold, mapped to the name used in the warning. */
48
+ const UNSUPPORTED_MODEL_OPTIONS = {
49
+ hooks: 'hooks',
50
+ scopes: 'scopes',
51
+ defaultScope: 'a default scope',
52
+ getterMethods: 'getterMethods',
53
+ setterMethods: 'setterMethods',
54
+ };
55
+ /** Decorators of sequelize-typescript for model behaviour. */
56
+ const UNSUPPORTED_CLASS_DECORATORS = {
57
+ Scopes: 'scopes',
58
+ DefaultScope: 'a default scope',
59
+ };
60
+ const HOOK_DECORATORS = new Set([
61
+ 'BeforeCreate',
62
+ 'AfterCreate',
63
+ 'BeforeUpdate',
64
+ 'AfterUpdate',
65
+ 'BeforeDestroy',
66
+ 'AfterDestroy',
67
+ 'BeforeSave',
68
+ 'AfterSave',
69
+ 'BeforeBulkCreate',
70
+ 'AfterBulkCreate',
71
+ 'BeforeValidate',
72
+ 'AfterValidate',
73
+ 'BeforeUpsert',
74
+ 'AfterUpsert',
75
+ 'BeforeFind',
76
+ 'AfterFind',
77
+ 'BeforeSync',
78
+ 'AfterSync',
79
+ 'BeforeDefine',
80
+ 'AfterDefine',
81
+ ]);
82
+ // ---------------------------------------------------------------------------
83
+ // Syntax helpers
84
+ // ---------------------------------------------------------------------------
85
+ const TRANSPARENT_NODES = new Set([
86
+ 'parenthesized_expression',
87
+ 'as_expression',
88
+ 'satisfies_expression',
89
+ 'non_null_expression',
90
+ 'type_assertion',
91
+ 'await_expression',
92
+ ]);
93
+ export function stripWrappers(node) {
94
+ let current = node;
95
+ while (TRANSPARENT_NODES.has(current.type)) {
96
+ const inner = current.namedChildren[0];
97
+ if (inner === undefined) {
98
+ break;
99
+ }
100
+ current = inner;
101
+ }
102
+ return current;
103
+ }
104
+ export function describeFile(path, root) {
105
+ const consts = new Map();
106
+ const stack = [root];
107
+ while (stack.length > 0) {
108
+ const node = stack.pop();
109
+ if (node === undefined) {
110
+ break;
111
+ }
112
+ if (node.type === 'variable_declarator') {
113
+ const nameNode = node.childForFieldName('name');
114
+ const valueNode = node.childForFieldName('value');
115
+ if (nameNode !== null &&
116
+ valueNode !== null &&
117
+ nameNode.type === 'identifier' &&
118
+ !consts.has(nameNode.text)) {
119
+ consts.set(nameNode.text, valueNode);
120
+ }
121
+ }
122
+ stack.push(...node.namedChildren);
123
+ }
124
+ return { path, label: basename(path), root, consts };
125
+ }
126
+ /** Follows `const x = ...` references (also across files) until a node that is not a plain name. */
127
+ export function resolveNode(node, ctx) {
128
+ let current = stripWrappers(node);
129
+ for (let depth = 0; depth < 8; depth += 1) {
130
+ if (current.type !== 'identifier') {
131
+ break;
132
+ }
133
+ const local = ctx.file.consts.get(current.text);
134
+ if (local !== undefined) {
135
+ current = stripWrappers(local);
136
+ continue;
137
+ }
138
+ const shared = ctx.project.sharedConsts.get(current.text);
139
+ const only = shared?.length === 1 ? shared[0] : undefined;
140
+ if (only === undefined) {
141
+ break;
142
+ }
143
+ current = stripWrappers(only.node);
144
+ }
145
+ return current;
146
+ }
147
+ function keyText(node) {
148
+ switch (node.type) {
149
+ case 'property_identifier':
150
+ case 'identifier':
151
+ return node.text;
152
+ case 'string': {
153
+ const value = evaluateNode(node);
154
+ return value.kind === 'string' ? value.value : undefined;
155
+ }
156
+ case 'number':
157
+ return node.text;
158
+ default:
159
+ return undefined;
160
+ }
161
+ }
162
+ /** The `key: value` entries of an object literal, following identifiers and spreads of known objects. */
163
+ export function objectEntries(node, ctx) {
164
+ const entries = new Map();
165
+ let incomplete = false;
166
+ const resolved = resolveNode(node, ctx);
167
+ if (resolved.type !== 'object') {
168
+ return { entries, incomplete: true };
169
+ }
170
+ for (const child of resolved.namedChildren) {
171
+ if (child.type === 'pair') {
172
+ const keyNode = child.childForFieldName('key');
173
+ const valueNode = child.childForFieldName('value');
174
+ const key = keyNode === null ? undefined : keyText(keyNode);
175
+ if (key === undefined || valueNode === null) {
176
+ incomplete = true;
177
+ }
178
+ else {
179
+ entries.set(key, valueNode);
180
+ }
181
+ }
182
+ else if (child.type === 'shorthand_property_identifier') {
183
+ entries.set(child.text, child);
184
+ }
185
+ else if (child.type === 'method_definition') {
186
+ const nameNode = child.childForFieldName('name');
187
+ const key = nameNode === null ? undefined : keyText(nameNode);
188
+ if (key !== undefined) {
189
+ entries.set(key, child);
190
+ }
191
+ }
192
+ else if (child.type === 'spread_element') {
193
+ const inner = child.namedChildren[0];
194
+ const spread = inner === undefined ? undefined : objectEntries(inner, ctx);
195
+ if (spread === undefined || spread.incomplete) {
196
+ incomplete = true;
197
+ }
198
+ for (const [key, value] of spread?.entries ?? []) {
199
+ entries.set(key, value);
200
+ }
201
+ }
202
+ }
203
+ return { entries, incomplete };
204
+ }
205
+ export function argsOf(call) {
206
+ const argumentsNode = call.childForFieldName('arguments');
207
+ return argumentsNode === null
208
+ ? []
209
+ : argumentsNode.namedChildren.filter((child) => child.type !== 'comment');
210
+ }
211
+ function valueOf(node, ctx) {
212
+ return evaluateNode(resolveNode(node, ctx));
213
+ }
214
+ function stringOf(node, ctx) {
215
+ if (node === undefined) {
216
+ return undefined;
217
+ }
218
+ const value = valueOf(node, ctx);
219
+ return value.kind === 'string' ? value.value : undefined;
220
+ }
221
+ function boolOf(node, ctx) {
222
+ if (node === undefined) {
223
+ return undefined;
224
+ }
225
+ const value = valueOf(node, ctx);
226
+ return value.kind === 'bool' ? value.value : undefined;
227
+ }
228
+ function numberOf(node, ctx) {
229
+ if (node === undefined) {
230
+ return undefined;
231
+ }
232
+ const value = valueOf(node, ctx);
233
+ return value.kind === 'number' ? value.value : undefined;
234
+ }
235
+ /** The text of a reference to a model: `User`, `models.User`, `sequelize.models.User`, `sequelize.model('User')`. */
236
+ function modelReference(node, ctx) {
237
+ const stripped = stripWrappers(node);
238
+ switch (stripped.type) {
239
+ case 'identifier':
240
+ case 'member_expression':
241
+ return lastSegment(stripped.text.replace(/\s+/g, ''));
242
+ case 'string':
243
+ return stringOf(stripped, ctx);
244
+ case 'call_expression': {
245
+ const callee = stripped.childForFieldName('function');
246
+ const first = argsOf(stripped)[0];
247
+ return callee !== null && /\.model$/.test(callee.text)
248
+ ? stringOf(first, ctx)
249
+ : undefined;
250
+ }
251
+ case 'arrow_function':
252
+ case 'function_expression': {
253
+ // sequelize-typescript: `() => User`
254
+ const body = stripped.childForFieldName('body');
255
+ return body === null ? undefined : modelReference(body, ctx);
256
+ }
257
+ default:
258
+ return undefined;
259
+ }
260
+ }
261
+ // ---------------------------------------------------------------------------
262
+ // Attributes
263
+ // ---------------------------------------------------------------------------
264
+ function describeThrough(node, ctx) {
265
+ if (node === undefined) {
266
+ return undefined;
267
+ }
268
+ const stripped = stripWrappers(node);
269
+ if (stripped.type === 'object') {
270
+ const { entries } = objectEntries(stripped, ctx);
271
+ const unique = boolOf(entries.get('unique'), ctx);
272
+ const described = describeThrough(entries.get('model'), ctx);
273
+ return unique === undefined ? described : { ...(described ?? {}), unique };
274
+ }
275
+ // A name that points at `sequelize.define(...)` is a model; only a string constant is a table name.
276
+ const table = stringOf(stripped, ctx);
277
+ if (table !== undefined) {
278
+ return { table };
279
+ }
280
+ const model = modelReference(stripped, ctx);
281
+ return model === undefined ? undefined : { model };
282
+ }
283
+ /**
284
+ * The value of `references.model`: a table name (also from a string constant), or the name of a model
285
+ * variable, which must not be followed to the `sequelize.define(...)` call it holds.
286
+ */
287
+ function modelOrTableName(node, ctx) {
288
+ const stripped = stripWrappers(node);
289
+ const resolved = valueOf(stripped, ctx);
290
+ return resolved.kind === 'string' ? resolved : evaluateNode(stripped);
291
+ }
292
+ /** Reads one attribute definition: `{ type, allowNull, ... }` or a bare `DataTypes.X`. */
293
+ export function readAttribute(name, node, ctx) {
294
+ const attribute = { name, origin: 'declared' };
295
+ const resolved = resolveNode(node, ctx);
296
+ if (resolved.type === 'object') {
297
+ applyAttributeEntries(attribute, objectEntries(resolved, ctx).entries, ctx);
298
+ }
299
+ else if (resolved.type === 'method_definition') {
300
+ attribute.hasAccessor = true;
301
+ }
302
+ else {
303
+ attribute.type = evaluateNode(resolved);
304
+ }
305
+ return attribute;
306
+ }
307
+ /** Applies the options of an attribute object (`allowNull`, `unique`, ...) to an attribute. */
308
+ export function applyAttributeEntries(attribute, entries, ctx) {
309
+ for (const [key, node] of entries) {
310
+ switch (key) {
311
+ case 'type':
312
+ attribute.type = valueOf(node, ctx);
313
+ break;
314
+ case 'allowNull':
315
+ attribute.allowNull = boolOf(node, ctx) ?? attribute.allowNull;
316
+ break;
317
+ case 'primaryKey':
318
+ attribute.primaryKey = boolOf(node, ctx) ?? attribute.primaryKey;
319
+ break;
320
+ case 'autoIncrement':
321
+ attribute.autoIncrement = boolOf(node, ctx) ?? attribute.autoIncrement;
322
+ break;
323
+ case 'unique': {
324
+ const value = valueOf(node, ctx);
325
+ if (value.kind === 'bool') {
326
+ if (value.value) {
327
+ attribute.unique = {};
328
+ }
329
+ else {
330
+ delete attribute.unique;
331
+ }
332
+ }
333
+ else if (value.kind === 'string') {
334
+ attribute.unique = { name: value.value };
335
+ }
336
+ else if (value.kind === 'object') {
337
+ const unique = value.properties['name'];
338
+ attribute.unique =
339
+ unique !== undefined && unique.kind === 'string'
340
+ ? { name: unique.value }
341
+ : {};
342
+ }
343
+ break;
344
+ }
345
+ case 'defaultValue':
346
+ attribute.defaultValue = valueOf(node, ctx);
347
+ break;
348
+ case 'references': {
349
+ const resolved = resolveNode(node, ctx);
350
+ if (resolved.type === 'object') {
351
+ const reference = objectEntries(resolved, ctx).entries;
352
+ const model = reference.get('model');
353
+ const referenceKey = stringOf(reference.get('key'), ctx);
354
+ attribute.references = {
355
+ ...(model === undefined
356
+ ? {}
357
+ : { model: modelOrTableName(model, ctx) }),
358
+ ...(referenceKey === undefined ? {} : { key: referenceKey }),
359
+ };
360
+ }
361
+ break;
362
+ }
363
+ case 'onDelete':
364
+ attribute.onDelete = stringOf(node, ctx) ?? attribute.onDelete;
365
+ break;
366
+ case 'onUpdate':
367
+ attribute.onUpdate = stringOf(node, ctx) ?? attribute.onUpdate;
368
+ break;
369
+ case 'field':
370
+ attribute.field = stringOf(node, ctx) ?? attribute.field;
371
+ break;
372
+ case 'comment':
373
+ attribute.comment = stringOf(node, ctx) ?? attribute.comment;
374
+ break;
375
+ case 'values': {
376
+ const value = valueOf(node, ctx);
377
+ if (value.kind === 'array') {
378
+ attribute.enumValues = value.items.flatMap((item) => item.kind === 'string' ? [item.value] : []);
379
+ }
380
+ break;
381
+ }
382
+ case 'get':
383
+ case 'set':
384
+ attribute.hasAccessor = true;
385
+ break;
386
+ default:
387
+ break;
388
+ }
389
+ }
390
+ }
391
+ function readKey(node, ctx) {
392
+ if (node === undefined) {
393
+ return undefined;
394
+ }
395
+ const resolved = resolveNode(node, ctx);
396
+ if (resolved.type === 'object') {
397
+ const { entries } = objectEntries(resolved, ctx);
398
+ const name = stringOf(entries.get('name'), ctx) ??
399
+ stringOf(entries.get('fieldName'), ctx);
400
+ const attribute = {
401
+ name: name ?? '',
402
+ origin: 'foreignKey',
403
+ };
404
+ applyAttributeEntries(attribute, entries, ctx);
405
+ return { ...(name === undefined ? {} : { name }), attribute };
406
+ }
407
+ const name = stringOf(resolved, ctx);
408
+ return name === undefined
409
+ ? undefined
410
+ : { name, attribute: { name, origin: 'foreignKey' } };
411
+ }
412
+ export function readAssociationOptions(node, ctx) {
413
+ const options = { unsupported: [] };
414
+ if (node === undefined) {
415
+ return options;
416
+ }
417
+ const resolved = resolveNode(node, ctx);
418
+ if (resolved.type !== 'object') {
419
+ // `@BelongsTo(() => User, 'authorId')`
420
+ const foreignKey = readKey(resolved, ctx);
421
+ if (foreignKey !== undefined) {
422
+ options.foreignKey = foreignKey;
423
+ }
424
+ return options;
425
+ }
426
+ const { entries } = objectEntries(resolved, ctx);
427
+ for (const [key, value] of entries) {
428
+ switch (key) {
429
+ case 'as': {
430
+ const alias = stringOf(value, ctx);
431
+ if (alias !== undefined) {
432
+ options.as = alias;
433
+ }
434
+ else {
435
+ const object = valueOf(value, ctx);
436
+ const plural = object.kind === 'object' ? object.properties['plural'] : undefined;
437
+ if (plural !== undefined && plural.kind === 'string') {
438
+ options.as = plural.value;
439
+ }
440
+ }
441
+ break;
442
+ }
443
+ case 'foreignKey': {
444
+ const foreignKey = readKey(value, ctx);
445
+ if (foreignKey !== undefined) {
446
+ options.foreignKey = foreignKey;
447
+ }
448
+ break;
449
+ }
450
+ case 'otherKey': {
451
+ const otherKey = readKey(value, ctx);
452
+ if (otherKey !== undefined) {
453
+ options.otherKey = otherKey;
454
+ }
455
+ break;
456
+ }
457
+ case 'sourceKey':
458
+ options.sourceKey = stringOf(value, ctx);
459
+ break;
460
+ case 'targetKey':
461
+ options.targetKey = stringOf(value, ctx);
462
+ break;
463
+ case 'onDelete':
464
+ options.onDelete = stringOf(value, ctx);
465
+ break;
466
+ case 'onUpdate':
467
+ options.onUpdate = stringOf(value, ctx);
468
+ break;
469
+ case 'constraints':
470
+ options.constraints = boolOf(value, ctx);
471
+ break;
472
+ case 'through': {
473
+ const through = describeThrough(value, ctx);
474
+ if (through !== undefined) {
475
+ options.through = through;
476
+ }
477
+ break;
478
+ }
479
+ case 'scope':
480
+ options.unsupported.push('scope (a polymorphic or scoped association)');
481
+ break;
482
+ default:
483
+ break;
484
+ }
485
+ }
486
+ return options;
487
+ }
488
+ // ---------------------------------------------------------------------------
489
+ // Models: sequelize.define, Model.init
490
+ // ---------------------------------------------------------------------------
491
+ function newModel(ctx, style, modelName, aliases) {
492
+ return {
493
+ file: ctx.file,
494
+ style,
495
+ modelName,
496
+ aliases,
497
+ attributes: [],
498
+ optionEntries: new Map(),
499
+ decoratorOptions: {},
500
+ indexes: [],
501
+ foreignKeyHints: new Map(),
502
+ accessors: [],
503
+ unsupported: [],
504
+ location: `${ctx.file.label}: model "${modelName}"`,
505
+ };
506
+ }
507
+ function addAttributes(model, node, ctx) {
508
+ if (node === undefined) {
509
+ return false;
510
+ }
511
+ const { entries, incomplete } = objectEntries(node, ctx);
512
+ if (!incomplete || entries.size > 0) {
513
+ for (const [name, value] of entries) {
514
+ model.attributes.push(readAttribute(name, value, ctx));
515
+ }
516
+ }
517
+ if (incomplete && entries.size > 0) {
518
+ ctx.project.warnings.push(`${model.location}: the attributes are not a plain object literal (a spread, a computed key or a value that could not be followed), so some columns may be missing.`);
519
+ }
520
+ return entries.size > 0 || !incomplete;
521
+ }
522
+ /** The name a call expression's result is stored under: `const User = ...`, `db.User = ...`. */
523
+ function boundName(call) {
524
+ let current = call.parent;
525
+ while (current !== null && TRANSPARENT_NODES.has(current.type)) {
526
+ current = current.parent;
527
+ }
528
+ if (current === null) {
529
+ return undefined;
530
+ }
531
+ if (current.type === 'variable_declarator') {
532
+ const nameNode = current.childForFieldName('name');
533
+ return nameNode !== null && nameNode.type === 'identifier'
534
+ ? nameNode.text
535
+ : undefined;
536
+ }
537
+ if (current.type === 'assignment_expression') {
538
+ const left = current.childForFieldName('left');
539
+ return left === null
540
+ ? undefined
541
+ : lastSegment(left.text.replace(/\s+/g, ''));
542
+ }
543
+ return undefined;
544
+ }
545
+ function collectDefine(call, ctx) {
546
+ const args = argsOf(call);
547
+ const nameNode = args[0];
548
+ const attributesNode = args[1];
549
+ const modelName = stringOf(nameNode, ctx);
550
+ if (nameNode === undefined || attributesNode === undefined) {
551
+ return;
552
+ }
553
+ if (modelName === undefined) {
554
+ ctx.project.warnings.push(`${ctx.file.label}: sequelize.define(${nameNode.text.slice(0, 40)}, ...) has a model name that is not a string literal, so the model was skipped.`);
555
+ return;
556
+ }
557
+ const bound = boundName(call);
558
+ const model = newModel(ctx, 'define', modelName, bound === undefined ? [] : [bound]);
559
+ if (!addAttributes(model, attributesNode, ctx)) {
560
+ ctx.project.warnings.push(`${model.location}: the attributes of sequelize.define could not be read statically, so the model was skipped.`);
561
+ return;
562
+ }
563
+ const optionsNode = args[2];
564
+ if (optionsNode !== undefined) {
565
+ model.optionEntries = objectEntries(optionsNode, ctx).entries;
566
+ }
567
+ ctx.project.models.push(model);
568
+ }
569
+ /** The name of the class a node is inside, or of the object `X.associate = ...` is assigned to. */
570
+ function enclosingModelName(node) {
571
+ let current = node.parent;
572
+ while (current !== null) {
573
+ if (current.type === 'class_declaration' ||
574
+ current.type === 'abstract_class_declaration' ||
575
+ current.type === 'class') {
576
+ return current.childForFieldName('name')?.text;
577
+ }
578
+ if (current.type === 'assignment_expression') {
579
+ const left = current.childForFieldName('left');
580
+ if (left !== null && left.type === 'member_expression') {
581
+ const objectNode = left.childForFieldName('object');
582
+ if (objectNode !== null) {
583
+ return lastSegment(objectNode.text.replace(/\s+/g, ''));
584
+ }
585
+ }
586
+ }
587
+ current = current.parent;
588
+ }
589
+ return undefined;
590
+ }
591
+ function collectInit(call, ctx) {
592
+ const args = argsOf(call);
593
+ const attributesNode = args[0];
594
+ const optionsNode = args[1];
595
+ if (attributesNode === undefined || optionsNode === undefined) {
596
+ return;
597
+ }
598
+ const options = objectEntries(optionsNode, ctx);
599
+ // `Model.init(attributes, { sequelize })` is the only shape that defines a model.
600
+ if (!options.entries.has('sequelize')) {
601
+ return;
602
+ }
603
+ const functionNode = call.childForFieldName('function');
604
+ const receiver = functionNode === null ? null : functionNode.childForFieldName('object');
605
+ if (receiver === null) {
606
+ return;
607
+ }
608
+ const className = receiver.type === 'identifier'
609
+ ? receiver.text
610
+ : receiver.type === 'this' || receiver.type === 'super'
611
+ ? enclosingModelName(call)
612
+ : undefined;
613
+ const modelName = stringOf(options.entries.get('modelName'), ctx) ?? className;
614
+ if (modelName === undefined) {
615
+ ctx.project.warnings.push(`${ctx.file.label}: a Model.init call has no modelName and no class name, so the model was skipped.`);
616
+ return;
617
+ }
618
+ const model = newModel(ctx, 'init', modelName, className === undefined ? [] : [className]);
619
+ if (!addAttributes(model, attributesNode, ctx)) {
620
+ ctx.project.warnings.push(`${model.location}: the attributes of Model.init could not be read statically, so the model was skipped.`);
621
+ return;
622
+ }
623
+ model.optionEntries = options.entries;
624
+ ctx.project.models.push(model);
625
+ }
626
+ // ---------------------------------------------------------------------------
627
+ // Association calls
628
+ // ---------------------------------------------------------------------------
629
+ function collectAssociationCall(call, method, receiver, ctx) {
630
+ const args = argsOf(call);
631
+ const targetNode = args[0];
632
+ if (targetNode === undefined) {
633
+ return;
634
+ }
635
+ const target = modelReference(targetNode, ctx);
636
+ const source = receiver.type === 'this'
637
+ ? 'this'
638
+ : (modelReference(receiver, ctx) ??
639
+ lastSegment(receiver.text.replace(/\s+/g, '')));
640
+ if (target === undefined) {
641
+ ctx.project.warnings.push(`${ctx.file.label}: ${source}.${method}(${targetNode.text.slice(0, 40)}) names its target in a way that cannot be resolved statically; the association was skipped.`);
642
+ return;
643
+ }
644
+ ctx.project.associations.push({
645
+ kind: method,
646
+ file: ctx.file,
647
+ source,
648
+ enclosing: receiver.type === 'this' ? enclosingModelName(call) : undefined,
649
+ target,
650
+ options: readAssociationOptions(args[1], ctx),
651
+ label: `${source === 'this' ? (enclosingModelName(call) ?? 'this') : source}.${method}(${target})`,
652
+ });
653
+ }
654
+ // ---------------------------------------------------------------------------
655
+ // Model options and indexes
656
+ // ---------------------------------------------------------------------------
657
+ function timestampOption(entries, key, fallback, ctx) {
658
+ const node = entries.get(key);
659
+ if (node !== undefined) {
660
+ const value = valueOf(node, ctx);
661
+ if (value.kind === 'string' && value.value !== '') {
662
+ return value.value;
663
+ }
664
+ if (value.kind === 'bool' && !value.value) {
665
+ return false;
666
+ }
667
+ }
668
+ return fallback ?? key;
669
+ }
670
+ /** Reads the options of a model, with the project defaults and the decorators' contributions below them. */
671
+ export function readModelOptions(model, project) {
672
+ const ctx = { file: model.file, project };
673
+ const entries = model.optionEntries;
674
+ const defaults = project.defaults;
675
+ const flag = (key, fallback) => {
676
+ const value = boolOf(entries.get(key), ctx);
677
+ const inherited = defaults[key];
678
+ return value ?? (typeof inherited === 'boolean' ? inherited : fallback);
679
+ };
680
+ const decorated = model.decoratorOptions;
681
+ const schema = stringOf(entries.get('schema'), ctx) ?? defaults.schema;
682
+ const version = entries.get('version');
683
+ const versionValue = version === undefined ? undefined : valueOf(version, ctx);
684
+ const nameNode = entries.get('name');
685
+ const nameValue = nameNode === undefined ? undefined : valueOf(nameNode, ctx);
686
+ const nameProperties = nameValue !== undefined && nameValue.kind === 'object'
687
+ ? nameValue.properties
688
+ : {};
689
+ const singular = nameProperties['singular'];
690
+ const plural = nameProperties['plural'];
691
+ const tableName = stringOf(entries.get('tableName'), ctx) ?? decorated.tableName;
692
+ return {
693
+ ...(tableName === undefined ? {} : { tableName }),
694
+ freezeTableName: flag('freezeTableName', false),
695
+ timestamps: flag('timestamps', true),
696
+ paranoid: flag('paranoid', false),
697
+ underscored: flag('underscored', false),
698
+ ...(schema === undefined ? {} : { schema }),
699
+ createdAt: timestampOption(entries, 'createdAt', decorated.createdAt ?? defaults.createdAt, ctx),
700
+ updatedAt: timestampOption(entries, 'updatedAt', decorated.updatedAt ?? defaults.updatedAt, ctx),
701
+ deletedAt: timestampOption(entries, 'deletedAt', decorated.deletedAt ?? defaults.deletedAt, ctx),
702
+ ...(versionValue !== undefined &&
703
+ versionValue.kind === 'bool' &&
704
+ versionValue.value
705
+ ? { version: 'version' }
706
+ : versionValue !== undefined && versionValue.kind === 'string'
707
+ ? { version: versionValue.value }
708
+ : {}),
709
+ ...(singular !== undefined && singular.kind === 'string'
710
+ ? { nameSingular: singular.value }
711
+ : {}),
712
+ ...(plural !== undefined && plural.kind === 'string'
713
+ ? { namePlural: plural.value }
714
+ : {}),
715
+ };
716
+ }
717
+ function indexFieldOf(node, ctx) {
718
+ const resolved = resolveNode(node, ctx);
719
+ const text = stringOf(resolved, ctx);
720
+ if (text !== undefined) {
721
+ return { name: text };
722
+ }
723
+ if (resolved.type === 'object') {
724
+ const { entries } = objectEntries(resolved, ctx);
725
+ const name = stringOf(entries.get('name'), ctx) ??
726
+ stringOf(entries.get('attribute'), ctx);
727
+ if (name !== undefined) {
728
+ const order = stringOf(entries.get('order'), ctx);
729
+ const length = numberOf(entries.get('length'), ctx);
730
+ const operator = stringOf(entries.get('operator'), ctx);
731
+ return {
732
+ name,
733
+ ...(order === undefined
734
+ ? {}
735
+ : { order: order.toUpperCase() === 'DESC' ? 'desc' : 'asc' }),
736
+ ...(length === undefined ? {} : { length }),
737
+ ...(operator === undefined ? {} : { operator }),
738
+ };
739
+ }
740
+ }
741
+ return 'expression';
742
+ }
743
+ /** Reads the `indexes` option (and the `uniqueKeys` option) of a model. */
744
+ export function readIndexes(model, project) {
745
+ const ctx = { file: model.file, project };
746
+ // sequelize-typescript lets the indexes option of @Table replace the ones its @Index decorators collect.
747
+ const replaced = model.indexes.length > 0 && model.optionEntries.has('indexes');
748
+ if (replaced) {
749
+ project.warnings.push(`${model.location}: the indexes option of @Table replaces the indexes declared with @Index, so sequelize-typescript ignores the latter; they were ignored.`);
750
+ }
751
+ const indexes = replaced ? [] : [...model.indexes];
752
+ const listNode = model.optionEntries.get('indexes');
753
+ const list = listNode === undefined ? undefined : resolveNode(listNode, ctx);
754
+ if (list !== undefined && list.type === 'array') {
755
+ for (const item of list.namedChildren) {
756
+ const raw = readIndex(item, ctx);
757
+ if (raw !== undefined) {
758
+ indexes.push(raw);
759
+ }
760
+ }
761
+ }
762
+ const keysNode = model.optionEntries.get('uniqueKeys');
763
+ const keys = keysNode === undefined ? undefined : resolveNode(keysNode, ctx);
764
+ if (keys !== undefined && keys.type === 'object') {
765
+ for (const [name, value] of objectEntries(keys, ctx).entries) {
766
+ const raw = readIndex(value, ctx);
767
+ if (raw !== undefined) {
768
+ indexes.push({ ...raw, unique: true, name: raw.name ?? name });
769
+ }
770
+ }
771
+ }
772
+ return indexes;
773
+ }
774
+ function readIndex(node, ctx) {
775
+ const { entries } = objectEntries(node, ctx);
776
+ const fieldsNode = entries.get('fields');
777
+ const fields = fieldsNode === undefined ? undefined : resolveNode(fieldsNode, ctx);
778
+ if (fields === undefined || fields.type !== 'array') {
779
+ return undefined;
780
+ }
781
+ const readFields = [];
782
+ let hasExpression = false;
783
+ for (const item of fields.namedChildren) {
784
+ const field = indexFieldOf(item, ctx);
785
+ if (field === 'expression') {
786
+ hasExpression = true;
787
+ }
788
+ else {
789
+ readFields.push(field);
790
+ }
791
+ }
792
+ const type = stringOf(entries.get('type'), ctx);
793
+ const upperType = type?.toUpperCase();
794
+ const name = stringOf(entries.get('name'), ctx);
795
+ const using = stringOf(entries.get('using'), ctx);
796
+ return {
797
+ fields: readFields,
798
+ unique: boolOf(entries.get('unique'), ctx) === true || upperType === 'UNIQUE',
799
+ ...(name === undefined ? {} : { name }),
800
+ ...(using === undefined ? {} : { using }),
801
+ ...(upperType === 'FULLTEXT' || upperType === 'SPATIAL'
802
+ ? { type: upperType }
803
+ : {}),
804
+ isPartial: entries.has('where'),
805
+ hasExpression,
806
+ };
807
+ }
808
+ /** Unsupported model options (hooks, scopes, getters, ...), as written in the options object. */
809
+ function noteUnsupportedOptions(model) {
810
+ for (const [key, label] of Object.entries(UNSUPPORTED_MODEL_OPTIONS)) {
811
+ if (model.optionEntries.has(key) && !model.unsupported.includes(label)) {
812
+ model.unsupported.push(label);
813
+ }
814
+ }
815
+ }
816
+ function decoratorOf(node) {
817
+ const expression = node.namedChildren[0];
818
+ if (expression === undefined) {
819
+ return undefined;
820
+ }
821
+ if (expression.type === 'call_expression') {
822
+ const callee = expression.childForFieldName('function');
823
+ return callee === null
824
+ ? undefined
825
+ : {
826
+ name: lastSegment(callee.text.replace(/\s+/g, '')),
827
+ args: argsOf(expression),
828
+ };
829
+ }
830
+ return {
831
+ name: lastSegment(expression.text.replace(/\s+/g, '')),
832
+ args: [],
833
+ };
834
+ }
835
+ /** The decorators written on a node (a class, an export statement or a property). */
836
+ function decoratorNodes(node) {
837
+ const decorators = [];
838
+ for (const child of node.namedChildren) {
839
+ const decorator = child.type === 'decorator' ? decoratorOf(child) : undefined;
840
+ if (decorator !== undefined) {
841
+ decorators.push(decorator);
842
+ }
843
+ }
844
+ return decorators;
845
+ }
846
+ /** The TypeScript type of a property, reduced to a name and whether null is allowed. */
847
+ function describeTsType(property) {
848
+ const annotation = property.childForFieldName('type');
849
+ const typeNode = annotation?.namedChildren[0];
850
+ let nullable = property.children.some((child) => child.type === '?');
851
+ let name;
852
+ const visit = (node) => {
853
+ switch (node.type) {
854
+ case 'union_type':
855
+ case 'parenthesized_type':
856
+ node.namedChildren.forEach(visit);
857
+ return;
858
+ case 'literal_type':
859
+ if (node.text === 'null' || node.text === 'undefined') {
860
+ nullable = true;
861
+ }
862
+ return;
863
+ case 'predefined_type':
864
+ case 'type_identifier':
865
+ if (node.text === 'null' || node.text === 'undefined') {
866
+ nullable = true;
867
+ }
868
+ else if (name === undefined) {
869
+ name = node.text;
870
+ }
871
+ return;
872
+ case 'generic_type': {
873
+ // `CreationOptional<number>`, `Promise<User>`, `Array<string>`
874
+ const base = node.namedChildren[0]?.text ?? '';
875
+ const inner = node.namedChildren[1]?.namedChildren[0];
876
+ if (inner !== undefined && TRANSPARENT_TYPES.has(base)) {
877
+ visit(inner);
878
+ }
879
+ else if (name === undefined) {
880
+ name = base;
881
+ }
882
+ return;
883
+ }
884
+ default:
885
+ if (name === undefined) {
886
+ name = node.text;
887
+ }
888
+ }
889
+ };
890
+ if (typeNode !== undefined) {
891
+ visit(typeNode);
892
+ }
893
+ return { name, nullable };
894
+ }
895
+ function applyDecorators(attribute, decorators, model, ctx) {
896
+ for (const decorator of decorators) {
897
+ const first = decorator.args[0];
898
+ switch (decorator.name) {
899
+ case 'Column': {
900
+ if (first === undefined) {
901
+ break;
902
+ }
903
+ const resolved = resolveNode(first, ctx);
904
+ if (resolved.type === 'object') {
905
+ applyAttributeEntries(attribute, objectEntries(resolved, ctx).entries, ctx);
906
+ }
907
+ else {
908
+ attribute.type = evaluateNode(resolved);
909
+ }
910
+ break;
911
+ }
912
+ case 'PrimaryKey':
913
+ attribute.primaryKey = true;
914
+ break;
915
+ case 'AutoIncrement':
916
+ attribute.autoIncrement = true;
917
+ break;
918
+ case 'AllowNull':
919
+ attribute.allowNull = boolOf(first, ctx) ?? true;
920
+ break;
921
+ case 'NotNull':
922
+ attribute.allowNull = false;
923
+ break;
924
+ case 'Unique': {
925
+ const value = first === undefined ? undefined : valueOf(first, ctx);
926
+ if (value !== undefined && value.kind === 'bool' && !value.value) {
927
+ delete attribute.unique;
928
+ }
929
+ else if (value !== undefined && value.kind === 'string') {
930
+ attribute.unique = { name: value.value };
931
+ }
932
+ else if (value !== undefined && value.kind === 'object') {
933
+ const name = value.properties['name'];
934
+ attribute.unique =
935
+ name !== undefined && name.kind === 'string'
936
+ ? { name: name.value }
937
+ : {};
938
+ }
939
+ else {
940
+ attribute.unique = {};
941
+ }
942
+ break;
943
+ }
944
+ case 'Default':
945
+ if (first !== undefined) {
946
+ attribute.defaultValue = valueOf(first, ctx);
947
+ }
948
+ break;
949
+ case 'Comment':
950
+ attribute.comment = stringOf(first, ctx) ?? attribute.comment;
951
+ break;
952
+ case 'CreatedAt':
953
+ case 'UpdatedAt':
954
+ case 'DeletedAt': {
955
+ const kind = decorator.name === 'CreatedAt'
956
+ ? 'createdAt'
957
+ : decorator.name === 'UpdatedAt'
958
+ ? 'updatedAt'
959
+ : 'deletedAt';
960
+ attribute.timestamp = kind;
961
+ model.decoratorOptions[kind] = attribute.name;
962
+ break;
963
+ }
964
+ case 'ForeignKey': {
965
+ const target = first === undefined ? undefined : modelReference(first, ctx);
966
+ if (target !== undefined) {
967
+ model.foreignKeyHints.set(attribute.name, target);
968
+ }
969
+ break;
970
+ }
971
+ case 'Index': {
972
+ model.indexes.push(...indexFromDecorator(attribute.name, decorator, ctx));
973
+ break;
974
+ }
975
+ default:
976
+ break;
977
+ }
978
+ }
979
+ }
980
+ /** `@Index`, `@Index('name')` and `@Index({ name, unique, using })` on a property. */
981
+ function indexFromDecorator(attributeName, decorator, ctx) {
982
+ const first = decorator.args[0];
983
+ let name;
984
+ let unique = false;
985
+ let using;
986
+ let type;
987
+ if (first !== undefined) {
988
+ const resolved = resolveNode(first, ctx);
989
+ if (resolved.type === 'object') {
990
+ const { entries } = objectEntries(resolved, ctx);
991
+ name = stringOf(entries.get('name'), ctx);
992
+ unique = boolOf(entries.get('unique'), ctx) === true;
993
+ using = stringOf(entries.get('using'), ctx);
994
+ type = stringOf(entries.get('type'), ctx)?.toUpperCase();
995
+ }
996
+ else {
997
+ name = stringOf(resolved, ctx);
998
+ }
999
+ }
1000
+ return [
1001
+ {
1002
+ fields: [{ name: attributeName }],
1003
+ unique: unique || type === 'UNIQUE',
1004
+ ...(name === undefined ? {} : { name }),
1005
+ ...(using === undefined ? {} : { using }),
1006
+ ...(type === 'FULLTEXT' || type === 'SPATIAL' ? { type } : {}),
1007
+ isPartial: false,
1008
+ hasExpression: false,
1009
+ },
1010
+ ];
1011
+ }
1012
+ /** Merges indexes that several properties contributed under one name into a composite index. */
1013
+ export function mergeNamedIndexes(indexes) {
1014
+ const merged = [];
1015
+ const byName = new Map();
1016
+ for (const index of indexes) {
1017
+ const existing = index.name === undefined ? undefined : byName.get(index.name);
1018
+ if (existing === undefined) {
1019
+ const copy = { ...index, fields: [...index.fields] };
1020
+ merged.push(copy);
1021
+ if (index.name !== undefined) {
1022
+ byName.set(index.name, copy);
1023
+ }
1024
+ }
1025
+ else if (index.fields.length === 1 && existing.fields.length > 0) {
1026
+ const only = index.fields[0];
1027
+ if (only !== undefined) {
1028
+ existing.fields.push(only);
1029
+ }
1030
+ existing.unique = existing.unique || index.unique;
1031
+ }
1032
+ else {
1033
+ merged.push(index);
1034
+ }
1035
+ }
1036
+ return merged;
1037
+ }
1038
+ function collectDecoratedClass(classNode, outerDecorators, ctx) {
1039
+ const nameNode = classNode.childForFieldName('name');
1040
+ const bodyNode = classNode.childForFieldName('body');
1041
+ if (nameNode === null || bodyNode === null) {
1042
+ return;
1043
+ }
1044
+ const decorators = [
1045
+ ...outerDecorators,
1046
+ ...decoratorNodes(classNode),
1047
+ ];
1048
+ const table = decorators.find((decorator) => decorator.name === 'Table');
1049
+ if (table === undefined) {
1050
+ return;
1051
+ }
1052
+ const className = nameNode.text;
1053
+ const optionsNode = table.args[0];
1054
+ const optionEntries = optionsNode === undefined ? undefined : objectEntries(optionsNode, ctx);
1055
+ const modelName = stringOf(optionEntries?.entries.get('modelName'), ctx) ?? className;
1056
+ const model = newModel(ctx, 'decorators', modelName, [className]);
1057
+ model.optionEntries = optionEntries?.entries ?? new Map();
1058
+ for (const decorator of decorators) {
1059
+ const unsupported = UNSUPPORTED_CLASS_DECORATORS[decorator.name];
1060
+ if (unsupported !== undefined && !model.unsupported.includes(unsupported)) {
1061
+ model.unsupported.push(unsupported);
1062
+ }
1063
+ }
1064
+ // Properties carry their decorators; for methods (accessors, hooks) they are separate siblings before it.
1065
+ let pending = [];
1066
+ for (const member of bodyNode.namedChildren) {
1067
+ if (member.type === 'decorator') {
1068
+ const decorator = decoratorOf(member);
1069
+ if (decorator !== undefined) {
1070
+ pending.push(decorator);
1071
+ }
1072
+ continue;
1073
+ }
1074
+ const carried = pending;
1075
+ pending = [];
1076
+ if (member.type !== 'public_field_definition' &&
1077
+ member.type !== 'method_definition' &&
1078
+ member.type !== 'abstract_method_signature') {
1079
+ continue;
1080
+ }
1081
+ const memberName = member.childForFieldName('name');
1082
+ if (memberName === null) {
1083
+ continue;
1084
+ }
1085
+ const memberDecorators = [
1086
+ ...carried,
1087
+ ...decoratorNodes(member),
1088
+ ];
1089
+ if (memberDecorators.length === 0) {
1090
+ continue;
1091
+ }
1092
+ if (member.type === 'method_definition' &&
1093
+ memberDecorators.some((decorator) => HOOK_DECORATORS.has(decorator.name))) {
1094
+ if (!model.unsupported.includes('hooks')) {
1095
+ model.unsupported.push('hooks');
1096
+ }
1097
+ continue;
1098
+ }
1099
+ const association = memberDecorators.find((decorator) => ASSOCIATION_DECORATORS[decorator.name] !== undefined);
1100
+ if (association !== undefined) {
1101
+ collectDecoratedAssociation(association, memberName.text, model, className, ctx);
1102
+ continue;
1103
+ }
1104
+ if (!memberDecorators.some((decorator) => COLUMN_DECORATORS.has(decorator.name))) {
1105
+ continue;
1106
+ }
1107
+ const attribute = {
1108
+ name: memberName.text,
1109
+ origin: 'declared',
1110
+ };
1111
+ if (member.type === 'public_field_definition') {
1112
+ const tsType = describeTsType(member);
1113
+ if (tsType.name !== undefined) {
1114
+ attribute.tsType = tsType.name;
1115
+ }
1116
+ attribute.tsNullable = tsType.nullable;
1117
+ }
1118
+ applyDecorators(attribute, memberDecorators, model, ctx);
1119
+ // `@CreatedAt` alone only names the timestamp column; Sequelize creates it. With `@Column` it is declared.
1120
+ if (memberDecorators.some((decorator) => COLUMN_DECORATORS.has(decorator.name) &&
1121
+ !TIMESTAMP_DECORATORS.has(decorator.name))) {
1122
+ model.attributes.push(attribute);
1123
+ }
1124
+ }
1125
+ ctx.project.models.push(model);
1126
+ }
1127
+ function collectDecoratedAssociation(decorator, propertyName, model, className, ctx) {
1128
+ const kind = ASSOCIATION_DECORATORS[decorator.name];
1129
+ const targetNode = decorator.args[0];
1130
+ if (kind === undefined || targetNode === undefined) {
1131
+ return;
1132
+ }
1133
+ const target = modelReference(targetNode, ctx);
1134
+ if (target === undefined) {
1135
+ ctx.project.warnings.push(`${model.location}: @${decorator.name} on "${propertyName}" names its target in a way that cannot be resolved statically; the association was skipped.`);
1136
+ return;
1137
+ }
1138
+ const second = decorator.args[1];
1139
+ let options;
1140
+ if (kind === 'belongsToMany') {
1141
+ // @BelongsToMany(() => Tag, () => PostTag, 'postId', 'tagId') or (() => Tag, { through })
1142
+ const secondResolved = second === undefined ? undefined : resolveNode(second, ctx);
1143
+ if (secondResolved !== undefined && secondResolved.type === 'object') {
1144
+ options = readAssociationOptions(secondResolved, ctx);
1145
+ }
1146
+ else {
1147
+ options = { unsupported: [] };
1148
+ const through = describeThrough(second, ctx);
1149
+ if (through !== undefined) {
1150
+ options.through = through;
1151
+ }
1152
+ const foreignKey = readKey(decorator.args[2], ctx);
1153
+ const otherKey = readKey(decorator.args[3], ctx);
1154
+ if (foreignKey !== undefined) {
1155
+ options.foreignKey = foreignKey;
1156
+ }
1157
+ if (otherKey !== undefined) {
1158
+ options.otherKey = otherKey;
1159
+ }
1160
+ }
1161
+ }
1162
+ else {
1163
+ options = readAssociationOptions(second, ctx);
1164
+ }
1165
+ if (options.as === undefined) {
1166
+ options.as = propertyName;
1167
+ }
1168
+ ctx.project.associations.push({
1169
+ kind,
1170
+ file: ctx.file,
1171
+ source: className,
1172
+ enclosing: className,
1173
+ target,
1174
+ options,
1175
+ propertyName,
1176
+ owner: model,
1177
+ label: `${className}.${propertyName} (@${decorator.name})`,
1178
+ });
1179
+ }
1180
+ // ---------------------------------------------------------------------------
1181
+ // Entry point of the walk
1182
+ // ---------------------------------------------------------------------------
1183
+ function isSequelizeConstructor(node) {
1184
+ const constructor = node.childForFieldName('constructor');
1185
+ return constructor !== null && /(^|\.)Sequelize$/.test(constructor.text);
1186
+ }
1187
+ function readGlobalDefaults(node, ctx) {
1188
+ const args = argsOf(node);
1189
+ for (const arg of args) {
1190
+ const resolved = resolveNode(arg, ctx);
1191
+ if (resolved.type !== 'object') {
1192
+ continue;
1193
+ }
1194
+ const define = objectEntries(resolved, ctx).entries.get('define');
1195
+ if (define === undefined) {
1196
+ continue;
1197
+ }
1198
+ const entries = objectEntries(define, ctx).entries;
1199
+ const defaults = {};
1200
+ for (const key of [
1201
+ 'freezeTableName',
1202
+ 'timestamps',
1203
+ 'paranoid',
1204
+ 'underscored',
1205
+ ]) {
1206
+ const value = boolOf(entries.get(key), ctx);
1207
+ if (value !== undefined) {
1208
+ defaults[key] = value;
1209
+ }
1210
+ }
1211
+ for (const key of ['createdAt', 'updatedAt', 'deletedAt']) {
1212
+ const entry = entries.get(key);
1213
+ const value = entry === undefined ? undefined : valueOf(entry, ctx);
1214
+ if (value !== undefined && value.kind === 'string') {
1215
+ defaults[key] = value.value;
1216
+ }
1217
+ else if (value !== undefined && value.kind === 'bool' && !value.value) {
1218
+ defaults[key] = false;
1219
+ }
1220
+ }
1221
+ const schema = stringOf(entries.get('schema'), ctx);
1222
+ if (schema !== undefined) {
1223
+ defaults.schema = schema;
1224
+ }
1225
+ if (Object.keys(ctx.project.defaults).length === 0) {
1226
+ Object.assign(ctx.project.defaults, defaults);
1227
+ }
1228
+ }
1229
+ }
1230
+ /** Walks one file and records its models, association calls and the shared `define` defaults. */
1231
+ export function collectFile(file, project) {
1232
+ const ctx = { file, project };
1233
+ const stack = [file.root];
1234
+ while (stack.length > 0) {
1235
+ const node = stack.pop();
1236
+ if (node === undefined) {
1237
+ break;
1238
+ }
1239
+ const children = node.namedChildren;
1240
+ for (let index = children.length - 1; index >= 0; index -= 1) {
1241
+ const child = children[index];
1242
+ if (child !== undefined) {
1243
+ stack.push(child);
1244
+ }
1245
+ }
1246
+ switch (node.type) {
1247
+ case 'call_expression': {
1248
+ const callee = node.childForFieldName('function');
1249
+ if (callee === null || callee.type !== 'member_expression') {
1250
+ break;
1251
+ }
1252
+ const property = callee.childForFieldName('property');
1253
+ const receiver = callee.childForFieldName('object');
1254
+ if (property === null || receiver === null) {
1255
+ break;
1256
+ }
1257
+ if (property.text === 'define') {
1258
+ collectDefine(node, ctx);
1259
+ }
1260
+ else if (property.text === 'init') {
1261
+ collectInit(node, ctx);
1262
+ }
1263
+ else if (ASSOCIATION_METHODS.has(property.text)) {
1264
+ collectAssociationCall(node, property.text, receiver, ctx);
1265
+ }
1266
+ break;
1267
+ }
1268
+ case 'new_expression':
1269
+ if (isSequelizeConstructor(node)) {
1270
+ readGlobalDefaults(node, ctx);
1271
+ }
1272
+ break;
1273
+ case 'export_statement': {
1274
+ // `@Table(...) export class X` keeps the decorators on the export statement.
1275
+ const decorators = decoratorNodes(node);
1276
+ const classNode = node.namedChildren.find((child) => child.type === 'class_declaration' ||
1277
+ child.type === 'abstract_class_declaration' ||
1278
+ child.type === 'class');
1279
+ if (classNode !== undefined && decorators.length > 0) {
1280
+ collectDecoratedClass(classNode, decorators, ctx);
1281
+ }
1282
+ break;
1283
+ }
1284
+ case 'class_declaration':
1285
+ case 'abstract_class_declaration':
1286
+ collectDecoratedClass(node, [], ctx);
1287
+ break;
1288
+ default:
1289
+ break;
1290
+ }
1291
+ }
1292
+ for (const model of project.models) {
1293
+ if (model.file === file && model.optionEntries.size > 0) {
1294
+ noteUnsupportedOptions(model);
1295
+ }
1296
+ }
1297
+ }
1298
+ //# sourceMappingURL=sequelizeCollect.js.map