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