ormbridge 0.8.0 → 0.10.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 (152) hide show
  1. package/README.md +2 -0
  2. package/dist/cliRunner.js +3 -3
  3. package/dist/cliRunner.js.map +1 -1
  4. package/dist/config.d.ts +3 -2
  5. package/dist/config.js.map +1 -1
  6. package/dist/emitters/efcore.d.ts +40 -0
  7. package/dist/emitters/efcore.js +686 -0
  8. package/dist/emitters/efcore.js.map +1 -0
  9. package/dist/emitters/efcoreColumns.d.ts +75 -0
  10. package/dist/emitters/efcoreColumns.js +492 -0
  11. package/dist/emitters/efcoreColumns.js.map +1 -0
  12. package/dist/emitters/efcoreNames.d.ts +30 -0
  13. package/dist/emitters/efcoreNames.js +268 -0
  14. package/dist/emitters/efcoreNames.js.map +1 -0
  15. package/dist/emitters/efcoreRender.d.ts +2 -0
  16. package/dist/emitters/efcoreRender.js +570 -0
  17. package/dist/emitters/efcoreRender.js.map +1 -0
  18. package/dist/emitters/efcoreTypes.d.ts +114 -0
  19. package/dist/emitters/efcoreTypes.js +2 -0
  20. package/dist/emitters/efcoreTypes.js.map +1 -0
  21. package/dist/emitters/jpa.d.ts +36 -0
  22. package/dist/emitters/jpa.js +253 -0
  23. package/dist/emitters/jpa.js.map +1 -0
  24. package/dist/emitters/jpaColumns.d.ts +25 -0
  25. package/dist/emitters/jpaColumns.js +341 -0
  26. package/dist/emitters/jpaColumns.js.map +1 -0
  27. package/dist/emitters/jpaDefaults.d.ts +24 -0
  28. package/dist/emitters/jpaDefaults.js +223 -0
  29. package/dist/emitters/jpaDefaults.js.map +1 -0
  30. package/dist/emitters/jpaEnums.d.ts +11 -0
  31. package/dist/emitters/jpaEnums.js +134 -0
  32. package/dist/emitters/jpaEnums.js.map +1 -0
  33. package/dist/emitters/jpaFormat.d.ts +16 -0
  34. package/dist/emitters/jpaFormat.js +73 -0
  35. package/dist/emitters/jpaFormat.js.map +1 -0
  36. package/dist/emitters/jpaKeys.d.ts +25 -0
  37. package/dist/emitters/jpaKeys.js +110 -0
  38. package/dist/emitters/jpaKeys.js.map +1 -0
  39. package/dist/emitters/jpaNames.d.ts +26 -0
  40. package/dist/emitters/jpaNames.js +234 -0
  41. package/dist/emitters/jpaNames.js.map +1 -0
  42. package/dist/emitters/jpaRelations.d.ts +41 -0
  43. package/dist/emitters/jpaRelations.js +304 -0
  44. package/dist/emitters/jpaRelations.js.map +1 -0
  45. package/dist/emitters/jpaRender.d.ts +10 -0
  46. package/dist/emitters/jpaRender.js +81 -0
  47. package/dist/emitters/jpaRender.js.map +1 -0
  48. package/dist/emitters/jpaScalars.d.ts +32 -0
  49. package/dist/emitters/jpaScalars.js +188 -0
  50. package/dist/emitters/jpaScalars.js.map +1 -0
  51. package/dist/emitters/jpaTable.d.ts +10 -0
  52. package/dist/emitters/jpaTable.js +109 -0
  53. package/dist/emitters/jpaTable.js.map +1 -0
  54. package/dist/emitters/jpaTypes.d.ts +85 -0
  55. package/dist/emitters/jpaTypes.js +36 -0
  56. package/dist/emitters/jpaTypes.js.map +1 -0
  57. package/dist/formats.d.ts +8 -4
  58. package/dist/formats.js +68 -0
  59. package/dist/formats.js.map +1 -1
  60. package/dist/index.d.ts +8 -0
  61. package/dist/index.js +4 -0
  62. package/dist/index.js.map +1 -1
  63. package/dist/io.js +51 -1
  64. package/dist/io.js.map +1 -1
  65. package/dist/parsers/csharpChains.d.ts +36 -0
  66. package/dist/parsers/csharpChains.js +157 -0
  67. package/dist/parsers/csharpChains.js.map +1 -0
  68. package/dist/parsers/csharpDeclarations.d.ts +3 -0
  69. package/dist/parsers/csharpDeclarations.js +293 -0
  70. package/dist/parsers/csharpDeclarations.js.map +1 -0
  71. package/dist/parsers/csharpSyntax.d.ts +130 -0
  72. package/dist/parsers/csharpSyntax.js +339 -0
  73. package/dist/parsers/csharpSyntax.js.map +1 -0
  74. package/dist/parsers/efcore.d.ts +17 -0
  75. package/dist/parsers/efcore.js +265 -0
  76. package/dist/parsers/efcore.js.map +1 -0
  77. package/dist/parsers/efcoreAnnotations.d.ts +19 -0
  78. package/dist/parsers/efcoreAnnotations.js +293 -0
  79. package/dist/parsers/efcoreAnnotations.js.map +1 -0
  80. package/dist/parsers/efcoreBuild.d.ts +13 -0
  81. package/dist/parsers/efcoreBuild.js +523 -0
  82. package/dist/parsers/efcoreBuild.js.map +1 -0
  83. package/dist/parsers/efcoreEntities.d.ts +13 -0
  84. package/dist/parsers/efcoreEntities.js +468 -0
  85. package/dist/parsers/efcoreEntities.js.map +1 -0
  86. package/dist/parsers/efcoreFields.d.ts +22 -0
  87. package/dist/parsers/efcoreFields.js +285 -0
  88. package/dist/parsers/efcoreFields.js.map +1 -0
  89. package/dist/parsers/efcoreFluent.d.ts +47 -0
  90. package/dist/parsers/efcoreFluent.js +375 -0
  91. package/dist/parsers/efcoreFluent.js.map +1 -0
  92. package/dist/parsers/efcoreModel.d.ts +209 -0
  93. package/dist/parsers/efcoreModel.js +2 -0
  94. package/dist/parsers/efcoreModel.js.map +1 -0
  95. package/dist/parsers/efcoreRelations.d.ts +46 -0
  96. package/dist/parsers/efcoreRelations.js +317 -0
  97. package/dist/parsers/efcoreRelations.js.map +1 -0
  98. package/dist/parsers/efcoreSteps.d.ts +89 -0
  99. package/dist/parsers/efcoreSteps.js +793 -0
  100. package/dist/parsers/efcoreSteps.js.map +1 -0
  101. package/dist/parsers/efcoreTypes.d.ts +35 -0
  102. package/dist/parsers/efcoreTypes.js +404 -0
  103. package/dist/parsers/efcoreTypes.js.map +1 -0
  104. package/dist/parsers/javaSyntax.d.ts +120 -0
  105. package/dist/parsers/javaSyntax.js +304 -0
  106. package/dist/parsers/javaSyntax.js.map +1 -0
  107. package/dist/parsers/javaValues.d.ts +5 -0
  108. package/dist/parsers/javaValues.js +187 -0
  109. package/dist/parsers/javaValues.js.map +1 -0
  110. package/dist/parsers/jpa.d.ts +17 -0
  111. package/dist/parsers/jpa.js +131 -0
  112. package/dist/parsers/jpa.js.map +1 -0
  113. package/dist/parsers/jpaAnnotations.d.ts +46 -0
  114. package/dist/parsers/jpaAnnotations.js +156 -0
  115. package/dist/parsers/jpaAnnotations.js.map +1 -0
  116. package/dist/parsers/jpaBuild.d.ts +4 -0
  117. package/dist/parsers/jpaBuild.js +297 -0
  118. package/dist/parsers/jpaBuild.js.map +1 -0
  119. package/dist/parsers/jpaClasses.d.ts +27 -0
  120. package/dist/parsers/jpaClasses.js +257 -0
  121. package/dist/parsers/jpaClasses.js.map +1 -0
  122. package/dist/parsers/jpaCollections.d.ts +12 -0
  123. package/dist/parsers/jpaCollections.js +106 -0
  124. package/dist/parsers/jpaCollections.js.map +1 -0
  125. package/dist/parsers/jpaColumns.d.ts +26 -0
  126. package/dist/parsers/jpaColumns.js +361 -0
  127. package/dist/parsers/jpaColumns.js.map +1 -0
  128. package/dist/parsers/jpaEntities.d.ts +9 -0
  129. package/dist/parsers/jpaEntities.js +181 -0
  130. package/dist/parsers/jpaEntities.js.map +1 -0
  131. package/dist/parsers/jpaIndexes.d.ts +12 -0
  132. package/dist/parsers/jpaIndexes.js +87 -0
  133. package/dist/parsers/jpaIndexes.js.map +1 -0
  134. package/dist/parsers/jpaJoins.d.ts +68 -0
  135. package/dist/parsers/jpaJoins.js +131 -0
  136. package/dist/parsers/jpaJoins.js.map +1 -0
  137. package/dist/parsers/jpaModel.d.ts +73 -0
  138. package/dist/parsers/jpaModel.js +2 -0
  139. package/dist/parsers/jpaModel.js.map +1 -0
  140. package/dist/parsers/jpaNotes.d.ts +20 -0
  141. package/dist/parsers/jpaNotes.js +138 -0
  142. package/dist/parsers/jpaNotes.js.map +1 -0
  143. package/dist/parsers/jpaRelations.d.ts +4 -0
  144. package/dist/parsers/jpaRelations.js +242 -0
  145. package/dist/parsers/jpaRelations.js.map +1 -0
  146. package/dist/parsers/jpaTypes.d.ts +23 -0
  147. package/dist/parsers/jpaTypes.js +232 -0
  148. package/dist/parsers/jpaTypes.js.map +1 -0
  149. package/dist/styles.d.ts +4 -2
  150. package/dist/styles.js +7 -1
  151. package/dist/styles.js.map +1 -1
  152. package/package.json +10 -1
@@ -0,0 +1,686 @@
1
+ import { findModel } from '../ir.js';
2
+ import { toSnakeCase } from '../naming.js';
3
+ import { prismaOnlyWarnings } from '../prismaOnlyConstructs.js';
4
+ import { normalizeSchema } from '../transforms.js';
5
+ import { buildColumn, isPostgres, } from './efcoreColumns.js';
6
+ import { DBCONTEXT_MEMBER_NAMES, DEFAULT_EFCORE_NAMESPACE, RESERVED_MEMBER_NAMES, RESERVED_TYPE_NAMES, pascalIdentifier, navigationName, pluralize, repairedIdentifier, uniqueName, } from './efcoreNames.js';
7
+ import { renderFiles } from './efcoreRender.js';
8
+ /**
9
+ * Writes Entity Framework Core 8 models (C#, `#nullable enable`): one entity class per model, a C# enum per
10
+ * enum, and an `AppDbContext` with a `DbSet<T>` for every entity. Two styles:
11
+ *
12
+ * - `annotations` (the default) puts what data annotations can say on the classes (`[Table]`, `[Column]`,
13
+ * `[Key]`, `[MaxLength]`, `[Precision]`, `[Index]`, `[ForeignKey]`, `[InverseProperty]`, `[DatabaseGenerated]`)
14
+ * and writes the rest (defaults, enum conversions, delete behaviors, many-to-many join tables) with the fluent
15
+ * API in `OnModelCreating`.
16
+ * - `fluent` writes plain classes and one `IEntityTypeConfiguration<T>` per entity that holds all of the mapping.
17
+ *
18
+ * Every table, column, key, index and foreign key is written explicitly, so the output creates the schema of the
19
+ * IR and the EF Core reader reads it back.
20
+ */
21
+ export { DEFAULT_EFCORE_NAMESPACE, isValidCsharpNamespace, } from './efcoreNames.js';
22
+ export const EFCORE_STYLES = ['annotations', 'fluent'];
23
+ /** True when a value is one of the EF Core `--style` values. */
24
+ export function isEfcoreStyle(value) {
25
+ return EFCORE_STYLES.includes(value);
26
+ }
27
+ // ---------------------------------------------------------------------------
28
+ // Entry point
29
+ // ---------------------------------------------------------------------------
30
+ export function emitEfcore(schema, options) {
31
+ const prepared = options.naming === 'normalize' ? normalizeSchema(schema) : schema;
32
+ const warnings = prismaOnlyWarnings(prepared, true).filter(isUnhandledWarning);
33
+ if (options.provider === 'mongodb') {
34
+ warnings.push('Provider "mongodb": EF Core is relational (use the MongoDB EF Core provider by hand); PostgreSQL column types were written.');
35
+ }
36
+ if (prepared.models.length === 0) {
37
+ return { files: {}, warnings };
38
+ }
39
+ const plan = planSchema(prepared, options, warnings);
40
+ return { files: renderFiles(plan), warnings: dedupe(warnings) };
41
+ }
42
+ function dedupe(values) {
43
+ return [...new Set(values)];
44
+ }
45
+ /** Drops the Prisma-only notes for things this emitter writes (schemas, views, defaults, composite keys, index details). */
46
+ function isUnhandledWarning(warning) {
47
+ if (warning.includes('the database default expression')) {
48
+ return false;
49
+ }
50
+ if (warning.includes('(Prisma Unsupported)')) {
51
+ return false;
52
+ }
53
+ if (warning.includes('is generated by Prisma Client and was dropped')) {
54
+ return false;
55
+ }
56
+ if (warning.includes('has no equivalent here; the relation was dropped')) {
57
+ return false;
58
+ }
59
+ if (warning.includes('the index type, clustering, sort order')) {
60
+ return false;
61
+ }
62
+ return !(warning.includes('(@@schema) was ignored') && !warning.startsWith('enum '));
63
+ }
64
+ // ---------------------------------------------------------------------------
65
+ // Planning
66
+ // ---------------------------------------------------------------------------
67
+ const DELETE_BEHAVIORS = {
68
+ cascade: 'Cascade',
69
+ setNull: 'SetNull',
70
+ restrict: 'Restrict',
71
+ noAction: 'NoAction',
72
+ setDefault: 'NoAction',
73
+ };
74
+ function planSchema(schema, options, warnings) {
75
+ const style = options.style ?? 'annotations';
76
+ const typeNames = new Set(RESERVED_TYPE_NAMES);
77
+ typeNames.add('AppDbContext');
78
+ const warn = (message) => {
79
+ warnings.push(message);
80
+ };
81
+ const enums = planEnums(schema, typeNames, warn);
82
+ const plans = new Map();
83
+ const dbSets = new Set(DBCONTEXT_MEMBER_NAMES);
84
+ for (const model of schema.models) {
85
+ const rawClass = /^[A-Z][A-Za-z0-9]*$/.test(model.name)
86
+ ? model.name
87
+ : (pascalIdentifier(model.name) ?? repairedIdentifier(model.name));
88
+ const reserved = typeNames.has(rawClass);
89
+ const className = uniqueName(reserved ? `${rawClass}Entity` : rawClass, typeNames);
90
+ if (className !== rawClass) {
91
+ warn(`${model.name}: "${rawClass}" is a name the generated code uses or another type has, so the class was named "${className}".`);
92
+ }
93
+ const conventional = /^[A-Z][A-Za-z0-9]*$/.test(model.tableName) &&
94
+ model.tableName !== className &&
95
+ !dbSets.has(model.tableName);
96
+ const dbSet = uniqueName(conventional ? model.tableName : pluralize(className), dbSets);
97
+ plans.set(model.name, {
98
+ model,
99
+ className,
100
+ dbSet,
101
+ tableName: model.tableName,
102
+ needsTable: model.tableName !== dbSet,
103
+ schema: model.schema,
104
+ used: new Set([className, ...RESERVED_MEMBER_NAMES]),
105
+ fieldProps: new Map(),
106
+ relationFks: new Map(),
107
+ relationNavs: new Map(),
108
+ columns: [],
109
+ navs: [],
110
+ keyProps: [],
111
+ keyless: false,
112
+ isView: model.isView === true,
113
+ keyName: model.primaryKeyName,
114
+ alternateKeys: [],
115
+ indexes: [],
116
+ links: [],
117
+ manyToMany: [],
118
+ autoUpdated: [],
119
+ });
120
+ }
121
+ // Property and navigation names ----------------------------------------------------------------
122
+ for (const model of schema.models) {
123
+ nameMembers(planOf(plans, model), warn);
124
+ }
125
+ const links = [];
126
+ const manyToMany = [];
127
+ for (const model of schema.models) {
128
+ planRelations(schema, plans, planOf(plans, model), links, manyToMany, warn);
129
+ }
130
+ for (const link of links) {
131
+ link.owner.links.push(link);
132
+ }
133
+ for (const join of manyToMany) {
134
+ join.owner.manyToMany.push(join);
135
+ }
136
+ // Keys, columns and indexes ----------------------------------------------------------------------
137
+ const context = { provider: options.provider, enums, warn };
138
+ for (const model of schema.models) {
139
+ planAlternateKeys(schema, planOf(plans, model));
140
+ }
141
+ for (const model of schema.models) {
142
+ planColumns(planOf(plans, model), context);
143
+ }
144
+ for (const model of schema.models) {
145
+ planIndexes(planOf(plans, model), options.provider, warn);
146
+ }
147
+ return {
148
+ schema,
149
+ style,
150
+ namespace: options.namespace ?? DEFAULT_EFCORE_NAMESPACE,
151
+ header: options.header !== false,
152
+ provider: options.provider,
153
+ models: schema.models.map((model) => planOf(plans, model)),
154
+ enums: [...enums.values()],
155
+ };
156
+ }
157
+ function planOf(plans, model) {
158
+ const plan = plans.get(model.name);
159
+ if (plan === undefined) {
160
+ throw new Error(`No plan exists for the model "${model.name}".`);
161
+ }
162
+ return plan;
163
+ }
164
+ /** Writes each IR enum as a C# enum and decides how its stored values are mapped. */
165
+ function planEnums(schema, typeNames, warn) {
166
+ const plans = new Map();
167
+ for (const enumeration of schema.enums) {
168
+ const raw = /^[A-Z][A-Za-z0-9]*$/.test(enumeration.name)
169
+ ? enumeration.name
170
+ : (pascalIdentifier(enumeration.name) ??
171
+ repairedIdentifier(enumeration.name));
172
+ const typeName = uniqueName(typeNames.has(raw) ? `${raw}Enum` : raw, typeNames);
173
+ if (typeName !== raw) {
174
+ warn(`enum ${enumeration.name}: "${raw}" is a name the generated code uses or another type has, so the enum was named "${typeName}".`);
175
+ }
176
+ const used = new Set();
177
+ const members = new Map();
178
+ for (const value of enumeration.values) {
179
+ const name = uniqueName(repairedIdentifier(value.name), used);
180
+ if (name !== value.name) {
181
+ warn(`enum ${enumeration.name}: the member "${value.name}" was written as "${name}".`);
182
+ }
183
+ members.set(value.name, name);
184
+ }
185
+ const numeric = enumeration.values.length > 0 &&
186
+ enumeration.values.every((value) => /^-?\d+$/.test(value.dbValue));
187
+ const namesMatch = enumeration.values.every((value) => members.get(value.name) === value.dbValue);
188
+ plans.set(enumeration.name, {
189
+ enumeration,
190
+ typeName,
191
+ members,
192
+ numeric,
193
+ namesMatch,
194
+ converterName: () => uniqueName(`${typeName}Converter`, typeNames),
195
+ });
196
+ }
197
+ return plans;
198
+ }
199
+ /** Gives every field and relation of a model its C# names. */
200
+ function nameMembers(plan, warn) {
201
+ const model = plan.model;
202
+ for (const field of model.fields) {
203
+ const base = pascalIdentifier(field.name) ?? repairedIdentifier(field.name);
204
+ const name = uniqueName(RESERVED_MEMBER_NAMES.has(base) || base === plan.className
205
+ ? `${base}Value`
206
+ : base, plan.used);
207
+ if (name !== base) {
208
+ warn(`${model.name}.${field.name}: "${base}" cannot be a property of ${plan.className}, so the property was named "${name}".`);
209
+ }
210
+ plan.fieldProps.set(field.name, name);
211
+ }
212
+ for (const relation of model.relations) {
213
+ if (relation.kind === 'manyToMany') {
214
+ continue;
215
+ }
216
+ const nav = navigationName(navBase(relation.name, plan), plan.used);
217
+ plan.relationNavs.set(relation.name, nav);
218
+ plan.relationFks.set(relation.name, uniqueName(`${nav}Id`, plan.used));
219
+ }
220
+ for (const key of model.compositeForeignKeys ?? []) {
221
+ plan.relationNavs.set(`composite:${key.name}`, navigationName(navBase(key.name, plan), plan.used));
222
+ }
223
+ for (const relation of model.relations) {
224
+ if (relation.kind === 'manyToMany') {
225
+ plan.relationNavs.set(relation.name, navigationName(navBase(relation.name, plan), plan.used));
226
+ }
227
+ }
228
+ }
229
+ function navBase(name, plan) {
230
+ const base = pascalIdentifier(name) ?? repairedIdentifier(name);
231
+ return RESERVED_MEMBER_NAMES.has(base) || base === plan.className
232
+ ? `${base}Ref`
233
+ : base;
234
+ }
235
+ /** The field a foreign key copies its type from: the referenced column, or the single primary key. */
236
+ function referencedField(schema, target, toField, depth = 0) {
237
+ if (depth > 5) {
238
+ return undefined;
239
+ }
240
+ const explicit = toField === undefined
241
+ ? undefined
242
+ : target.fields.find((field) => field.name === toField || field.columnName === toField);
243
+ if (explicit !== undefined) {
244
+ return explicit;
245
+ }
246
+ if (target.compositePrimaryKey !== undefined) {
247
+ const only = target.compositePrimaryKey.length === 1
248
+ ? target.compositePrimaryKey[0]
249
+ : undefined;
250
+ return target.fields.find((field) => field.name === only);
251
+ }
252
+ const key = target.fields.find((field) => field.isPrimaryKey);
253
+ if (key !== undefined) {
254
+ return key;
255
+ }
256
+ const primary = target.relations.find((relation) => relation.isPrimaryKey === true);
257
+ const chained = primary === undefined ? undefined : findModel(schema, primary.targetModel);
258
+ return primary === undefined || chained === undefined
259
+ ? undefined
260
+ : referencedField(schema, chained, primary.toField, depth + 1);
261
+ }
262
+ /** True when the model has a single-column primary key. */
263
+ function hasSingleKey(model) {
264
+ if (model.compositePrimaryKey !== undefined) {
265
+ return model.compositePrimaryKey.length === 1;
266
+ }
267
+ return (model.fields.filter((field) => field.isPrimaryKey).length +
268
+ model.relations.filter((relation) => relation.isPrimaryKey === true).length ===
269
+ 1);
270
+ }
271
+ function planRelations(schema, plans, plan, links, manyToMany, warn) {
272
+ const model = plan.model;
273
+ for (const relation of model.relations) {
274
+ const label = `${model.name}.${relation.name}`;
275
+ const target = findModel(schema, relation.targetModel);
276
+ const targetPlan = plans.get(relation.targetModel);
277
+ if (target === undefined || targetPlan === undefined) {
278
+ warn(`${label}: the model ${relation.targetModel} is not in the input, so the relation was skipped${relation.kind === 'manyToMany' ? '' : ' and its column left out'}.`);
279
+ plan.relationFks.delete(relation.name);
280
+ plan.relationNavs.delete(relation.name);
281
+ continue;
282
+ }
283
+ if (relation.kind === 'manyToMany') {
284
+ planManyToMany(plan, targetPlan, relation, manyToMany, warn);
285
+ continue;
286
+ }
287
+ const key = referencedField(schema, target, relation.toField);
288
+ const nav = plan.relationNavs.get(relation.name);
289
+ const foreignKey = plan.relationFks.get(relation.name);
290
+ if (key === undefined || nav === undefined || foreignKey === undefined) {
291
+ warn(`${label}: ${relation.targetModel} has no single-column key to reference, so the relation was skipped and its column left out.`);
292
+ plan.relationFks.delete(relation.name);
293
+ plan.relationNavs.delete(relation.name);
294
+ continue;
295
+ }
296
+ const oneToOne = relation.kind === 'oneToOne';
297
+ const inverse = relation.relatedName === undefined
298
+ ? undefined
299
+ : inverseName(targetPlan, relation.relatedName);
300
+ const behavior = DELETE_BEHAVIORS[relation.onDelete];
301
+ if (relation.onDelete === 'setDefault') {
302
+ warn(`${label}: EF Core has no SET DEFAULT delete behavior; NO ACTION was written.`);
303
+ }
304
+ const required = !relation.isNullable;
305
+ if (required && relation.onDelete === 'setNull') {
306
+ warn(`${label}: SET NULL on a required relation; the column is NOT NULL, so deleting a ${relation.targetModel} that is referenced fails.`);
307
+ }
308
+ const isDefault = required
309
+ ? relation.onDelete === 'cascade'
310
+ : relation.onDelete === 'noAction';
311
+ const toKey = relation.toField === undefined
312
+ ? undefined
313
+ : target.fields.find((field) => field.name === relation.toField ||
314
+ field.columnName === relation.toField);
315
+ links.push({
316
+ owner: plan,
317
+ target: targetPlan,
318
+ relationName: relation.name,
319
+ relation,
320
+ nav,
321
+ inverse,
322
+ oneToOne,
323
+ foreignKeys: [foreignKey],
324
+ principalKeys: toKey !== undefined && !toKey.isPrimaryKey
325
+ ? [targetPlan.fieldProps.get(toKey.name) ?? toKey.name]
326
+ : undefined,
327
+ behavior: isDefault ? undefined : behavior,
328
+ required,
329
+ constraintName: relation.constraintName,
330
+ keyField: key,
331
+ });
332
+ if (inverse !== undefined) {
333
+ targetPlan.navs.push({
334
+ property: inverse,
335
+ typeName: plan.className,
336
+ collection: !oneToOne,
337
+ nullable: oneToOne,
338
+ inverseOf: { className: plan.className, property: nav },
339
+ });
340
+ }
341
+ }
342
+ for (const key of model.compositeForeignKeys ?? []) {
343
+ planCompositeKey(schema, plans, plan, key, links, warn);
344
+ }
345
+ }
346
+ function inverseName(target, relatedName) {
347
+ const base = pascalIdentifier(relatedName) ?? repairedIdentifier(relatedName);
348
+ const safe = RESERVED_MEMBER_NAMES.has(base) || base === target.className
349
+ ? `${base}Ref`
350
+ : base;
351
+ return navigationName(safe, target.used);
352
+ }
353
+ function planCompositeKey(schema, plans, plan, key, links, warn) {
354
+ const label = `${plan.model.name}.${key.name}`;
355
+ const target = findModel(schema, key.targetModel);
356
+ const targetPlan = plans.get(key.targetModel);
357
+ const nav = plan.relationNavs.get(`composite:${key.name}`);
358
+ if (target === undefined || targetPlan === undefined || nav === undefined) {
359
+ warn(`${label}: the model ${key.targetModel} is not in the input, so the composite foreign key was skipped.`);
360
+ return;
361
+ }
362
+ const localProps = key.fields.map((name) => plan.fieldProps.get(name));
363
+ const principalProps = key.references.map((name) => targetPlan.fieldProps.get(name));
364
+ if (localProps.some((name) => name === undefined) ||
365
+ principalProps.some((name) => name === undefined)) {
366
+ warn(`${label}: a column of the composite foreign key is not a field of the models, so it was skipped.`);
367
+ return;
368
+ }
369
+ const locals = localProps.filter((name) => name !== undefined);
370
+ const principals = principalProps.filter((name) => name !== undefined);
371
+ const oneToOne = key.kind === 'oneToOne';
372
+ const inverse = key.relatedName === undefined
373
+ ? undefined
374
+ : inverseName(targetPlan, key.relatedName);
375
+ const required = !key.isNullable;
376
+ if (required && key.onDelete === 'setNull') {
377
+ warn(`${label}: SET NULL on a required relation; the columns are NOT NULL, so deleting a ${key.targetModel} that is referenced fails.`);
378
+ }
379
+ const isPrimary = sameList(key.references, target.compositePrimaryKey ?? []);
380
+ links.push({
381
+ owner: plan,
382
+ target: targetPlan,
383
+ relationName: key.name,
384
+ composite: key,
385
+ nav,
386
+ inverse,
387
+ oneToOne,
388
+ foreignKeys: locals,
389
+ principalKeys: isPrimary ? undefined : principals,
390
+ behavior: DELETE_BEHAVIORS[key.onDelete],
391
+ required,
392
+ constraintName: key.constraintName,
393
+ keyField: undefined,
394
+ });
395
+ if (key.onDelete === 'setDefault') {
396
+ warn(`${label}: EF Core has no SET DEFAULT delete behavior; NO ACTION was written.`);
397
+ }
398
+ if (key.onUpdate !== undefined) {
399
+ warn(`${label}: the onUpdate action "${key.onUpdate}" is not supported by this format and was ignored.`);
400
+ }
401
+ if (inverse !== undefined) {
402
+ targetPlan.navs.push({
403
+ property: inverse,
404
+ typeName: plan.className,
405
+ collection: !oneToOne,
406
+ nullable: oneToOne,
407
+ inverseOf: { className: plan.className, property: nav },
408
+ });
409
+ }
410
+ }
411
+ function sameList(first, second) {
412
+ return (first.length === second.length &&
413
+ first.every((value, index) => value === second[index]));
414
+ }
415
+ function planManyToMany(plan, targetPlan, relation, manyToMany, warn) {
416
+ const label = `${plan.model.name}.${relation.name}`;
417
+ const nav = plan.relationNavs.get(relation.name);
418
+ if (nav === undefined ||
419
+ !hasSingleKey(plan.model) ||
420
+ !hasSingleKey(targetPlan.model)) {
421
+ warn(`${label}: a many-to-many relation needs a single-column key on both models; the relation was skipped.`);
422
+ plan.relationNavs.delete(relation.name);
423
+ return;
424
+ }
425
+ const isSelf = plan.model.name === relation.targetModel;
426
+ const owner = toSnakeCase(plan.model.name);
427
+ const other = toSnakeCase(relation.targetModel);
428
+ const inverse = relation.relatedName === undefined
429
+ ? undefined
430
+ : inverseName(targetPlan, relation.relatedName);
431
+ manyToMany.push({
432
+ owner: plan,
433
+ target: targetPlan,
434
+ relationName: relation.name,
435
+ nav,
436
+ inverse,
437
+ joinTable: `${plan.model.tableName}_${relation.name}`,
438
+ ownerColumn: `${isSelf ? 'from_' : ''}${owner}_id`,
439
+ targetColumn: `${isSelf ? 'to_' : ''}${other}_id`,
440
+ });
441
+ plan.navs.push({
442
+ property: nav,
443
+ typeName: targetPlan.className,
444
+ collection: true,
445
+ nullable: false,
446
+ });
447
+ if (inverse !== undefined) {
448
+ targetPlan.navs.push({
449
+ property: inverse,
450
+ typeName: plan.className,
451
+ collection: true,
452
+ nullable: false,
453
+ });
454
+ }
455
+ }
456
+ /** Fields that another model's foreign key points at (`toField`) need an alternate key, not an index. */
457
+ function planAlternateKeys(schema, plan) {
458
+ const model = plan.model;
459
+ for (const other of schema.models) {
460
+ for (const relation of other.relations) {
461
+ if (relation.targetModel !== model.name ||
462
+ relation.toField === undefined) {
463
+ continue;
464
+ }
465
+ const field = model.fields.find((candidate) => candidate.name === relation.toField ||
466
+ candidate.columnName === relation.toField);
467
+ const property = field === undefined || field.isPrimaryKey
468
+ ? undefined
469
+ : plan.fieldProps.get(field.name);
470
+ if (property !== undefined &&
471
+ !plan.alternateKeys.some((key) => sameList(key, [property]))) {
472
+ plan.alternateKeys.push([property]);
473
+ }
474
+ }
475
+ }
476
+ for (const other of schema.models) {
477
+ for (const key of other.compositeForeignKeys ?? []) {
478
+ if (key.targetModel !== model.name) {
479
+ continue;
480
+ }
481
+ if (sameList(key.references, model.compositePrimaryKey ?? [])) {
482
+ continue;
483
+ }
484
+ const properties = key.references
485
+ .map((name) => plan.fieldProps.get(name))
486
+ .filter((name) => name !== undefined);
487
+ if (properties.length === key.references.length &&
488
+ !plan.alternateKeys.some((existing) => sameList(existing, properties))) {
489
+ plan.alternateKeys.push(properties);
490
+ }
491
+ }
492
+ }
493
+ }
494
+ /** Builds the columns of a model: its fields, then the foreign key column of every relation. */
495
+ function planColumns(plan, context) {
496
+ const model = plan.model;
497
+ const keyNames = model.compositePrimaryKey ?? [
498
+ ...model.fields
499
+ .filter((field) => field.isPrimaryKey)
500
+ .map((field) => field.name),
501
+ ...model.relations
502
+ .filter((relation) => relation.isPrimaryKey === true)
503
+ .map((relation) => relation.name),
504
+ ];
505
+ const keyProps = [];
506
+ for (const name of keyNames) {
507
+ const property = plan.fieldProps.get(name) ?? plan.relationFks.get(name);
508
+ if (property === undefined) {
509
+ context.warn(`${model.name}: the primary key member "${name}" is not a field or relation of the model and was left out of the key.`);
510
+ }
511
+ else {
512
+ keyProps.push(property);
513
+ }
514
+ }
515
+ plan.keyProps = keyProps;
516
+ const singleKey = keyProps.length === 1;
517
+ plan.keyless = keyProps.length === 0;
518
+ if (plan.keyless && !plan.isView) {
519
+ context.warn(`${model.name}: the model has no primary key; EF Core needs a key, so the class was written as keyless ([Keyless]).`);
520
+ // EF Core maps a keyless entity to no table unless the table is named explicitly.
521
+ plan.needsTable = true;
522
+ }
523
+ const columns = [];
524
+ for (const field of model.fields) {
525
+ const property = plan.fieldProps.get(field.name) ?? field.name;
526
+ const isKey = keyProps.includes(property);
527
+ const spec = buildColumn(context, `${model.name}.${field.name}`, isKey && keyProps.length > 1 ? { ...field, isUnique: false } : field, { propertyName: property, isKey, singleKey });
528
+ columns.push({ property, spec, isKey, fieldName: field.name });
529
+ if (spec.autoUpdated) {
530
+ plan.autoUpdated.push(property);
531
+ }
532
+ }
533
+ for (const link of plan.links) {
534
+ if (link.relation === undefined) {
535
+ continue;
536
+ }
537
+ const relation = link.relation;
538
+ const key = link.keyField;
539
+ const property = link.foreignKeys[0];
540
+ if (key === undefined || property === undefined) {
541
+ continue;
542
+ }
543
+ const isKey = keyProps.includes(property);
544
+ // The column copies the type of the key it references, without the key's own default or flags.
545
+ const column = {
546
+ name: relation.name,
547
+ columnName: relation.columnName,
548
+ type: key.type,
549
+ isPrimaryKey: false,
550
+ isUnique: false,
551
+ isNullable: relation.isNullable,
552
+ isAutoUpdated: false,
553
+ ...(key.maxLength === undefined ? {} : { maxLength: key.maxLength }),
554
+ ...(key.maxDigits === undefined ? {} : { maxDigits: key.maxDigits }),
555
+ ...(key.decimalPlaces === undefined
556
+ ? {}
557
+ : { decimalPlaces: key.decimalPlaces }),
558
+ ...(key.enumName === undefined ? {} : { enumName: key.enumName }),
559
+ };
560
+ const spec = buildColumn(context, `${model.name}.${relation.name}`, column, { propertyName: property, isKey, singleKey });
561
+ columns.push({ property, spec, isKey, relationName: relation.name });
562
+ }
563
+ plan.columns = columns;
564
+ // Navigations in declaration order: the references of this model, then its collections.
565
+ const references = plan.links.map((link) => {
566
+ const target = link.target;
567
+ return {
568
+ property: link.nav,
569
+ typeName: target.className,
570
+ collection: false,
571
+ nullable: !link.required,
572
+ foreignKeys: link.foreignKeys,
573
+ ...(link.inverse === undefined
574
+ ? {}
575
+ : {
576
+ inverseOf: { className: target.className, property: link.inverse },
577
+ }),
578
+ };
579
+ });
580
+ plan.navs = [...references, ...plan.navs];
581
+ }
582
+ /** Collects the indexes of a model: unique fields, one-to-one keys and the declared indexes. */
583
+ function planIndexes(plan, provider, warn) {
584
+ const model = plan.model;
585
+ const indexes = [];
586
+ const seen = new Set();
587
+ const alternate = new Set(plan.alternateKeys.map((key) => key.join(',')));
588
+ const add = (index) => {
589
+ const signature = `${index.unique ? 'u' : 'i'}|${index.properties.join(',')}|${index.name ?? ''}`;
590
+ if (seen.has(signature)) {
591
+ return;
592
+ }
593
+ seen.add(signature);
594
+ indexes.push(index);
595
+ };
596
+ const isKey = (properties) => sameList(properties, plan.keyProps);
597
+ const covered = (properties) => indexes.some((index) => index.unique && sameList(index.properties, properties));
598
+ for (const field of model.fields) {
599
+ const property = plan.fieldProps.get(field.name);
600
+ if (property === undefined ||
601
+ !field.isUnique ||
602
+ field.isPrimaryKey ||
603
+ isKey([property])) {
604
+ continue;
605
+ }
606
+ if (alternate.has(property)) {
607
+ continue;
608
+ }
609
+ add({
610
+ properties: [property],
611
+ unique: true,
612
+ descending: [],
613
+ ...(field.uniqueName === undefined ? {} : { name: field.uniqueName }),
614
+ });
615
+ }
616
+ for (const link of plan.links) {
617
+ if (link.relation?.kind === 'oneToOne' &&
618
+ link.relation.isPrimaryKey !== true &&
619
+ !covered(link.foreignKeys)) {
620
+ add({
621
+ properties: link.foreignKeys,
622
+ unique: true,
623
+ descending: [],
624
+ });
625
+ }
626
+ }
627
+ for (const index of model.indexes) {
628
+ const properties = [];
629
+ let skip = false;
630
+ for (const name of index.fields) {
631
+ const property = plan.fieldProps.get(name) ?? plan.relationFks.get(name);
632
+ if (property === undefined) {
633
+ warn(`${model.name}: the index (${index.fields.join(', ')}) refers to "${name}", which is not a field or relation of the model; the index was skipped.`);
634
+ skip = true;
635
+ break;
636
+ }
637
+ properties.push(property);
638
+ }
639
+ if (skip) {
640
+ continue;
641
+ }
642
+ if (index.isUnique &&
643
+ index.name === undefined &&
644
+ alternate.has(properties.join(','))) {
645
+ continue;
646
+ }
647
+ add(indexOf(model, index, properties, provider, warn));
648
+ }
649
+ plan.indexes = indexes;
650
+ }
651
+ function indexOf(model, index, properties, provider, warn) {
652
+ const label = `${model.name} index (${index.fields.join(', ')})`;
653
+ const descending = index.fields.map((name) => index.fieldOptions?.[name]?.sort === 'desc');
654
+ let method;
655
+ if (index.method !== undefined) {
656
+ if (isPostgres(provider)) {
657
+ method = index.method.toLowerCase();
658
+ }
659
+ else {
660
+ warn(`${label}: the ${index.method} index method only exists in PostgreSQL (Npgsql); it was dropped.`);
661
+ }
662
+ }
663
+ const unsupported = [];
664
+ for (const options of Object.values(index.fieldOptions ?? {})) {
665
+ if (options.length !== undefined) {
666
+ unsupported.push('prefix length');
667
+ }
668
+ if (options.ops !== undefined) {
669
+ unsupported.push('operator class');
670
+ }
671
+ }
672
+ if (index.clustered !== undefined) {
673
+ unsupported.push('clustering');
674
+ }
675
+ if (unsupported.length > 0) {
676
+ warn(`${label}: the ${[...new Set(unsupported)].join(' and ')} option has no EF Core equivalent and was dropped.`);
677
+ }
678
+ return {
679
+ properties,
680
+ unique: index.isUnique,
681
+ descending: descending.some((flag) => flag) ? descending : [],
682
+ ...(index.name === undefined ? {} : { name: index.name }),
683
+ ...(method === undefined ? {} : { method }),
684
+ };
685
+ }
686
+ //# sourceMappingURL=efcore.js.map