@autobe/compiler 0.3.23 → 0.4.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.
- package/lib/AutoBeCompiler.d.ts +4 -5
- package/lib/AutoBeCompiler.js +5 -8
- package/lib/AutoBeCompiler.js.map +1 -1
- package/lib/AutoBePrismaCompiler.d.ts +3 -1
- package/lib/AutoBePrismaCompiler.js +12 -0
- package/lib/AutoBePrismaCompiler.js.map +1 -1
- package/lib/prisma/validatePrismaApplication.d.ts +2 -0
- package/lib/prisma/validatePrismaApplication.js +249 -0
- package/lib/prisma/validatePrismaApplication.js.map +1 -0
- package/lib/prisma/writePrismaApplication.d.ts +2 -0
- package/lib/prisma/writePrismaApplication.js +198 -0
- package/lib/prisma/writePrismaApplication.js.map +1 -0
- package/lib/utils/ArrayUtil.d.ts +1 -0
- package/lib/utils/ArrayUtil.js +13 -0
- package/lib/utils/ArrayUtil.js.map +1 -1
- package/lib/utils/MapUtil.d.ts +3 -0
- package/lib/utils/MapUtil.js +16 -0
- package/lib/utils/MapUtil.js.map +1 -0
- package/package.json +2 -2
- package/src/AutoBeCompiler.ts +9 -15
- package/src/AutoBePrismaCompiler.ts +17 -0
- package/src/prisma/validatePrismaApplication.ts +331 -0
- package/src/prisma/writePrismaApplication.ts +250 -0
- package/src/utils/ArrayUtil.ts +10 -0
- package/src/utils/MapUtil.ts +10 -0
package/src/AutoBeCompiler.ts
CHANGED
|
@@ -3,8 +3,6 @@ import {
|
|
|
3
3
|
IAutoBeCompiler,
|
|
4
4
|
IAutoBeInterfaceCompiler,
|
|
5
5
|
IAutoBePrismaCompiler,
|
|
6
|
-
IAutoBePrismaCompilerProps,
|
|
7
|
-
IAutoBePrismaCompilerResult,
|
|
8
6
|
IAutoBeTypeScriptCompiler,
|
|
9
7
|
IAutoBeTypeScriptCompilerProps,
|
|
10
8
|
IAutoBeTypeScriptCompilerResult,
|
|
@@ -15,27 +13,23 @@ import { AutoBePrismaCompiler } from "./AutoBePrismaCompiler";
|
|
|
15
13
|
import { AutoBeTypeScriptCompiler } from "./AutoBeTypeScriptCompiler";
|
|
16
14
|
|
|
17
15
|
export class AutoBeCompiler implements IAutoBeCompiler {
|
|
18
|
-
|
|
19
|
-
private readonly interface_: IAutoBeInterfaceCompiler =
|
|
20
|
-
new AutoBeInterfaceCompiler();
|
|
21
|
-
private readonly typescript_: IAutoBeTypeScriptCompiler =
|
|
22
|
-
new AutoBeTypeScriptCompiler();
|
|
23
|
-
|
|
24
|
-
public prisma(
|
|
25
|
-
props: IAutoBePrismaCompilerProps,
|
|
26
|
-
): Promise<IAutoBePrismaCompilerResult> {
|
|
27
|
-
return this.prisma_.compile(props);
|
|
28
|
-
}
|
|
16
|
+
public readonly prisma: IAutoBePrismaCompiler = new AutoBePrismaCompiler();
|
|
29
17
|
|
|
30
18
|
public interface(
|
|
31
19
|
document: AutoBeOpenApi.IDocument,
|
|
32
20
|
): Promise<Record<string, string>> {
|
|
33
|
-
return this.
|
|
21
|
+
return this.interface_compiler_.compile(document);
|
|
34
22
|
}
|
|
35
23
|
|
|
36
24
|
public typescript(
|
|
37
25
|
props: IAutoBeTypeScriptCompilerProps,
|
|
38
26
|
): Promise<IAutoBeTypeScriptCompilerResult> {
|
|
39
|
-
return this.
|
|
27
|
+
return this.typescript_compiler_.compile(props);
|
|
40
28
|
}
|
|
29
|
+
|
|
30
|
+
private readonly interface_compiler_: IAutoBeInterfaceCompiler =
|
|
31
|
+
new AutoBeInterfaceCompiler();
|
|
32
|
+
|
|
33
|
+
private readonly typescript_compiler_: IAutoBeTypeScriptCompiler =
|
|
34
|
+
new AutoBeTypeScriptCompiler();
|
|
41
35
|
}
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
|
+
AutoBePrisma,
|
|
2
3
|
IAutoBePrismaCompiler,
|
|
3
4
|
IAutoBePrismaCompilerProps,
|
|
4
5
|
IAutoBePrismaCompilerResult,
|
|
6
|
+
IAutoBePrismaValidation,
|
|
5
7
|
} from "@autobe/interface";
|
|
6
8
|
import { EmbedPrisma } from "embed-prisma";
|
|
7
9
|
|
|
10
|
+
import { validatePrismaApplication } from "./prisma/validatePrismaApplication";
|
|
11
|
+
import { writePrismaApplication } from "./prisma/writePrismaApplication";
|
|
12
|
+
|
|
8
13
|
export class AutoBePrismaCompiler implements IAutoBePrismaCompiler {
|
|
9
14
|
public async compile(
|
|
10
15
|
props: IAutoBePrismaCompilerProps,
|
|
@@ -12,4 +17,16 @@ export class AutoBePrismaCompiler implements IAutoBePrismaCompiler {
|
|
|
12
17
|
const compiler: EmbedPrisma = new EmbedPrisma();
|
|
13
18
|
return compiler.compile(props.files);
|
|
14
19
|
}
|
|
20
|
+
|
|
21
|
+
public async validate(
|
|
22
|
+
app: AutoBePrisma.IApplication,
|
|
23
|
+
): Promise<IAutoBePrismaValidation> {
|
|
24
|
+
return validatePrismaApplication(app);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
public async write(
|
|
28
|
+
app: AutoBePrisma.IApplication,
|
|
29
|
+
): Promise<Record<string, string>> {
|
|
30
|
+
return writePrismaApplication(app);
|
|
31
|
+
}
|
|
15
32
|
}
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import { AutoBePrisma, IAutoBePrismaValidation } from "@autobe/interface";
|
|
2
|
+
import { HashMap, hash } from "tstl";
|
|
3
|
+
|
|
4
|
+
import { MapUtil } from "../utils/MapUtil";
|
|
5
|
+
|
|
6
|
+
export function validatePrismaApplication(
|
|
7
|
+
application: AutoBePrisma.IApplication,
|
|
8
|
+
): IAutoBePrismaValidation {
|
|
9
|
+
const dict: Map<string, AutoBePrisma.IModel> = new Map(
|
|
10
|
+
application.files
|
|
11
|
+
.map((file) => file.models)
|
|
12
|
+
.flat()
|
|
13
|
+
.map((model) => [model.name, model]),
|
|
14
|
+
);
|
|
15
|
+
const errors: IAutoBePrismaValidation.IError[] = [
|
|
16
|
+
...validateDuplicatedFiles(application),
|
|
17
|
+
...validateDuplicatedModels(application),
|
|
18
|
+
...application.files
|
|
19
|
+
.map((file, fi) =>
|
|
20
|
+
file.models.map((model, mi) => {
|
|
21
|
+
const accessor: string = `application.files[${fi}].models[${mi}]`;
|
|
22
|
+
return [
|
|
23
|
+
...validateDuplicatedFields(model, accessor),
|
|
24
|
+
...validateDuplicatedIndexes(model, accessor),
|
|
25
|
+
...validateIndexes(model, accessor),
|
|
26
|
+
...validateReferences(model, accessor, dict),
|
|
27
|
+
];
|
|
28
|
+
}),
|
|
29
|
+
)
|
|
30
|
+
.flat(2),
|
|
31
|
+
];
|
|
32
|
+
return errors.length === 0
|
|
33
|
+
? { success: true, data: application }
|
|
34
|
+
: { success: false, data: application, errors };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/* -----------------------------------------------------------
|
|
38
|
+
DUPLICATES
|
|
39
|
+
----------------------------------------------------------- */
|
|
40
|
+
function validateDuplicatedFiles(
|
|
41
|
+
app: AutoBePrisma.IApplication,
|
|
42
|
+
): IAutoBePrismaValidation.IError[] {
|
|
43
|
+
interface IFileContainer {
|
|
44
|
+
file: AutoBePrisma.IFile;
|
|
45
|
+
index: number;
|
|
46
|
+
}
|
|
47
|
+
const group: Map<string, IFileContainer[]> = new Map();
|
|
48
|
+
app.files.forEach((file, fileIndex) => {
|
|
49
|
+
const container: IFileContainer = { file, index: fileIndex };
|
|
50
|
+
MapUtil.take(group, file.filename, () => []).push(container);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const errors: IAutoBePrismaValidation.IError[] = [];
|
|
54
|
+
for (const array of group.values())
|
|
55
|
+
if (array.length !== 1)
|
|
56
|
+
array.forEach((container, i) => {
|
|
57
|
+
errors.push({
|
|
58
|
+
path: `application.files[${container.index}]`,
|
|
59
|
+
table: null,
|
|
60
|
+
field: null,
|
|
61
|
+
message: [
|
|
62
|
+
`File ${container.file.filename} is duplicated.`,
|
|
63
|
+
"",
|
|
64
|
+
"Accessors of the other duplicated files are:",
|
|
65
|
+
"",
|
|
66
|
+
...array
|
|
67
|
+
.filter((_oppo, j) => i !== j)
|
|
68
|
+
.map((oppo) => `- application.files[${oppo.index}]`),
|
|
69
|
+
].join("\n"),
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
return errors;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function validateDuplicatedModels(
|
|
76
|
+
app: AutoBePrisma.IApplication,
|
|
77
|
+
): IAutoBePrismaValidation.IError[] {
|
|
78
|
+
interface IModelContainer {
|
|
79
|
+
file: AutoBePrisma.IFile;
|
|
80
|
+
model: AutoBePrisma.IModel;
|
|
81
|
+
fileIndex: number;
|
|
82
|
+
modelIndex: number;
|
|
83
|
+
}
|
|
84
|
+
const modelContainers: Map<string, IModelContainer[]> = new Map();
|
|
85
|
+
app.files.forEach((file, fileIndex) => {
|
|
86
|
+
file.models.forEach((model, modelIndex) => {
|
|
87
|
+
const container: IModelContainer = {
|
|
88
|
+
file,
|
|
89
|
+
model,
|
|
90
|
+
fileIndex,
|
|
91
|
+
modelIndex,
|
|
92
|
+
};
|
|
93
|
+
MapUtil.take(modelContainers, model.name, () => []).push(container);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const errors: IAutoBePrismaValidation.IError[] = [];
|
|
98
|
+
for (const array of modelContainers.values())
|
|
99
|
+
if (array.length !== 1)
|
|
100
|
+
array.forEach((container, i) => {
|
|
101
|
+
errors.push({
|
|
102
|
+
path: `application.files[${container.fileIndex}].models[${container.modelIndex}]`,
|
|
103
|
+
table: container.model.name,
|
|
104
|
+
field: null,
|
|
105
|
+
message: [
|
|
106
|
+
`Model ${container.model.name} is duplicated.`,
|
|
107
|
+
"",
|
|
108
|
+
"Accessors of the other duplicated models are:",
|
|
109
|
+
"",
|
|
110
|
+
...array
|
|
111
|
+
.filter((_oppo, j) => i !== j)
|
|
112
|
+
.map(
|
|
113
|
+
(oppo) =>
|
|
114
|
+
`- application.files[${oppo.fileIndex}].models[${oppo.modelIndex}]`,
|
|
115
|
+
),
|
|
116
|
+
].join("\n"),
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
return errors;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function validateDuplicatedFields(
|
|
123
|
+
model: AutoBePrisma.IModel,
|
|
124
|
+
accessor: string,
|
|
125
|
+
): IAutoBePrismaValidation.IError[] {
|
|
126
|
+
const group: Map<string, string[]> = new Map();
|
|
127
|
+
MapUtil.take(group, model.primaryField.name, () => []).push(
|
|
128
|
+
`${accessor}.primaryField.name`,
|
|
129
|
+
);
|
|
130
|
+
model.foreignFields.forEach((field, i) =>
|
|
131
|
+
MapUtil.take(group, field.name, () => []).push(
|
|
132
|
+
`${accessor}.foreignFields[${i}].name`,
|
|
133
|
+
),
|
|
134
|
+
);
|
|
135
|
+
model.plainFields.forEach((field, i) =>
|
|
136
|
+
MapUtil.take(group, field.name, () => []).push(
|
|
137
|
+
`${accessor}.plainFields[${i}].name`,
|
|
138
|
+
),
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
const errors: IAutoBePrismaValidation.IError[] = [];
|
|
142
|
+
for (const [field, array] of group)
|
|
143
|
+
if (array.length !== 1)
|
|
144
|
+
array.forEach((path, i) => {
|
|
145
|
+
errors.push({
|
|
146
|
+
path,
|
|
147
|
+
table: model.name,
|
|
148
|
+
field,
|
|
149
|
+
message: [
|
|
150
|
+
`Field ${field} is duplicated.`,
|
|
151
|
+
"",
|
|
152
|
+
"Accessors of the other duplicated fields are:",
|
|
153
|
+
"",
|
|
154
|
+
...array.filter((_oppo, j) => i !== j).map((a) => `- ${a}`),
|
|
155
|
+
].join("\n"),
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
return errors;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function validateDuplicatedIndexes(
|
|
162
|
+
model: AutoBePrisma.IModel,
|
|
163
|
+
accessor: string,
|
|
164
|
+
): IAutoBePrismaValidation.IError[] {
|
|
165
|
+
const group: HashMap<string[], string[]> = new HashMap(
|
|
166
|
+
(x) => hash(...x),
|
|
167
|
+
(x, y) => x.length === y.length && x.every((v, i) => v === y[i]),
|
|
168
|
+
);
|
|
169
|
+
model.uniqueIndexes.forEach((unique, i) =>
|
|
170
|
+
group
|
|
171
|
+
.take(unique.fieldNames, () => [])
|
|
172
|
+
.push(`${accessor}.uniqueIndexes[${i}].fieldNames`),
|
|
173
|
+
);
|
|
174
|
+
model.plainIndexes.forEach((plain, i) =>
|
|
175
|
+
group
|
|
176
|
+
.take(plain.fieldNames, () => [])
|
|
177
|
+
.push(`${accessor}.plainIndexes[${i}].fieldNames`),
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
const errors: IAutoBePrismaValidation.IError[] = [];
|
|
181
|
+
for (const { first: fieldNames, second: array } of group)
|
|
182
|
+
if (array.length !== 1)
|
|
183
|
+
array.forEach((path, i) => {
|
|
184
|
+
errors.push({
|
|
185
|
+
path,
|
|
186
|
+
table: model.name,
|
|
187
|
+
field: null,
|
|
188
|
+
message: [
|
|
189
|
+
`Duplicated index found (${fieldNames.join(", ")})`,
|
|
190
|
+
"",
|
|
191
|
+
"Accessors of the other duplicated indexes are:",
|
|
192
|
+
"",
|
|
193
|
+
...array.filter((_oppo, j) => i !== j).map((a) => `- ${a}`),
|
|
194
|
+
].join("\n"),
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
if (
|
|
199
|
+
model.ginIndexes.length !==
|
|
200
|
+
new Set(model.ginIndexes.map((g) => g.fieldName)).size
|
|
201
|
+
)
|
|
202
|
+
errors.push({
|
|
203
|
+
path: `${accessor}.ginIndexes[].fieldName`,
|
|
204
|
+
table: model.name,
|
|
205
|
+
field: null,
|
|
206
|
+
message: [
|
|
207
|
+
"Duplicated GIN index found.",
|
|
208
|
+
"",
|
|
209
|
+
"GIN index can only be used once per field.",
|
|
210
|
+
"Please remove the duplicated GIN indexes.",
|
|
211
|
+
].join("\n"),
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
return errors;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/* -----------------------------------------------------------
|
|
218
|
+
VALIDATIONS
|
|
219
|
+
----------------------------------------------------------- */
|
|
220
|
+
function validateIndexes(
|
|
221
|
+
model: AutoBePrisma.IModel,
|
|
222
|
+
accessor: string,
|
|
223
|
+
): IAutoBePrismaValidation.IError[] {
|
|
224
|
+
// EMENSION
|
|
225
|
+
model.uniqueIndexes = model.uniqueIndexes.filter(
|
|
226
|
+
(unique) =>
|
|
227
|
+
unique.fieldNames.length !== 0 &&
|
|
228
|
+
unique.fieldNames[0] !== model.primaryField.name,
|
|
229
|
+
);
|
|
230
|
+
model.plainIndexes = model.plainIndexes.filter(
|
|
231
|
+
(plain) =>
|
|
232
|
+
plain.fieldNames.length !== 0 &&
|
|
233
|
+
plain.fieldNames[0] !== model.primaryField.name,
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
const errors: IAutoBePrismaValidation.IError[] = [];
|
|
237
|
+
const columnNames: Set<string> = new Set([
|
|
238
|
+
model.primaryField.name,
|
|
239
|
+
...model.foreignFields.map((field) => field.name),
|
|
240
|
+
...model.plainFields.map((field) => field.name),
|
|
241
|
+
]);
|
|
242
|
+
|
|
243
|
+
// COLUMN LEVEL VALIDATION
|
|
244
|
+
const validate = <T>(props: {
|
|
245
|
+
title: string;
|
|
246
|
+
indexes: T[];
|
|
247
|
+
fieldNames: (idx: T) => string[];
|
|
248
|
+
accessor: (i: number, j: number) => string;
|
|
249
|
+
additional?: (idx: T, i: number) => void;
|
|
250
|
+
}) => {
|
|
251
|
+
props.indexes.forEach((idx, i) => {
|
|
252
|
+
// FIND TARGET FIELD
|
|
253
|
+
props.fieldNames(idx).forEach((name, j) => {
|
|
254
|
+
if (!columnNames.has(name))
|
|
255
|
+
errors.push({
|
|
256
|
+
path: `${accessor}.${props.accessor(i, j)}`,
|
|
257
|
+
table: model.name,
|
|
258
|
+
field: null,
|
|
259
|
+
message: `Field ${name} does not exist in model ${model.name}.`,
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
};
|
|
264
|
+
validate({
|
|
265
|
+
title: "unique index",
|
|
266
|
+
indexes: model.uniqueIndexes,
|
|
267
|
+
fieldNames: (idx) => idx.fieldNames,
|
|
268
|
+
accessor: (i, j) => `uniqueIndexes[${i}].fieldNames[${j}]`,
|
|
269
|
+
});
|
|
270
|
+
validate({
|
|
271
|
+
title: "index",
|
|
272
|
+
indexes: model.plainIndexes,
|
|
273
|
+
fieldNames: (idx) => idx.fieldNames,
|
|
274
|
+
accessor: (i, j) => `plainIndexes[${i}].fieldNames[${j}]`,
|
|
275
|
+
});
|
|
276
|
+
validate({
|
|
277
|
+
title: "index",
|
|
278
|
+
indexes: model.ginIndexes,
|
|
279
|
+
fieldNames: (idx) => [idx.fieldName],
|
|
280
|
+
accessor: (i) => `ginIndexes[${i}].fieldName`,
|
|
281
|
+
additional: (gin, i) => {
|
|
282
|
+
const pIndex: number = model.plainFields.findIndex(
|
|
283
|
+
(plain) => plain.name === gin.fieldName,
|
|
284
|
+
);
|
|
285
|
+
if (pIndex === -1)
|
|
286
|
+
errors.push({
|
|
287
|
+
path: `${accessor}.ginIndexes[${i}].fieldName`,
|
|
288
|
+
table: model.name,
|
|
289
|
+
field: null,
|
|
290
|
+
message: [
|
|
291
|
+
"GIN index can only be used on string typed field.",
|
|
292
|
+
`However, the target field ${gin.fieldName} does not exist",
|
|
293
|
+
"in the {@link plainFields}.`,
|
|
294
|
+
].join("\n"),
|
|
295
|
+
});
|
|
296
|
+
else if (model.plainFields[pIndex].type !== "string")
|
|
297
|
+
errors.push({
|
|
298
|
+
path: `${accessor}.ginIndexes[${i}].fieldName`,
|
|
299
|
+
table: model.name,
|
|
300
|
+
field: model.plainFields[pIndex].name,
|
|
301
|
+
message: [
|
|
302
|
+
"GIN index can only be used on string typed field.",
|
|
303
|
+
`However, the target field ${gin.fieldName} is not string,`,
|
|
304
|
+
`but ${model.plainFields[pIndex].type}.`,
|
|
305
|
+
"",
|
|
306
|
+
`- accessor of the wrong typed field: ${`${accessor}.plainFields[${pIndex}].type`}`,
|
|
307
|
+
].join("\n"),
|
|
308
|
+
});
|
|
309
|
+
},
|
|
310
|
+
});
|
|
311
|
+
return errors;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function validateReferences(
|
|
315
|
+
model: AutoBePrisma.IModel,
|
|
316
|
+
accessor: string,
|
|
317
|
+
dict: Map<string, AutoBePrisma.IModel>,
|
|
318
|
+
): IAutoBePrismaValidation.IError[] {
|
|
319
|
+
const errors: IAutoBePrismaValidation.IError[] = [];
|
|
320
|
+
model.foreignFields.forEach((field, i) => {
|
|
321
|
+
const target = dict.get(field.relation.targetModel);
|
|
322
|
+
if (target === undefined)
|
|
323
|
+
errors.push({
|
|
324
|
+
path: `${accessor}.foreignFields[${i}].relation.targetModel`,
|
|
325
|
+
table: model.name,
|
|
326
|
+
field: field.name,
|
|
327
|
+
message: `Target model ${field.relation.targetModel} does not exist.`,
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
return errors;
|
|
331
|
+
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { AutoBePrisma } from "@autobe/interface";
|
|
2
|
+
|
|
3
|
+
import { ArrayUtil } from "../utils/ArrayUtil";
|
|
4
|
+
import { MapUtil } from "../utils/MapUtil";
|
|
5
|
+
|
|
6
|
+
export function writePrismaApplication(
|
|
7
|
+
app: AutoBePrisma.IApplication,
|
|
8
|
+
): Record<string, string> {
|
|
9
|
+
for (const file of app.files)
|
|
10
|
+
for (const model of file.models) fillMappingName(model);
|
|
11
|
+
return {
|
|
12
|
+
...Object.fromEntries(
|
|
13
|
+
app.files
|
|
14
|
+
.filter((file) => file.filename !== "main.prisma")
|
|
15
|
+
.map((file) => [file.filename, writeFile(app, file)]),
|
|
16
|
+
),
|
|
17
|
+
"main.prisma": MAIN_FILE,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function writeFile(
|
|
22
|
+
app: AutoBePrisma.IApplication,
|
|
23
|
+
file: AutoBePrisma.IFile,
|
|
24
|
+
): string {
|
|
25
|
+
return file.models.map((model) => writeModel(app, file, model)).join("\n\n");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function writeModel(
|
|
29
|
+
app: AutoBePrisma.IApplication,
|
|
30
|
+
file: AutoBePrisma.IFile,
|
|
31
|
+
model: AutoBePrisma.IModel,
|
|
32
|
+
): string {
|
|
33
|
+
return [
|
|
34
|
+
writeComment(
|
|
35
|
+
[
|
|
36
|
+
model.description,
|
|
37
|
+
"",
|
|
38
|
+
...(model.material ? [] : [`@namespace ${file.namespace}`]),
|
|
39
|
+
"@author AutoBE - https://github.com/wrtnlabs/autobe",
|
|
40
|
+
].join("\n"),
|
|
41
|
+
),
|
|
42
|
+
`model ${model.name} {`,
|
|
43
|
+
indent(
|
|
44
|
+
ArrayUtil.paddle([writeColumns(model), writeRelations(app, model)]).join(
|
|
45
|
+
"\n",
|
|
46
|
+
),
|
|
47
|
+
),
|
|
48
|
+
"}",
|
|
49
|
+
].join("\n");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function fillMappingName(model: AutoBePrisma.IModel): void {
|
|
53
|
+
const group: Map<string, AutoBePrisma.IForeignField[]> = new Map();
|
|
54
|
+
for (const ff of model.foreignFields) {
|
|
55
|
+
MapUtil.take(group, ff.relation.targetModel, () => []).push(ff);
|
|
56
|
+
if (ff.relation.targetModel == model.name)
|
|
57
|
+
ff.relation.mappingName = "recursive";
|
|
58
|
+
}
|
|
59
|
+
for (const array of group.values())
|
|
60
|
+
if (array.length !== 1)
|
|
61
|
+
for (const ff of array) {
|
|
62
|
+
ff.relation.mappingName = `${model.name}_of_${ff.name}`;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/* -----------------------------------------------------------
|
|
67
|
+
COLUMNS
|
|
68
|
+
----------------------------------------------------------- */
|
|
69
|
+
function writeColumns(model: AutoBePrisma.IModel): string[] {
|
|
70
|
+
return [
|
|
71
|
+
"//----",
|
|
72
|
+
"// COLUMNS",
|
|
73
|
+
"//----",
|
|
74
|
+
writePrimary(model.primaryField),
|
|
75
|
+
...model.foreignFields.map((x) => ["", writeField(x)]).flat(),
|
|
76
|
+
...model.plainFields.map((x) => ["", writeField(x)]).flat(),
|
|
77
|
+
];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function writePrimary(field: AutoBePrisma.IPrimaryField): string {
|
|
81
|
+
return [
|
|
82
|
+
writeComment(field.description),
|
|
83
|
+
`${field.name} String @id @db.Uuid`,
|
|
84
|
+
].join("\n");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function writeField(field: AutoBePrisma.IPlainField): string {
|
|
88
|
+
const logical: string = LOGICAL_TYPES[field.type];
|
|
89
|
+
const physical: string | undefined =
|
|
90
|
+
PHYSICAL_TYPES[field.type as keyof typeof PHYSICAL_TYPES];
|
|
91
|
+
return [
|
|
92
|
+
writeComment(field.description),
|
|
93
|
+
[
|
|
94
|
+
field.name,
|
|
95
|
+
`${logical}${field.nullable ? "?" : ""}`,
|
|
96
|
+
...(physical ? [physical] : []),
|
|
97
|
+
].join(" "),
|
|
98
|
+
].join("\n");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/* -----------------------------------------------------------
|
|
102
|
+
RELATIONS
|
|
103
|
+
----------------------------------------------------------- */
|
|
104
|
+
function writeRelations(
|
|
105
|
+
app: AutoBePrisma.IApplication,
|
|
106
|
+
model: AutoBePrisma.IModel,
|
|
107
|
+
): string[] {
|
|
108
|
+
interface IHasRelationship {
|
|
109
|
+
modelName: string;
|
|
110
|
+
unique: boolean;
|
|
111
|
+
mappingName?: string;
|
|
112
|
+
}
|
|
113
|
+
const hasRelationships: IHasRelationship[] = app.files
|
|
114
|
+
.map((otherFile) =>
|
|
115
|
+
otherFile.models.map((otherModel) =>
|
|
116
|
+
otherModel.foreignFields
|
|
117
|
+
.filter(
|
|
118
|
+
(otherForeign) => otherForeign.relation.targetModel === model.name,
|
|
119
|
+
)
|
|
120
|
+
.map((otherForeign) => ({
|
|
121
|
+
modelName: otherModel.name,
|
|
122
|
+
unique: otherForeign.unique,
|
|
123
|
+
mappingName: otherForeign.relation.mappingName,
|
|
124
|
+
})),
|
|
125
|
+
),
|
|
126
|
+
)
|
|
127
|
+
.flat(2);
|
|
128
|
+
const foreignIndexes: AutoBePrisma.IForeignField[] =
|
|
129
|
+
model.foreignFields.filter(
|
|
130
|
+
(f) =>
|
|
131
|
+
model.uniqueIndexes.every((u) => u.fieldNames[0] !== f.name) &&
|
|
132
|
+
model.plainIndexes.every((p) => p.fieldNames[0] !== f.name),
|
|
133
|
+
);
|
|
134
|
+
const contents: string[][] = [
|
|
135
|
+
model.foreignFields.map(writeConstraint),
|
|
136
|
+
hasRelationships.map((r) =>
|
|
137
|
+
[
|
|
138
|
+
r.mappingName ?? r.modelName,
|
|
139
|
+
`${r.modelName}${r.unique ? "?" : "[]"}`,
|
|
140
|
+
...(r.mappingName ? [`@relation("${r.mappingName}")`] : []),
|
|
141
|
+
].join(" "),
|
|
142
|
+
),
|
|
143
|
+
foreignIndexes.map(writeForeignIndex),
|
|
144
|
+
[
|
|
145
|
+
...model.uniqueIndexes.map(writeUniqueIndex),
|
|
146
|
+
...model.plainIndexes.map(writePlainIndex),
|
|
147
|
+
...model.ginIndexes.map(writeGinIndex),
|
|
148
|
+
],
|
|
149
|
+
];
|
|
150
|
+
if (contents.every((c) => c.length === 0)) return [];
|
|
151
|
+
return [
|
|
152
|
+
"//----",
|
|
153
|
+
"// RELATIONS",
|
|
154
|
+
"//----",
|
|
155
|
+
// paddled content
|
|
156
|
+
...ArrayUtil.paddle(contents),
|
|
157
|
+
];
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function writeConstraint(field: AutoBePrisma.IForeignField): string {
|
|
161
|
+
return [
|
|
162
|
+
field.relation.name,
|
|
163
|
+
`${field.relation.targetModel}${field.nullable ? "?" : ""}`,
|
|
164
|
+
`@relation(${[
|
|
165
|
+
...(field.relation.mappingName
|
|
166
|
+
? [`"${field.relation.mappingName}"`]
|
|
167
|
+
: []),
|
|
168
|
+
`fields: [${field.name}]`,
|
|
169
|
+
`references: [id]`,
|
|
170
|
+
`onDelete: Cascade`,
|
|
171
|
+
].join(", ")})`,
|
|
172
|
+
].join(" ");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function writeForeignIndex(field: AutoBePrisma.IForeignField): string {
|
|
176
|
+
return `@@${field.unique ? "unique" : "index"}([${field.name}])`;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function writeUniqueIndex(field: AutoBePrisma.IUniqueIndex): string {
|
|
180
|
+
return `@@unique([${field.fieldNames.join(", ")}])`;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function writePlainIndex(field: AutoBePrisma.IPlainIndex): string {
|
|
184
|
+
return `@@index([${field.fieldNames.join(", ")}])`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function writeGinIndex(field: AutoBePrisma.IGinIndex): string {
|
|
188
|
+
return `@@index([${field.fieldName}(ops: raw("gin_trgm_ops"))], type: Gin)`;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/* -----------------------------------------------------------
|
|
192
|
+
BACKGROUND
|
|
193
|
+
----------------------------------------------------------- */
|
|
194
|
+
function writeComment(content: string): string {
|
|
195
|
+
return content
|
|
196
|
+
.split("\r\n")
|
|
197
|
+
.join("\n")
|
|
198
|
+
.split("\n")
|
|
199
|
+
.map((str) => `///${str.length ? ` ${str}` : ""}`)
|
|
200
|
+
.join("\n")
|
|
201
|
+
.trim();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function indent(content: string): string {
|
|
205
|
+
return content
|
|
206
|
+
.split("\r\n")
|
|
207
|
+
.join("\n")
|
|
208
|
+
.split("\n")
|
|
209
|
+
.map((str) => ` ${str}`)
|
|
210
|
+
.join("\n");
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const LOGICAL_TYPES = {
|
|
214
|
+
// native types
|
|
215
|
+
boolean: "Boolean",
|
|
216
|
+
int: "Int",
|
|
217
|
+
double: "Float",
|
|
218
|
+
string: "String",
|
|
219
|
+
// formats
|
|
220
|
+
datetime: "DateTime",
|
|
221
|
+
uuid: "String",
|
|
222
|
+
uri: "String",
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const PHYSICAL_TYPES = {
|
|
226
|
+
int: "@db.Integer",
|
|
227
|
+
double: "@db.DoublePrecision",
|
|
228
|
+
uuid: "@db.Uuid",
|
|
229
|
+
datetime: "@db.Timestamptz",
|
|
230
|
+
uri: "@db.VarChar(80000)",
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
const MAIN_FILE = `
|
|
234
|
+
generator client {
|
|
235
|
+
provider = "prisma-client-js"
|
|
236
|
+
previewFeatures = ["postgresqlExtensions", "views"]
|
|
237
|
+
binaryTargets = ["native"]
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
datasource db {
|
|
241
|
+
provider = "postgresql"
|
|
242
|
+
url = env("DATABASE_URL")
|
|
243
|
+
extensions = []
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
generator markdown {
|
|
247
|
+
provider = "prisma-markdown"
|
|
248
|
+
output = "../docs/ERD.md"
|
|
249
|
+
}
|
|
250
|
+
`.trim();
|
package/src/utils/ArrayUtil.ts
CHANGED
|
@@ -8,4 +8,14 @@ export namespace ArrayUtil {
|
|
|
8
8
|
result[i] = await callback(array[i], i, array);
|
|
9
9
|
return result;
|
|
10
10
|
}
|
|
11
|
+
|
|
12
|
+
export function paddle(contents: string[][]): string[] {
|
|
13
|
+
const output: string[] = [];
|
|
14
|
+
contents.forEach((c) => {
|
|
15
|
+
if (c.length === 0) return;
|
|
16
|
+
else if (output.length === 0) output.push(...c);
|
|
17
|
+
else output.push("", ...c);
|
|
18
|
+
});
|
|
19
|
+
return output;
|
|
20
|
+
}
|
|
11
21
|
}
|