@autobe/compiler 0.30.0-dev.20260315 → 0.30.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 (52) hide show
  1. package/LICENSE +661 -661
  2. package/lib/database/validateDatabaseApplication.js +318 -318
  3. package/lib/raw/AutoBeCompilerCommonTemplate.js +5 -5
  4. package/lib/raw/AutoBeCompilerCommonTemplate.js.map +1 -1
  5. package/lib/raw/AutoBeCompilerInterfaceTemplate.js +4 -4
  6. package/lib/raw/AutoBeCompilerInterfaceTemplate.js.map +1 -1
  7. package/lib/raw/AutoBeCompilerRealizeTemplate.js +27 -27
  8. package/lib/raw/AutoBeCompilerRealizeTemplate.js.map +1 -1
  9. package/lib/raw/AutoBeCompilerRealizeTemplateOfPostgres.js +4 -4
  10. package/lib/raw/AutoBeCompilerRealizeTemplateOfPostgres.js.map +1 -1
  11. package/lib/raw/AutoBeCompilerRealizeTemplateOfSQLite.js +4 -4
  12. package/lib/raw/AutoBeCompilerRealizeTemplateOfSQLite.js.map +1 -1
  13. package/lib/raw/AutoBeCompilerTestTemplate.js +8 -8
  14. package/lib/raw/AutoBeCompilerTestTemplate.js.map +1 -1
  15. package/lib/raw/nestjs.json +1640 -1640
  16. package/lib/raw/test.json +130 -130
  17. package/package.json +4 -4
  18. package/src/AutoBeCompiler.ts +93 -93
  19. package/src/AutoBeTypeScriptCompiler.ts +136 -136
  20. package/src/database/AutoBeDatabaseCompiler.ts +48 -48
  21. package/src/database/validateDatabaseApplication.ts +873 -873
  22. package/src/index.ts +5 -5
  23. package/src/interface/AutoBeInterfaceCompiler.ts +79 -79
  24. package/src/raw/AutoBeCompilerCommonTemplate.ts +5 -5
  25. package/src/raw/AutoBeCompilerInterfaceTemplate.ts +4 -4
  26. package/src/raw/AutoBeCompilerRealizeTemplate.ts +27 -27
  27. package/src/raw/AutoBeCompilerRealizeTemplateOfPostgres.ts +4 -4
  28. package/src/raw/AutoBeCompilerRealizeTemplateOfSQLite.ts +4 -4
  29. package/src/raw/AutoBeCompilerTestTemplate.ts +8 -8
  30. package/src/raw/nestjs.json +1640 -1640
  31. package/src/raw/test.json +130 -130
  32. package/src/realize/AutoBeRealizeCompiler.ts +42 -42
  33. package/src/realize/testRealizeProject.ts +78 -78
  34. package/src/realize/writeRealizeControllers.ts +217 -217
  35. package/src/test/AutoBeTestCompiler.ts +112 -112
  36. package/src/test/programmers/AutoBeTestAccessorProgrammer.ts +42 -42
  37. package/src/test/programmers/AutoBeTestFunctionalProgrammer.ts +87 -87
  38. package/src/test/programmers/AutoBeTestLiteralProgrammer.ts +65 -65
  39. package/src/test/programmers/AutoBeTestOperatorProgrammer.ts +84 -84
  40. package/src/test/programmers/AutoBeTestPredicateProgrammer.ts +131 -131
  41. package/src/test/programmers/AutoBeTestRandomProgrammer.ts +304 -304
  42. package/src/test/programmers/AutoBeTestStatementProgrammer.ts +154 -154
  43. package/src/test/programmers/IAutoBeTestApiFunction.ts +6 -6
  44. package/src/test/programmers/IAutoBeTestProgrammerContext.ts +11 -11
  45. package/src/test/programmers/writeTestExpression.ts +29 -29
  46. package/src/test/programmers/writeTestFunction.ts +103 -103
  47. package/src/test/programmers/writeTestStatement.ts +19 -19
  48. package/src/utils/ArrayUtil.ts +21 -21
  49. package/src/utils/FilePrinter.ts +67 -67
  50. package/src/utils/ProcessUtil.ts +14 -14
  51. package/src/utils/shrinkCompileResult.ts +16 -16
  52. package/README.md +0 -261
@@ -1,873 +1,873 @@
1
- import { AutoBeDatabase, IAutoBeDatabaseValidation } from "@autobe/interface";
2
- import { AutoBeEscaper, MapUtil, StringUtil } from "@autobe/utils";
3
- import { HashMap, hash } from "tstl";
4
-
5
- export function validateDatabaseApplication(
6
- application: AutoBeDatabase.IApplication,
7
- ): IAutoBeDatabaseValidation {
8
- const dict: Map<string, IModelContainer> = new Map(
9
- application.files
10
- .map((file, fi) =>
11
- file.models.map(
12
- (model, mi) =>
13
- [
14
- model.name,
15
- {
16
- file,
17
- model,
18
- fileIndex: fi,
19
- modelIndex: mi,
20
- },
21
- ] satisfies [string, IModelContainer],
22
- ),
23
- )
24
- .flat(),
25
- );
26
- const errors: IAutoBeDatabaseValidation.IError[] = [
27
- ...validateDuplicatedFiles(application),
28
- ...validateDuplicatedModels(application),
29
- ...application.files
30
- .map((file, fi) =>
31
- file.models.map((model, mi) => {
32
- const accessor: string = `application.files[${fi}].models[${mi}]`;
33
- return [
34
- ...validateDuplicatedFields(dict, model, accessor),
35
- ...validateDuplicatedIndexes(model, accessor),
36
- ...validateValidNames(model, accessor),
37
- ...validateIndexes(model, accessor),
38
- ...validateReferences(model, accessor, dict),
39
- ...validateDuplicatedRelationOppositeNames(dict, model),
40
- ];
41
- }),
42
- )
43
- .flat(2),
44
- ];
45
- return errors.length === 0
46
- ? {
47
- success: true,
48
- data: application,
49
- }
50
- : {
51
- success: false,
52
- data: application,
53
- errors,
54
- };
55
- }
56
-
57
- interface IModelContainer {
58
- file: AutoBeDatabase.IFile;
59
- model: AutoBeDatabase.IModel;
60
- fileIndex: number;
61
- modelIndex: number;
62
- }
63
-
64
- /* -----------------------------------------------------------
65
- DUPLICATES
66
- ----------------------------------------------------------- */
67
- function validateDuplicatedFiles(
68
- app: AutoBeDatabase.IApplication,
69
- ): IAutoBeDatabaseValidation.IError[] {
70
- interface IFileContainer {
71
- file: AutoBeDatabase.IFile;
72
- index: number;
73
- }
74
- const group: Map<string, IFileContainer[]> = new Map();
75
- app.files.forEach((file, fileIndex) => {
76
- const container: IFileContainer = { file, index: fileIndex };
77
- MapUtil.take(group, file.filename, () => []).push(container);
78
- });
79
-
80
- const errors: IAutoBeDatabaseValidation.IError[] = [];
81
- for (const array of group.values())
82
- if (array.length !== 1)
83
- array.forEach((container, i) => {
84
- errors.push({
85
- path: `application.files[${container.index}]`,
86
- table: null,
87
- field: null,
88
- message: StringUtil.trim`
89
- File ${container.file.filename} is duplicated.
90
-
91
- Accessors of the other duplicated files are:
92
-
93
- ${array
94
- .filter((_oppo, j) => i !== j)
95
- .map((oppo) => `- application.files[${oppo.index}]`)
96
- .join("\n")},
97
- `,
98
- });
99
- });
100
- return errors;
101
- }
102
-
103
- function validateDuplicatedModels(
104
- app: AutoBeDatabase.IApplication,
105
- ): IAutoBeDatabaseValidation.IError[] {
106
- const modelContainers: Map<string, IModelContainer[]> = new Map();
107
- app.files.forEach((file, fileIndex) => {
108
- file.models.forEach((model, modelIndex) => {
109
- const container: IModelContainer = {
110
- file,
111
- model,
112
- fileIndex,
113
- modelIndex,
114
- };
115
- MapUtil.take(modelContainers, model.name, () => []).push(container);
116
- });
117
- });
118
-
119
- const errors: IAutoBeDatabaseValidation.IError[] = [];
120
- for (const array of modelContainers.values())
121
- if (array.length !== 1)
122
- array.forEach((container, i) => {
123
- errors.push({
124
- path: `application.files[${container.fileIndex}].models[${container.modelIndex}]`,
125
- table: container.model.name,
126
- field: null,
127
- message: StringUtil.trim`
128
- Model ${container.model.name} is duplicated.
129
-
130
- Accessors of the other duplicated models are:
131
-
132
- ${array
133
- .filter((_oppo, j) => i !== j)
134
- .map(
135
- (oppo) =>
136
- `- application.files[${oppo.fileIndex}].models[${oppo.modelIndex}]`,
137
- )
138
- .join("\n")},
139
- `,
140
- });
141
- });
142
- return errors;
143
- }
144
-
145
- function validateDuplicatedFields(
146
- dict: Map<string, IModelContainer>,
147
- model: AutoBeDatabase.IModel,
148
- accessor: string,
149
- ): IAutoBeDatabaseValidation.IError[] {
150
- const errors: IAutoBeDatabaseValidation.IError[] = [];
151
-
152
- // FIND DUPLICATED FIELDS
153
- const group: Map<string, string[]> = new Map();
154
- MapUtil.take(group, model.primaryField.name, () => []).push(
155
- `${accessor}.primaryField.name`,
156
- );
157
- model.foreignFields.forEach((field, i) => {
158
- MapUtil.take(group, field.name, () => []).push(
159
- `${accessor}.foreignFields[${i}].name`,
160
- );
161
- MapUtil.take(group, field.relation.name, () => []).push(
162
- `${accessor}.foreignFields[${i}].relation.name`,
163
- );
164
- });
165
- model.plainFields.forEach((field, i) =>
166
- MapUtil.take(group, field.name, () => []).push(
167
- `${accessor}.plainFields[${i}].name`,
168
- ),
169
- );
170
- for (const [field, array] of group)
171
- if (array.length !== 1)
172
- array.forEach((path, i) => {
173
- errors.push({
174
- path,
175
- table: model.name,
176
- field,
177
- message: StringUtil.trim`
178
- Field ${field} is duplicated.
179
-
180
- Accessors of the other duplicated fields are:
181
-
182
- ${array
183
- .filter((_oppo, j) => i !== j)
184
- .map((a) => `- ${a}`)
185
- .join("\n")},
186
- `,
187
- });
188
- });
189
-
190
- // FIND TABLE NAMED FIELDS
191
- model.plainFields.forEach((field, i) => {
192
- if (dict.has(field.name) === true)
193
- errors.push({
194
- path: `${accessor}.plainFields[${i}].name`,
195
- table: model.name,
196
- field: field.name,
197
- message: StringUtil.trim`
198
- Field name conflicts with an existing table name.
199
-
200
- **What happened?**
201
- The field "${field.name}" in model "${model.name}" has the same name as another table "${field.name}".
202
- This can cause confusion and potential issues in the generated code.
203
-
204
- **Why is this a problem?**
205
- - Naming conflicts can lead to ambiguous references in your code
206
- - It may cause issues with Prisma's relation inference
207
- - It makes the schema harder to understand and maintain
208
-
209
- **How to fix this:**
210
-
211
- 1. **If this is a denormalization field (pre-calculated value):**
212
- - Consider if this field is really necessary
213
- - If it's storing aggregated data from the related table, it might be better to calculate it dynamically
214
- - Remove the field if it's redundant
215
-
216
- 2. **If this is a legitimate field:**
217
- - Rename the field to be more descriptive
218
- - Good naming examples:
219
- - Instead of "user", use "user_name" or "user_id"
220
- - Instead of "order", use "order_status" or "order_count"
221
- - Instead of "product", use "product_name" or "product_code"
222
-
223
- 3. **Naming best practices:**
224
- - Use specific, descriptive names that indicate the field's purpose
225
- - Avoid using table names as field names
226
- - Consider adding a suffix or prefix to clarify the field's role
227
-
228
- Please rename the field or remove it if unnecessary.
229
- `,
230
- });
231
- });
232
- return errors;
233
- }
234
-
235
- function validateDuplicatedIndexes(
236
- model: AutoBeDatabase.IModel,
237
- accessor: string,
238
- ): IAutoBeDatabaseValidation.IError[] {
239
- const errors: IAutoBeDatabaseValidation.IError[] = [];
240
-
241
- // FIND DUPLICATED INDEXES
242
- const group: HashMap<string[], string[]> = new HashMap(
243
- (x) => hash(...x),
244
- (x, y) => x.length === y.length && x.every((v, i) => v === y[i]),
245
- );
246
- model.uniqueIndexes.forEach((unique, i) =>
247
- group
248
- .take(unique.fieldNames, () => [])
249
- .push(`${accessor}.uniqueIndexes[${i}].fieldNames`),
250
- );
251
- model.plainIndexes.forEach((plain, i) =>
252
- group
253
- .take(plain.fieldNames, () => [])
254
- .push(`${accessor}.plainIndexes[${i}].fieldNames`),
255
- );
256
- model.ginIndexes.forEach((gin, i) =>
257
- group
258
- .take([gin.fieldName], () => [])
259
- .push(`${accessor}.ginIndexes[${i}].fieldName`),
260
- );
261
- for (const { first: fieldNames, second: array } of group)
262
- if (array.length !== 1)
263
- array.forEach((path, i) => {
264
- errors.push({
265
- path,
266
- table: model.name,
267
- field: null,
268
- message: StringUtil.trim`
269
- Duplicated index found (${fieldNames.join(", ")}).
270
-
271
- Accessors of the other duplicated indexes are:
272
-
273
- ${array
274
- .filter((_oppo, j) => i !== j)
275
- .map((a) => `- ${a}`)
276
- .join("\n")},
277
- `,
278
- });
279
- });
280
-
281
- // SUBSET RELATIONS
282
- model.uniqueIndexes.forEach((unique, i) => {
283
- if (unique.fieldNames.length <= 1) return;
284
- unique.fieldNames.forEach((_, j, array) => {
285
- if (j === array.length - 1) return;
286
- const subset: string[] = unique.fieldNames.slice(0, j);
287
- if (
288
- j === 0 &&
289
- model.foreignFields.some(
290
- (f) => f.name === subset[0] && f.unique === true,
291
- )
292
- )
293
- errors.push({
294
- path: `${accessor}.uniqueIndexes[${i}].fieldNames[0]`,
295
- table: model.name,
296
- field: null,
297
- message: StringUtil.trim`
298
- Duplicated unique index found (${subset[0]}).
299
-
300
- You have defined an unique index that is already included,
301
- in the foreign field with unique constraint.
302
-
303
- Remove this unique index to avoid the duplication.
304
- `,
305
- });
306
- const cIndex: number = model.uniqueIndexes.findIndex(
307
- (u) =>
308
- u.fieldNames.length === subset.length &&
309
- u.fieldNames.every((name, k) => name === subset[k]),
310
- );
311
- if (cIndex !== -1)
312
- errors.push({
313
- path: `${accessor}.uniqueIndexes[${i}].fieldNames`,
314
- table: model.name,
315
- field: null,
316
- message: StringUtil.trim`
317
- Redundant subset unique index detected.
318
-
319
- **What is a subset unique index problem?**
320
- When you have a unique index on multiple fields, any subset of those fields is automatically unique too.
321
- This is a fundamental property of unique constraints in databases.
322
-
323
- **Current situation:**
324
- - You have a unique index on: (${unique.fieldNames.join(", ")})
325
- - But there's already a unique index on its subset: (${subset.join(", ")})
326
- - This makes the larger unique index redundant for uniqueness purposes
327
-
328
- **Why is this a problem?**
329
- 1. **Logical redundancy**: If (A) is unique, then (A, B) is automatically unique
330
- 2. **Performance overhead**: Maintaining unnecessary indexes slows down write operations
331
- 3. **Storage waste**: Each index consumes disk space
332
- 4. **Confusion**: It's unclear which uniqueness constraint is the intended one
333
-
334
- **Example to illustrate:**
335
- If email is unique, then (email, name) is automatically unique because:
336
- - No two records can have the same email
337
- - Therefore, no two records can have the same (email, name) combination
338
-
339
- **How to fix:**
340
- Choose one of these solutions based on your needs:
341
-
342
- 1. **If you need uniqueness only:**
343
- - Keep just the subset unique index: (${subset.join(", ")})
344
- - Remove the larger unique index
345
-
346
- 2. **If you need the multi-field index for query performance:**
347
- - Keep the subset as unique index: (${subset.join(", ")})
348
- - Change the larger index to a plain (non-unique) index for performance
349
-
350
- 3. **If the subset unique was added by mistake:**
351
- - Remove the subset unique index
352
- - Keep the multi-field unique index
353
-
354
- Please review your uniqueness requirements and adjust accordingly.
355
- `,
356
- });
357
- });
358
- });
359
-
360
- // SUPERSET RELATIONS
361
- model.plainIndexes.forEach((x, i) => {
362
- model.plainIndexes.forEach((y, j) => {
363
- if (i === j) return;
364
- else if (
365
- x.fieldNames.length < y.fieldNames.length &&
366
- x.fieldNames.every((n, z) => y.fieldNames[z] === n)
367
- )
368
- errors.push({
369
- path: `${accessor}.plainIndexes[${i}].fieldNames`,
370
- table: model.name,
371
- field: null,
372
- message: StringUtil.trim`
373
- Inefficient subset index detected - superset index exists.
374
-
375
- **What is a subset/superset index problem?**
376
- In database indexing, when you have an index on (A, B, C), it can efficiently serve queries
377
- that filter by A, or by (A, B), or by (A, B, C). This is called index prefix matching.
378
-
379
- **Current situation:**
380
- - You have a plain index on: (${x.fieldNames.join(", ")})
381
- - But there's already a plain index on its superset: (${y.fieldNames.join(", ")})
382
- - The subset index is redundant because the superset can handle the same queries
383
-
384
- **Why is this a problem?**
385
- 1. **Query efficiency**: The superset index can handle all queries the subset can
386
- 2. **Storage waste**: You're maintaining two indexes where one would suffice
387
- 3. **Write performance**: Each index slows down INSERT, UPDATE, and DELETE operations
388
- 4. **Maintenance overhead**: More indexes mean more work for the database
389
-
390
- **How indexes work (example):**
391
- If you have an index on (country, city, street):
392
- - ✅ Can efficiently find by country
393
- - ✅ Can efficiently find by country + city
394
- - ✅ Can efficiently find by country + city + street
395
- - ❌ Cannot efficiently find by city alone
396
- - ❌ Cannot efficiently find by street alone
397
-
398
- **How to fix:**
399
- Remove the subset index (${x.fieldNames.join(", ")}) because:
400
- - The superset index (${y.fieldNames.join(", ")}) already covers these queries
401
- - You'll save storage space and improve write performance
402
- - Query performance will remain the same
403
-
404
- **When to keep both indexes:**
405
- Only if the subset index is UNIQUE (which it isn't in this case), as unique
406
- constraints serve a different purpose than performance optimization.
407
-
408
- Please remove the redundant subset index.
409
- `,
410
- });
411
- });
412
- });
413
-
414
- // CONSIDER GIN INDEXES
415
- if (
416
- model.ginIndexes.length !==
417
- new Set(model.ginIndexes.map((g) => g.fieldName)).size
418
- )
419
- errors.push({
420
- path: `${accessor}.ginIndexes`,
421
- table: model.name,
422
- field: null,
423
- message: StringUtil.trim`
424
- Duplicated GIN index found.
425
-
426
- GIN index can only be used once per field.
427
-
428
- Please remove the duplicated GIN indexes.
429
- `,
430
- });
431
-
432
- return errors;
433
- }
434
-
435
- function validateDuplicatedRelationOppositeNames(
436
- dict: Map<string, IModelContainer>,
437
- model: AutoBeDatabase.IModel,
438
- ): IAutoBeDatabaseValidation.IError[] {
439
- interface IOppositeNameContainer {
440
- container: IModelContainer;
441
- foreignField: AutoBeDatabase.IForeignField;
442
- foreignFieldIndex: number;
443
- }
444
- const errors: IAutoBeDatabaseValidation.IError[] = [];
445
- const group: Map<string, IOppositeNameContainer[]> = new Map();
446
-
447
- // Collect all field/relation names in the target model
448
- const targetFieldNames = new Set<string>([
449
- model.primaryField.name,
450
- ...model.foreignFields.map((f) => f.name),
451
- ...model.foreignFields.map((f) => f.relation.name),
452
- ...model.plainFields.map((f) => f.name),
453
- ]);
454
-
455
- for (const c of dict.values()) {
456
- c.model.foreignFields.forEach((ff, i) => {
457
- if (ff.relation.targetModel !== model.name) return;
458
- MapUtil.take(group, ff.relation.oppositeName, () => []).push({
459
- container: c,
460
- foreignField: ff,
461
- foreignFieldIndex: i,
462
- });
463
- });
464
- }
465
-
466
- // Check oppositeName conflicts with target model's existing fields
467
- for (const [oppositeName, array] of group) {
468
- if (targetFieldNames.has(oppositeName)) {
469
- array.forEach((item) => {
470
- const c = item.container;
471
- const ff = item.foreignField;
472
- errors.push({
473
- path: `application.files[${c.fileIndex}].models[${c.modelIndex}].foreignFields[${item.foreignFieldIndex}].relation.oppositeName`,
474
- table: c.model.name,
475
- field: ff.name,
476
- message: StringUtil.trim`
477
- oppositeName "${oppositeName}" conflicts with existing field in target model "${model.name}".
478
-
479
- **What happened?**
480
- The oppositeName "${oppositeName}" would create a reverse relation property in "${model.name}",
481
- but "${model.name}" already has a field or relation with that name.
482
-
483
- **Why is this a problem?**
484
- - Prisma cannot have two properties with the same name in a model
485
- - This will cause Prisma schema compilation errors
486
- - The reverse relation would overwrite or conflict with the existing field
487
-
488
- **How to fix:**
489
- Choose a different oppositeName that doesn't conflict with existing fields in "${model.name}".
490
-
491
- **Naming suggestions:**
492
- - Add a descriptive prefix/suffix: "${oppositeName}List", "${oppositeName}Items", "related${oppositeName.charAt(0).toUpperCase() + oppositeName.slice(1)}"
493
- - Use the source model name: "${c.model.name.replace(/_/g, "").toLowerCase()}s"
494
- `,
495
- });
496
- });
497
- }
498
- }
499
-
500
- // Check duplicate oppositeNames among relations targeting the same model
501
- for (const [oppositeName, array] of group) {
502
- if (array.length === 1) continue;
503
- array.forEach((item, i) => {
504
- const c = item.container;
505
- const ff = item.foreignField;
506
- errors.push({
507
- path: `application.files[${c.fileIndex}].models[${c.modelIndex}].foreignFields[${item.foreignFieldIndex}].relation.oppositeName`,
508
- table: c.model.name,
509
- field: ff.name,
510
- message: StringUtil.trim`
511
- Duplicated relation oppositeName "${oppositeName}" detected on target model "${model.name}".
512
-
513
- **What is oppositeName?**
514
- In Prisma relations, oppositeName defines the name of the reverse relation field
515
- on the target model. It allows the target model to access related records through
516
- this named property.
517
-
518
- **Current situation:**
519
- Multiple foreign key fields from different models are trying to create reverse
520
- relations on "${model.name}" with the same oppositeName "${oppositeName}".
521
-
522
- **Conflicting relations:**
523
- ${array
524
- .filter((_, j) => i !== j)
525
- .map(
526
- (oppo) =>
527
- `- Model "${oppo.container.model.name}", field "${oppo.foreignField.name}" (accessor: application.files[${oppo.container.fileIndex}].models[${oppo.container.modelIndex}].foreignFields[${oppo.foreignFieldIndex}].relation.oppositeName)`,
528
- )
529
- .join("\n")}
530
-
531
- **Why is this a problem?**
532
- - Prisma requires unique relation names within a model
533
- - When "${model.name}" tries to access related records, it won't know which relation to use
534
- - This will cause Prisma schema compilation errors
535
-
536
- **How to fix:**
537
- Each relation pointing to "${model.name}" must have a unique oppositeName.
538
-
539
- **Naming suggestions:**
540
- - Use descriptive names that indicate the relationship's purpose
541
- - Include the source model name for clarity
542
- - Examples:
543
- - Instead of both using "orders", use "customerOrders" and "sellerOrders"
544
- - Instead of both using "users", use "createdByUser" and "assignedToUser"
545
- - For "${c.model.name}" → "${model.name}": consider "${c.model.name.charAt(0).toLowerCase() + c.model.name.slice(1)}s" or a more descriptive name
546
-
547
- Please rename the oppositeName to be unique across all relations targeting "${model.name}".
548
- `,
549
- });
550
- });
551
- }
552
- return errors;
553
- }
554
-
555
- /* -----------------------------------------------------------
556
- VALIDATIONS
557
- ----------------------------------------------------------- */
558
- function validateValidNames(
559
- model: AutoBeDatabase.IModel,
560
- accessor: string,
561
- ): IAutoBeDatabaseValidation.IError[] {
562
- const errors: IAutoBeDatabaseValidation.IError[] = [];
563
- const validate = (props: {
564
- value: string;
565
- accessor: string;
566
- field: string | null;
567
- }): void => {
568
- if (AutoBeEscaper.reserved(props.value))
569
- errors.push({
570
- path: props.accessor,
571
- table: model.name,
572
- field: props.field,
573
- message: StringUtil.trim`
574
- The name "${props.value}" is a system reserved keyword and cannot be used.
575
- `,
576
- });
577
- else if (AutoBeEscaper.variable(props.value) === false)
578
- errors.push({
579
- path: props.accessor,
580
- table: model.name,
581
- field: props.field,
582
- message: StringUtil.trim`
583
- The name "${props.value}" is not a valid identifier.
584
-
585
- Change to a valid identifier which can be a variable name in programming languages.
586
- `,
587
- });
588
- };
589
-
590
- // TABLE NAME
591
- validate({
592
- value: model.name,
593
- accessor: `${accessor}.name`,
594
- field: null,
595
- });
596
-
597
- // FIELDS
598
- validate({
599
- value: model.primaryField.name,
600
- accessor: `${accessor}.primaryField.name`,
601
- field: model.primaryField.name,
602
- });
603
- model.foreignFields.forEach((ff, i) => {
604
- validate({
605
- value: ff.name,
606
- accessor: `${accessor}.foreignFields[${i}].name`,
607
- field: ff.name,
608
- });
609
- validate({
610
- value: ff.relation.name,
611
- accessor: `${accessor}.foreignFields[${i}].relation.name`,
612
- field: ff.name,
613
- });
614
- validate({
615
- value: ff.relation.oppositeName,
616
- accessor: `${accessor}.foreignFields[${i}].relation.oppositeName`,
617
- field: ff.name,
618
- });
619
- if (ff.relation.mappingName)
620
- validate({
621
- value: ff.relation.mappingName,
622
- accessor: `${accessor}.foreignFields[${i}].relation.mappingName`,
623
- field: ff.name,
624
- });
625
- });
626
- model.plainFields.forEach((pf, i) =>
627
- validate({
628
- value: pf.name,
629
- accessor: `${accessor}.plainFields[${i}].name`,
630
- field: pf.name,
631
- }),
632
- );
633
- return errors;
634
- }
635
-
636
- function validateIndexes(
637
- model: AutoBeDatabase.IModel,
638
- accessor: string,
639
- ): IAutoBeDatabaseValidation.IError[] {
640
- // EMENSION
641
- model.uniqueIndexes = model.uniqueIndexes.filter(
642
- (unique) =>
643
- unique.fieldNames.length !== 0 &&
644
- unique.fieldNames[0] !== model.primaryField.name,
645
- );
646
- model.plainIndexes = model.plainIndexes.filter(
647
- (plain) =>
648
- plain.fieldNames.length !== 0 &&
649
- plain.fieldNames[0] !== model.primaryField.name,
650
- );
651
-
652
- const errors: IAutoBeDatabaseValidation.IError[] = [];
653
- const columnNames: Set<string> = new Set([
654
- model.primaryField.name,
655
- ...model.foreignFields.map((field) => field.name),
656
- ...model.plainFields.map((field) => field.name),
657
- ]);
658
-
659
- // COLUMN LEVEL VALIDATION
660
- const validate = <T>(props: {
661
- title: string;
662
- indexes: T[];
663
- fieldNames: (idx: T) => string[];
664
- accessor: (i: number, j: number) => string;
665
- additional?: (idx: T, i: number) => void;
666
- }) => {
667
- props.indexes.forEach((idx, i) => {
668
- // FIND TARGET FIELD
669
- props.fieldNames(idx).forEach((name, j) => {
670
- if (!columnNames.has(name))
671
- errors.push({
672
- path: `${accessor}.${props.accessor(i, j)}`,
673
- table: model.name,
674
- field: null,
675
- message: `Field ${name} does not exist in model ${model.name}.`,
676
- });
677
- if (props.additional) props.additional(idx, i);
678
- });
679
- });
680
- };
681
- validate({
682
- title: "unique index",
683
- indexes: model.uniqueIndexes,
684
- fieldNames: (idx) => idx.fieldNames,
685
- accessor: (i, j) => `uniqueIndexes[${i}].fieldNames[${j}]`,
686
- });
687
- validate({
688
- title: "index",
689
- indexes: model.plainIndexes,
690
- fieldNames: (idx) => idx.fieldNames,
691
- accessor: (i, j) => `plainIndexes[${i}].fieldNames[${j}]`,
692
- });
693
- validate({
694
- title: "index",
695
- indexes: model.ginIndexes,
696
- fieldNames: (idx) => [idx.fieldName],
697
- accessor: (i) => `ginIndexes[${i}].fieldName`,
698
- additional: (gin, i) => {
699
- const pIndex: number = model.plainFields.findIndex(
700
- (plain) => plain.name === gin.fieldName,
701
- );
702
- if (pIndex === -1)
703
- errors.push({
704
- path: `${accessor}.ginIndexes[${i}].fieldName`,
705
- table: model.name,
706
- field: null,
707
- message: StringUtil.trim`
708
- GIN index cannot be applied to this field.
709
-
710
- **What is a GIN index?**
711
- GIN (Generalized Inverted Index) is a special index type in PostgreSQL designed for
712
- full-text search and operations on complex data types. In AutoBE, GIN indexes are
713
- used exclusively for string fields to enable efficient text searching.
714
-
715
- **Current problem:**
716
- The field "${gin.fieldName}" specified for GIN index does not exist in the plain fields
717
- of model "${model.name}".
718
-
719
- **Possible causes:**
720
- 1. The field name is misspelled
721
- 2. The field is a foreign key field (not a plain field)
722
- 3. The field was removed but the index definition remained
723
-
724
- **How to fix:**
725
- 1. Check if the field name is correct
726
- 2. Ensure the field exists in the plainFields array
727
- 3. Make sure the field is of type "string" (GIN indexes only work with strings)
728
- 4. If the field doesn't exist, either:
729
- - Add the missing string field to plainFields
730
- - Remove this GIN index definition
731
-
732
- **Example of correct GIN index usage:**
733
- plainFields: [
734
- { name: "content", type: "string" } // ✓ Can use GIN index
735
- ]
736
- ginIndexes: [
737
- { fieldName: "content" } // ✓ Correct
738
- ]
739
- `,
740
- });
741
- else if (model.plainFields[pIndex].type !== "string")
742
- errors.push({
743
- path: `${accessor}.ginIndexes[${i}].fieldName`,
744
- table: model.name,
745
- field: model.plainFields[pIndex].name,
746
- message: StringUtil.trim`
747
- GIN index type mismatch - requires string field.
748
-
749
- **What is a GIN index?**
750
- GIN (Generalized Inverted Index) is PostgreSQL's specialized index for full-text search.
751
- It's designed to efficiently search within text content, making it perfect for features like:
752
- - Search functionality in articles or posts
753
- - Finding keywords in product descriptions
754
- - Filtering by text content
755
-
756
- **Current problem:**
757
- You're trying to apply a GIN index to field "${gin.fieldName}" which is of type "${model.plainFields[pIndex].type}".
758
- GIN indexes can ONLY be applied to "string" type fields.
759
-
760
- **Why string fields only?**
761
- GIN indexes work by breaking down text into searchable tokens (words, phrases).
762
- Other data types like numbers, booleans, or dates don't have this text structure.
763
-
764
- **How to fix:**
765
-
766
- 1. **If you need text search on this field:**
767
- - Change the field type to "string"
768
- - Example: If storing a product code as number, consider storing as string instead
769
-
770
- 2. **If the field should remain as ${model.plainFields[pIndex].type}:**
771
- - Remove the GIN index for this field
772
- - Use a regular index instead (plainIndexes)
773
- - Consider if you really need an index on this field
774
-
775
- 3. **Alternative indexing strategies:**
776
- - For ${model.plainFields[pIndex].type} fields, use plainIndexes for general performance
777
- - For unique ${model.plainFields[pIndex].type} values, use uniqueIndexes
778
- - GIN indexes should be reserved for text search scenarios only
779
-
780
- **Location of the field:**
781
- - Field definition: ${`${accessor}.plainFields[${pIndex}]`}
782
-
783
- Please either change the field type to "string" or remove the GIN index.
784
- `,
785
- });
786
- },
787
- });
788
- return errors;
789
- }
790
-
791
- function validateReferences(
792
- model: AutoBeDatabase.IModel,
793
- accessor: string,
794
- dict: Map<string, IModelContainer>,
795
- ): IAutoBeDatabaseValidation.IError[] {
796
- const errors: IAutoBeDatabaseValidation.IError[] = [];
797
-
798
- model.foreignFields.forEach((field, i) => {
799
- // DUPLICATED NAME
800
-
801
- const target = dict.get(field.relation.targetModel);
802
- if (target === undefined) {
803
- // CHECK EXISTENCE
804
- errors.push({
805
- path: `${accessor}.foreignFields[${i}].relation.targetModel`,
806
- table: model.name,
807
- field: field.name,
808
- message: `Target model ${field.relation.targetModel} does not exist.`,
809
- });
810
- } else if (target.model !== model) {
811
- // CHECK CROSS REFERENCE
812
- target.model.foreignFields.forEach((oppo, j) => {
813
- if (oppo.relation.targetModel === model.name) {
814
- errors.push({
815
- path: `${accessor}.foreignFields[${i}].relation.targetModel`,
816
- table: model.name,
817
- field: field.name,
818
- message: StringUtil.trim`
819
- Cross-reference dependency detected between models.
820
-
821
- **What is Cross-reference dependency?**
822
- A cross-reference dependency (also known as circular dependency) occurs when two models
823
- reference each other through foreign key fields. This creates a circular relationship
824
- where Model A references Model B, and Model B also references Model A.
825
-
826
- **Current situation:**
827
- - ${model.name} model has a foreign key field "${field.name}" that references ${field.relation.targetModel}
828
- - ${field.relation.targetModel} model also has a foreign key field that references ${model.name}
829
- - Location of opposite reference: application.files[${target.fileIndex}].models[${target.modelIndex}].foreignFields[${j}].relation.targetModel
830
-
831
- **Why is this a problem?**
832
- Circular dependencies can cause issues with:
833
- - Database initialization (which table to create first?)
834
- - Data insertion (which record to insert first?)
835
- - Cascading updates and deletes
836
- - Query performance and complexity
837
-
838
- **How to fix this:**
839
- You need to remove one of the foreign key relationships. Here's how to decide:
840
-
841
- 1. **Identify the primary relationship direction**
842
- - Which model is the "parent" and which is the "child"?
843
- - Which relationship is essential for your business logic?
844
- - Example: In User ↔ Profile, User is typically the parent
845
-
846
- 2. **Remove the redundant foreign key**
847
- - Keep the foreign key in the child model pointing to the parent
848
- - Remove the foreign key in the parent model pointing to the child
849
- - You can still access the reverse relationship through Prisma's implicit relations
850
-
851
- 3. **Update any affected indexes**
852
- - Remove indexes that include the deleted foreign key field
853
- - Update composite indexes if necessary
854
-
855
- **Example solution:**
856
- If you have:
857
- - User model with profileId foreign key
858
- - Profile model with userId foreign key
859
-
860
- You should:
861
- - Keep userId in Profile (child references parent)
862
- - Remove profileId from User
863
- - Access user's profile through: user.profile (Prisma will handle this)
864
-
865
- Please eliminate the circular dependency and regenerate the schema.`,
866
- });
867
- }
868
- });
869
- }
870
- });
871
-
872
- return errors;
873
- }
1
+ import { AutoBeDatabase, IAutoBeDatabaseValidation } from "@autobe/interface";
2
+ import { AutoBeEscaper, MapUtil, StringUtil } from "@autobe/utils";
3
+ import { HashMap, hash } from "tstl";
4
+
5
+ export function validateDatabaseApplication(
6
+ application: AutoBeDatabase.IApplication,
7
+ ): IAutoBeDatabaseValidation {
8
+ const dict: Map<string, IModelContainer> = new Map(
9
+ application.files
10
+ .map((file, fi) =>
11
+ file.models.map(
12
+ (model, mi) =>
13
+ [
14
+ model.name,
15
+ {
16
+ file,
17
+ model,
18
+ fileIndex: fi,
19
+ modelIndex: mi,
20
+ },
21
+ ] satisfies [string, IModelContainer],
22
+ ),
23
+ )
24
+ .flat(),
25
+ );
26
+ const errors: IAutoBeDatabaseValidation.IError[] = [
27
+ ...validateDuplicatedFiles(application),
28
+ ...validateDuplicatedModels(application),
29
+ ...application.files
30
+ .map((file, fi) =>
31
+ file.models.map((model, mi) => {
32
+ const accessor: string = `application.files[${fi}].models[${mi}]`;
33
+ return [
34
+ ...validateDuplicatedFields(dict, model, accessor),
35
+ ...validateDuplicatedIndexes(model, accessor),
36
+ ...validateValidNames(model, accessor),
37
+ ...validateIndexes(model, accessor),
38
+ ...validateReferences(model, accessor, dict),
39
+ ...validateDuplicatedRelationOppositeNames(dict, model),
40
+ ];
41
+ }),
42
+ )
43
+ .flat(2),
44
+ ];
45
+ return errors.length === 0
46
+ ? {
47
+ success: true,
48
+ data: application,
49
+ }
50
+ : {
51
+ success: false,
52
+ data: application,
53
+ errors,
54
+ };
55
+ }
56
+
57
+ interface IModelContainer {
58
+ file: AutoBeDatabase.IFile;
59
+ model: AutoBeDatabase.IModel;
60
+ fileIndex: number;
61
+ modelIndex: number;
62
+ }
63
+
64
+ /* -----------------------------------------------------------
65
+ DUPLICATES
66
+ ----------------------------------------------------------- */
67
+ function validateDuplicatedFiles(
68
+ app: AutoBeDatabase.IApplication,
69
+ ): IAutoBeDatabaseValidation.IError[] {
70
+ interface IFileContainer {
71
+ file: AutoBeDatabase.IFile;
72
+ index: number;
73
+ }
74
+ const group: Map<string, IFileContainer[]> = new Map();
75
+ app.files.forEach((file, fileIndex) => {
76
+ const container: IFileContainer = { file, index: fileIndex };
77
+ MapUtil.take(group, file.filename, () => []).push(container);
78
+ });
79
+
80
+ const errors: IAutoBeDatabaseValidation.IError[] = [];
81
+ for (const array of group.values())
82
+ if (array.length !== 1)
83
+ array.forEach((container, i) => {
84
+ errors.push({
85
+ path: `application.files[${container.index}]`,
86
+ table: null,
87
+ field: null,
88
+ message: StringUtil.trim`
89
+ File ${container.file.filename} is duplicated.
90
+
91
+ Accessors of the other duplicated files are:
92
+
93
+ ${array
94
+ .filter((_oppo, j) => i !== j)
95
+ .map((oppo) => `- application.files[${oppo.index}]`)
96
+ .join("\n")},
97
+ `,
98
+ });
99
+ });
100
+ return errors;
101
+ }
102
+
103
+ function validateDuplicatedModels(
104
+ app: AutoBeDatabase.IApplication,
105
+ ): IAutoBeDatabaseValidation.IError[] {
106
+ const modelContainers: Map<string, IModelContainer[]> = new Map();
107
+ app.files.forEach((file, fileIndex) => {
108
+ file.models.forEach((model, modelIndex) => {
109
+ const container: IModelContainer = {
110
+ file,
111
+ model,
112
+ fileIndex,
113
+ modelIndex,
114
+ };
115
+ MapUtil.take(modelContainers, model.name, () => []).push(container);
116
+ });
117
+ });
118
+
119
+ const errors: IAutoBeDatabaseValidation.IError[] = [];
120
+ for (const array of modelContainers.values())
121
+ if (array.length !== 1)
122
+ array.forEach((container, i) => {
123
+ errors.push({
124
+ path: `application.files[${container.fileIndex}].models[${container.modelIndex}]`,
125
+ table: container.model.name,
126
+ field: null,
127
+ message: StringUtil.trim`
128
+ Model ${container.model.name} is duplicated.
129
+
130
+ Accessors of the other duplicated models are:
131
+
132
+ ${array
133
+ .filter((_oppo, j) => i !== j)
134
+ .map(
135
+ (oppo) =>
136
+ `- application.files[${oppo.fileIndex}].models[${oppo.modelIndex}]`,
137
+ )
138
+ .join("\n")},
139
+ `,
140
+ });
141
+ });
142
+ return errors;
143
+ }
144
+
145
+ function validateDuplicatedFields(
146
+ dict: Map<string, IModelContainer>,
147
+ model: AutoBeDatabase.IModel,
148
+ accessor: string,
149
+ ): IAutoBeDatabaseValidation.IError[] {
150
+ const errors: IAutoBeDatabaseValidation.IError[] = [];
151
+
152
+ // FIND DUPLICATED FIELDS
153
+ const group: Map<string, string[]> = new Map();
154
+ MapUtil.take(group, model.primaryField.name, () => []).push(
155
+ `${accessor}.primaryField.name`,
156
+ );
157
+ model.foreignFields.forEach((field, i) => {
158
+ MapUtil.take(group, field.name, () => []).push(
159
+ `${accessor}.foreignFields[${i}].name`,
160
+ );
161
+ MapUtil.take(group, field.relation.name, () => []).push(
162
+ `${accessor}.foreignFields[${i}].relation.name`,
163
+ );
164
+ });
165
+ model.plainFields.forEach((field, i) =>
166
+ MapUtil.take(group, field.name, () => []).push(
167
+ `${accessor}.plainFields[${i}].name`,
168
+ ),
169
+ );
170
+ for (const [field, array] of group)
171
+ if (array.length !== 1)
172
+ array.forEach((path, i) => {
173
+ errors.push({
174
+ path,
175
+ table: model.name,
176
+ field,
177
+ message: StringUtil.trim`
178
+ Field ${field} is duplicated.
179
+
180
+ Accessors of the other duplicated fields are:
181
+
182
+ ${array
183
+ .filter((_oppo, j) => i !== j)
184
+ .map((a) => `- ${a}`)
185
+ .join("\n")},
186
+ `,
187
+ });
188
+ });
189
+
190
+ // FIND TABLE NAMED FIELDS
191
+ model.plainFields.forEach((field, i) => {
192
+ if (dict.has(field.name) === true)
193
+ errors.push({
194
+ path: `${accessor}.plainFields[${i}].name`,
195
+ table: model.name,
196
+ field: field.name,
197
+ message: StringUtil.trim`
198
+ Field name conflicts with an existing table name.
199
+
200
+ **What happened?**
201
+ The field "${field.name}" in model "${model.name}" has the same name as another table "${field.name}".
202
+ This can cause confusion and potential issues in the generated code.
203
+
204
+ **Why is this a problem?**
205
+ - Naming conflicts can lead to ambiguous references in your code
206
+ - It may cause issues with Prisma's relation inference
207
+ - It makes the schema harder to understand and maintain
208
+
209
+ **How to fix this:**
210
+
211
+ 1. **If this is a denormalization field (pre-calculated value):**
212
+ - Consider if this field is really necessary
213
+ - If it's storing aggregated data from the related table, it might be better to calculate it dynamically
214
+ - Remove the field if it's redundant
215
+
216
+ 2. **If this is a legitimate field:**
217
+ - Rename the field to be more descriptive
218
+ - Good naming examples:
219
+ - Instead of "user", use "user_name" or "user_id"
220
+ - Instead of "order", use "order_status" or "order_count"
221
+ - Instead of "product", use "product_name" or "product_code"
222
+
223
+ 3. **Naming best practices:**
224
+ - Use specific, descriptive names that indicate the field's purpose
225
+ - Avoid using table names as field names
226
+ - Consider adding a suffix or prefix to clarify the field's role
227
+
228
+ Please rename the field or remove it if unnecessary.
229
+ `,
230
+ });
231
+ });
232
+ return errors;
233
+ }
234
+
235
+ function validateDuplicatedIndexes(
236
+ model: AutoBeDatabase.IModel,
237
+ accessor: string,
238
+ ): IAutoBeDatabaseValidation.IError[] {
239
+ const errors: IAutoBeDatabaseValidation.IError[] = [];
240
+
241
+ // FIND DUPLICATED INDEXES
242
+ const group: HashMap<string[], string[]> = new HashMap(
243
+ (x) => hash(...x),
244
+ (x, y) => x.length === y.length && x.every((v, i) => v === y[i]),
245
+ );
246
+ model.uniqueIndexes.forEach((unique, i) =>
247
+ group
248
+ .take(unique.fieldNames, () => [])
249
+ .push(`${accessor}.uniqueIndexes[${i}].fieldNames`),
250
+ );
251
+ model.plainIndexes.forEach((plain, i) =>
252
+ group
253
+ .take(plain.fieldNames, () => [])
254
+ .push(`${accessor}.plainIndexes[${i}].fieldNames`),
255
+ );
256
+ model.ginIndexes.forEach((gin, i) =>
257
+ group
258
+ .take([gin.fieldName], () => [])
259
+ .push(`${accessor}.ginIndexes[${i}].fieldName`),
260
+ );
261
+ for (const { first: fieldNames, second: array } of group)
262
+ if (array.length !== 1)
263
+ array.forEach((path, i) => {
264
+ errors.push({
265
+ path,
266
+ table: model.name,
267
+ field: null,
268
+ message: StringUtil.trim`
269
+ Duplicated index found (${fieldNames.join(", ")}).
270
+
271
+ Accessors of the other duplicated indexes are:
272
+
273
+ ${array
274
+ .filter((_oppo, j) => i !== j)
275
+ .map((a) => `- ${a}`)
276
+ .join("\n")},
277
+ `,
278
+ });
279
+ });
280
+
281
+ // SUBSET RELATIONS
282
+ model.uniqueIndexes.forEach((unique, i) => {
283
+ if (unique.fieldNames.length <= 1) return;
284
+ unique.fieldNames.forEach((_, j, array) => {
285
+ if (j === array.length - 1) return;
286
+ const subset: string[] = unique.fieldNames.slice(0, j);
287
+ if (
288
+ j === 0 &&
289
+ model.foreignFields.some(
290
+ (f) => f.name === subset[0] && f.unique === true,
291
+ )
292
+ )
293
+ errors.push({
294
+ path: `${accessor}.uniqueIndexes[${i}].fieldNames[0]`,
295
+ table: model.name,
296
+ field: null,
297
+ message: StringUtil.trim`
298
+ Duplicated unique index found (${subset[0]}).
299
+
300
+ You have defined an unique index that is already included,
301
+ in the foreign field with unique constraint.
302
+
303
+ Remove this unique index to avoid the duplication.
304
+ `,
305
+ });
306
+ const cIndex: number = model.uniqueIndexes.findIndex(
307
+ (u) =>
308
+ u.fieldNames.length === subset.length &&
309
+ u.fieldNames.every((name, k) => name === subset[k]),
310
+ );
311
+ if (cIndex !== -1)
312
+ errors.push({
313
+ path: `${accessor}.uniqueIndexes[${i}].fieldNames`,
314
+ table: model.name,
315
+ field: null,
316
+ message: StringUtil.trim`
317
+ Redundant subset unique index detected.
318
+
319
+ **What is a subset unique index problem?**
320
+ When you have a unique index on multiple fields, any subset of those fields is automatically unique too.
321
+ This is a fundamental property of unique constraints in databases.
322
+
323
+ **Current situation:**
324
+ - You have a unique index on: (${unique.fieldNames.join(", ")})
325
+ - But there's already a unique index on its subset: (${subset.join(", ")})
326
+ - This makes the larger unique index redundant for uniqueness purposes
327
+
328
+ **Why is this a problem?**
329
+ 1. **Logical redundancy**: If (A) is unique, then (A, B) is automatically unique
330
+ 2. **Performance overhead**: Maintaining unnecessary indexes slows down write operations
331
+ 3. **Storage waste**: Each index consumes disk space
332
+ 4. **Confusion**: It's unclear which uniqueness constraint is the intended one
333
+
334
+ **Example to illustrate:**
335
+ If email is unique, then (email, name) is automatically unique because:
336
+ - No two records can have the same email
337
+ - Therefore, no two records can have the same (email, name) combination
338
+
339
+ **How to fix:**
340
+ Choose one of these solutions based on your needs:
341
+
342
+ 1. **If you need uniqueness only:**
343
+ - Keep just the subset unique index: (${subset.join(", ")})
344
+ - Remove the larger unique index
345
+
346
+ 2. **If you need the multi-field index for query performance:**
347
+ - Keep the subset as unique index: (${subset.join(", ")})
348
+ - Change the larger index to a plain (non-unique) index for performance
349
+
350
+ 3. **If the subset unique was added by mistake:**
351
+ - Remove the subset unique index
352
+ - Keep the multi-field unique index
353
+
354
+ Please review your uniqueness requirements and adjust accordingly.
355
+ `,
356
+ });
357
+ });
358
+ });
359
+
360
+ // SUPERSET RELATIONS
361
+ model.plainIndexes.forEach((x, i) => {
362
+ model.plainIndexes.forEach((y, j) => {
363
+ if (i === j) return;
364
+ else if (
365
+ x.fieldNames.length < y.fieldNames.length &&
366
+ x.fieldNames.every((n, z) => y.fieldNames[z] === n)
367
+ )
368
+ errors.push({
369
+ path: `${accessor}.plainIndexes[${i}].fieldNames`,
370
+ table: model.name,
371
+ field: null,
372
+ message: StringUtil.trim`
373
+ Inefficient subset index detected - superset index exists.
374
+
375
+ **What is a subset/superset index problem?**
376
+ In database indexing, when you have an index on (A, B, C), it can efficiently serve queries
377
+ that filter by A, or by (A, B), or by (A, B, C). This is called index prefix matching.
378
+
379
+ **Current situation:**
380
+ - You have a plain index on: (${x.fieldNames.join(", ")})
381
+ - But there's already a plain index on its superset: (${y.fieldNames.join(", ")})
382
+ - The subset index is redundant because the superset can handle the same queries
383
+
384
+ **Why is this a problem?**
385
+ 1. **Query efficiency**: The superset index can handle all queries the subset can
386
+ 2. **Storage waste**: You're maintaining two indexes where one would suffice
387
+ 3. **Write performance**: Each index slows down INSERT, UPDATE, and DELETE operations
388
+ 4. **Maintenance overhead**: More indexes mean more work for the database
389
+
390
+ **How indexes work (example):**
391
+ If you have an index on (country, city, street):
392
+ - ✅ Can efficiently find by country
393
+ - ✅ Can efficiently find by country + city
394
+ - ✅ Can efficiently find by country + city + street
395
+ - ❌ Cannot efficiently find by city alone
396
+ - ❌ Cannot efficiently find by street alone
397
+
398
+ **How to fix:**
399
+ Remove the subset index (${x.fieldNames.join(", ")}) because:
400
+ - The superset index (${y.fieldNames.join(", ")}) already covers these queries
401
+ - You'll save storage space and improve write performance
402
+ - Query performance will remain the same
403
+
404
+ **When to keep both indexes:**
405
+ Only if the subset index is UNIQUE (which it isn't in this case), as unique
406
+ constraints serve a different purpose than performance optimization.
407
+
408
+ Please remove the redundant subset index.
409
+ `,
410
+ });
411
+ });
412
+ });
413
+
414
+ // CONSIDER GIN INDEXES
415
+ if (
416
+ model.ginIndexes.length !==
417
+ new Set(model.ginIndexes.map((g) => g.fieldName)).size
418
+ )
419
+ errors.push({
420
+ path: `${accessor}.ginIndexes`,
421
+ table: model.name,
422
+ field: null,
423
+ message: StringUtil.trim`
424
+ Duplicated GIN index found.
425
+
426
+ GIN index can only be used once per field.
427
+
428
+ Please remove the duplicated GIN indexes.
429
+ `,
430
+ });
431
+
432
+ return errors;
433
+ }
434
+
435
+ function validateDuplicatedRelationOppositeNames(
436
+ dict: Map<string, IModelContainer>,
437
+ model: AutoBeDatabase.IModel,
438
+ ): IAutoBeDatabaseValidation.IError[] {
439
+ interface IOppositeNameContainer {
440
+ container: IModelContainer;
441
+ foreignField: AutoBeDatabase.IForeignField;
442
+ foreignFieldIndex: number;
443
+ }
444
+ const errors: IAutoBeDatabaseValidation.IError[] = [];
445
+ const group: Map<string, IOppositeNameContainer[]> = new Map();
446
+
447
+ // Collect all field/relation names in the target model
448
+ const targetFieldNames = new Set<string>([
449
+ model.primaryField.name,
450
+ ...model.foreignFields.map((f) => f.name),
451
+ ...model.foreignFields.map((f) => f.relation.name),
452
+ ...model.plainFields.map((f) => f.name),
453
+ ]);
454
+
455
+ for (const c of dict.values()) {
456
+ c.model.foreignFields.forEach((ff, i) => {
457
+ if (ff.relation.targetModel !== model.name) return;
458
+ MapUtil.take(group, ff.relation.oppositeName, () => []).push({
459
+ container: c,
460
+ foreignField: ff,
461
+ foreignFieldIndex: i,
462
+ });
463
+ });
464
+ }
465
+
466
+ // Check oppositeName conflicts with target model's existing fields
467
+ for (const [oppositeName, array] of group) {
468
+ if (targetFieldNames.has(oppositeName)) {
469
+ array.forEach((item) => {
470
+ const c = item.container;
471
+ const ff = item.foreignField;
472
+ errors.push({
473
+ path: `application.files[${c.fileIndex}].models[${c.modelIndex}].foreignFields[${item.foreignFieldIndex}].relation.oppositeName`,
474
+ table: c.model.name,
475
+ field: ff.name,
476
+ message: StringUtil.trim`
477
+ oppositeName "${oppositeName}" conflicts with existing field in target model "${model.name}".
478
+
479
+ **What happened?**
480
+ The oppositeName "${oppositeName}" would create a reverse relation property in "${model.name}",
481
+ but "${model.name}" already has a field or relation with that name.
482
+
483
+ **Why is this a problem?**
484
+ - Prisma cannot have two properties with the same name in a model
485
+ - This will cause Prisma schema compilation errors
486
+ - The reverse relation would overwrite or conflict with the existing field
487
+
488
+ **How to fix:**
489
+ Choose a different oppositeName that doesn't conflict with existing fields in "${model.name}".
490
+
491
+ **Naming suggestions:**
492
+ - Add a descriptive prefix/suffix: "${oppositeName}List", "${oppositeName}Items", "related${oppositeName.charAt(0).toUpperCase() + oppositeName.slice(1)}"
493
+ - Use the source model name: "${c.model.name.replace(/_/g, "").toLowerCase()}s"
494
+ `,
495
+ });
496
+ });
497
+ }
498
+ }
499
+
500
+ // Check duplicate oppositeNames among relations targeting the same model
501
+ for (const [oppositeName, array] of group) {
502
+ if (array.length === 1) continue;
503
+ array.forEach((item, i) => {
504
+ const c = item.container;
505
+ const ff = item.foreignField;
506
+ errors.push({
507
+ path: `application.files[${c.fileIndex}].models[${c.modelIndex}].foreignFields[${item.foreignFieldIndex}].relation.oppositeName`,
508
+ table: c.model.name,
509
+ field: ff.name,
510
+ message: StringUtil.trim`
511
+ Duplicated relation oppositeName "${oppositeName}" detected on target model "${model.name}".
512
+
513
+ **What is oppositeName?**
514
+ In Prisma relations, oppositeName defines the name of the reverse relation field
515
+ on the target model. It allows the target model to access related records through
516
+ this named property.
517
+
518
+ **Current situation:**
519
+ Multiple foreign key fields from different models are trying to create reverse
520
+ relations on "${model.name}" with the same oppositeName "${oppositeName}".
521
+
522
+ **Conflicting relations:**
523
+ ${array
524
+ .filter((_, j) => i !== j)
525
+ .map(
526
+ (oppo) =>
527
+ `- Model "${oppo.container.model.name}", field "${oppo.foreignField.name}" (accessor: application.files[${oppo.container.fileIndex}].models[${oppo.container.modelIndex}].foreignFields[${oppo.foreignFieldIndex}].relation.oppositeName)`,
528
+ )
529
+ .join("\n")}
530
+
531
+ **Why is this a problem?**
532
+ - Prisma requires unique relation names within a model
533
+ - When "${model.name}" tries to access related records, it won't know which relation to use
534
+ - This will cause Prisma schema compilation errors
535
+
536
+ **How to fix:**
537
+ Each relation pointing to "${model.name}" must have a unique oppositeName.
538
+
539
+ **Naming suggestions:**
540
+ - Use descriptive names that indicate the relationship's purpose
541
+ - Include the source model name for clarity
542
+ - Examples:
543
+ - Instead of both using "orders", use "customerOrders" and "sellerOrders"
544
+ - Instead of both using "users", use "createdByUser" and "assignedToUser"
545
+ - For "${c.model.name}" → "${model.name}": consider "${c.model.name.charAt(0).toLowerCase() + c.model.name.slice(1)}s" or a more descriptive name
546
+
547
+ Please rename the oppositeName to be unique across all relations targeting "${model.name}".
548
+ `,
549
+ });
550
+ });
551
+ }
552
+ return errors;
553
+ }
554
+
555
+ /* -----------------------------------------------------------
556
+ VALIDATIONS
557
+ ----------------------------------------------------------- */
558
+ function validateValidNames(
559
+ model: AutoBeDatabase.IModel,
560
+ accessor: string,
561
+ ): IAutoBeDatabaseValidation.IError[] {
562
+ const errors: IAutoBeDatabaseValidation.IError[] = [];
563
+ const validate = (props: {
564
+ value: string;
565
+ accessor: string;
566
+ field: string | null;
567
+ }): void => {
568
+ if (AutoBeEscaper.reserved(props.value))
569
+ errors.push({
570
+ path: props.accessor,
571
+ table: model.name,
572
+ field: props.field,
573
+ message: StringUtil.trim`
574
+ The name "${props.value}" is a system reserved keyword and cannot be used.
575
+ `,
576
+ });
577
+ else if (AutoBeEscaper.variable(props.value) === false)
578
+ errors.push({
579
+ path: props.accessor,
580
+ table: model.name,
581
+ field: props.field,
582
+ message: StringUtil.trim`
583
+ The name "${props.value}" is not a valid identifier.
584
+
585
+ Change to a valid identifier which can be a variable name in programming languages.
586
+ `,
587
+ });
588
+ };
589
+
590
+ // TABLE NAME
591
+ validate({
592
+ value: model.name,
593
+ accessor: `${accessor}.name`,
594
+ field: null,
595
+ });
596
+
597
+ // FIELDS
598
+ validate({
599
+ value: model.primaryField.name,
600
+ accessor: `${accessor}.primaryField.name`,
601
+ field: model.primaryField.name,
602
+ });
603
+ model.foreignFields.forEach((ff, i) => {
604
+ validate({
605
+ value: ff.name,
606
+ accessor: `${accessor}.foreignFields[${i}].name`,
607
+ field: ff.name,
608
+ });
609
+ validate({
610
+ value: ff.relation.name,
611
+ accessor: `${accessor}.foreignFields[${i}].relation.name`,
612
+ field: ff.name,
613
+ });
614
+ validate({
615
+ value: ff.relation.oppositeName,
616
+ accessor: `${accessor}.foreignFields[${i}].relation.oppositeName`,
617
+ field: ff.name,
618
+ });
619
+ if (ff.relation.mappingName)
620
+ validate({
621
+ value: ff.relation.mappingName,
622
+ accessor: `${accessor}.foreignFields[${i}].relation.mappingName`,
623
+ field: ff.name,
624
+ });
625
+ });
626
+ model.plainFields.forEach((pf, i) =>
627
+ validate({
628
+ value: pf.name,
629
+ accessor: `${accessor}.plainFields[${i}].name`,
630
+ field: pf.name,
631
+ }),
632
+ );
633
+ return errors;
634
+ }
635
+
636
+ function validateIndexes(
637
+ model: AutoBeDatabase.IModel,
638
+ accessor: string,
639
+ ): IAutoBeDatabaseValidation.IError[] {
640
+ // EMENSION
641
+ model.uniqueIndexes = model.uniqueIndexes.filter(
642
+ (unique) =>
643
+ unique.fieldNames.length !== 0 &&
644
+ unique.fieldNames[0] !== model.primaryField.name,
645
+ );
646
+ model.plainIndexes = model.plainIndexes.filter(
647
+ (plain) =>
648
+ plain.fieldNames.length !== 0 &&
649
+ plain.fieldNames[0] !== model.primaryField.name,
650
+ );
651
+
652
+ const errors: IAutoBeDatabaseValidation.IError[] = [];
653
+ const columnNames: Set<string> = new Set([
654
+ model.primaryField.name,
655
+ ...model.foreignFields.map((field) => field.name),
656
+ ...model.plainFields.map((field) => field.name),
657
+ ]);
658
+
659
+ // COLUMN LEVEL VALIDATION
660
+ const validate = <T>(props: {
661
+ title: string;
662
+ indexes: T[];
663
+ fieldNames: (idx: T) => string[];
664
+ accessor: (i: number, j: number) => string;
665
+ additional?: (idx: T, i: number) => void;
666
+ }) => {
667
+ props.indexes.forEach((idx, i) => {
668
+ // FIND TARGET FIELD
669
+ props.fieldNames(idx).forEach((name, j) => {
670
+ if (!columnNames.has(name))
671
+ errors.push({
672
+ path: `${accessor}.${props.accessor(i, j)}`,
673
+ table: model.name,
674
+ field: null,
675
+ message: `Field ${name} does not exist in model ${model.name}.`,
676
+ });
677
+ if (props.additional) props.additional(idx, i);
678
+ });
679
+ });
680
+ };
681
+ validate({
682
+ title: "unique index",
683
+ indexes: model.uniqueIndexes,
684
+ fieldNames: (idx) => idx.fieldNames,
685
+ accessor: (i, j) => `uniqueIndexes[${i}].fieldNames[${j}]`,
686
+ });
687
+ validate({
688
+ title: "index",
689
+ indexes: model.plainIndexes,
690
+ fieldNames: (idx) => idx.fieldNames,
691
+ accessor: (i, j) => `plainIndexes[${i}].fieldNames[${j}]`,
692
+ });
693
+ validate({
694
+ title: "index",
695
+ indexes: model.ginIndexes,
696
+ fieldNames: (idx) => [idx.fieldName],
697
+ accessor: (i) => `ginIndexes[${i}].fieldName`,
698
+ additional: (gin, i) => {
699
+ const pIndex: number = model.plainFields.findIndex(
700
+ (plain) => plain.name === gin.fieldName,
701
+ );
702
+ if (pIndex === -1)
703
+ errors.push({
704
+ path: `${accessor}.ginIndexes[${i}].fieldName`,
705
+ table: model.name,
706
+ field: null,
707
+ message: StringUtil.trim`
708
+ GIN index cannot be applied to this field.
709
+
710
+ **What is a GIN index?**
711
+ GIN (Generalized Inverted Index) is a special index type in PostgreSQL designed for
712
+ full-text search and operations on complex data types. In AutoBE, GIN indexes are
713
+ used exclusively for string fields to enable efficient text searching.
714
+
715
+ **Current problem:**
716
+ The field "${gin.fieldName}" specified for GIN index does not exist in the plain fields
717
+ of model "${model.name}".
718
+
719
+ **Possible causes:**
720
+ 1. The field name is misspelled
721
+ 2. The field is a foreign key field (not a plain field)
722
+ 3. The field was removed but the index definition remained
723
+
724
+ **How to fix:**
725
+ 1. Check if the field name is correct
726
+ 2. Ensure the field exists in the plainFields array
727
+ 3. Make sure the field is of type "string" (GIN indexes only work with strings)
728
+ 4. If the field doesn't exist, either:
729
+ - Add the missing string field to plainFields
730
+ - Remove this GIN index definition
731
+
732
+ **Example of correct GIN index usage:**
733
+ plainFields: [
734
+ { name: "content", type: "string" } // ✓ Can use GIN index
735
+ ]
736
+ ginIndexes: [
737
+ { fieldName: "content" } // ✓ Correct
738
+ ]
739
+ `,
740
+ });
741
+ else if (model.plainFields[pIndex].type !== "string")
742
+ errors.push({
743
+ path: `${accessor}.ginIndexes[${i}].fieldName`,
744
+ table: model.name,
745
+ field: model.plainFields[pIndex].name,
746
+ message: StringUtil.trim`
747
+ GIN index type mismatch - requires string field.
748
+
749
+ **What is a GIN index?**
750
+ GIN (Generalized Inverted Index) is PostgreSQL's specialized index for full-text search.
751
+ It's designed to efficiently search within text content, making it perfect for features like:
752
+ - Search functionality in articles or posts
753
+ - Finding keywords in product descriptions
754
+ - Filtering by text content
755
+
756
+ **Current problem:**
757
+ You're trying to apply a GIN index to field "${gin.fieldName}" which is of type "${model.plainFields[pIndex].type}".
758
+ GIN indexes can ONLY be applied to "string" type fields.
759
+
760
+ **Why string fields only?**
761
+ GIN indexes work by breaking down text into searchable tokens (words, phrases).
762
+ Other data types like numbers, booleans, or dates don't have this text structure.
763
+
764
+ **How to fix:**
765
+
766
+ 1. **If you need text search on this field:**
767
+ - Change the field type to "string"
768
+ - Example: If storing a product code as number, consider storing as string instead
769
+
770
+ 2. **If the field should remain as ${model.plainFields[pIndex].type}:**
771
+ - Remove the GIN index for this field
772
+ - Use a regular index instead (plainIndexes)
773
+ - Consider if you really need an index on this field
774
+
775
+ 3. **Alternative indexing strategies:**
776
+ - For ${model.plainFields[pIndex].type} fields, use plainIndexes for general performance
777
+ - For unique ${model.plainFields[pIndex].type} values, use uniqueIndexes
778
+ - GIN indexes should be reserved for text search scenarios only
779
+
780
+ **Location of the field:**
781
+ - Field definition: ${`${accessor}.plainFields[${pIndex}]`}
782
+
783
+ Please either change the field type to "string" or remove the GIN index.
784
+ `,
785
+ });
786
+ },
787
+ });
788
+ return errors;
789
+ }
790
+
791
+ function validateReferences(
792
+ model: AutoBeDatabase.IModel,
793
+ accessor: string,
794
+ dict: Map<string, IModelContainer>,
795
+ ): IAutoBeDatabaseValidation.IError[] {
796
+ const errors: IAutoBeDatabaseValidation.IError[] = [];
797
+
798
+ model.foreignFields.forEach((field, i) => {
799
+ // DUPLICATED NAME
800
+
801
+ const target = dict.get(field.relation.targetModel);
802
+ if (target === undefined) {
803
+ // CHECK EXISTENCE
804
+ errors.push({
805
+ path: `${accessor}.foreignFields[${i}].relation.targetModel`,
806
+ table: model.name,
807
+ field: field.name,
808
+ message: `Target model ${field.relation.targetModel} does not exist.`,
809
+ });
810
+ } else if (target.model !== model) {
811
+ // CHECK CROSS REFERENCE
812
+ target.model.foreignFields.forEach((oppo, j) => {
813
+ if (oppo.relation.targetModel === model.name) {
814
+ errors.push({
815
+ path: `${accessor}.foreignFields[${i}].relation.targetModel`,
816
+ table: model.name,
817
+ field: field.name,
818
+ message: StringUtil.trim`
819
+ Cross-reference dependency detected between models.
820
+
821
+ **What is Cross-reference dependency?**
822
+ A cross-reference dependency (also known as circular dependency) occurs when two models
823
+ reference each other through foreign key fields. This creates a circular relationship
824
+ where Model A references Model B, and Model B also references Model A.
825
+
826
+ **Current situation:**
827
+ - ${model.name} model has a foreign key field "${field.name}" that references ${field.relation.targetModel}
828
+ - ${field.relation.targetModel} model also has a foreign key field that references ${model.name}
829
+ - Location of opposite reference: application.files[${target.fileIndex}].models[${target.modelIndex}].foreignFields[${j}].relation.targetModel
830
+
831
+ **Why is this a problem?**
832
+ Circular dependencies can cause issues with:
833
+ - Database initialization (which table to create first?)
834
+ - Data insertion (which record to insert first?)
835
+ - Cascading updates and deletes
836
+ - Query performance and complexity
837
+
838
+ **How to fix this:**
839
+ You need to remove one of the foreign key relationships. Here's how to decide:
840
+
841
+ 1. **Identify the primary relationship direction**
842
+ - Which model is the "parent" and which is the "child"?
843
+ - Which relationship is essential for your business logic?
844
+ - Example: In User ↔ Profile, User is typically the parent
845
+
846
+ 2. **Remove the redundant foreign key**
847
+ - Keep the foreign key in the child model pointing to the parent
848
+ - Remove the foreign key in the parent model pointing to the child
849
+ - You can still access the reverse relationship through Prisma's implicit relations
850
+
851
+ 3. **Update any affected indexes**
852
+ - Remove indexes that include the deleted foreign key field
853
+ - Update composite indexes if necessary
854
+
855
+ **Example solution:**
856
+ If you have:
857
+ - User model with profileId foreign key
858
+ - Profile model with userId foreign key
859
+
860
+ You should:
861
+ - Keep userId in Profile (child references parent)
862
+ - Remove profileId from User
863
+ - Access user's profile through: user.profile (Prisma will handle this)
864
+
865
+ Please eliminate the circular dependency and regenerate the schema.`,
866
+ });
867
+ }
868
+ });
869
+ }
870
+ });
871
+
872
+ return errors;
873
+ }