@autobe/utils 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.
@@ -1,456 +1,456 @@
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;
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;