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.
- package/README.md +1 -0
- package/dist/cliRunner.js +7 -5
- package/dist/cliRunner.js.map +1 -1
- package/dist/config.d.ts +6 -3
- package/dist/config.js +3 -3
- package/dist/config.js.map +1 -1
- package/dist/emitters/sequelize.d.ts +35 -0
- package/dist/emitters/sequelize.js +1276 -0
- package/dist/emitters/sequelize.js.map +1 -0
- package/dist/formats.d.ts +6 -5
- package/dist/formats.js +38 -4
- package/dist/formats.js.map +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/io.js +29 -1
- package/dist/io.js.map +1 -1
- package/dist/parsers/sequelize.d.ts +15 -0
- package/dist/parsers/sequelize.js +906 -0
- package/dist/parsers/sequelize.js.map +1 -0
- package/dist/parsers/sequelizeAssociations.d.ts +45 -0
- package/dist/parsers/sequelizeAssociations.js +449 -0
- package/dist/parsers/sequelizeAssociations.js.map +1 -0
- package/dist/parsers/sequelizeCollect.d.ts +33 -0
- package/dist/parsers/sequelizeCollect.js +1298 -0
- package/dist/parsers/sequelizeCollect.js.map +1 -0
- package/dist/parsers/sequelizeInflection.d.ts +14 -0
- package/dist/parsers/sequelizeInflection.js +451 -0
- package/dist/parsers/sequelizeInflection.js.map +1 -0
- package/dist/parsers/sequelizeModel.d.ts +185 -0
- package/dist/parsers/sequelizeModel.js +2 -0
- package/dist/parsers/sequelizeModel.js.map +1 -0
- package/dist/parsers/sequelizeTypes.d.ts +24 -0
- package/dist/parsers/sequelizeTypes.js +248 -0
- package/dist/parsers/sequelizeTypes.js.map +1 -0
- package/dist/styles.d.ts +17 -0
- package/dist/styles.js +40 -0
- package/dist/styles.js.map +1 -0
- package/package.json +2 -1
|
@@ -0,0 +1,1276 @@
|
|
|
1
|
+
import { findModel } from '../ir.js';
|
|
2
|
+
import { toCamelCase, toPascalCase, toSnakeCase } from '../naming.js';
|
|
3
|
+
import { underscore } from '../parsers/sequelizeInflection.js';
|
|
4
|
+
import { prismaOnlyWarnings } from '../prismaOnlyConstructs.js';
|
|
5
|
+
import { normalizeSchema } from '../transforms.js';
|
|
6
|
+
export const SEQUELIZE_STYLES = ['init', 'define'];
|
|
7
|
+
/** True when a value is one of the Sequelize `--style` values. */
|
|
8
|
+
export function isSequelizeStyle(value) {
|
|
9
|
+
return SEQUELIZE_STYLES.includes(value);
|
|
10
|
+
}
|
|
11
|
+
const INDENT = ' ';
|
|
12
|
+
const MAX_LINE_WIDTH = 80;
|
|
13
|
+
const IDENTIFIER_PATTERN = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
14
|
+
const CREATED_FIELD_PATTERN = /^(created_?at|created|date_?created|createdon)$/i;
|
|
15
|
+
/** Names a generated model file declares or imports, which a class cannot take. */
|
|
16
|
+
const RESERVED_CLASS_NAMES = new Set([
|
|
17
|
+
'Model',
|
|
18
|
+
'Sequelize',
|
|
19
|
+
'DataTypes',
|
|
20
|
+
'CreationOptional',
|
|
21
|
+
'InferAttributes',
|
|
22
|
+
'InferCreationAttributes',
|
|
23
|
+
'NonAttribute',
|
|
24
|
+
'Buffer',
|
|
25
|
+
'Date',
|
|
26
|
+
'Object',
|
|
27
|
+
'Array',
|
|
28
|
+
'Record',
|
|
29
|
+
'String',
|
|
30
|
+
'Number',
|
|
31
|
+
'Boolean',
|
|
32
|
+
'Models',
|
|
33
|
+
]);
|
|
34
|
+
/** Words an attribute cannot be called on a Sequelize instance without shadowing a member of `Model`. */
|
|
35
|
+
const RESERVED_ATTRIBUTES = new Set([
|
|
36
|
+
'constructor',
|
|
37
|
+
'prototype',
|
|
38
|
+
'sequelize',
|
|
39
|
+
'dataValues',
|
|
40
|
+
'isNewRecord',
|
|
41
|
+
]);
|
|
42
|
+
const ON_ACTIONS = {
|
|
43
|
+
cascade: 'CASCADE',
|
|
44
|
+
setNull: 'SET NULL',
|
|
45
|
+
restrict: 'RESTRICT',
|
|
46
|
+
noAction: 'NO ACTION',
|
|
47
|
+
setDefault: 'SET DEFAULT',
|
|
48
|
+
};
|
|
49
|
+
/** The index method the IR spells like Prisma, as Sequelize's `using` writes it. */
|
|
50
|
+
const INDEX_METHODS = {
|
|
51
|
+
btree: 'BTREE',
|
|
52
|
+
hash: 'HASH',
|
|
53
|
+
gin: 'GIN',
|
|
54
|
+
gist: 'GIST',
|
|
55
|
+
spgist: 'SPGIST',
|
|
56
|
+
brin: 'BRIN',
|
|
57
|
+
};
|
|
58
|
+
/** Types Sequelize can name for a column the IR calls `unsupported`, by the database type it read. */
|
|
59
|
+
const POSTGRES_ONLY_TYPES = {
|
|
60
|
+
geometry: 'GEOMETRY',
|
|
61
|
+
geography: 'GEOGRAPHY',
|
|
62
|
+
cidr: 'CIDR',
|
|
63
|
+
macaddr: 'MACADDR',
|
|
64
|
+
tsvector: 'TSVECTOR',
|
|
65
|
+
citext: 'CITEXT',
|
|
66
|
+
};
|
|
67
|
+
const RANGE_SUBTYPES = {
|
|
68
|
+
int: 'INTEGER',
|
|
69
|
+
bigInt: 'BIGINT',
|
|
70
|
+
decimal: 'DECIMAL',
|
|
71
|
+
date: 'DATEONLY',
|
|
72
|
+
dateTime: 'DATE',
|
|
73
|
+
};
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// Entry point
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
export function emitSequelize(schema, options) {
|
|
78
|
+
const prepared = options.naming === 'normalize' ? normalizeSchema(schema) : schema;
|
|
79
|
+
const style = options.style ?? 'init';
|
|
80
|
+
const context = {
|
|
81
|
+
schema: prepared,
|
|
82
|
+
options,
|
|
83
|
+
style,
|
|
84
|
+
dialect: dialectOf(options.provider),
|
|
85
|
+
warnings: prismaOnlyWarnings(prepared, true).filter(isUnhandledWarning),
|
|
86
|
+
plans: new Map(),
|
|
87
|
+
inverseAliases: new Map(),
|
|
88
|
+
usesLiteral: false,
|
|
89
|
+
};
|
|
90
|
+
if (options.provider === 'mongodb') {
|
|
91
|
+
context.warnings.push('Provider "mongodb": Sequelize is relational (it has no MongoDB dialect; use Mongoose); PostgreSQL column types were written.');
|
|
92
|
+
}
|
|
93
|
+
const files = {};
|
|
94
|
+
if (prepared.models.length === 0) {
|
|
95
|
+
return { files, warnings: context.warnings };
|
|
96
|
+
}
|
|
97
|
+
planModels(context);
|
|
98
|
+
planInverseAliases(context);
|
|
99
|
+
const associations = {
|
|
100
|
+
byOwner: new Map(),
|
|
101
|
+
ordered: [],
|
|
102
|
+
};
|
|
103
|
+
const extension = style === 'init' ? 'ts' : 'js';
|
|
104
|
+
const rendered = new Map();
|
|
105
|
+
for (const model of prepared.models) {
|
|
106
|
+
context.usesLiteral = false;
|
|
107
|
+
rendered.set(model.name, renderModel(context, planOf(context, model), associations));
|
|
108
|
+
}
|
|
109
|
+
for (const model of prepared.models) {
|
|
110
|
+
const plan = planOf(context, model);
|
|
111
|
+
const result = rendered.get(model.name);
|
|
112
|
+
if (result === undefined) {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
files[`${plan.fileBase}.${extension}`] =
|
|
116
|
+
style === 'init'
|
|
117
|
+
? writeInitFile(context, plan, result, associations)
|
|
118
|
+
: writeDefineFile(plan, result, associations);
|
|
119
|
+
}
|
|
120
|
+
files[`index.${extension}`] =
|
|
121
|
+
style === 'init'
|
|
122
|
+
? writeInitIndex(context, associations)
|
|
123
|
+
: writeDefineIndex(context);
|
|
124
|
+
return { files, warnings: context.warnings };
|
|
125
|
+
}
|
|
126
|
+
/** Drops the Prisma-only notes for things this emitter writes (schemas, defaults, actions, index details). */
|
|
127
|
+
function isUnhandledWarning(warning) {
|
|
128
|
+
if (warning.includes('the onUpdate action')) {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
if (warning.includes('the database default expression')) {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
if (warning.includes('(Prisma Unsupported)')) {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
if (warning.includes('is generated by Prisma Client and was dropped')) {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
if (warning.includes('full-text indexes are not supported here')) {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
if (warning.includes('the index type, clustering')) {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
return !(warning.includes('(@@schema) was ignored') && !warning.startsWith('enum '));
|
|
147
|
+
}
|
|
148
|
+
function dialectOf(provider) {
|
|
149
|
+
switch (provider) {
|
|
150
|
+
case 'mysql':
|
|
151
|
+
return 'mysql';
|
|
152
|
+
case 'sqlite':
|
|
153
|
+
return 'sqlite';
|
|
154
|
+
case 'sqlserver':
|
|
155
|
+
return 'mssql';
|
|
156
|
+
default:
|
|
157
|
+
return 'postgres';
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// ---------------------------------------------------------------------------
|
|
161
|
+
// Small helpers
|
|
162
|
+
// ---------------------------------------------------------------------------
|
|
163
|
+
function quote(value) {
|
|
164
|
+
const escaped = value
|
|
165
|
+
.replace(/\\/g, '\\\\')
|
|
166
|
+
.replace(/\n/g, '\\n')
|
|
167
|
+
.replace(/\r/g, '\\r');
|
|
168
|
+
if (value.includes("'") && !value.includes('"')) {
|
|
169
|
+
return `"${escaped}"`;
|
|
170
|
+
}
|
|
171
|
+
return `'${escaped.replace(/'/g, "\\'")}'`;
|
|
172
|
+
}
|
|
173
|
+
function uniqueName(baseName, used) {
|
|
174
|
+
let candidate = baseName;
|
|
175
|
+
let suffix = 2;
|
|
176
|
+
while (used.has(candidate)) {
|
|
177
|
+
candidate = `${baseName}${suffix}`;
|
|
178
|
+
suffix += 1;
|
|
179
|
+
}
|
|
180
|
+
used.add(candidate);
|
|
181
|
+
return candidate;
|
|
182
|
+
}
|
|
183
|
+
function identifier(context, owner, raw) {
|
|
184
|
+
if (IDENTIFIER_PATTERN.test(raw)) {
|
|
185
|
+
return raw;
|
|
186
|
+
}
|
|
187
|
+
const replaced = raw.replace(/[^A-Za-z0-9_$]/g, '_');
|
|
188
|
+
const safe = /^[0-9]/.test(replaced) ? `_${replaced}` : replaced;
|
|
189
|
+
const result = safe === '' ? '_' : safe;
|
|
190
|
+
context.warnings.push(`${owner}: "${raw}" is not a valid JavaScript identifier; it was written as "${result}".`);
|
|
191
|
+
return result;
|
|
192
|
+
}
|
|
193
|
+
function compact(entries) {
|
|
194
|
+
return entries.filter((entry) => entry !== undefined);
|
|
195
|
+
}
|
|
196
|
+
/** `{ a, b }` on one line when it fits after `prefix`, otherwise one entry per line. */
|
|
197
|
+
function objectLines(prefix, entries, indent, suffix) {
|
|
198
|
+
const inline = `${indent}${prefix}{ ${entries.join(', ')} }${suffix}`;
|
|
199
|
+
if (entries.length === 0) {
|
|
200
|
+
return [`${indent}${prefix}{}${suffix}`];
|
|
201
|
+
}
|
|
202
|
+
if (inline.length <= MAX_LINE_WIDTH) {
|
|
203
|
+
return [inline];
|
|
204
|
+
}
|
|
205
|
+
return [
|
|
206
|
+
`${indent}${prefix}{`,
|
|
207
|
+
...entries.map((entry) => `${indent}${INDENT}${entry},`),
|
|
208
|
+
`${indent}}${suffix}`,
|
|
209
|
+
];
|
|
210
|
+
}
|
|
211
|
+
function isIntegerKey(field) {
|
|
212
|
+
return field.type === 'int' || field.type === 'bigInt';
|
|
213
|
+
}
|
|
214
|
+
function planOf(context, model) {
|
|
215
|
+
const plan = context.plans.get(model.name);
|
|
216
|
+
if (plan === undefined) {
|
|
217
|
+
throw new Error(`No plan exists for the model "${model.name}".`);
|
|
218
|
+
}
|
|
219
|
+
return plan;
|
|
220
|
+
}
|
|
221
|
+
function enumOf(context, field) {
|
|
222
|
+
return field.enumName === undefined
|
|
223
|
+
? undefined
|
|
224
|
+
: context.schema.enums.find((candidate) => candidate.name === field.enumName);
|
|
225
|
+
}
|
|
226
|
+
function enumMember(enumeration, raw) {
|
|
227
|
+
return (enumeration.values.find((value) => value.name === raw) ??
|
|
228
|
+
enumeration.values.find((value) => value.dbValue === raw));
|
|
229
|
+
}
|
|
230
|
+
// ---------------------------------------------------------------------------
|
|
231
|
+
// Planning: names, timestamps, key-like columns
|
|
232
|
+
// ---------------------------------------------------------------------------
|
|
233
|
+
function keyLikeFields(context, model) {
|
|
234
|
+
const keyed = new Set();
|
|
235
|
+
for (const field of model.fields) {
|
|
236
|
+
if (field.isPrimaryKey || field.isUnique) {
|
|
237
|
+
keyed.add(field.name);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
for (const name of model.compositePrimaryKey ?? []) {
|
|
241
|
+
keyed.add(name);
|
|
242
|
+
}
|
|
243
|
+
for (const index of model.indexes) {
|
|
244
|
+
for (const name of index.fields) {
|
|
245
|
+
keyed.add(name);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
for (const other of context.schema.models) {
|
|
249
|
+
for (const relation of other.relations) {
|
|
250
|
+
if (relation.targetModel === model.name && relation.toField) {
|
|
251
|
+
keyed.add(relation.toField);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return keyed;
|
|
256
|
+
}
|
|
257
|
+
function attributeName(context, model, rawName) {
|
|
258
|
+
const styled = context.options.naming === 'normalize' ? toCamelCase(rawName) : rawName;
|
|
259
|
+
const safe = identifier(context, `${model.name}.${rawName}`, styled === '' ? rawName : styled);
|
|
260
|
+
return RESERVED_ATTRIBUTES.has(safe) ? `${safe}_` : safe;
|
|
261
|
+
}
|
|
262
|
+
/** `underscored` is the setting under which the most attribute names already map onto their column names. */
|
|
263
|
+
function chooseUnderscored(context, model, attrs) {
|
|
264
|
+
if (context.options.naming === 'normalize') {
|
|
265
|
+
return true;
|
|
266
|
+
}
|
|
267
|
+
let underscored = 0;
|
|
268
|
+
let plain = 0;
|
|
269
|
+
for (const field of model.fields) {
|
|
270
|
+
const attr = attrs.get(field.name) ?? field.name;
|
|
271
|
+
const snake = underscore(attr) === field.columnName;
|
|
272
|
+
const same = attr === field.columnName;
|
|
273
|
+
if (snake && !same) {
|
|
274
|
+
underscored += 1;
|
|
275
|
+
}
|
|
276
|
+
else if (same && !snake) {
|
|
277
|
+
plain += 1;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return underscored > plain;
|
|
281
|
+
}
|
|
282
|
+
function planModels(context) {
|
|
283
|
+
const fileBases = new Set(['index']);
|
|
284
|
+
const classNames = new Set(RESERVED_CLASS_NAMES);
|
|
285
|
+
for (const model of context.schema.models) {
|
|
286
|
+
// Sequelize model names are PascalCase; `legacy_order` is written as `LegacyOrder`.
|
|
287
|
+
const pascal = /^[A-Z][A-Za-z0-9]*$/.test(model.name)
|
|
288
|
+
? model.name
|
|
289
|
+
: toPascalCase(model.name);
|
|
290
|
+
const rawClass = identifier(context, model.name, pascal === '' ? model.name : pascal);
|
|
291
|
+
const className = uniqueName(RESERVED_CLASS_NAMES.has(rawClass) ? `${rawClass}Model` : rawClass, classNames);
|
|
292
|
+
if (RESERVED_CLASS_NAMES.has(rawClass)) {
|
|
293
|
+
context.warnings.push(`${model.name}: "${rawClass}" is a name the generated files use, so the class and the model were named "${className}".`);
|
|
294
|
+
}
|
|
295
|
+
const base = toCamelCase(className) || 'model';
|
|
296
|
+
const used = new Set();
|
|
297
|
+
const attrs = new Map();
|
|
298
|
+
for (const field of model.fields) {
|
|
299
|
+
attrs.set(field.name, uniqueName(attributeName(context, model, field.name), used));
|
|
300
|
+
}
|
|
301
|
+
const keys = new Map();
|
|
302
|
+
const aliases = new Map();
|
|
303
|
+
for (const relation of model.relations) {
|
|
304
|
+
if (relation.kind === 'manyToMany') {
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
keys.set(relation.name, uniqueName(attributeName(context, model, relation.columnName), used));
|
|
308
|
+
}
|
|
309
|
+
for (const relation of model.relations) {
|
|
310
|
+
aliases.set(relation.name, uniqueName(attributeName(context, model, relation.name), used));
|
|
311
|
+
}
|
|
312
|
+
const plan = {
|
|
313
|
+
model,
|
|
314
|
+
className,
|
|
315
|
+
fileBase: uniqueName(base, fileBases),
|
|
316
|
+
underscored: chooseUnderscored(context, model, attrs),
|
|
317
|
+
attrs,
|
|
318
|
+
keys,
|
|
319
|
+
aliases,
|
|
320
|
+
used,
|
|
321
|
+
timestamps: {},
|
|
322
|
+
keyLike: keyLikeFields(context, model),
|
|
323
|
+
};
|
|
324
|
+
plan.timestamps = planTimestamps(plan);
|
|
325
|
+
context.plans.set(model.name, plan);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
/** The column Sequelize derives for an attribute. */
|
|
329
|
+
function derivedColumn(plan, attribute) {
|
|
330
|
+
return plan.underscored ? underscore(attribute) : attribute;
|
|
331
|
+
}
|
|
332
|
+
/** Finds the fields that Sequelize's `createdAt` and `updatedAt` options can stand for. */
|
|
333
|
+
function planTimestamps(plan) {
|
|
334
|
+
const model = plan.model;
|
|
335
|
+
const composite = new Set(model.compositePrimaryKey ?? []);
|
|
336
|
+
const eligible = (field) => field.type === 'dateTime' &&
|
|
337
|
+
!field.isPrimaryKey &&
|
|
338
|
+
!composite.has(field.name) &&
|
|
339
|
+
field.arrayDepth === undefined &&
|
|
340
|
+
field.generated === undefined;
|
|
341
|
+
const created = model.fields.find((field) => eligible(field) &&
|
|
342
|
+
!field.isAutoUpdated &&
|
|
343
|
+
CREATED_FIELD_PATTERN.test(field.name));
|
|
344
|
+
const updated = model.fields.find((field) => eligible(field) && field.isAutoUpdated);
|
|
345
|
+
return {
|
|
346
|
+
...(created === undefined ? {} : { created }),
|
|
347
|
+
...(updated === undefined ? {} : { updated }),
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* True when Sequelize can add the column itself: the attribute is not written because `timestamps`
|
|
352
|
+
* creates exactly that column. A nullable column, one with another default or a unique flag, or one whose
|
|
353
|
+
* name `underscored` does not derive is declared next to the option instead.
|
|
354
|
+
*/
|
|
355
|
+
function isImplicitTimestamp(plan, field, kind) {
|
|
356
|
+
const attr = plan.attrs.get(field.name) ?? field.name;
|
|
357
|
+
const defaultOk = field.default === undefined ||
|
|
358
|
+
(kind === 'created' && field.default.kind === 'now');
|
|
359
|
+
return (defaultOk &&
|
|
360
|
+
!field.isNullable &&
|
|
361
|
+
!field.isUnique &&
|
|
362
|
+
field.enumName === undefined &&
|
|
363
|
+
derivedColumn(plan, attr) === field.columnName);
|
|
364
|
+
}
|
|
365
|
+
function planInverseAliases(context) {
|
|
366
|
+
for (const model of context.schema.models) {
|
|
367
|
+
for (const relation of model.relations) {
|
|
368
|
+
if (relation.relatedName === undefined) {
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
const target = findModel(context.schema, relation.targetModel);
|
|
372
|
+
if (target === undefined) {
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
const targetPlan = planOf(context, target);
|
|
376
|
+
const safe = identifier(context, `${model.name}.${relation.name}`, context.options.naming === 'normalize'
|
|
377
|
+
? toCamelCase(relation.relatedName)
|
|
378
|
+
: relation.relatedName);
|
|
379
|
+
context.inverseAliases.set(`${model.name}.${relation.name}`, uniqueName(safe, targetPlan.used));
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
// ---------------------------------------------------------------------------
|
|
384
|
+
// Column types
|
|
385
|
+
// ---------------------------------------------------------------------------
|
|
386
|
+
function typeSpecOf(context, label, field, keyLike) {
|
|
387
|
+
const depth = field.arrayDepth ?? 0;
|
|
388
|
+
if (depth === 0) {
|
|
389
|
+
return scalarTypeSpec(context, label, field, keyLike);
|
|
390
|
+
}
|
|
391
|
+
const element = scalarTypeSpec(context, label, { ...field, arrayDepth: undefined }, keyLike);
|
|
392
|
+
const ts = `${element.ts.includes(' ') ? `(${element.ts})` : element.ts}${'[]'.repeat(depth)}`;
|
|
393
|
+
if (context.dialect === 'postgres') {
|
|
394
|
+
let expression = element.expression;
|
|
395
|
+
for (let level = 0; level < depth; level += 1) {
|
|
396
|
+
expression = `DataTypes.ARRAY(${expression})`;
|
|
397
|
+
}
|
|
398
|
+
return { expression, ts };
|
|
399
|
+
}
|
|
400
|
+
const jsonType = jsonTypeOf({ ...context, warnings: [] }, label, 'array');
|
|
401
|
+
context.warnings.push(`${label}: ${dialectName(context.dialect)} has no array column type; the array was written as ${jsonType.startsWith('DataTypes.JSON') ? 'JSON' : 'TEXT'}.`);
|
|
402
|
+
return { expression: jsonType, ts };
|
|
403
|
+
}
|
|
404
|
+
function dialectName(dialect) {
|
|
405
|
+
switch (dialect) {
|
|
406
|
+
case 'mysql':
|
|
407
|
+
return 'MySQL';
|
|
408
|
+
case 'sqlite':
|
|
409
|
+
return 'SQLite';
|
|
410
|
+
case 'mssql':
|
|
411
|
+
return 'SQL Server';
|
|
412
|
+
default:
|
|
413
|
+
return 'PostgreSQL';
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
/** JSON where the dialect has it; Sequelize's SQL Server dialect has none, so text holds the document. */
|
|
417
|
+
function jsonTypeOf(context, label, what) {
|
|
418
|
+
switch (context.dialect) {
|
|
419
|
+
case 'postgres':
|
|
420
|
+
return 'DataTypes.JSONB';
|
|
421
|
+
case 'mssql':
|
|
422
|
+
context.warnings.push(`${label}: SQL Server has no JSON column type before SQL Server 2025 (Sequelize would create a column of type JSON); the ${what} was written as TEXT.`);
|
|
423
|
+
return 'DataTypes.TEXT';
|
|
424
|
+
default:
|
|
425
|
+
return 'DataTypes.JSON';
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
function scalarTypeSpec(context, label, field, keyLike) {
|
|
429
|
+
const dialect = context.dialect;
|
|
430
|
+
if (field.enumName !== undefined) {
|
|
431
|
+
const enumeration = enumOf(context, field);
|
|
432
|
+
if (enumeration === undefined) {
|
|
433
|
+
context.warnings.push(`${label}: enum "${field.enumName}" does not exist in the schema; the column was written as a plain string.`);
|
|
434
|
+
}
|
|
435
|
+
else if (dialect === 'mssql') {
|
|
436
|
+
// Sequelize's SQL Server ENUM is a VARCHAR with a CHECK constraint that leaves out NOT NULL and the default.
|
|
437
|
+
context.warnings.push(`${label}: Sequelize's ENUM for SQL Server drops NOT NULL and the default; the column was written as STRING and the values are only used for typing.`);
|
|
438
|
+
return {
|
|
439
|
+
expression: 'DataTypes.STRING',
|
|
440
|
+
ts: enumUnion(enumeration),
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
else {
|
|
444
|
+
return {
|
|
445
|
+
expression: `DataTypes.ENUM(${enumeration.values
|
|
446
|
+
.map((value) => quote(value.dbValue))
|
|
447
|
+
.join(', ')})`,
|
|
448
|
+
ts: enumUnion(enumeration),
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
switch (field.type) {
|
|
453
|
+
case 'string':
|
|
454
|
+
if (field.maxLength !== undefined) {
|
|
455
|
+
return {
|
|
456
|
+
expression: `DataTypes.STRING(${field.maxLength})`,
|
|
457
|
+
ts: 'string',
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
// Sequelize has no unbounded varchar: TEXT keeps long values, but cannot be a key in MySQL.
|
|
461
|
+
return {
|
|
462
|
+
expression: keyLike ? 'DataTypes.STRING' : 'DataTypes.TEXT',
|
|
463
|
+
ts: 'string',
|
|
464
|
+
};
|
|
465
|
+
case 'text':
|
|
466
|
+
return { expression: 'DataTypes.TEXT', ts: 'string' };
|
|
467
|
+
case 'uuid':
|
|
468
|
+
return { expression: 'DataTypes.UUID', ts: 'string' };
|
|
469
|
+
case 'int':
|
|
470
|
+
return { expression: 'DataTypes.INTEGER', ts: 'number' };
|
|
471
|
+
case 'bigInt':
|
|
472
|
+
return { expression: 'DataTypes.BIGINT', ts: 'string' };
|
|
473
|
+
case 'float':
|
|
474
|
+
return { expression: 'DataTypes.DOUBLE', ts: 'number' };
|
|
475
|
+
case 'decimal': {
|
|
476
|
+
const precision = field.maxDigits;
|
|
477
|
+
const scale = field.decimalPlaces;
|
|
478
|
+
const args = precision === undefined
|
|
479
|
+
? ''
|
|
480
|
+
: scale === undefined
|
|
481
|
+
? `(${precision})`
|
|
482
|
+
: `(${precision}, ${scale})`;
|
|
483
|
+
return { expression: `DataTypes.DECIMAL${args}`, ts: 'string' };
|
|
484
|
+
}
|
|
485
|
+
case 'boolean':
|
|
486
|
+
return { expression: 'DataTypes.BOOLEAN', ts: 'boolean' };
|
|
487
|
+
case 'dateTime':
|
|
488
|
+
return { expression: 'DataTypes.DATE', ts: 'Date' };
|
|
489
|
+
case 'date':
|
|
490
|
+
return { expression: 'DataTypes.DATEONLY', ts: 'string' };
|
|
491
|
+
case 'time':
|
|
492
|
+
return { expression: 'DataTypes.TIME', ts: 'string' };
|
|
493
|
+
case 'json':
|
|
494
|
+
return {
|
|
495
|
+
expression: jsonTypeOf(context, label, 'column'),
|
|
496
|
+
ts: 'unknown',
|
|
497
|
+
};
|
|
498
|
+
case 'bytes':
|
|
499
|
+
return { expression: 'DataTypes.BLOB', ts: 'Buffer' };
|
|
500
|
+
case 'duration':
|
|
501
|
+
context.warnings.push(`${label}: Sequelize has no interval type; the duration was written as BIGINT (microseconds).`);
|
|
502
|
+
return { expression: 'DataTypes.BIGINT', ts: 'string' };
|
|
503
|
+
case 'ipAddress':
|
|
504
|
+
return dialect === 'postgres'
|
|
505
|
+
? { expression: 'DataTypes.INET', ts: 'string' }
|
|
506
|
+
: { expression: 'DataTypes.STRING(45)', ts: 'string' };
|
|
507
|
+
case 'hstore':
|
|
508
|
+
if (dialect === 'postgres') {
|
|
509
|
+
return {
|
|
510
|
+
expression: 'DataTypes.HSTORE',
|
|
511
|
+
ts: 'Record<string, string | null>',
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
context.warnings.push(`${label}: hstore exists only on PostgreSQL; the field was written as ${dialect === 'mssql' ? 'TEXT' : 'JSON'}.`);
|
|
515
|
+
return {
|
|
516
|
+
expression: jsonTypeOf({ ...context, warnings: [] }, label, 'field'),
|
|
517
|
+
ts: 'Record<string, string | null>',
|
|
518
|
+
};
|
|
519
|
+
case 'range': {
|
|
520
|
+
if (dialect === 'postgres') {
|
|
521
|
+
const subtype = RANGE_SUBTYPES[field.rangeOf ?? 'int'] ?? 'INTEGER';
|
|
522
|
+
return {
|
|
523
|
+
expression: `DataTypes.RANGE(DataTypes.${subtype})`,
|
|
524
|
+
ts: 'unknown',
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
context.warnings.push(`${label}: range columns exist only on PostgreSQL; the field was written as TEXT.`);
|
|
528
|
+
return { expression: 'DataTypes.TEXT', ts: 'string' };
|
|
529
|
+
}
|
|
530
|
+
case 'unsupported': {
|
|
531
|
+
const raw = (field.unsupportedType ?? '').toLowerCase();
|
|
532
|
+
const known = POSTGRES_ONLY_TYPES[raw];
|
|
533
|
+
if (known !== undefined && dialect === 'postgres') {
|
|
534
|
+
return { expression: `DataTypes.${known}`, ts: 'unknown' };
|
|
535
|
+
}
|
|
536
|
+
context.warnings.push(`${label}: the database type "${field.unsupportedType ?? 'unknown'}" has no Sequelize data type${known === undefined ? '' : ` on ${dialectName(dialect)}`}; the column was written as TEXT.`);
|
|
537
|
+
return { expression: 'DataTypes.TEXT', ts: 'string' };
|
|
538
|
+
}
|
|
539
|
+
default:
|
|
540
|
+
context.warnings.push(`${label}: unknown field type "${String(field.type)}"; it was written as TEXT.`);
|
|
541
|
+
return { expression: 'DataTypes.TEXT', ts: 'string' };
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
function enumUnion(enumeration) {
|
|
545
|
+
return enumeration.values
|
|
546
|
+
.map((value) => quote(value.dbValue))
|
|
547
|
+
.join(' | ');
|
|
548
|
+
}
|
|
549
|
+
// ---------------------------------------------------------------------------
|
|
550
|
+
// Defaults
|
|
551
|
+
// ---------------------------------------------------------------------------
|
|
552
|
+
/** The value of `defaultValue` for a field, or undefined when it has none or Sequelize cannot express it. */
|
|
553
|
+
function defaultOf(context, label, field) {
|
|
554
|
+
const value = field.default;
|
|
555
|
+
if (value === undefined) {
|
|
556
|
+
return undefined;
|
|
557
|
+
}
|
|
558
|
+
const enumeration = enumOf(context, field);
|
|
559
|
+
switch (value.kind) {
|
|
560
|
+
case 'autoIncrement':
|
|
561
|
+
return undefined;
|
|
562
|
+
case 'now':
|
|
563
|
+
return 'DataTypes.NOW';
|
|
564
|
+
case 'uuid':
|
|
565
|
+
if (value.version !== undefined && value.version !== 1) {
|
|
566
|
+
if (value.version !== 4) {
|
|
567
|
+
context.warnings.push(`${label}: Sequelize generates UUID versions 1 and 4 only; the version ${value.version} default was written as UUIDV4.`);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
return value.version === 1 ? 'DataTypes.UUIDV1' : 'DataTypes.UUIDV4';
|
|
571
|
+
case 'enumValue': {
|
|
572
|
+
const member = enumeration === undefined
|
|
573
|
+
? undefined
|
|
574
|
+
: enumMember(enumeration, value.value);
|
|
575
|
+
if (member === undefined) {
|
|
576
|
+
context.warnings.push(`${label}: the enum default "${value.value}" does not match a member of the enum; it was written as a string.`);
|
|
577
|
+
return quote(value.value);
|
|
578
|
+
}
|
|
579
|
+
return quote(member.dbValue);
|
|
580
|
+
}
|
|
581
|
+
case 'dbExpression':
|
|
582
|
+
context.usesLiteral = true;
|
|
583
|
+
return `Sequelize.literal(${quote(defaultExpression(context, value.expression))})`;
|
|
584
|
+
case 'clientGenerated':
|
|
585
|
+
context.warnings.push(`${label}: the ${value.generator}() default is generated by Prisma Client; Sequelize has no equivalent and the default was dropped.`);
|
|
586
|
+
return undefined;
|
|
587
|
+
case 'literal':
|
|
588
|
+
return literalDefault(context, label, field, value.value, enumeration);
|
|
589
|
+
default:
|
|
590
|
+
return undefined;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
function literalDefault(context, label, field, value, enumeration) {
|
|
594
|
+
if (typeof value !== 'string') {
|
|
595
|
+
return String(value);
|
|
596
|
+
}
|
|
597
|
+
if (enumeration !== undefined) {
|
|
598
|
+
const member = enumMember(enumeration, value);
|
|
599
|
+
return quote(member === undefined ? value : member.dbValue);
|
|
600
|
+
}
|
|
601
|
+
if ((field.arrayDepth ?? 0) > 0) {
|
|
602
|
+
if (value === '[]') {
|
|
603
|
+
return '[]';
|
|
604
|
+
}
|
|
605
|
+
context.warnings.push(`${label}: the array default ${value} cannot be written; it was dropped.`);
|
|
606
|
+
return undefined;
|
|
607
|
+
}
|
|
608
|
+
if (field.type === 'json') {
|
|
609
|
+
try {
|
|
610
|
+
return jsonLiteral(JSON.parse(value));
|
|
611
|
+
}
|
|
612
|
+
catch {
|
|
613
|
+
return quote(value);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
if (field.type === 'hstore' && value === '{}') {
|
|
617
|
+
return '{}';
|
|
618
|
+
}
|
|
619
|
+
if (field.type === 'bytes') {
|
|
620
|
+
context.warnings.push(`${label}: a literal default on a binary column cannot be written; it was dropped.`);
|
|
621
|
+
return undefined;
|
|
622
|
+
}
|
|
623
|
+
return quote(value);
|
|
624
|
+
}
|
|
625
|
+
/** An expression default that every dialect reads without parentheses. */
|
|
626
|
+
const PLAIN_DEFAULT = /^(current_(timestamp|date|time)(\(\d*\))?|localtimestamp|now\(\)|null|true|false|-?\d+(\.\d+)?|'(?:[^']|'')*')$/i;
|
|
627
|
+
/** SQLite and MySQL accept an expression as a default only in parentheses: `DEFAULT (uuid())`. */
|
|
628
|
+
function defaultExpression(context, expression) {
|
|
629
|
+
const trimmed = expression.trim();
|
|
630
|
+
const needsParentheses = (context.dialect === 'sqlite' || context.dialect === 'mysql') &&
|
|
631
|
+
!PLAIN_DEFAULT.test(trimmed) &&
|
|
632
|
+
!/^\(.*\)$/s.test(trimmed);
|
|
633
|
+
return needsParentheses ? `(${trimmed})` : trimmed;
|
|
634
|
+
}
|
|
635
|
+
function jsonLiteral(value) {
|
|
636
|
+
if (value === null) {
|
|
637
|
+
return 'null';
|
|
638
|
+
}
|
|
639
|
+
if (typeof value === 'string') {
|
|
640
|
+
return quote(value);
|
|
641
|
+
}
|
|
642
|
+
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
643
|
+
return String(value);
|
|
644
|
+
}
|
|
645
|
+
if (Array.isArray(value)) {
|
|
646
|
+
return `[${value.map(jsonLiteral).join(', ')}]`;
|
|
647
|
+
}
|
|
648
|
+
if (typeof value === 'object') {
|
|
649
|
+
const parts = Object.entries(value).map(([key, item]) => `${IDENTIFIER_PATTERN.test(key) ? key : quote(key)}: ${jsonLiteral(item)}`);
|
|
650
|
+
return parts.length === 0 ? '{}' : `{ ${parts.join(', ')} }`;
|
|
651
|
+
}
|
|
652
|
+
return 'null';
|
|
653
|
+
}
|
|
654
|
+
// ---------------------------------------------------------------------------
|
|
655
|
+
// Attributes
|
|
656
|
+
// ---------------------------------------------------------------------------
|
|
657
|
+
function columnEntry(plan, attribute, column) {
|
|
658
|
+
return derivedColumn(plan, attribute) === column
|
|
659
|
+
? undefined
|
|
660
|
+
: `field: ${quote(column)}`;
|
|
661
|
+
}
|
|
662
|
+
function fieldAttribute(context, plan, field) {
|
|
663
|
+
const model = plan.model;
|
|
664
|
+
const label = `${model.name}.${field.name}`;
|
|
665
|
+
const name = plan.attrs.get(field.name) ?? field.name;
|
|
666
|
+
const spec = typeSpecOf(context, label, field, plan.keyLike.has(field.name));
|
|
667
|
+
const inCompositeKey = model.compositePrimaryKey?.includes(field.name) ?? false;
|
|
668
|
+
const isKey = field.isPrimaryKey || inCompositeKey;
|
|
669
|
+
if (field.generated !== undefined) {
|
|
670
|
+
context.warnings.push(`${label}: Sequelize has no generated columns, so the expression ${field.generated.expression} was dropped and the column written as a regular one.`);
|
|
671
|
+
}
|
|
672
|
+
if (field.isAutoUpdated && plan.timestamps.updated !== field) {
|
|
673
|
+
context.warnings.push(`${label}: only a date-time column can be Sequelize's updatedAt; it was written as a plain column that the application has to refresh.`);
|
|
674
|
+
}
|
|
675
|
+
let autoIncrement = false;
|
|
676
|
+
if (field.default?.kind === 'autoIncrement') {
|
|
677
|
+
if (field.isPrimaryKey && !inCompositeKey && isIntegerKey(field)) {
|
|
678
|
+
autoIncrement = true;
|
|
679
|
+
}
|
|
680
|
+
else {
|
|
681
|
+
context.warnings.push(`${label}: an auto-increment default is only supported on a single integer primary key; it was dropped.`);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
const defaultValue = defaultOf(context, label, field);
|
|
685
|
+
const sqliteInteger = autoIncrement && context.dialect === 'sqlite' && field.type === 'bigInt';
|
|
686
|
+
const entries = compact([
|
|
687
|
+
isKey ? 'primaryKey: true' : undefined,
|
|
688
|
+
autoIncrement ? 'autoIncrement: true' : undefined,
|
|
689
|
+
!isKey && !field.isNullable ? 'allowNull: false' : undefined,
|
|
690
|
+
field.isUnique && !isKey
|
|
691
|
+
? field.uniqueName === undefined
|
|
692
|
+
? 'unique: true'
|
|
693
|
+
: `unique: ${quote(field.uniqueName)}`
|
|
694
|
+
: undefined,
|
|
695
|
+
defaultValue === undefined ? undefined : `defaultValue: ${defaultValue}`,
|
|
696
|
+
columnEntry(plan, name, field.columnName),
|
|
697
|
+
]);
|
|
698
|
+
const optional = field.isNullable ||
|
|
699
|
+
autoIncrement ||
|
|
700
|
+
defaultValue !== undefined ||
|
|
701
|
+
field.default?.kind === 'autoIncrement';
|
|
702
|
+
return {
|
|
703
|
+
name,
|
|
704
|
+
// SQLite only allows AUTOINCREMENT on INTEGER PRIMARY KEY, which already holds 64 bits.
|
|
705
|
+
type: sqliteInteger ? 'DataTypes.INTEGER' : spec.expression,
|
|
706
|
+
entries,
|
|
707
|
+
ts: sqliteInteger ? 'number' : spec.ts,
|
|
708
|
+
optional,
|
|
709
|
+
isReadonly: false,
|
|
710
|
+
implicit: false,
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
/** The columns Sequelize adds for `timestamps`: required date-times that it fills in itself. */
|
|
714
|
+
function implicitTimestamp(name) {
|
|
715
|
+
return {
|
|
716
|
+
name,
|
|
717
|
+
type: 'DataTypes.DATE',
|
|
718
|
+
entries: ['allowNull: false'],
|
|
719
|
+
ts: 'Date',
|
|
720
|
+
optional: true,
|
|
721
|
+
isReadonly: true,
|
|
722
|
+
implicit: true,
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
/** Resolves the column a relation points at: an explicit `toField`, otherwise the single primary key. */
|
|
726
|
+
function referencedKey(context, target, toField, depth = 0) {
|
|
727
|
+
if (depth > 5) {
|
|
728
|
+
return undefined;
|
|
729
|
+
}
|
|
730
|
+
const plan = planOf(context, target);
|
|
731
|
+
const explicit = toField === undefined
|
|
732
|
+
? undefined
|
|
733
|
+
: target.fields.find((field) => field.name === toField || field.columnName === toField);
|
|
734
|
+
const column = explicit ?? target.fields.find((field) => field.isPrimaryKey);
|
|
735
|
+
if (column !== undefined) {
|
|
736
|
+
return { field: column, attr: plan.attrs.get(column.name) ?? column.name };
|
|
737
|
+
}
|
|
738
|
+
const primary = target.relations.find((relation) => relation.isPrimaryKey === true);
|
|
739
|
+
if (primary === undefined) {
|
|
740
|
+
return undefined;
|
|
741
|
+
}
|
|
742
|
+
const chained = findModel(context.schema, primary.targetModel);
|
|
743
|
+
const chainedKey = chained === undefined
|
|
744
|
+
? undefined
|
|
745
|
+
: referencedKey(context, chained, primary.toField, depth + 1);
|
|
746
|
+
return chainedKey === undefined
|
|
747
|
+
? undefined
|
|
748
|
+
: {
|
|
749
|
+
field: chainedKey.field,
|
|
750
|
+
attr: plan.keys.get(primary.name) ?? chainedKey.attr,
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
function keyAttribute(context, plan, relation, key) {
|
|
754
|
+
const model = plan.model;
|
|
755
|
+
const label = `${model.name}.${relation.name}`;
|
|
756
|
+
const name = plan.keys.get(relation.name) ?? relation.columnName;
|
|
757
|
+
const spec = typeSpecOf(context, label, {
|
|
758
|
+
...key.field,
|
|
759
|
+
default: undefined,
|
|
760
|
+
isUnique: false,
|
|
761
|
+
isNullable: relation.isNullable,
|
|
762
|
+
arrayDepth: undefined,
|
|
763
|
+
}, true);
|
|
764
|
+
const isKey = relation.isPrimaryKey === true ||
|
|
765
|
+
(model.compositePrimaryKey?.includes(relation.name) ?? false);
|
|
766
|
+
const entries = compact([
|
|
767
|
+
isKey ? 'primaryKey: true' : undefined,
|
|
768
|
+
!isKey && !relation.isNullable ? 'allowNull: false' : undefined,
|
|
769
|
+
relation.kind === 'oneToOne' && !isKey ? 'unique: true' : undefined,
|
|
770
|
+
columnEntry(plan, name, relation.columnName),
|
|
771
|
+
]);
|
|
772
|
+
return {
|
|
773
|
+
name,
|
|
774
|
+
type: spec.expression,
|
|
775
|
+
entries,
|
|
776
|
+
ts: spec.ts,
|
|
777
|
+
optional: relation.isNullable,
|
|
778
|
+
isReadonly: false,
|
|
779
|
+
implicit: false,
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
// ---------------------------------------------------------------------------
|
|
783
|
+
// Indexes
|
|
784
|
+
// ---------------------------------------------------------------------------
|
|
785
|
+
function indexColumn(context, model, name) {
|
|
786
|
+
const relation = model.relations.find((candidate) => candidate.kind !== 'manyToMany' && candidate.name === name) ??
|
|
787
|
+
model.relations.find((candidate) => candidate.kind !== 'manyToMany' && candidate.columnName === name);
|
|
788
|
+
if (relation !== undefined) {
|
|
789
|
+
return relation.columnName;
|
|
790
|
+
}
|
|
791
|
+
const field = model.fields.find((candidate) => candidate.name === name) ??
|
|
792
|
+
model.fields.find((candidate) => candidate.columnName === name);
|
|
793
|
+
void context;
|
|
794
|
+
return field?.columnName;
|
|
795
|
+
}
|
|
796
|
+
function indexField(column, options) {
|
|
797
|
+
if (options === undefined) {
|
|
798
|
+
return quote(column);
|
|
799
|
+
}
|
|
800
|
+
const entries = compact([
|
|
801
|
+
`name: ${quote(column)}`,
|
|
802
|
+
options.sort === 'desc' ? `order: 'DESC'` : undefined,
|
|
803
|
+
options.length === undefined ? undefined : `length: ${options.length}`,
|
|
804
|
+
options.ops === undefined ? undefined : `operator: ${quote(options.ops)}`,
|
|
805
|
+
]);
|
|
806
|
+
return entries.length === 1 ? quote(column) : `{ ${entries.join(', ')} }`;
|
|
807
|
+
}
|
|
808
|
+
function indexEntries(context, model, index) {
|
|
809
|
+
const label = `${model.name} index (${index.fields.join(', ')})`;
|
|
810
|
+
const fields = [];
|
|
811
|
+
for (const name of index.fields) {
|
|
812
|
+
const column = indexColumn(context, model, name);
|
|
813
|
+
if (column === undefined) {
|
|
814
|
+
context.warnings.push(`${label}: "${name}" is not a column of the model; the index was skipped.`);
|
|
815
|
+
return undefined;
|
|
816
|
+
}
|
|
817
|
+
fields.push(indexField(column, index.fieldOptions?.[name]));
|
|
818
|
+
}
|
|
819
|
+
const dialect = context.dialect;
|
|
820
|
+
let using;
|
|
821
|
+
if (index.method !== undefined) {
|
|
822
|
+
const method = INDEX_METHODS[index.method.toLowerCase()];
|
|
823
|
+
if (method === undefined) {
|
|
824
|
+
context.warnings.push(`${label}: the index method "${index.method}" is not known to Sequelize and was dropped.`);
|
|
825
|
+
}
|
|
826
|
+
else if (dialect === 'postgres' || dialect === 'mysql') {
|
|
827
|
+
using = method;
|
|
828
|
+
}
|
|
829
|
+
else {
|
|
830
|
+
context.warnings.push(`${label}: ${dialectName(dialect)} has no index methods; USING ${method} was dropped.`);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
let fullText = false;
|
|
834
|
+
if (index.kind === 'fulltext') {
|
|
835
|
+
if (dialect === 'mysql') {
|
|
836
|
+
fullText = true;
|
|
837
|
+
}
|
|
838
|
+
else {
|
|
839
|
+
context.warnings.push(`${label}: full-text indexes are a MySQL feature in Sequelize; it was written as an ordinary index.`);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
if (index.clustered !== undefined) {
|
|
843
|
+
context.warnings.push(`${label}: clustered and non-clustered options have no Sequelize equivalent and were dropped.`);
|
|
844
|
+
}
|
|
845
|
+
return compact([
|
|
846
|
+
index.name === undefined ? undefined : `name: ${quote(index.name)}`,
|
|
847
|
+
index.isUnique ? 'unique: true' : undefined,
|
|
848
|
+
fullText ? `type: 'FULLTEXT'` : undefined,
|
|
849
|
+
using === undefined ? undefined : `using: ${quote(using)}`,
|
|
850
|
+
`fields: [${fields.join(', ')}]`,
|
|
851
|
+
]);
|
|
852
|
+
}
|
|
853
|
+
function renderModel(context, plan, associations) {
|
|
854
|
+
const model = plan.model;
|
|
855
|
+
const attributes = [];
|
|
856
|
+
const timestamps = plan.timestamps;
|
|
857
|
+
const timestampKinds = new Map();
|
|
858
|
+
if (timestamps.created !== undefined) {
|
|
859
|
+
timestampKinds.set(timestamps.created, 'created');
|
|
860
|
+
}
|
|
861
|
+
if (timestamps.updated !== undefined) {
|
|
862
|
+
timestampKinds.set(timestamps.updated, 'updated');
|
|
863
|
+
}
|
|
864
|
+
for (const field of model.fields) {
|
|
865
|
+
const kind = timestampKinds.get(field);
|
|
866
|
+
if (kind !== undefined && isImplicitTimestamp(plan, field, kind)) {
|
|
867
|
+
attributes.push(implicitTimestamp(plan.attrs.get(field.name) ?? field.name));
|
|
868
|
+
continue;
|
|
869
|
+
}
|
|
870
|
+
attributes.push(fieldAttribute(context, plan, field));
|
|
871
|
+
}
|
|
872
|
+
for (const relation of model.relations) {
|
|
873
|
+
renderRelation(context, plan, relation, attributes, associations);
|
|
874
|
+
}
|
|
875
|
+
const hasPrimaryKey = model.fields.some((field) => field.isPrimaryKey) ||
|
|
876
|
+
model.relations.some((relation) => relation.isPrimaryKey === true) ||
|
|
877
|
+
(model.compositePrimaryKey?.length ?? 0) > 0;
|
|
878
|
+
let removeId = false;
|
|
879
|
+
if (model.isView === true) {
|
|
880
|
+
context.warnings.push(`${model.name}: this is a database view; it was written as a model of the same name, but Sequelize cannot define a view, so sync() would create a table instead.`);
|
|
881
|
+
}
|
|
882
|
+
if (!hasPrimaryKey) {
|
|
883
|
+
const idAttribute = attributes.find((attribute) => attribute.name === 'id');
|
|
884
|
+
if (idAttribute === undefined) {
|
|
885
|
+
removeId = true;
|
|
886
|
+
context.warnings.push(`${model.name}: the model has no primary key; Sequelize would add an "id" column, so removeAttribute('id') was written. Most Sequelize features need a key.`);
|
|
887
|
+
}
|
|
888
|
+
else {
|
|
889
|
+
idAttribute.entries = [
|
|
890
|
+
'primaryKey: true',
|
|
891
|
+
...idAttribute.entries.filter((entry) => entry !== 'allowNull: false'),
|
|
892
|
+
];
|
|
893
|
+
context.warnings.push(`${model.name}: the model has no primary key, and Sequelize refuses a model with an "id" column that is not one; "id" was written with primaryKey: true.`);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
const options = [`tableName: ${quote(model.tableName)}`];
|
|
897
|
+
if (model.schema !== undefined) {
|
|
898
|
+
if (context.dialect === 'sqlite') {
|
|
899
|
+
// Sequelize would name the SQLite table "schema.table" instead.
|
|
900
|
+
context.warnings.push(`${model.name}: SQLite has no database schemas; the schema "${model.schema}" was ignored.`);
|
|
901
|
+
}
|
|
902
|
+
else {
|
|
903
|
+
options.push(`schema: ${quote(model.schema)}`);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
const enabled = timestamps.created !== undefined || timestamps.updated !== undefined;
|
|
907
|
+
options.push(`timestamps: ${enabled ? 'true' : 'false'}`);
|
|
908
|
+
if (enabled) {
|
|
909
|
+
const createdName = timestamps.created === undefined
|
|
910
|
+
? undefined
|
|
911
|
+
: plan.attrs.get(timestamps.created.name);
|
|
912
|
+
const updatedName = timestamps.updated === undefined
|
|
913
|
+
? undefined
|
|
914
|
+
: plan.attrs.get(timestamps.updated.name);
|
|
915
|
+
if (createdName === undefined) {
|
|
916
|
+
options.push('createdAt: false');
|
|
917
|
+
}
|
|
918
|
+
else if (createdName !== 'createdAt') {
|
|
919
|
+
options.push(`createdAt: ${quote(createdName)}`);
|
|
920
|
+
}
|
|
921
|
+
if (updatedName === undefined) {
|
|
922
|
+
options.push('updatedAt: false');
|
|
923
|
+
}
|
|
924
|
+
else if (updatedName !== 'updatedAt') {
|
|
925
|
+
options.push(`updatedAt: ${quote(updatedName)}`);
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
if (plan.underscored) {
|
|
929
|
+
options.push('underscored: true');
|
|
930
|
+
}
|
|
931
|
+
const indexLines = [];
|
|
932
|
+
for (const index of model.indexes) {
|
|
933
|
+
const entries = indexEntries(context, model, index);
|
|
934
|
+
if (entries !== undefined) {
|
|
935
|
+
indexLines.push(...objectLines('', entries, INDENT, ','));
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
return {
|
|
939
|
+
attributes,
|
|
940
|
+
options,
|
|
941
|
+
indexLines,
|
|
942
|
+
removeId,
|
|
943
|
+
usesLiteral: context.usesLiteral,
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
function actionOptions(relation) {
|
|
947
|
+
const entries = [
|
|
948
|
+
`onDelete: ${quote(ON_ACTIONS[relation.onDelete])}`,
|
|
949
|
+
];
|
|
950
|
+
// ON UPDATE CASCADE is what Sequelize adds to every key; only another action is written.
|
|
951
|
+
if (relation.onUpdate !== undefined && relation.onUpdate !== 'cascade') {
|
|
952
|
+
entries.push(`onUpdate: ${quote(ON_ACTIONS[relation.onUpdate])}`);
|
|
953
|
+
}
|
|
954
|
+
return entries;
|
|
955
|
+
}
|
|
956
|
+
function addAssociation(associations, spec) {
|
|
957
|
+
const list = associations.byOwner.get(spec.owner) ?? [];
|
|
958
|
+
list.push(spec);
|
|
959
|
+
associations.byOwner.set(spec.owner, list);
|
|
960
|
+
associations.ordered.push(spec);
|
|
961
|
+
}
|
|
962
|
+
function renderRelation(context, plan, relation, attributes, associations) {
|
|
963
|
+
const model = plan.model;
|
|
964
|
+
const label = `${model.name}.${relation.name}`;
|
|
965
|
+
const target = findModel(context.schema, relation.targetModel);
|
|
966
|
+
if (target === undefined) {
|
|
967
|
+
context.warnings.push(`${label}: target model "${relation.targetModel}" does not exist in the schema; the relation was skipped.`);
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
const targetPlan = planOf(context, target);
|
|
971
|
+
const alias = plan.aliases.get(relation.name) ?? relation.name;
|
|
972
|
+
const inverse = context.inverseAliases.get(label);
|
|
973
|
+
if (relation.kind === 'manyToMany') {
|
|
974
|
+
renderManyToMany(context, plan, targetPlan, relation, alias, inverse, associations);
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
977
|
+
const key = referencedKey(context, target, relation.toField);
|
|
978
|
+
if (key === undefined) {
|
|
979
|
+
context.warnings.push(`${label}: target model "${target.name}" has no single-column primary key to reference; the relation was skipped.`);
|
|
980
|
+
return;
|
|
981
|
+
}
|
|
982
|
+
if (relation.onDelete === 'setNull' && !relation.isNullable) {
|
|
983
|
+
context.warnings.push(`${label}: onDelete SET NULL on a required relation will fail at the database level; review the relation.`);
|
|
984
|
+
}
|
|
985
|
+
const attribute = keyAttribute(context, plan, relation, key);
|
|
986
|
+
attributes.push(attribute);
|
|
987
|
+
const keyOption = `foreignKey: ${quote(attribute.name)}`;
|
|
988
|
+
const toKey = relation.toField === undefined ? undefined : key.attr;
|
|
989
|
+
addAssociation(associations, {
|
|
990
|
+
owner: plan.className,
|
|
991
|
+
method: 'belongsTo',
|
|
992
|
+
relation: label,
|
|
993
|
+
target: targetPlan.className,
|
|
994
|
+
alias,
|
|
995
|
+
options: compact([
|
|
996
|
+
`as: ${quote(alias)}`,
|
|
997
|
+
keyOption,
|
|
998
|
+
toKey === undefined ? undefined : `targetKey: ${quote(toKey)}`,
|
|
999
|
+
...actionOptions(relation),
|
|
1000
|
+
]),
|
|
1001
|
+
});
|
|
1002
|
+
if (inverse !== undefined) {
|
|
1003
|
+
addAssociation(associations, {
|
|
1004
|
+
owner: targetPlan.className,
|
|
1005
|
+
method: relation.kind === 'oneToOne' ? 'hasOne' : 'hasMany',
|
|
1006
|
+
relation: label,
|
|
1007
|
+
target: plan.className,
|
|
1008
|
+
alias: inverse,
|
|
1009
|
+
options: compact([
|
|
1010
|
+
`as: ${quote(inverse)}`,
|
|
1011
|
+
keyOption,
|
|
1012
|
+
toKey === undefined ? undefined : `sourceKey: ${quote(toKey)}`,
|
|
1013
|
+
...actionOptions(relation),
|
|
1014
|
+
]),
|
|
1015
|
+
});
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
function renderManyToMany(context, plan, targetPlan, relation, alias, inverse, associations) {
|
|
1019
|
+
const model = plan.model;
|
|
1020
|
+
const target = targetPlan.model;
|
|
1021
|
+
const label = `${model.name}.${relation.name}`;
|
|
1022
|
+
const hasSingleKey = (candidate) => candidate.fields.filter((field) => field.isPrimaryKey).length ===
|
|
1023
|
+
1;
|
|
1024
|
+
if (!hasSingleKey(model) || !hasSingleKey(target)) {
|
|
1025
|
+
context.warnings.push(`${label}: a many-to-many relation needs a single-column primary key on both models; the relation was skipped.`);
|
|
1026
|
+
return;
|
|
1027
|
+
}
|
|
1028
|
+
const isSelf = model.name === target.name;
|
|
1029
|
+
const ownerColumn = `${isSelf ? 'from_' : ''}${toSnakeCase(model.name)}_id`;
|
|
1030
|
+
const targetColumn = `${isSelf ? 'to_' : ''}${toSnakeCase(target.name)}_id`;
|
|
1031
|
+
// The join table the other emitters derive; Sequelize creates it itself.
|
|
1032
|
+
const table = quote(`${model.tableName}_${relation.name}`);
|
|
1033
|
+
addAssociation(associations, {
|
|
1034
|
+
owner: plan.className,
|
|
1035
|
+
method: 'belongsToMany',
|
|
1036
|
+
relation: label,
|
|
1037
|
+
target: targetPlan.className,
|
|
1038
|
+
alias,
|
|
1039
|
+
options: [
|
|
1040
|
+
`as: ${quote(alias)}`,
|
|
1041
|
+
`through: ${table}`,
|
|
1042
|
+
`foreignKey: ${quote(ownerColumn)}`,
|
|
1043
|
+
`otherKey: ${quote(targetColumn)}`,
|
|
1044
|
+
'timestamps: false',
|
|
1045
|
+
],
|
|
1046
|
+
});
|
|
1047
|
+
if (inverse !== undefined) {
|
|
1048
|
+
addAssociation(associations, {
|
|
1049
|
+
owner: targetPlan.className,
|
|
1050
|
+
method: 'belongsToMany',
|
|
1051
|
+
relation: label,
|
|
1052
|
+
target: plan.className,
|
|
1053
|
+
alias: inverse,
|
|
1054
|
+
options: [
|
|
1055
|
+
`as: ${quote(inverse)}`,
|
|
1056
|
+
`through: ${table}`,
|
|
1057
|
+
`foreignKey: ${quote(targetColumn)}`,
|
|
1058
|
+
`otherKey: ${quote(ownerColumn)}`,
|
|
1059
|
+
'timestamps: false',
|
|
1060
|
+
],
|
|
1061
|
+
});
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
// ---------------------------------------------------------------------------
|
|
1065
|
+
// Output: attribute and association text
|
|
1066
|
+
// ---------------------------------------------------------------------------
|
|
1067
|
+
function attributeLines(attribute, indent) {
|
|
1068
|
+
const key = IDENTIFIER_PATTERN.test(attribute.name)
|
|
1069
|
+
? attribute.name
|
|
1070
|
+
: quote(attribute.name);
|
|
1071
|
+
if (attribute.entries.length === 0) {
|
|
1072
|
+
return [`${indent}${key}: ${attribute.type},`];
|
|
1073
|
+
}
|
|
1074
|
+
return objectLines(`${key}: `, [`type: ${attribute.type}`, ...attribute.entries], indent, ',');
|
|
1075
|
+
}
|
|
1076
|
+
function associationLines(spec, targetExpression, indent) {
|
|
1077
|
+
const head = `${spec.owner}.${spec.method}(${targetExpression}, `;
|
|
1078
|
+
const inline = `${indent}${head}{ ${spec.options.join(', ')} });`;
|
|
1079
|
+
if (inline.length <= MAX_LINE_WIDTH) {
|
|
1080
|
+
return [inline];
|
|
1081
|
+
}
|
|
1082
|
+
return [
|
|
1083
|
+
`${indent}${spec.owner}.${spec.method}(${targetExpression}, {`,
|
|
1084
|
+
...spec.options.map((entry) => `${indent}${INDENT}${entry},`),
|
|
1085
|
+
`${indent}});`,
|
|
1086
|
+
];
|
|
1087
|
+
}
|
|
1088
|
+
function optionBlock(leading, rendered, indent) {
|
|
1089
|
+
const lines = [];
|
|
1090
|
+
const entries = [...leading, ...rendered.options];
|
|
1091
|
+
const inline = `${indent}{ ${[...entries].join(', ')} }`;
|
|
1092
|
+
if (rendered.indexLines.length === 0 && inline.length <= MAX_LINE_WIDTH) {
|
|
1093
|
+
return [inline];
|
|
1094
|
+
}
|
|
1095
|
+
lines.push(`${indent}{`);
|
|
1096
|
+
for (const entry of entries) {
|
|
1097
|
+
lines.push(`${indent}${INDENT}${entry},`);
|
|
1098
|
+
}
|
|
1099
|
+
if (rendered.indexLines.length > 0) {
|
|
1100
|
+
lines.push(`${indent}${INDENT}indexes: [`);
|
|
1101
|
+
for (const line of rendered.indexLines) {
|
|
1102
|
+
lines.push(`${indent}${INDENT}${line}`);
|
|
1103
|
+
}
|
|
1104
|
+
lines.push(`${indent}${INDENT}],`);
|
|
1105
|
+
}
|
|
1106
|
+
lines.push(`${indent}}`);
|
|
1107
|
+
return lines;
|
|
1108
|
+
}
|
|
1109
|
+
// ---------------------------------------------------------------------------
|
|
1110
|
+
// Output: init style (TypeScript)
|
|
1111
|
+
// ---------------------------------------------------------------------------
|
|
1112
|
+
function creationType(attribute, nullable) {
|
|
1113
|
+
const base = nullable ? `${attribute.ts} | null` : attribute.ts;
|
|
1114
|
+
return attribute.optional ? `CreationOptional<${base}>` : base;
|
|
1115
|
+
}
|
|
1116
|
+
function writeInitFile(context, plan, rendered, associations) {
|
|
1117
|
+
const own = associations.byOwner.get(plan.className) ?? [];
|
|
1118
|
+
const nullableByName = new Set();
|
|
1119
|
+
for (const field of plan.model.fields) {
|
|
1120
|
+
if (field.isNullable) {
|
|
1121
|
+
nullableByName.add(plan.attrs.get(field.name) ?? field.name);
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
for (const relation of plan.model.relations) {
|
|
1125
|
+
const key = plan.keys.get(relation.name);
|
|
1126
|
+
if (relation.isNullable && key !== undefined) {
|
|
1127
|
+
nullableByName.add(key);
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
const needsCreationOptional = rendered.attributes.some((attribute) => attribute.optional);
|
|
1131
|
+
const classLines = rendered.attributes.map((attribute) => `${INDENT}declare ${attribute.isReadonly ? 'readonly ' : ''}${attribute.name}: ${creationType(attribute, nullableByName.has(attribute.name))};`);
|
|
1132
|
+
const importedTypes = new Set();
|
|
1133
|
+
const associationLinesOut = [];
|
|
1134
|
+
for (const spec of own) {
|
|
1135
|
+
const many = spec.method === 'hasMany' || spec.method === 'belongsToMany';
|
|
1136
|
+
if (spec.target !== plan.className) {
|
|
1137
|
+
importedTypes.add(spec.target);
|
|
1138
|
+
}
|
|
1139
|
+
associationLinesOut.push(`${INDENT}declare ${spec.alias}?: NonAttribute<${spec.target}${many ? '[]' : ''}>;`);
|
|
1140
|
+
}
|
|
1141
|
+
const valueImports = ['DataTypes', 'Model'];
|
|
1142
|
+
const typeImports = ['InferAttributes', 'InferCreationAttributes'];
|
|
1143
|
+
if (rendered.usesLiteral) {
|
|
1144
|
+
valueImports.push('Sequelize');
|
|
1145
|
+
}
|
|
1146
|
+
else {
|
|
1147
|
+
typeImports.push('Sequelize');
|
|
1148
|
+
}
|
|
1149
|
+
if (needsCreationOptional) {
|
|
1150
|
+
typeImports.push('CreationOptional');
|
|
1151
|
+
}
|
|
1152
|
+
if (associationLinesOut.length > 0) {
|
|
1153
|
+
typeImports.push('NonAttribute');
|
|
1154
|
+
}
|
|
1155
|
+
typeImports.sort((first, second) => first.localeCompare(second));
|
|
1156
|
+
const specifiers = [
|
|
1157
|
+
...valueImports,
|
|
1158
|
+
...typeImports.map((name) => `type ${name}`),
|
|
1159
|
+
];
|
|
1160
|
+
const lines = [
|
|
1161
|
+
'import {',
|
|
1162
|
+
...specifiers.map((specifier) => `${INDENT}${specifier},`),
|
|
1163
|
+
`} from 'sequelize';`,
|
|
1164
|
+
];
|
|
1165
|
+
const typeFiles = [...importedTypes].sort();
|
|
1166
|
+
for (const name of typeFiles) {
|
|
1167
|
+
lines.push(`import type { ${name} } from '${fileOf(context, name)}';`);
|
|
1168
|
+
}
|
|
1169
|
+
lines.push('', ...classHeader(plan), ...classLines);
|
|
1170
|
+
if (associationLinesOut.length > 0) {
|
|
1171
|
+
lines.push('', ...associationLinesOut);
|
|
1172
|
+
}
|
|
1173
|
+
lines.push('', `${INDENT}static initModel(sequelize: Sequelize): typeof ${plan.className} {`, `${INDENT}${INDENT}${plan.className}.init(`, `${INDENT}${INDENT}${INDENT}{`);
|
|
1174
|
+
for (const attribute of rendered.attributes) {
|
|
1175
|
+
lines.push(...attributeLines(attribute, INDENT.repeat(4)));
|
|
1176
|
+
}
|
|
1177
|
+
lines.push(`${INDENT}${INDENT}${INDENT}},`, ...optionBlock(['sequelize', `modelName: ${quote(plan.className)}`], rendered, INDENT.repeat(3)), `${INDENT}${INDENT});`);
|
|
1178
|
+
if (rendered.removeId) {
|
|
1179
|
+
lines.push(`${INDENT}${INDENT}${plan.className}.removeAttribute('id');`);
|
|
1180
|
+
}
|
|
1181
|
+
lines.push(`${INDENT}${INDENT}return ${plan.className};`, `${INDENT}}`, '}', '');
|
|
1182
|
+
return lines.join('\n');
|
|
1183
|
+
}
|
|
1184
|
+
function classHeader(plan) {
|
|
1185
|
+
const name = plan.className;
|
|
1186
|
+
const inline = `export class ${name} extends Model<InferAttributes<${name}>, InferCreationAttributes<${name}>> {`;
|
|
1187
|
+
if (inline.length <= MAX_LINE_WIDTH) {
|
|
1188
|
+
return [inline];
|
|
1189
|
+
}
|
|
1190
|
+
return [
|
|
1191
|
+
`export class ${name} extends Model<`,
|
|
1192
|
+
`${INDENT}InferAttributes<${name}>,`,
|
|
1193
|
+
`${INDENT}InferCreationAttributes<${name}>`,
|
|
1194
|
+
'> {',
|
|
1195
|
+
];
|
|
1196
|
+
}
|
|
1197
|
+
/** Import path of another model's file, found by class name. */
|
|
1198
|
+
function fileOf(context, className) {
|
|
1199
|
+
for (const plan of context.plans.values()) {
|
|
1200
|
+
if (plan.className === className) {
|
|
1201
|
+
return `./${plan.fileBase}.js`;
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
return `./${className}.js`;
|
|
1205
|
+
}
|
|
1206
|
+
function writeInitIndex(context, associations) {
|
|
1207
|
+
const plans = context.schema.models.map((model) => planOf(context, model));
|
|
1208
|
+
const lines = [`import type { Sequelize } from 'sequelize';`];
|
|
1209
|
+
for (const plan of plans) {
|
|
1210
|
+
lines.push(`import { ${plan.className} } from './${plan.fileBase}.js';`);
|
|
1211
|
+
}
|
|
1212
|
+
lines.push('', `export { ${plans.map((plan) => plan.className).join(', ')} };`, '', '/** Initializes every model on a Sequelize instance, then sets up the associations. */', 'export function initModels(sequelize: Sequelize) {');
|
|
1213
|
+
for (const plan of plans) {
|
|
1214
|
+
lines.push(`${INDENT}${plan.className}.initModel(sequelize);`);
|
|
1215
|
+
}
|
|
1216
|
+
// The relations keep the order of the schema: a many-to-many pair is read with its first call as the owner.
|
|
1217
|
+
let previous;
|
|
1218
|
+
for (const spec of associations.ordered) {
|
|
1219
|
+
if (spec.relation !== previous) {
|
|
1220
|
+
lines.push('');
|
|
1221
|
+
previous = spec.relation;
|
|
1222
|
+
}
|
|
1223
|
+
lines.push(...associationLines(spec, spec.target, INDENT));
|
|
1224
|
+
}
|
|
1225
|
+
lines.push('', `${INDENT}return { ${plans.map((plan) => plan.className).join(', ')} };`, '}', '', 'export type Models = ReturnType<typeof initModels>;', '');
|
|
1226
|
+
return lines.join('\n');
|
|
1227
|
+
}
|
|
1228
|
+
// ---------------------------------------------------------------------------
|
|
1229
|
+
// Output: define style (sequelize-cli CommonJS)
|
|
1230
|
+
// ---------------------------------------------------------------------------
|
|
1231
|
+
function writeDefineFile(plan, rendered, associations) {
|
|
1232
|
+
const own = associations.byOwner.get(plan.className) ?? [];
|
|
1233
|
+
const lines = [`'use strict';`, ''];
|
|
1234
|
+
if (rendered.usesLiteral) {
|
|
1235
|
+
lines.push(`const { Sequelize } = require('sequelize');`, '');
|
|
1236
|
+
}
|
|
1237
|
+
lines.push('module.exports = (sequelize, DataTypes) => {', `${INDENT}const ${plan.className} = sequelize.define(`, `${INDENT}${INDENT}${quote(plan.className)},`, `${INDENT}${INDENT}{`);
|
|
1238
|
+
for (const attribute of rendered.attributes) {
|
|
1239
|
+
if (!attribute.implicit) {
|
|
1240
|
+
lines.push(...attributeLines(attribute, INDENT.repeat(3)));
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
lines.push(`${INDENT}${INDENT}},`, ...optionBlock([], rendered, INDENT.repeat(2)), `${INDENT});`);
|
|
1244
|
+
if (rendered.removeId) {
|
|
1245
|
+
lines.push('', `${INDENT}${plan.className}.removeAttribute('id');`);
|
|
1246
|
+
}
|
|
1247
|
+
if (own.length > 0) {
|
|
1248
|
+
lines.push('', `${INDENT}${plan.className}.associate = (models) => {`);
|
|
1249
|
+
for (const spec of own) {
|
|
1250
|
+
lines.push(...associationLines(spec, `models.${spec.target}`, INDENT.repeat(2)));
|
|
1251
|
+
}
|
|
1252
|
+
lines.push(`${INDENT}};`);
|
|
1253
|
+
}
|
|
1254
|
+
lines.push('', `${INDENT}return ${plan.className};`, '};', '');
|
|
1255
|
+
return lines.join('\n');
|
|
1256
|
+
}
|
|
1257
|
+
function writeDefineIndex(context) {
|
|
1258
|
+
const plans = context.schema.models.map((model) => planOf(context, model));
|
|
1259
|
+
const lines = [
|
|
1260
|
+
`'use strict';`,
|
|
1261
|
+
'',
|
|
1262
|
+
`const { DataTypes } = require('sequelize');`,
|
|
1263
|
+
'',
|
|
1264
|
+
'// Defines every model on a Sequelize instance, then runs the associate functions:',
|
|
1265
|
+
"// const { initModels } = require('./models');",
|
|
1266
|
+
'// const models = initModels(sequelize);',
|
|
1267
|
+
'function initModels(sequelize) {',
|
|
1268
|
+
`${INDENT}const models = {`,
|
|
1269
|
+
];
|
|
1270
|
+
for (const plan of plans) {
|
|
1271
|
+
lines.push(`${INDENT}${INDENT}${plan.className}: require('./${plan.fileBase}')(sequelize, DataTypes),`);
|
|
1272
|
+
}
|
|
1273
|
+
lines.push(`${INDENT}};`, `${INDENT}Object.values(models).forEach((model) => {`, `${INDENT}${INDENT}if (model.associate) {`, `${INDENT}${INDENT}${INDENT}model.associate(models);`, `${INDENT}${INDENT}}`, `${INDENT}});`, `${INDENT}return models;`, '}', '', 'module.exports = { initModels };', '');
|
|
1274
|
+
return lines.join('\n');
|
|
1275
|
+
}
|
|
1276
|
+
//# sourceMappingURL=sequelize.js.map
|