@autobe/utils 0.29.2 → 0.30.0-dev.20260315

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 (63) hide show
  1. package/LICENSE +661 -661
  2. package/README.md +261 -0
  3. package/lib/AutoBeEscaper.d.ts +4 -0
  4. package/lib/AutoBeEscaper.js +85 -0
  5. package/lib/AutoBeEscaper.js.map +1 -0
  6. package/lib/StringUtil.d.ts +1 -0
  7. package/lib/StringUtil.js +15 -36
  8. package/lib/StringUtil.js.map +1 -1
  9. package/lib/aggregate/AutoBeProcessAggregateFactory.js +9 -0
  10. package/lib/aggregate/AutoBeProcessAggregateFactory.js.map +1 -1
  11. package/lib/index.d.ts +1 -0
  12. package/lib/index.js +1 -0
  13. package/lib/index.js.map +1 -1
  14. package/lib/interface/AutoBeOpenApiEndpointComparator.d.ts +1 -0
  15. package/lib/interface/AutoBeOpenApiEndpointComparator.js +4 -0
  16. package/lib/interface/AutoBeOpenApiEndpointComparator.js.map +1 -1
  17. package/lib/interface/AutoBeOpenApiTypeChecker.d.ts +14 -0
  18. package/lib/interface/AutoBeOpenApiTypeChecker.js +67 -0
  19. package/lib/interface/AutoBeOpenApiTypeChecker.js.map +1 -1
  20. package/lib/interface/invertOpenApiDocument.d.ts +1 -1
  21. package/lib/interface/invertOpenApiDocument.js +22 -7
  22. package/lib/interface/invertOpenApiDocument.js.map +1 -1
  23. package/lib/interface/missedOpenApiSchemas.js +3 -3
  24. package/lib/interface/missedOpenApiSchemas.js.map +1 -1
  25. package/lib/interface/revertOpenApiAccessor.js +2 -2
  26. package/lib/interface/revertOpenApiAccessor.js.map +1 -1
  27. package/lib/interface/transformOpenApiDocument.d.ts +2 -2
  28. package/lib/interface/transformOpenApiDocument.js +14 -11
  29. package/lib/interface/transformOpenApiDocument.js.map +1 -1
  30. package/lib/prisma/writePrismaApplication.d.ts +2 -2
  31. package/lib/prisma/writePrismaApplication.js +75 -66
  32. package/lib/prisma/writePrismaApplication.js.map +1 -1
  33. package/lib/test/validateTestExpression.js +3 -1
  34. package/lib/test/validateTestExpression.js.map +1 -1
  35. package/lib/test/validateTestStatement.js +3 -1
  36. package/lib/test/validateTestStatement.js.map +1 -1
  37. package/package.json +5 -5
  38. package/src/ArrayUtil.ts +21 -21
  39. package/src/AutoBeEscaper.ts +83 -0
  40. package/src/MapUtil.ts +10 -10
  41. package/src/StringUtil.ts +34 -59
  42. package/src/aggregate/AutoBeFunctionCallingMetricFactory.ts +44 -44
  43. package/src/aggregate/AutoBeProcessAggregateFactory.ts +161 -152
  44. package/src/aggregate/TokenUsageComputer.ts +49 -49
  45. package/src/aggregate/index.ts +3 -3
  46. package/src/index.ts +10 -9
  47. package/src/interface/AutoBeOpenApiEndpointComparator.ts +27 -20
  48. package/src/interface/AutoBeOpenApiTypeChecker.ts +127 -43
  49. package/src/interface/index.ts +6 -6
  50. package/src/interface/invertOpenApiDocument.ts +80 -68
  51. package/src/interface/missedOpenApiSchemas.ts +25 -25
  52. package/src/interface/revertOpenApiAccessor.ts +25 -25
  53. package/src/interface/transformOpenApiDocument.ts +94 -91
  54. package/src/prisma/index.ts +1 -1
  55. package/src/prisma/writePrismaApplication.ts +456 -439
  56. package/src/test/AutoBeTestExpressionValidator.ts +253 -253
  57. package/src/test/AutoBeTestStatementValidator.ts +72 -72
  58. package/src/test/IAutoBeTextValidateContext.ts +10 -10
  59. package/src/test/index.ts +2 -2
  60. package/src/test/validateTestApiOperateStatement.ts +92 -92
  61. package/src/test/validateTestExpression.ts +16 -10
  62. package/src/test/validateTestFunction.ts +7 -7
  63. package/src/test/validateTestStatement.ts +16 -10
@@ -1,439 +1,456 @@
1
- import { AutoBePrisma } from "@autobe/interface";
2
- import crypto from "crypto";
3
-
4
- import { ArrayUtil } from "../ArrayUtil";
5
- import { MapUtil } from "../MapUtil";
6
- import { StringUtil } from "../StringUtil";
7
-
8
- export function writePrismaApplication(props: {
9
- dbms: "postgres" | "sqlite";
10
- application: AutoBePrisma.IApplication;
11
- }): Record<string, string> {
12
- for (const file of props.application.files)
13
- for (const model of file.models) fillMappingName(model);
14
- return {
15
- ...Object.fromEntries(
16
- props.application.files
17
- .filter((file) => file.filename !== "main.prisma")
18
- .map((file) => [
19
- file.filename,
20
- writeFile({
21
- ...props,
22
- file,
23
- }),
24
- ]),
25
- ),
26
- "main.prisma":
27
- props.dbms === "postgres" ? POSTGRES_MAIN_FILE : SQLITE_MAIN_FILE,
28
- };
29
- }
30
-
31
- function writeFile(props: {
32
- dbms: "postgres" | "sqlite";
33
- application: AutoBePrisma.IApplication;
34
- file: AutoBePrisma.IFile;
35
- }): string {
36
- return props.file.models
37
- .map((model) =>
38
- writeModel({
39
- ...props,
40
- model,
41
- }),
42
- )
43
- .join("\n\n");
44
- }
45
-
46
- function writeModel(props: {
47
- dbms: "postgres" | "sqlite";
48
- application: AutoBePrisma.IApplication;
49
- file: AutoBePrisma.IFile;
50
- model: AutoBePrisma.IModel;
51
- }): string {
52
- return [
53
- writeComment(
54
- [
55
- props.model.description,
56
- "",
57
- ...(props.model.material ? [] : [`@namespace ${props.file.namespace}`]),
58
- "@author AutoBE - https://github.com/wrtnlabs/autobe",
59
- ].join("\n"),
60
- 80,
61
- ),
62
- `model ${props.model.name} {`,
63
- addIndent(
64
- ArrayUtil.paddle([writeColumns(props), writeRelations(props)]).join("\n"),
65
- ),
66
- "}",
67
- ].join("\n");
68
- }
69
-
70
- function fillMappingName(model: AutoBePrisma.IModel): void {
71
- const group: Map<string, AutoBePrisma.IForeignField[]> = new Map();
72
- for (const ff of model.foreignFields) {
73
- MapUtil.take(group, ff.relation.targetModel, () => []).push(ff);
74
- if (ff.relation.targetModel == model.name)
75
- ff.relation.mappingName = "recursive";
76
- }
77
- for (const array of group.values())
78
- if (array.length !== 1)
79
- for (const ff of array)
80
- ff.relation.mappingName = shortName(`${model.name}_of_${ff.name}`);
81
- }
82
-
83
- /* -----------------------------------------------------------
84
- COLUMNS
85
- ----------------------------------------------------------- */
86
- function writeColumns(props: {
87
- dbms: "postgres" | "sqlite";
88
- model: AutoBePrisma.IModel;
89
- }): string[] {
90
- return [
91
- "//----",
92
- "// COLUMNS",
93
- "//----",
94
- writePrimary({
95
- dbms: props.dbms,
96
- model: props.model,
97
- field: props.model.primaryField,
98
- }),
99
- ...props.model.foreignFields
100
- .map((x) => [
101
- "",
102
- writeField({
103
- dbms: props.dbms,
104
- field: x,
105
- }),
106
- ])
107
- .flat(),
108
- ...props.model.plainFields
109
- .map((x) => [
110
- "",
111
- writeField({
112
- dbms: props.dbms,
113
- field: x,
114
- }),
115
- ])
116
- .flat(),
117
- ];
118
- }
119
-
120
- function writePrimary(props: {
121
- dbms: "postgres" | "sqlite";
122
- model: AutoBePrisma.IModel;
123
- field: AutoBePrisma.IPrimaryField;
124
- }): string {
125
- const type: string | undefined =
126
- props.dbms === "postgres" ? POSTGRES_PHYSICAL_TYPES.uuid : undefined;
127
- const pkeyName: string = `${props.model.name}__pkey`;
128
- const signature: string =
129
- pkeyName.length <= MAX_IDENTIFIER_LENGTH
130
- ? "@id"
131
- : `@id(map: "${shortName(pkeyName)}")`;
132
- return [
133
- writeComment(props.field.description, 78),
134
- `${props.field.name} String ${signature}${type ? ` ${type}` : ""}`,
135
- ].join("\n");
136
- }
137
-
138
- function writeField(props: {
139
- dbms: "postgres" | "sqlite";
140
- field: AutoBePrisma.IPlainField;
141
- }): string {
142
- const logical: string = LOGICAL_TYPES[props.field.type];
143
- const physical: string | undefined =
144
- props.dbms === "postgres"
145
- ? POSTGRES_PHYSICAL_TYPES[
146
- props.field.type as keyof typeof POSTGRES_PHYSICAL_TYPES
147
- ]
148
- : undefined;
149
- return [
150
- writeComment(props.field.description, 78),
151
- [
152
- props.field.name,
153
- `${logical}${props.field.nullable ? "?" : ""}`,
154
- ...(physical ? [physical] : []),
155
- ].join(" "),
156
- ].join("\n");
157
- }
158
-
159
- /* -----------------------------------------------------------
160
- RELATIONS
161
- ----------------------------------------------------------- */
162
- function writeRelations(props: {
163
- dbms: "postgres" | "sqlite";
164
- application: AutoBePrisma.IApplication;
165
- model: AutoBePrisma.IModel;
166
- }): string[] {
167
- interface IHasRelationship {
168
- modelName: string;
169
- unique: boolean;
170
- mappingName?: string;
171
- }
172
- const hasRelationships: IHasRelationship[] = props.application.files
173
- .map((otherFile) =>
174
- otherFile.models.map((otherModel) =>
175
- otherModel.foreignFields
176
- .filter(
177
- (otherForeign) =>
178
- otherForeign.relation.targetModel === props.model.name,
179
- )
180
- .map((otherForeign) => ({
181
- modelName: otherModel.name,
182
- unique: otherForeign.unique,
183
- mappingName: otherForeign.relation.mappingName,
184
- })),
185
- ),
186
- )
187
- .flat(2);
188
- const foreignIndexes: AutoBePrisma.IForeignField[] =
189
- props.model.foreignFields.filter((f) => {
190
- if (f.unique === true)
191
- return props.model.uniqueIndexes.every(
192
- (u) => u.fieldNames.length !== 1 || u.fieldNames[0] !== f.name,
193
- );
194
- return (
195
- props.model.uniqueIndexes.every((u) => u.fieldNames[0] !== f.name) &&
196
- props.model.plainIndexes.every((p) => p.fieldNames[0] !== f.name)
197
- );
198
- });
199
- const contents: string[][] = [
200
- props.model.foreignFields.map((foreign) =>
201
- writeConstraint({
202
- dbms: props.dbms,
203
- model: props.model,
204
- foreign,
205
- }),
206
- ),
207
- hasRelationships.map((r) =>
208
- [
209
- r.mappingName ?? r.modelName,
210
- `${r.modelName}${r.unique ? "?" : "[]"}`,
211
- ...(r.mappingName ? [`@relation("${r.mappingName}")`] : []),
212
- ].join(" "),
213
- ),
214
- foreignIndexes.map((field) =>
215
- writeForeignIndex({
216
- model: props.model,
217
- field,
218
- }),
219
- ),
220
- [
221
- ...props.model.uniqueIndexes.map((unique) =>
222
- writeUniqueIndex({
223
- model: props.model,
224
- unique,
225
- }),
226
- ),
227
- ...props.model.plainIndexes.map((plain) =>
228
- writePlainIndex({
229
- model: props.model,
230
- plain,
231
- }),
232
- ),
233
- ...(props.dbms === "postgres"
234
- ? props.model.ginIndexes.map((gin) =>
235
- writeGinIndex({
236
- model: props.model,
237
- gin,
238
- }),
239
- )
240
- : []),
241
- ],
242
- ];
243
- if (contents.every((c) => c.length === 0)) return [];
244
- return [
245
- "//----",
246
- "// RELATIONS",
247
- "//----",
248
- // paddled content
249
- ...ArrayUtil.paddle(contents),
250
- ];
251
- }
252
-
253
- function writeConstraint(props: {
254
- dbms: "postgres" | "sqlite";
255
- model: AutoBePrisma.IModel;
256
- foreign: AutoBePrisma.IForeignField;
257
- }): string {
258
- // spellchecker:ignore-next-line
259
- const name: string = `${props.model.name}_${props.foreign.name}_rela`;
260
- const tooMuchLong: boolean =
261
- props.dbms === "postgres" && name.length > MAX_IDENTIFIER_LENGTH;
262
- const body: string = [
263
- props.foreign.relation.name,
264
- `${props.foreign.relation.targetModel}${props.foreign.nullable ? "?" : ""}`,
265
- `@relation(${[
266
- ...(props.foreign.relation.mappingName
267
- ? [`"${props.foreign.relation.mappingName}"`]
268
- : []),
269
- `fields: [${props.foreign.name}]`,
270
- `references: [id]`,
271
- `onDelete: Cascade`,
272
- ...(tooMuchLong ? [`map: "${shortName(name)}"`] : []),
273
- ].join(", ")})`,
274
- ].join(" ");
275
- return tooMuchLong
276
- ? StringUtil.trim`
277
- // spellchecker: ignore-next-line
278
- ${body}
279
- `
280
- : body;
281
- }
282
-
283
- function writeForeignIndex(props: {
284
- model: AutoBePrisma.IModel;
285
- field: AutoBePrisma.IForeignField;
286
- }): string {
287
- const name: string = `${props.model.name}_${props.field.name}_fkey`;
288
- const prefix: string = `@@${props.field.unique === true ? "unique" : "index"}([${props.field.name}]`;
289
- if (name.length <= MAX_IDENTIFIER_LENGTH) return `${prefix})`;
290
- return StringUtil.trim`
291
- // spellchecker: ignore-next-line
292
- ${prefix}, map: "${shortName(name)}")
293
- `;
294
- }
295
-
296
- function writeUniqueIndex(props: {
297
- model: AutoBePrisma.IModel;
298
- unique: AutoBePrisma.IUniqueIndex;
299
- }): string {
300
- const name: string = `${props.model.name}_${props.unique.fieldNames.join("_")}_key`;
301
- const prefix: string = `@@unique([${props.unique.fieldNames.join(", ")}]`;
302
- if (name.length <= MAX_IDENTIFIER_LENGTH) return `${prefix})`;
303
- return StringUtil.trim`
304
- // spellchecker: ignore-next-line
305
- ${prefix}, map: "${shortName(name)}")
306
- `;
307
- }
308
-
309
- function writePlainIndex(props: {
310
- model: AutoBePrisma.IModel;
311
- plain: AutoBePrisma.IPlainIndex;
312
- }): string {
313
- const name: string = `${props.model.name}_${props.plain.fieldNames.join("_")}_idx`;
314
- const prefix: string = `@@index([${props.plain.fieldNames.join(", ")}]`;
315
- if (name.length <= MAX_IDENTIFIER_LENGTH) return `${prefix})`;
316
- return StringUtil.trim`
317
- // spellchecker: ignore-next-line
318
- ${prefix}, map: "${shortName(name)}")
319
- `;
320
- }
321
-
322
- function writeGinIndex(props: {
323
- model: AutoBePrisma.IModel;
324
- gin: AutoBePrisma.IGinIndex;
325
- }): string {
326
- const name: string = `${props.model.name}_${props.gin.fieldName}_idx`;
327
- const prefix: string = `@@index([${props.gin.fieldName}(ops: raw("gin_trgm_ops"))], type: Gin`;
328
- if (name.length <= MAX_IDENTIFIER_LENGTH) return `${prefix})`;
329
- return StringUtil.trim`
330
- // spellchecker: ignore-next-line
331
- ${prefix}, map: "${shortName(name)}")
332
- `;
333
- }
334
-
335
- /* -----------------------------------------------------------
336
- BACKGROUND
337
- ----------------------------------------------------------- */
338
- function writeComment(content: string, length: number): string {
339
- return content
340
- .split("\r\n")
341
- .join("\n")
342
- .split("\n")
343
- .map((line) => line.trim())
344
- .map((line) => {
345
- // 77자에서 "/// " 4자를 73자가 실제 컨텐츠 최대 길이
346
- if (line.length <= length - 4) return [line];
347
- const words: string[] = line.split(" ");
348
- const result: string[] = [];
349
- let currentLine = "";
350
-
351
- for (const word of words) {
352
- const potentialLine = currentLine ? `${currentLine} ${word}` : word;
353
- if (potentialLine.length <= 73) {
354
- currentLine = potentialLine;
355
- } else {
356
- if (currentLine) result.push(currentLine);
357
- currentLine = word;
358
- }
359
- }
360
-
361
- if (currentLine) result.push(currentLine);
362
- return result;
363
- })
364
- .flat()
365
- .map((str) => `///${str.length ? ` ${str}` : ""}`)
366
- .join("\n")
367
- .trim();
368
- }
369
-
370
- function addIndent(content: string): string {
371
- return content
372
- .split("\r\n")
373
- .join("\n")
374
- .split("\n")
375
- .map((str) => ` ${str}`)
376
- .join("\n");
377
- }
378
-
379
- function shortName(name: string): string {
380
- if (name.length <= MAX_IDENTIFIER_LENGTH) return name;
381
- const hash: string = crypto
382
- .createHash("md5")
383
- .update(name)
384
- .digest("hex")
385
- .substring(0, HASH_TRUNCATION_LENGTH);
386
- return `${name.substring(0, MAX_IDENTIFIER_LENGTH - HASH_TRUNCATION_LENGTH - 1)}_${hash}`;
387
- }
388
-
389
- const LOGICAL_TYPES = {
390
- // native types
391
- boolean: "Boolean",
392
- int: "Int",
393
- double: "Float",
394
- string: "String",
395
- // formats
396
- datetime: "DateTime",
397
- uuid: "String",
398
- uri: "String",
399
- };
400
- const POSTGRES_PHYSICAL_TYPES = {
401
- int: "@db.Integer",
402
- double: "@db.DoublePrecision",
403
- uuid: "@db.Uuid",
404
- datetime: "@db.Timestamptz",
405
- uri: "@db.VarChar(80000)",
406
- };
407
-
408
- const POSTGRES_MAIN_FILE = StringUtil.trim`
409
- generator client {
410
- provider = "prisma-client-js"
411
- engineType = "client"
412
- previewFeatures = ["postgresqlExtensions", "views"]
413
- }
414
- datasource db {
415
- provider = "postgresql"
416
- url = env("DATABASE_URL")
417
- extensions = [pg_trgm]
418
- }
419
- generator markdown {
420
- provider = "prisma-markdown"
421
- output = "../../docs/ERD.md"
422
- }
423
- `;
424
- const SQLITE_MAIN_FILE = StringUtil.trim`
425
- generator client {
426
- provider = "prisma-client-js"
427
- engineType = "client"
428
- }
429
- datasource db {
430
- provider = "sqlite"
431
- url = "file:../db.sqlite"
432
- }
433
- generator markdown {
434
- provider = "prisma-markdown"
435
- output = "../../docs/ERD.md"
436
- }
437
- `;
438
- const MAX_IDENTIFIER_LENGTH = 63;
439
- const HASH_TRUNCATION_LENGTH = 8;
1
+ import { AutoBeDatabase } from "@autobe/interface";
2
+ import crypto from "crypto";
3
+
4
+ import { ArrayUtil } from "../ArrayUtil";
5
+ import { MapUtil } from "../MapUtil";
6
+ import { StringUtil } from "../StringUtil";
7
+
8
+ export function writePrismaApplication(props: {
9
+ dbms: "postgres" | "sqlite";
10
+ application: AutoBeDatabase.IApplication;
11
+ }): Record<string, string> {
12
+ for (const file of props.application.files)
13
+ for (const model of file.models) fillMappingName(model);
14
+ return {
15
+ ...Object.fromEntries(
16
+ props.application.files
17
+ .filter((file) => file.filename !== "main.prisma")
18
+ .map((file) => [
19
+ file.filename,
20
+ writeFile({
21
+ ...props,
22
+ file,
23
+ }),
24
+ ]),
25
+ ),
26
+ "main.prisma":
27
+ props.dbms === "postgres" ? POSTGRES_MAIN_FILE : SQLITE_MAIN_FILE,
28
+ };
29
+ }
30
+
31
+ function writeFile(props: {
32
+ dbms: "postgres" | "sqlite";
33
+ application: AutoBeDatabase.IApplication;
34
+ file: AutoBeDatabase.IFile;
35
+ }): string {
36
+ return props.file.models
37
+ .map((model) =>
38
+ writeModel({
39
+ ...props,
40
+ model,
41
+ }),
42
+ )
43
+ .join("\n\n");
44
+ }
45
+
46
+ function writeModel(props: {
47
+ dbms: "postgres" | "sqlite";
48
+ application: AutoBeDatabase.IApplication;
49
+ file: AutoBeDatabase.IFile;
50
+ model: AutoBeDatabase.IModel;
51
+ }): string {
52
+ return [
53
+ writeComment(
54
+ [
55
+ props.model.description,
56
+ "",
57
+ ...(props.model.material ? [] : [`@namespace ${props.file.namespace}`]),
58
+ "@author AutoBE - https://github.com/wrtnlabs/autobe",
59
+ ].join("\n"),
60
+ 80,
61
+ ),
62
+ `model ${props.model.name} {`,
63
+ addIndent(
64
+ ArrayUtil.paddle([writeColumns(props), writeRelations(props)]).join("\n"),
65
+ ),
66
+ "}",
67
+ ].join("\n");
68
+ }
69
+
70
+ function fillMappingName(model: AutoBeDatabase.IModel): void {
71
+ const group: Map<string, AutoBeDatabase.IForeignField[]> = new Map();
72
+ for (const ff of model.foreignFields) {
73
+ MapUtil.take(group, ff.relation.targetModel, () => []).push(ff);
74
+ if (ff.relation.targetModel == model.name)
75
+ ff.relation.mappingName = "recursive";
76
+ }
77
+ for (const array of group.values())
78
+ if (array.length !== 1)
79
+ for (const ff of array)
80
+ ff.relation.mappingName = shortName(`${model.name}_of_${ff.name}`);
81
+ }
82
+
83
+ /* -----------------------------------------------------------
84
+ COLUMNS
85
+ ----------------------------------------------------------- */
86
+ function writeColumns(props: {
87
+ dbms: "postgres" | "sqlite";
88
+ model: AutoBeDatabase.IModel;
89
+ }): string[] {
90
+ return [
91
+ "//----",
92
+ "// COLUMNS",
93
+ "//----",
94
+ writePrimary({
95
+ dbms: props.dbms,
96
+ model: props.model,
97
+ field: props.model.primaryField,
98
+ }),
99
+ ...props.model.foreignFields
100
+ .map((x) => [
101
+ "",
102
+ writeField({
103
+ dbms: props.dbms,
104
+ field: x,
105
+ }),
106
+ ])
107
+ .flat(),
108
+ ...props.model.plainFields
109
+ .map((x) => [
110
+ "",
111
+ writeField({
112
+ dbms: props.dbms,
113
+ field: x,
114
+ }),
115
+ ])
116
+ .flat(),
117
+ ];
118
+ }
119
+
120
+ function writePrimary(props: {
121
+ dbms: "postgres" | "sqlite";
122
+ model: AutoBeDatabase.IModel;
123
+ field: AutoBeDatabase.IPrimaryField;
124
+ }): string {
125
+ const type: string | undefined =
126
+ props.dbms === "postgres" ? POSTGRES_PHYSICAL_TYPES.uuid : undefined;
127
+ const pkeyName: string = `${props.model.name}__pkey`;
128
+ const signature: string =
129
+ pkeyName.length <= MAX_IDENTIFIER_LENGTH
130
+ ? "@id"
131
+ : `@id(map: "${shortName(pkeyName)}")`;
132
+ return [
133
+ writeComment(props.field.description, 78),
134
+ `${props.field.name} String ${signature}${type ? ` ${type}` : ""}`,
135
+ ].join("\n");
136
+ }
137
+
138
+ function writeField(props: {
139
+ dbms: "postgres" | "sqlite";
140
+ field: AutoBeDatabase.IPlainField;
141
+ }): string {
142
+ const logical: string = LOGICAL_TYPES[props.field.type];
143
+ const physical: string | undefined =
144
+ props.dbms === "postgres"
145
+ ? POSTGRES_PHYSICAL_TYPES[
146
+ props.field.type as keyof typeof POSTGRES_PHYSICAL_TYPES
147
+ ]
148
+ : undefined;
149
+ return [
150
+ writeComment(props.field.description, 78),
151
+ [
152
+ props.field.name,
153
+ `${logical}${props.field.nullable ? "?" : ""}`,
154
+ ...(physical ? [physical] : []),
155
+ ].join(" "),
156
+ ].join("\n");
157
+ }
158
+
159
+ /* -----------------------------------------------------------
160
+ RELATIONS
161
+ ----------------------------------------------------------- */
162
+ function writeRelations(props: {
163
+ dbms: "postgres" | "sqlite";
164
+ application: AutoBeDatabase.IApplication;
165
+ model: AutoBeDatabase.IModel;
166
+ }): string[] {
167
+ interface IHasRelationship {
168
+ modelName: string;
169
+ unique: boolean;
170
+ oppositeName: string;
171
+ mappingName?: string;
172
+ }
173
+ const hasRelationships: IHasRelationship[] = props.application.files
174
+ .map((otherFile) =>
175
+ otherFile.models.map((otherModel) =>
176
+ otherModel.foreignFields
177
+ .filter(
178
+ (otherForeign) =>
179
+ otherForeign.relation.targetModel === props.model.name,
180
+ )
181
+ .map((otherForeign) => ({
182
+ modelName: otherModel.name,
183
+ unique: otherForeign.unique,
184
+ oppositeName: otherForeign.relation.oppositeName,
185
+ mappingName: otherForeign.relation.mappingName,
186
+ })),
187
+ ),
188
+ )
189
+ .flat(2);
190
+ const foreignIndexes: AutoBeDatabase.IForeignField[] =
191
+ props.model.foreignFields.filter((f) => {
192
+ if (f.unique === true)
193
+ return props.model.uniqueIndexes.every(
194
+ (u) => u.fieldNames.length !== 1 || u.fieldNames[0] !== f.name,
195
+ );
196
+ return (
197
+ props.model.uniqueIndexes.every((u) => u.fieldNames[0] !== f.name) &&
198
+ props.model.plainIndexes.every((p) => p.fieldNames[0] !== f.name)
199
+ );
200
+ });
201
+
202
+ const print = (title: string, content: string[]): string[] => {
203
+ if (content.length === 0) return [];
204
+ return [
205
+ // title
206
+ "//----",
207
+ ...title.split("\n").map((l) => `// ${l}`),
208
+ "//----",
209
+ // main content
210
+ ...content,
211
+ ];
212
+ };
213
+ return ArrayUtil.paddle([
214
+ print(
215
+ StringUtil.trim`
216
+ BELONGED RELATIONS,
217
+ - format: (propertyKey targetModel constraint)
218
+ `,
219
+ props.model.foreignFields.map((foreign) =>
220
+ writeConstraint({
221
+ dbms: props.dbms,
222
+ model: props.model,
223
+ foreign,
224
+ }),
225
+ ),
226
+ ),
227
+ print(
228
+ StringUtil.trim`
229
+ HAS RELATIONS
230
+ - format: (propertyKey targetModel)
231
+ `,
232
+ hasRelationships.map((r) =>
233
+ [
234
+ r.oppositeName ?? r.mappingName ?? r.modelName, // for legacy histories
235
+ `${r.modelName}${r.unique ? "?" : "[]"}`,
236
+ ...(r.mappingName ? [`@relation("${r.mappingName}")`] : []),
237
+ ].join(" "),
238
+ ),
239
+ ),
240
+ print("INDEXES", [
241
+ ...foreignIndexes.map((field) =>
242
+ writeForeignIndex({
243
+ model: props.model,
244
+ field,
245
+ }),
246
+ ),
247
+ ...props.model.uniqueIndexes.map((unique) =>
248
+ writeUniqueIndex({
249
+ model: props.model,
250
+ unique,
251
+ }),
252
+ ),
253
+ ...props.model.plainIndexes.map((plain) =>
254
+ writePlainIndex({
255
+ model: props.model,
256
+ plain,
257
+ }),
258
+ ),
259
+ ...(props.dbms === "postgres"
260
+ ? props.model.ginIndexes.map((gin) =>
261
+ writeGinIndex({
262
+ model: props.model,
263
+ gin,
264
+ }),
265
+ )
266
+ : []),
267
+ ]),
268
+ ]);
269
+ }
270
+
271
+ function writeConstraint(props: {
272
+ dbms: "postgres" | "sqlite";
273
+ model: AutoBeDatabase.IModel;
274
+ foreign: AutoBeDatabase.IForeignField;
275
+ }): string {
276
+ // spellchecker:ignore-next-line
277
+ const name: string = `${props.model.name}_${props.foreign.name}_rela`;
278
+ const tooMuchLong: boolean =
279
+ props.dbms === "postgres" && name.length > MAX_IDENTIFIER_LENGTH;
280
+ const body: string = [
281
+ props.foreign.relation.name,
282
+ `${props.foreign.relation.targetModel}${props.foreign.nullable ? "?" : ""}`,
283
+ `@relation(${[
284
+ ...(props.foreign.relation.mappingName
285
+ ? [`"${props.foreign.relation.mappingName}"`]
286
+ : []),
287
+ `fields: [${props.foreign.name}]`,
288
+ `references: [id]`,
289
+ `onDelete: Cascade`,
290
+ ...(tooMuchLong ? [`map: "${shortName(name)}"`] : []),
291
+ ].join(", ")})`,
292
+ ].join(" ");
293
+ return tooMuchLong
294
+ ? StringUtil.trim`
295
+ // spellchecker: ignore-next-line
296
+ ${body}
297
+ `
298
+ : body;
299
+ }
300
+
301
+ function writeForeignIndex(props: {
302
+ model: AutoBeDatabase.IModel;
303
+ field: AutoBeDatabase.IForeignField;
304
+ }): string {
305
+ const name: string = `${props.model.name}_${props.field.name}_fkey`;
306
+ const prefix: string = `@@${props.field.unique === true ? "unique" : "index"}([${props.field.name}]`;
307
+ if (name.length <= MAX_IDENTIFIER_LENGTH) return `${prefix})`;
308
+ return StringUtil.trim`
309
+ // spellchecker: ignore-next-line
310
+ ${prefix}, map: "${shortName(name)}")
311
+ `;
312
+ }
313
+
314
+ function writeUniqueIndex(props: {
315
+ model: AutoBeDatabase.IModel;
316
+ unique: AutoBeDatabase.IUniqueIndex;
317
+ }): string {
318
+ const name: string = `${props.model.name}_${props.unique.fieldNames.join("_")}_key`;
319
+ const prefix: string = `@@unique([${props.unique.fieldNames.join(", ")}]`;
320
+ if (name.length <= MAX_IDENTIFIER_LENGTH) return `${prefix})`;
321
+ return StringUtil.trim`
322
+ // spellchecker: ignore-next-line
323
+ ${prefix}, map: "${shortName(name)}")
324
+ `;
325
+ }
326
+
327
+ function writePlainIndex(props: {
328
+ model: AutoBeDatabase.IModel;
329
+ plain: AutoBeDatabase.IPlainIndex;
330
+ }): string {
331
+ const name: string = `${props.model.name}_${props.plain.fieldNames.join("_")}_idx`;
332
+ const prefix: string = `@@index([${props.plain.fieldNames.join(", ")}]`;
333
+ if (name.length <= MAX_IDENTIFIER_LENGTH) return `${prefix})`;
334
+ return StringUtil.trim`
335
+ // spellchecker: ignore-next-line
336
+ ${prefix}, map: "${shortName(name)}")
337
+ `;
338
+ }
339
+
340
+ function writeGinIndex(props: {
341
+ model: AutoBeDatabase.IModel;
342
+ gin: AutoBeDatabase.IGinIndex;
343
+ }): string {
344
+ const name: string = `${props.model.name}_${props.gin.fieldName}_idx`;
345
+ const prefix: string = `@@index([${props.gin.fieldName}(ops: raw("gin_trgm_ops"))], type: Gin`;
346
+ if (name.length <= MAX_IDENTIFIER_LENGTH) return `${prefix})`;
347
+ return StringUtil.trim`
348
+ // spellchecker: ignore-next-line
349
+ ${prefix}, map: "${shortName(name)}")
350
+ `;
351
+ }
352
+
353
+ /* -----------------------------------------------------------
354
+ BACKGROUND
355
+ ----------------------------------------------------------- */
356
+ function writeComment(content: string, length: number): string {
357
+ return content
358
+ .split("\r\n")
359
+ .join("\n")
360
+ .split("\n")
361
+ .map((line) => line.trim())
362
+ .map((line) => {
363
+ if (line.length <= length - 4) return [line];
364
+ const words: string[] = line.split(" ");
365
+ const result: string[] = [];
366
+ let currentLine = "";
367
+
368
+ for (const word of words) {
369
+ const potentialLine = currentLine ? `${currentLine} ${word}` : word;
370
+ if (potentialLine.length <= 73) {
371
+ currentLine = potentialLine;
372
+ } else {
373
+ if (currentLine) result.push(currentLine);
374
+ currentLine = word;
375
+ }
376
+ }
377
+
378
+ if (currentLine) result.push(currentLine);
379
+ return result;
380
+ })
381
+ .flat()
382
+ .map((str) => `///${str.length ? ` ${str}` : ""}`)
383
+ .join("\n")
384
+ .trim();
385
+ }
386
+
387
+ function addIndent(content: string): string {
388
+ return content
389
+ .split("\r\n")
390
+ .join("\n")
391
+ .split("\n")
392
+ .map((str) => ` ${str}`)
393
+ .join("\n");
394
+ }
395
+
396
+ function shortName(name: string): string {
397
+ if (name.length <= MAX_IDENTIFIER_LENGTH) return name;
398
+ const hash: string = crypto
399
+ .createHash("md5")
400
+ .update(name)
401
+ .digest("hex")
402
+ .substring(0, HASH_TRUNCATION_LENGTH);
403
+ return `${name.substring(0, MAX_IDENTIFIER_LENGTH - HASH_TRUNCATION_LENGTH - 1)}_${hash}`;
404
+ }
405
+
406
+ const LOGICAL_TYPES = {
407
+ // native types
408
+ boolean: "Boolean",
409
+ int: "Int",
410
+ double: "Float",
411
+ string: "String",
412
+ // formats
413
+ datetime: "DateTime",
414
+ uuid: "String",
415
+ uri: "String",
416
+ };
417
+ const POSTGRES_PHYSICAL_TYPES = {
418
+ int: "@db.Integer",
419
+ double: "@db.DoublePrecision",
420
+ uuid: "@db.Uuid",
421
+ datetime: "@db.Timestamptz",
422
+ uri: "@db.VarChar(80000)",
423
+ };
424
+
425
+ const POSTGRES_MAIN_FILE = StringUtil.trim`
426
+ generator client {
427
+ provider = "prisma-client"
428
+ previewFeatures = ["postgresqlExtensions", "views"]
429
+ output = "../../src/prisma"
430
+ moduleFormat = "cjs"
431
+ }
432
+ datasource db {
433
+ provider = "postgresql"
434
+ extensions = [pg_trgm]
435
+ }
436
+ generator markdown {
437
+ provider = "prisma-markdown"
438
+ output = "../../docs/ERD.md"
439
+ }
440
+ `;
441
+ const SQLITE_MAIN_FILE = StringUtil.trim`
442
+ generator client {
443
+ provider = "prisma-client"
444
+ output = "../../src/prisma"
445
+ moduleFormat = "cjs"
446
+ }
447
+ datasource db {
448
+ provider = "sqlite"
449
+ }
450
+ generator markdown {
451
+ provider = "prisma-markdown"
452
+ output = "../../docs/ERD.md"
453
+ }
454
+ `;
455
+ const MAX_IDENTIFIER_LENGTH = 63;
456
+ const HASH_TRUNCATION_LENGTH = 8;