@postxl/generator 0.24.1 → 0.25.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/dist/generators/indices/emptydatabasemigration.generator.js +10 -1
- package/dist/generators/indices/seed-service.generator.js +1 -1
- package/dist/generators/indices/seed-template-decoder.generator.js +3 -3
- package/dist/generators/models/businesslogic.generator.js +4 -2
- package/dist/generators/models/repository.generator.d.ts +11 -1
- package/dist/generators/models/repository.generator.js +623 -454
- package/dist/generators/models/seed.generator.js +1 -1
- package/dist/lib/schema/schema.d.ts +1 -1
- package/dist/lib/schema/zod.js +1 -1
- package/dist/lib/utils/jsdoc.d.ts +4 -0
- package/dist/lib/utils/jsdoc.js +13 -0
- package/dist/prisma/parse.js +6 -2
- package/package.json +4 -6
|
@@ -1,206 +1,68 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.generateMockRepository = exports.generateRepository = void 0;
|
|
4
|
+
const imports_1 = require("../../lib/imports");
|
|
5
|
+
const meta_1 = require("../../lib/meta");
|
|
4
6
|
const fields_1 = require("../../lib/schema/fields");
|
|
5
|
-
const string_1 = require("../../lib/utils/string");
|
|
6
7
|
const schema_1 = require("../../lib/schema/schema");
|
|
7
|
-
const meta_1 = require("../../lib/meta");
|
|
8
|
-
const imports_1 = require("../../lib/imports");
|
|
9
8
|
const types_1 = require("../../lib/schema/types");
|
|
9
|
+
const jsdoc_1 = require("../../lib/utils/jsdoc");
|
|
10
|
+
const string_1 = require("../../lib/utils/string");
|
|
10
11
|
/**
|
|
11
12
|
* Generates repository data structure for a given model.
|
|
13
|
+
* Based on the model, the repository is generated slightly differently, based on:
|
|
14
|
+
* - if the model has a default field, the repository will have a public variable "defaultValue"
|
|
15
|
+
* that is set and verified during init
|
|
16
|
+
* - if the model has a generated id, the repository will have a function "generateNextId" that
|
|
17
|
+
* is used to generate the next id. The `create` and `createMany` functions will use this function
|
|
18
|
+
* and allow being called with an id.
|
|
19
|
+
* - unique string fields are ensured to be unique
|
|
20
|
+
* - max length string fields are ensured to not exceed their max length
|
|
21
|
+
* - index for fields marked with index attribute
|
|
22
|
+
* - relations are indexed by the foreign key
|
|
12
23
|
*/
|
|
13
24
|
function generateRepository({ model, meta }) {
|
|
14
|
-
|
|
15
|
-
const { idField, fields } = model;
|
|
16
|
-
const decoder = meta.data.repository.decoderFnName;
|
|
17
|
-
const uniqueStringFields = fields.filter(fields_1.isUniqueStringField);
|
|
18
|
-
const maxLengthStringFields = fields.filter(fields_1.isMaxLengthStringField);
|
|
19
|
-
const relations = fields
|
|
20
|
-
.filter(schema_1.isFieldRelation)
|
|
21
|
-
.map((field) => (Object.assign(Object.assign({}, field), { meta: (0, meta_1.getModelMetadata)({ model: field.relationToModel }), fieldMeta: (0, meta_1.getFieldMetadata)({ field }) })));
|
|
22
|
-
const getIndexDefinition = (fieldNames) => {
|
|
23
|
-
const fields = fieldNames.map((f) => {
|
|
24
|
-
const result = model.fields.find((field) => field.name === f);
|
|
25
|
-
if (!result) {
|
|
26
|
-
throw new Error(`Index field ${f} not found in model ${model.name}`);
|
|
27
|
-
}
|
|
28
|
-
if (result.kind !== 'relation') {
|
|
29
|
-
throw new Error(`Index field ${f} is not a relation in model ${model.name}`);
|
|
30
|
-
}
|
|
31
|
-
const meta = (0, meta_1.getModelMetadata)({ model: result.relationToModel });
|
|
32
|
-
return Object.assign(Object.assign({}, result), { meta });
|
|
33
|
-
});
|
|
34
|
-
const [firstField, ...restFields] = fields;
|
|
35
|
-
if (!restFields || restFields.length === 0) {
|
|
36
|
-
throw new Error(`Index for model ${model.name} must have at least 2 fields!`);
|
|
37
|
-
}
|
|
38
|
-
return {
|
|
39
|
-
fields,
|
|
40
|
-
name: (0, string_1.toCamelCase)(`${firstField.name}${restFields.map((f) => (0, string_1.toPascalCase)(f.name)).join('')}Index`),
|
|
41
|
-
};
|
|
42
|
-
};
|
|
43
|
-
const indexes = model.attributes.index ? [getIndexDefinition(model.attributes.index)] : [];
|
|
44
|
-
const defaultValueInitFn = model.defaultField
|
|
45
|
-
? `
|
|
46
|
-
if (item.${(_a = model.defaultField) === null || _a === void 0 ? void 0 : _a.name}) {
|
|
47
|
-
if (this.defaultValue) {
|
|
48
|
-
console.warn(\`More than one default ${meta.userFriendlyName} found! \${this.defaultValue.id} and \${item.id}\`)
|
|
49
|
-
}
|
|
50
|
-
this.defaultValue = item
|
|
51
|
-
}
|
|
52
|
-
`
|
|
53
|
-
: '';
|
|
54
|
-
const defaultValueInitCheckFn = `
|
|
55
|
-
if (!this.db.isCLI && !this.defaultValue) {
|
|
56
|
-
throw new Error('No default ${meta.userFriendlyName} found!')
|
|
57
|
-
}
|
|
58
|
-
`;
|
|
59
|
-
const { isGenerated } = idField;
|
|
60
|
-
const idIntInitFn = `this.currentMaxId = (await this.db.${meta.data.repository.getMethodFnName}.aggregate({ _max: { ${idField.sourceName}: true } }))._max.${idField.sourceName} ?? 0`;
|
|
25
|
+
const { idField } = model;
|
|
61
26
|
const imports = imports_1.ImportsGenerator.from(meta.data.repoFilePath)
|
|
62
27
|
.addImport({
|
|
63
|
-
items: [model.typeName, model.brandedIdType
|
|
28
|
+
items: [model.typeName, model.brandedIdType],
|
|
64
29
|
from: meta.types.importPath,
|
|
65
30
|
})
|
|
66
31
|
.addImport({
|
|
67
32
|
items: [(0, types_1.toClassName)('Repository')],
|
|
68
33
|
from: (0, types_1.toFileName)(model.schemaConfig.paths.dataLibPath + 'repository.type'),
|
|
69
34
|
});
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
relations.forEach((r) => {
|
|
74
|
-
imports.addImport({
|
|
75
|
-
items: [r.meta.types.brandedIdType],
|
|
76
|
-
from: r.meta.types.importPath,
|
|
77
|
-
});
|
|
78
|
-
});
|
|
35
|
+
const blocks = generateBlocks({ model, meta, imports });
|
|
36
|
+
const mainBlocks = generateMainBuildingBlocks({ model, meta, imports, blocks });
|
|
79
37
|
return `
|
|
80
38
|
import { Injectable, Logger } from '@nestjs/common'
|
|
81
|
-
${
|
|
82
|
-
? `
|
|
83
|
-
import { ${indexes.length === 0 ? '' : 'NestedMap'} } from '@pxl/common'
|
|
84
|
-
`
|
|
85
|
-
: `
|
|
86
|
-
import { DbService, ${model.sourceName} as DbType } from '@${model.schemaConfig.project}/db'
|
|
87
|
-
import { format, pluralize ${indexes.length === 0 ? '' : ', NestedMap'} } from '@pxl/common'
|
|
88
|
-
`}
|
|
89
|
-
|
|
39
|
+
${blocks.id.libraryImports}
|
|
90
40
|
${imports.generate()}
|
|
91
41
|
|
|
92
42
|
@Injectable()
|
|
93
|
-
export class ${meta.data.repositoryClassName} implements Repository
|
|
94
|
-
${model.typeName},
|
|
95
|
-
${idField.unbrandedTypeName}
|
|
96
|
-
> {
|
|
43
|
+
export class ${meta.data.repositoryClassName} implements Repository<${model.typeName}, ${idField.unbrandedTypeName}> {
|
|
97
44
|
protected data: Map<${model.brandedIdType}, ${model.typeName}> = new Map()
|
|
98
45
|
protected logger = new Logger(${meta.data.repositoryClassName}.name)
|
|
99
46
|
|
|
100
|
-
${relations
|
|
101
|
-
.map((r) => `
|
|
102
|
-
protected ${r.name}Map: Map<${r.meta.types.brandedIdType}, Map<${model.brandedIdType}, ${model.typeName}>> = new Map()
|
|
103
|
-
`)
|
|
104
|
-
.join('\n')}
|
|
105
|
-
|
|
106
|
-
${model.defaultField
|
|
107
|
-
? `
|
|
108
|
-
// We can safely skip the assignment here as this is done in the init function
|
|
109
|
-
public defaultValue!: ${model.typeName}
|
|
110
|
-
`
|
|
111
|
-
: ''}
|
|
112
|
-
|
|
113
|
-
${isGenerated
|
|
114
|
-
? `
|
|
115
|
-
|
|
116
|
-
protected currentMaxId = 0\n
|
|
117
|
-
public get nextId(): ${model.brandedIdType} {
|
|
118
|
-
return ${meta.types.toBrandedIdTypeFnName}(this.currentMaxId + 1)
|
|
119
|
-
}
|
|
120
|
-
`
|
|
121
|
-
: ''}
|
|
122
|
-
|
|
123
|
-
protected uniqueIds = {
|
|
124
|
-
${uniqueStringFields.map((f) => `'${f.name}': new Map<string, ${model.typeName}>()`).join(',\n')}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
${indexes
|
|
128
|
-
.map((i) => `
|
|
129
|
-
protected ${i.name} = new NestedMap<${i.fields.map((f) => f.meta.types.brandedIdType).join(',')}, ${model.typeName}>(
|
|
130
|
-
${i.fields.map((f) => `(x) => x.${f.name}`).join(',')}
|
|
131
|
-
)
|
|
132
|
-
`)
|
|
133
|
-
.join('\n')}
|
|
134
|
-
${model.attributes.inMemoryOnly
|
|
135
|
-
? ''
|
|
136
|
-
: `
|
|
137
|
-
constructor(${model.attributes.inMemoryOnly ? '' : 'protected '}db: DbService) {}
|
|
138
|
-
`}
|
|
47
|
+
${blocks.relations.mapDeclarations.join('\n')}
|
|
139
48
|
|
|
140
|
-
|
|
141
|
-
this.data.clear()
|
|
142
|
-
|
|
143
|
-
${uniqueStringFields.map((f) => `this.uniqueIds.${f.name}.clear()`).join('\n')}
|
|
144
|
-
${model.defaultField
|
|
145
|
-
? `
|
|
146
|
-
// We re-initialize the default value to undefined so the check for exactly one item does not fail upon re-initialization
|
|
147
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-non-null-assertion
|
|
148
|
-
this.defaultValue = undefined!
|
|
149
|
-
`
|
|
150
|
-
: ''}
|
|
151
|
-
|
|
152
|
-
${indexes.map((i) => `this.${i.name}.clear()`).join('\n')}
|
|
153
|
-
|
|
154
|
-
${model.attributes.inMemoryOnly
|
|
155
|
-
? 'return Promise.resolve()'
|
|
156
|
-
: `
|
|
157
|
-
|
|
158
|
-
const data = await this.db.${meta.data.repository.getMethodFnName}.findMany({})
|
|
159
|
-
|
|
160
|
-
for (const rawItem of data) {
|
|
161
|
-
const item = this.${decoder}(rawItem)
|
|
162
|
-
this.set(item)
|
|
163
|
-
|
|
164
|
-
${model.defaultField ? defaultValueInitFn : ''}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
${isGenerated ? idIntInitFn : ''}
|
|
49
|
+
${blocks.default.publicVariableDeclaration}
|
|
168
50
|
|
|
169
|
-
|
|
51
|
+
${blocks.id.generateNextIdFunctionName}
|
|
170
52
|
|
|
171
|
-
|
|
172
|
-
${
|
|
173
|
-
.map((i) => `this.logger.log(\`\${this.${i.name}.size} ${model.typeName} loaded into index ${i.name}\`)`)
|
|
174
|
-
.join('\n')}
|
|
175
|
-
`}
|
|
53
|
+
protected uniqueIds = {
|
|
54
|
+
${blocks.uniqueStringFields.mapDeclarations.join(',\n')}
|
|
176
55
|
}
|
|
56
|
+
|
|
57
|
+
${blocks.index.nestedMapDeclarations.join('\n')}
|
|
177
58
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
await this.init()
|
|
182
|
-
await this.createMany(data)
|
|
183
|
-
`
|
|
184
|
-
: `
|
|
185
|
-
if (!this.db.useE2ETestDB) {
|
|
186
|
-
const errorMsg =
|
|
187
|
-
'ReInit() shall only be called in tests using MockRepositories or in DB configured for E2E tests!'
|
|
188
|
-
this.logger.error(errorMsg)
|
|
189
|
-
throw new Error(errorMsg)
|
|
190
|
-
}
|
|
191
|
-
await this.db.runOnlyOnTestDb(() => this.db.${meta.data.repository.getMethodFnName}.createMany({ data: data.map((i) => this.toCreateItem(i)) }))
|
|
192
|
-
return this.init()
|
|
193
|
-
`}
|
|
194
|
-
}
|
|
59
|
+
${mainBlocks.constructorCode}
|
|
60
|
+
|
|
61
|
+
${mainBlocks.initCode}
|
|
195
62
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
: `
|
|
200
|
-
await this.db.runOnlyOnTestDb(() => this.db.$executeRaw\`DELETE FROM ${model.sourceSchemaName !== undefined ? `"${model.sourceSchemaName}".` : ''}"${model.sourceName}"\`)
|
|
201
|
-
`}
|
|
202
|
-
return this.init()
|
|
203
|
-
}
|
|
63
|
+
${mainBlocks.reInitCode}
|
|
64
|
+
|
|
65
|
+
${mainBlocks.deleteAllCode}
|
|
204
66
|
|
|
205
67
|
public get(id: ${model.brandedIdType} | null): ${model.typeName} | null {
|
|
206
68
|
if (id === null) {
|
|
@@ -217,16 +79,7 @@ export class ${meta.data.repositoryClassName} implements Repository<
|
|
|
217
79
|
return Array.from(this.data.values())
|
|
218
80
|
}
|
|
219
81
|
|
|
220
|
-
${
|
|
221
|
-
.map((i) => `
|
|
222
|
-
public getFrom${indexes.length === 1 ? 'Index' : (0, string_1.toPascalCase)(i.name)}
|
|
223
|
-
({ ${i.fields.map((f) => f.name).join(',')} }: { ${i.fields
|
|
224
|
-
.map((f) => `${f.name} : ${f.meta.types.brandedIdType}`)
|
|
225
|
-
.join(';')} }): ${model.typeName} | null {
|
|
226
|
-
return this.${i.name}.get(${i.fields.map((f) => f.name).join(',')}) ?? null
|
|
227
|
-
}
|
|
228
|
-
`)
|
|
229
|
-
.join('\n')}
|
|
82
|
+
${blocks.index.getterFunctions.join('\n')}
|
|
230
83
|
|
|
231
84
|
public filter(predicate: (item: ${model.typeName}) => boolean): ${model.typeName}[] {
|
|
232
85
|
return this.getAllAsArray().filter(predicate)
|
|
@@ -240,206 +93,507 @@ export class ${meta.data.repositoryClassName} implements Repository<
|
|
|
240
93
|
return this.data.size
|
|
241
94
|
}
|
|
242
95
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
96
|
+
/**${(0, jsdoc_1.convertToJsDocComments)([
|
|
97
|
+
`Checks that item has the ${idField.name} field.`,
|
|
98
|
+
`In case none exists, ${blocks.id.verifyFunctionComment}`,
|
|
99
|
+
blocks.uniqueStringFields.verifyFunctionComment,
|
|
100
|
+
blocks.maxLength.verifyFunctionComment,
|
|
101
|
+
])}
|
|
102
|
+
*/
|
|
103
|
+
private verifyItem(item: ${blocks.id.verifyFunctionParameterType}): ${model.typeName} {
|
|
104
|
+
${blocks.id.verifyCode}
|
|
251
105
|
|
|
252
|
-
${
|
|
106
|
+
${blocks.maxLength.verifyCode.join('\n')}
|
|
253
107
|
|
|
254
|
-
${uniqueStringFields.
|
|
108
|
+
${blocks.uniqueStringFields.verifyCode.join('\n')}
|
|
255
109
|
|
|
256
110
|
return {
|
|
257
|
-
${
|
|
111
|
+
${idField.name},
|
|
112
|
+
${[...model.fields.values()]
|
|
113
|
+
.filter((f) => f.kind !== 'id')
|
|
114
|
+
.map((f) => `${f.name}: item.${f.name}`)
|
|
115
|
+
.join(',\n')}
|
|
258
116
|
}
|
|
259
117
|
}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
public async createWithId(item: Omit<${model.typeName}, '${idField.name}'> & {
|
|
266
|
-
id: ${model.brandedIdType}
|
|
267
|
-
}): Promise<${model.typeName}> {
|
|
268
|
-
${model.attributes.inMemoryOnly
|
|
269
|
-
? `
|
|
270
|
-
${maxLengthStringFields.map((f) => `this.${getEnsureMaxLengthFnName(f)}(item)`).join('\n')}
|
|
271
|
-
${uniqueStringFields.map((f) => `this.${getEnsureUniqueFnName(f)}(item)`).join('\n')}
|
|
272
|
-
const newItem = await Promise.resolve(item)`
|
|
273
|
-
: `
|
|
274
|
-
const newItem = this.${decoder}(
|
|
275
|
-
await this.db.${meta.data.repository.getMethodFnName}.create({
|
|
276
|
-
data: this.toCreateItem(item as ${model.typeName}),
|
|
277
|
-
}),
|
|
278
|
-
)`}
|
|
279
|
-
|
|
280
|
-
this.set(newItem)
|
|
281
|
-
return newItem
|
|
118
|
+
|
|
119
|
+
private toCreateItem(item: ${model.typeName}) {
|
|
120
|
+
return {
|
|
121
|
+
${[...model.fields.values()].map((f) => `${f.sourceName}: item.${f.name}`).join(',\n')}
|
|
122
|
+
}
|
|
282
123
|
}
|
|
124
|
+
|
|
125
|
+
${mainBlocks.createCode}
|
|
283
126
|
|
|
284
|
-
${
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
public async create(
|
|
290
|
-
item: Omit<${model.typeName}, 'id'>
|
|
291
|
-
): Promise<${model.typeName}> {
|
|
292
|
-
${model.attributes.inMemoryOnly && !isGenerated
|
|
293
|
-
? `throw new Error('Create without Id not possible without autogenerated Id. Please adjust schema if you need this function!')`
|
|
294
|
-
: `
|
|
295
|
-
|
|
296
|
-
${isGenerated ? `const id = ++this.currentMaxId\n` : ''}
|
|
297
|
-
|
|
298
|
-
${maxLengthStringFields.map((f) => `this.${getEnsureMaxLengthFnName(f)}(item)`).join('\n')}
|
|
299
|
-
|
|
300
|
-
${uniqueStringFields.map((f) => `this.${getEnsureUniqueFnName(f)}(item)`).join('\n')}
|
|
301
|
-
|
|
302
|
-
${model.attributes.inMemoryOnly
|
|
303
|
-
? `
|
|
304
|
-
const newItem = await Promise.resolve({ ...item, id: ${meta.types.toBrandedIdTypeFnName}(id) })
|
|
305
|
-
`
|
|
306
|
-
: `
|
|
307
|
-
const newItem = this.${decoder}(
|
|
308
|
-
await this.db.${meta.data.repository.getMethodFnName}.create({
|
|
309
|
-
data: {
|
|
310
|
-
${isGenerated ? `${idField.sourceName}: id,` : ''}
|
|
311
|
-
${[...model.fields.values()]
|
|
312
|
-
.filter((f) => f.kind !== 'id')
|
|
313
|
-
.map((f) => `${f.sourceName}: item.${f.name}`)
|
|
314
|
-
.join(',\n')}
|
|
315
|
-
},
|
|
316
|
-
}),
|
|
317
|
-
)`}
|
|
127
|
+
${mainBlocks.createManyCode}
|
|
128
|
+
|
|
129
|
+
${mainBlocks.updateCode}
|
|
130
|
+
|
|
131
|
+
${mainBlocks.deleteCode}
|
|
318
132
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
}
|
|
133
|
+
${blocks.relations.getterFunctions.join('\n')}
|
|
134
|
+
|
|
135
|
+
${blocks.maxLength.ensureMaxLengthFunctions.join('\n')}
|
|
322
136
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
}
|
|
330
|
-
: ''}
|
|
137
|
+
${blocks.uniqueStringFields.ensureUniqueFunctions.join('\n\n')}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Function that adds/updates a given item to the internal data store, indexes, etc.
|
|
141
|
+
*/
|
|
142
|
+
private set(item: ${model.typeName}): void {
|
|
143
|
+
${blocks.id.setCode}
|
|
331
144
|
|
|
332
|
-
|
|
333
|
-
? 'await Promise.resolve()'
|
|
334
|
-
: `
|
|
335
|
-
await this.db.${meta.data.repository.getMethodFnName}.createMany({ data: items.map(item => ({
|
|
336
|
-
${[...model.fields.values()].map((f) => `${f.sourceName}: item.${f.name}`).join(',\n')}}))
|
|
337
|
-
})`}
|
|
145
|
+
this.data.set(item.id, item)
|
|
338
146
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
}
|
|
147
|
+
${blocks.uniqueStringFields.setCode.join('\n')}
|
|
148
|
+
|
|
149
|
+
${blocks.relations.setCode.join('\n')}
|
|
150
|
+
|
|
151
|
+
${blocks.index.setCode.join('\n')}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Function that removes a given item from the internal data store, indexes, etc.
|
|
156
|
+
*/
|
|
157
|
+
private remove(item: ${model.typeName}): void {
|
|
158
|
+
this.data.delete(item.id)
|
|
159
|
+
${blocks.uniqueStringFields.removeCode.join('\n')}
|
|
342
160
|
|
|
343
|
-
|
|
161
|
+
${blocks.relations.removeCode.join('\n')}
|
|
162
|
+
|
|
163
|
+
${blocks.index.removeCode.join('\n')}
|
|
344
164
|
}
|
|
345
|
-
|
|
346
|
-
public async update(item: Partial<${model.typeName}> & {
|
|
347
|
-
id: ${model.brandedIdType}
|
|
348
|
-
}): Promise<${model.typeName}> {
|
|
349
|
-
const existingItem = this.get(item.id)
|
|
350
165
|
|
|
351
|
-
|
|
352
|
-
|
|
166
|
+
${mainBlocks.databaseDecoderCode}
|
|
167
|
+
}
|
|
168
|
+
`;
|
|
169
|
+
}
|
|
170
|
+
exports.generateRepository = generateRepository;
|
|
171
|
+
/**
|
|
172
|
+
* Generates a mock repository data structure for a given model: same a repository, but in memory only
|
|
173
|
+
*/
|
|
174
|
+
function generateMockRepository({ model: modelSource, meta: metaSource, }) {
|
|
175
|
+
// We re-use the repository block, but we change the meta data to use the mock repository name and the model to be in memory only
|
|
176
|
+
const meta = Object.assign(Object.assign({}, metaSource), { data: Object.assign(Object.assign({}, metaSource.data), { repositoryClassName: metaSource.data.mockRepositoryClassName, repoFilePath: metaSource.data.mockRepoFilePath }) });
|
|
177
|
+
const model = Object.assign(Object.assign({}, modelSource), { attributes: Object.assign(Object.assign({}, modelSource.attributes), { inMemoryOnly: true }) });
|
|
178
|
+
return generateRepository({ model, meta });
|
|
179
|
+
}
|
|
180
|
+
exports.generateMockRepository = generateMockRepository;
|
|
181
|
+
function generateMainBuildingBlocks({ model, meta, imports, blocks, }) {
|
|
182
|
+
if (model.attributes.inMemoryOnly) {
|
|
183
|
+
return generateMainBuildingBlocks_InMemoryOnly({ model, meta, imports, blocks });
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
return generateMainBuildingBlocks_InDatabase({ model, meta, imports, blocks });
|
|
353
187
|
}
|
|
188
|
+
}
|
|
189
|
+
function generateMainBuildingBlocks_InMemoryOnly({ model, meta, blocks, }) {
|
|
190
|
+
const { idField } = model;
|
|
191
|
+
return {
|
|
192
|
+
constructorCode: '',
|
|
193
|
+
initCode: `
|
|
194
|
+
public async init() {
|
|
195
|
+
this.data.clear()
|
|
196
|
+
|
|
197
|
+
${blocks.uniqueStringFields.clearCode.join('\n')}
|
|
198
|
+
${blocks.default.init.resetCode}
|
|
199
|
+
|
|
200
|
+
${blocks.index.initCode.join('\n')}
|
|
201
|
+
|
|
202
|
+
return Promise.resolve()
|
|
203
|
+
}`,
|
|
204
|
+
reInitCode: `
|
|
205
|
+
public async reInit(data: ${model.typeName}[]): Promise<void> {
|
|
206
|
+
await this.init()
|
|
207
|
+
await this.createMany(data)
|
|
208
|
+
}`,
|
|
209
|
+
deleteAllCode: `
|
|
210
|
+
public async deleteAll(): Promise<void> {
|
|
211
|
+
return this.init()
|
|
212
|
+
}`,
|
|
213
|
+
createCode: `
|
|
214
|
+
// Non-mocked version is async - so we keep type-compatible signatures for create() and createWithId()
|
|
215
|
+
// eslint-disable-next-line @typescript-eslint/require-await, @typescript-eslint/no-unused-vars
|
|
216
|
+
public async create(item: Omit<${model.typeName}, '${idField.name}'> & Partial<{
|
|
217
|
+
id: ${idField.unbrandedTypeName}
|
|
218
|
+
}>): Promise<${model.typeName}> {
|
|
219
|
+
const newItem = await Promise.resolve(this.verifyItem(item))
|
|
220
|
+
|
|
221
|
+
this.set(newItem)
|
|
222
|
+
return newItem
|
|
223
|
+
}`,
|
|
224
|
+
createManyCode: `
|
|
225
|
+
public async createMany(items: ${blocks.id.verifyFunctionParameterType}[]) {
|
|
226
|
+
const newItems = items.map((item) => this.verifyItem(item))
|
|
227
|
+
|
|
228
|
+
await Promise.resolve()
|
|
354
229
|
|
|
230
|
+
for (const item of newItems) {
|
|
231
|
+
this.set(item)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return newItems
|
|
235
|
+
}`,
|
|
236
|
+
updateCode: `
|
|
237
|
+
public async update(item: Partial<${model.typeName}> & {
|
|
238
|
+
id: ${model.brandedIdType}
|
|
239
|
+
}): Promise<${model.typeName}> {
|
|
240
|
+
const existingItem = this.get(item.id)
|
|
355
241
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
242
|
+
if (!existingItem) {
|
|
243
|
+
throw new Error(\`Could not update ${meta.userFriendlyName} with id \${item.id}. Not found!\`)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
${blocks.maxLength.updateCode.join('\n')}
|
|
248
|
+
|
|
249
|
+
${blocks.uniqueStringFields.updateCode.join('\n')}
|
|
250
|
+
|
|
251
|
+
const newItem = await Promise.resolve({ ...existingItem, ...item })
|
|
252
|
+
|
|
253
|
+
this.remove(existingItem)
|
|
254
|
+
this.set(newItem)
|
|
255
|
+
|
|
256
|
+
return newItem
|
|
257
|
+
}`,
|
|
258
|
+
deleteCode: `
|
|
259
|
+
public async delete(id: ${model.brandedIdType}): Promise<void> {
|
|
260
|
+
const existingItem = this.get(id)
|
|
261
|
+
if (!existingItem) {
|
|
262
|
+
throw new Error(\`Could not delete ${model.typeName} with id \${id}. Not found!\`)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
await Promise.resolve()
|
|
266
|
+
|
|
267
|
+
this.remove(existingItem)
|
|
268
|
+
}`,
|
|
269
|
+
databaseDecoderCode: ``,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
function generateMainBuildingBlocks_InDatabase({ model, meta, imports, blocks, }) {
|
|
273
|
+
const decoderFunctionName = meta.data.repository.decoderFnName;
|
|
274
|
+
const { idField } = model;
|
|
275
|
+
imports
|
|
276
|
+
.addImport({ items: [meta.types.zodDecoderFnName], from: meta.types.importPath })
|
|
277
|
+
.addImport({
|
|
278
|
+
items: [`DbService`, `${model.sourceName} as DbType`].map(types_1.toTypeName),
|
|
279
|
+
from: (0, types_1.toFileName)('@pxl/db'),
|
|
372
280
|
})
|
|
373
|
-
.
|
|
281
|
+
.addImport({
|
|
282
|
+
items: [`format`, `pluralize`].map(types_1.toTypeName),
|
|
283
|
+
from: (0, types_1.toFileName)('@pxl/common'),
|
|
284
|
+
});
|
|
285
|
+
return {
|
|
286
|
+
constructorCode: `constructor(protected db: DbService) {}`,
|
|
287
|
+
initCode: `
|
|
288
|
+
public async init() {
|
|
289
|
+
this.data.clear()
|
|
290
|
+
|
|
291
|
+
${blocks.uniqueStringFields.clearCode.join('\n')}
|
|
292
|
+
${blocks.default.init.resetCode}
|
|
293
|
+
|
|
294
|
+
${blocks.index.initCode.join('\n')}
|
|
295
|
+
|
|
296
|
+
const data = await this.db.${meta.data.repository.getMethodFnName}.findMany({})
|
|
297
|
+
|
|
298
|
+
for (const rawItem of data) {
|
|
299
|
+
const item = this.${decoderFunctionName}(rawItem)
|
|
300
|
+
this.set(item)
|
|
301
|
+
|
|
302
|
+
${blocks.default.init.setCode}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
${blocks.id.initCode}
|
|
306
|
+
|
|
307
|
+
${blocks.default.init.checkCode}
|
|
308
|
+
|
|
309
|
+
this.logger.log(\`\${format(this.data.size)} \${pluralize('${model.typeName}', this.data.size)} loaded\`)
|
|
310
|
+
${blocks.index.initLogCode.join('\n')}
|
|
311
|
+
}`,
|
|
312
|
+
reInitCode: `
|
|
313
|
+
public async reInit(data: ${model.typeName}[]): Promise<void> {
|
|
314
|
+
if (!this.db.useE2ETestDB) {
|
|
315
|
+
const errorMsg =
|
|
316
|
+
'ReInit() shall only be called in tests using MockRepositories or in DB configured for E2E tests!'
|
|
317
|
+
this.logger.error(errorMsg)
|
|
318
|
+
throw new Error(errorMsg)
|
|
319
|
+
}
|
|
320
|
+
await this.db.runOnlyOnTestDb(() => this.db.${meta.data.repository.getMethodFnName}.createMany({ data: data.map((i) => this.toCreateItem(i)) }))
|
|
321
|
+
return this.init()
|
|
322
|
+
}`,
|
|
323
|
+
deleteAllCode: `
|
|
324
|
+
public async deleteAll(): Promise<void> {
|
|
325
|
+
await this.db.runOnlyOnTestDb(() => this.db.$executeRaw\`DELETE FROM ${model.sourceSchemaName !== undefined ? `"${model.sourceSchemaName}".` : ''}"${model.sourceName}"\`)
|
|
326
|
+
|
|
327
|
+
return this.init()
|
|
328
|
+
}
|
|
329
|
+
`,
|
|
330
|
+
createCode: `
|
|
331
|
+
public async create(item: Omit<${model.typeName}, '${idField.name}'> & Partial<{
|
|
332
|
+
id: ${idField.unbrandedTypeName}
|
|
333
|
+
}>): Promise<${model.typeName}> {
|
|
334
|
+
const newItem = this.${decoderFunctionName}(
|
|
335
|
+
await this.db.${meta.data.repository.getMethodFnName}.create({
|
|
336
|
+
data: this.toCreateItem(this.verifyItem(item)),
|
|
337
|
+
}),
|
|
338
|
+
)
|
|
374
339
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
340
|
+
this.set(newItem)
|
|
341
|
+
return newItem
|
|
342
|
+
}`,
|
|
343
|
+
createManyCode: `
|
|
344
|
+
public async createMany(items: ${blocks.id.verifyFunctionParameterType}[]) {
|
|
345
|
+
const newItems = items.map((item) => this.verifyItem(item))
|
|
346
|
+
|
|
347
|
+
await this.db.${meta.data.repository.getMethodFnName}.createMany({ data: newItems.map(i => this.toCreateItem(i)) })
|
|
348
|
+
|
|
349
|
+
for (const item of newItems) {
|
|
350
|
+
this.set(item)
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return newItems
|
|
354
|
+
}`,
|
|
355
|
+
updateCode: `
|
|
356
|
+
public async update(item: Partial<${model.typeName}> & {
|
|
357
|
+
id: ${model.brandedIdType}
|
|
358
|
+
}): Promise<${model.typeName}> {
|
|
359
|
+
const existingItem = this.get(item.id)
|
|
360
|
+
|
|
361
|
+
if (!existingItem) {
|
|
362
|
+
throw new Error(\`Could not update ${meta.userFriendlyName} with id \${item.id}. Not found!\`)
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
${blocks.maxLength.updateCode.join('\n')}
|
|
367
|
+
|
|
368
|
+
${blocks.uniqueStringFields.updateCode.join('\n')}
|
|
369
|
+
|
|
370
|
+
const newItem = this.${decoderFunctionName}(
|
|
371
|
+
await this.db.${meta.data.repository.getMethodFnName}.update({
|
|
372
|
+
where: {
|
|
373
|
+
${idField.sourceName}: item.${idField.name},
|
|
374
|
+
},
|
|
375
|
+
data: {
|
|
376
|
+
${[...model.fields.values()]
|
|
387
377
|
.filter((f) => f.kind !== 'id')
|
|
388
378
|
.map((f) => f.isRequired
|
|
389
379
|
? `${f.sourceName}: item.${f.name} ?? existingItem.${f.name}`
|
|
390
380
|
: `${f.sourceName}: item.${f.name}`)
|
|
391
381
|
.join(',\n')}
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
382
|
+
},
|
|
383
|
+
}),
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
this.remove(existingItem)
|
|
387
|
+
this.set(newItem)
|
|
388
|
+
|
|
389
|
+
return newItem
|
|
390
|
+
}`,
|
|
391
|
+
deleteCode: `
|
|
392
|
+
public async delete(id: ${model.brandedIdType}): Promise<void> {
|
|
393
|
+
const existingItem = this.get(id)
|
|
394
|
+
if (!existingItem) {
|
|
395
|
+
throw new Error(\`Could not delete ${model.typeName} with id \${id}. Not found!\`)
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
await this.db.${meta.data.repository.getMethodFnName}.delete({ where: { ${idField.sourceName}:id } })
|
|
399
|
+
|
|
400
|
+
this.remove(existingItem)
|
|
401
|
+
}`,
|
|
402
|
+
databaseDecoderCode: `
|
|
403
|
+
/**
|
|
404
|
+
* Utility function that converts a given Database object to a TypeScript model instance
|
|
405
|
+
*/
|
|
406
|
+
private ${decoderFunctionName}(item: Pick<DbType, ${[...model.fields.values()]
|
|
407
|
+
.map((f) => `'${f.sourceName}'`)
|
|
408
|
+
.join(' | ')}>): ${model.typeName} {
|
|
409
|
+
return ${meta.types.zodDecoderFnName}.parse({
|
|
410
|
+
${[...model.fields.values()].map((f) => `${f.name}: item.${f.sourceName}`).join(',\n')}
|
|
411
|
+
})
|
|
412
|
+
}`,
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
function generateBlocks({ model, meta, imports, }) {
|
|
416
|
+
return {
|
|
417
|
+
id: generateIdBlocks({ model, meta, imports }),
|
|
418
|
+
default: generateDefaultBlocks({ model, meta }),
|
|
419
|
+
uniqueStringFields: generateUniqueFieldsBlocks({ model, meta }),
|
|
420
|
+
maxLength: generateMaxLengthBlocks({ model, meta }),
|
|
421
|
+
index: generateIndexBlocks({ model, meta, imports }),
|
|
422
|
+
relations: generateRelationsBlocks({ model, meta, imports }),
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
function generateIdBlocks({ model, meta, imports, }) {
|
|
426
|
+
const idField = model.idField;
|
|
427
|
+
if (!idField.isGenerated) {
|
|
428
|
+
return generateIdBlocks_NoGeneration({ idField, model, meta });
|
|
406
429
|
}
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
430
|
+
if (idField.schemaType === 'Int') {
|
|
431
|
+
return generateIdBlock_Int({ idField, model, meta, imports });
|
|
432
|
+
}
|
|
433
|
+
if (idField.schemaType === 'String') {
|
|
434
|
+
return generateIdBlock_UUID({ idField, model, meta, imports });
|
|
435
|
+
}
|
|
436
|
+
throw new Error(`Repository block only supports Id generation for number and strings! Found ${idField.schemaType} for model ${model.name} instead!`);
|
|
437
|
+
}
|
|
438
|
+
function generateIdBlocks_NoGeneration({ idField, model, meta, }) {
|
|
439
|
+
return {
|
|
440
|
+
libraryImports: '',
|
|
441
|
+
generateNextIdFunctionName: '',
|
|
442
|
+
initCode: '',
|
|
443
|
+
verifyFunctionComment: `an error is thrown as field has no default setting in schema.`,
|
|
444
|
+
verifyFunctionParameterType: model.typeName,
|
|
445
|
+
verifyCode: `
|
|
446
|
+
if (item.${idField.name} === undefined) {
|
|
447
|
+
throw new Error('Id field ${idField.name} is required!')
|
|
448
|
+
}
|
|
449
|
+
const ${idField.name} = ${meta.types.toBrandedIdTypeFnName}(item.${idField.name})`,
|
|
450
|
+
setCode: '',
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
function generateIdBlock_Int({ idField, model, meta, imports, }) {
|
|
454
|
+
imports.addImport({
|
|
455
|
+
items: [meta.types.toBrandedIdTypeFnName],
|
|
456
|
+
from: meta.types.importPath,
|
|
457
|
+
});
|
|
458
|
+
return {
|
|
459
|
+
libraryImports: '',
|
|
460
|
+
generateNextIdFunctionName: `
|
|
461
|
+
protected currentMaxId = 0
|
|
462
|
+
public generateNextId(): ${model.brandedIdType} {
|
|
463
|
+
return ${meta.types.toBrandedIdTypeFnName}(++this.currentMaxId)
|
|
464
|
+
}`,
|
|
465
|
+
initCode: `this.currentMaxId = (await this.db.${meta.data.repository.getMethodFnName}.aggregate({ _max: { ${idField.sourceName}: true } }))._max.${idField.sourceName} ?? 0`,
|
|
466
|
+
verifyFunctionComment: 'the id is generated by increasing the highest former id and assigned to the item.',
|
|
467
|
+
verifyFunctionParameterType: `(Omit<${model.typeName}, '${idField.name}'> & Partial<{${idField.name}: ${idField.unbrandedTypeName}}>)`,
|
|
468
|
+
verifyCode: `const ${idField.name} = (item.${idField.name} !== undefined) ? ${meta.types.toBrandedIdTypeFnName}(item.${idField.name}) : this.generateNextId()`,
|
|
469
|
+
setCode: `if (item.id > this.currentMaxId) { this.currentMaxId = item.id }`,
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
function generateIdBlock_UUID({ idField, model, meta, imports, }) {
|
|
473
|
+
imports.addImport({
|
|
474
|
+
items: [meta.types.toBrandedIdTypeFnName],
|
|
475
|
+
from: meta.types.importPath,
|
|
476
|
+
});
|
|
477
|
+
return {
|
|
478
|
+
libraryImports: `import { randomUUID } from 'crypto'`,
|
|
479
|
+
generateNextIdFunctionName: `
|
|
480
|
+
public generateNextId(): ${model.brandedIdType} {
|
|
481
|
+
return ${meta.types.toBrandedIdTypeFnName}(randomUUID())
|
|
482
|
+
}`,
|
|
483
|
+
initCode: '',
|
|
484
|
+
verifyFunctionComment: 'a new UUID is generated and assigned to the item.',
|
|
485
|
+
verifyFunctionParameterType: `(Omit<${model.typeName}, '${idField.name}'> & Partial<{${idField.name}: ${idField.unbrandedTypeName}}>)`,
|
|
486
|
+
verifyCode: `const ${idField.name} = (item.${idField.name} !== undefined) ? ${meta.types.toBrandedIdTypeFnName}(item.${idField.name}) : this.generateNextId()`,
|
|
487
|
+
setCode: '',
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
function generateDefaultBlocks({ model, meta }) {
|
|
491
|
+
const defaultField = model.defaultField;
|
|
492
|
+
if (!defaultField) {
|
|
493
|
+
return {
|
|
494
|
+
init: {
|
|
495
|
+
resetCode: '',
|
|
496
|
+
setCode: '',
|
|
497
|
+
checkCode: '',
|
|
498
|
+
},
|
|
499
|
+
publicVariableDeclaration: '',
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
return {
|
|
503
|
+
init: {
|
|
504
|
+
resetCode: `
|
|
505
|
+
// We re-initialize the default value to undefined so the check for exactly one item does not fail upon re-initialization
|
|
506
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-non-null-assertion
|
|
507
|
+
this.defaultValue = undefined!
|
|
508
|
+
`,
|
|
509
|
+
setCode: `
|
|
510
|
+
if (item.${defaultField.name}) {
|
|
511
|
+
if (this.defaultValue) {
|
|
512
|
+
console.warn(\`More than one default ${meta.userFriendlyName} found! \${this.defaultValue.id} and \${item.id}\`)
|
|
513
|
+
}
|
|
514
|
+
this.defaultValue = item
|
|
515
|
+
}`,
|
|
516
|
+
checkCode: `
|
|
517
|
+
if (!this.db.isCLI && !this.defaultValue) {
|
|
518
|
+
throw new Error('No default ${meta.userFriendlyName} found!')
|
|
519
|
+
}`,
|
|
520
|
+
},
|
|
521
|
+
publicVariableDeclaration: `
|
|
522
|
+
// We can safely skip the assignment here as this is done in the init function
|
|
523
|
+
public defaultValue!: ${model.typeName}
|
|
524
|
+
`,
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
function generateUniqueFieldsBlocks({ model }) {
|
|
528
|
+
const fields = model.fields.filter(fields_1.isUniqueStringField);
|
|
529
|
+
const result = {
|
|
530
|
+
mapDeclarations: [],
|
|
531
|
+
clearCode: [],
|
|
532
|
+
verifyFunctionComment: '',
|
|
533
|
+
verifyCode: [],
|
|
534
|
+
updateCode: [],
|
|
535
|
+
ensureUniqueFunctions: [],
|
|
536
|
+
setCode: [],
|
|
537
|
+
removeCode: [],
|
|
538
|
+
};
|
|
539
|
+
for (const f of fields) {
|
|
540
|
+
result.mapDeclarations.push(`'${f.name}': new Map<string, ${model.typeName}>()`);
|
|
541
|
+
result.clearCode.push(`this.uniqueIds.${f.name}.clear()`);
|
|
542
|
+
result.verifyCode.push(`this.${getEnsureUniqueFnName(f)}(item)`);
|
|
543
|
+
result.updateCode.push(`
|
|
544
|
+
if (item.${f.name} !== undefined && existingItem.${f.name} !== item.${f.name}) {
|
|
545
|
+
this.${getEnsureUniqueFnName(f)}(item)
|
|
546
|
+
}`);
|
|
547
|
+
result.ensureUniqueFunctions.push(`
|
|
548
|
+
/**
|
|
549
|
+
* Utility function that ensures that the ${f.name} field is unique
|
|
550
|
+
*/
|
|
551
|
+
private ${getEnsureUniqueFnName(f)}(item: { ${f.name}?: string }) {
|
|
552
|
+
if (!item.${f.name}) return
|
|
553
|
+
if (!this.uniqueIds.${f.name}.has(item.${f.name})) return
|
|
554
|
+
let counter = 1
|
|
415
555
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
556
|
+
let ${f.name}: string
|
|
557
|
+
const source${f.name} =${!f.attributes.maxLength ? `item.${f.name}` : `item.${f.name}.substring(0, ${f.attributes.maxLength - 5})`}
|
|
558
|
+
|
|
559
|
+
do {
|
|
560
|
+
${f.name} = \`\${source${f.name}} (\${++counter})\`
|
|
561
|
+
} while (this.uniqueIds.${f.name}.has(${f.name}))
|
|
562
|
+
|
|
563
|
+
this.logger.log(\`${model.typeName} ${f.name} "\${item.${f.name}}" already exists. Renaming to "\${${f.name}}")\`)
|
|
564
|
+
item.${f.name} = ${f.name}
|
|
565
|
+
}`);
|
|
566
|
+
result.setCode.push(`this.uniqueIds.${f.name}.set(item.${f.name}, item)`);
|
|
567
|
+
result.removeCode.push(`this.uniqueIds.${f.name}.delete(item.${f.name})`);
|
|
428
568
|
}
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
public ${r.fieldMeta.getByForeignKeyIdsMethodFnName}(id: ${r.meta.types.brandedIdType}): ${model.brandedIdType}[] {
|
|
434
|
-
const s = this.${r.name}Map.get(id)
|
|
435
|
-
if (!s) return []
|
|
436
|
-
return Array.from(s.keys())
|
|
569
|
+
if (fields.length > 1) {
|
|
570
|
+
result.verifyFunctionComment = `In case a value of the fields ${fields
|
|
571
|
+
.map((f) => f.name)
|
|
572
|
+
.join(', ')} is not unique, it is renamed.\n`;
|
|
437
573
|
}
|
|
438
|
-
|
|
439
|
-
.
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
574
|
+
else if (fields.length === 1) {
|
|
575
|
+
result.verifyFunctionComment = `In case the value of the field ${fields[0].name} is not unique, it is renamed.\n`;
|
|
576
|
+
}
|
|
577
|
+
return result;
|
|
578
|
+
}
|
|
579
|
+
function getEnsureUniqueFnName(field) {
|
|
580
|
+
return `ensureUnique${(0, string_1.toPascalCase)(field.name)}`;
|
|
581
|
+
}
|
|
582
|
+
function generateMaxLengthBlocks({ model }) {
|
|
583
|
+
const fields = model.fields.filter(fields_1.isMaxLengthStringField);
|
|
584
|
+
const result = {
|
|
585
|
+
verifyFunctionComment: '',
|
|
586
|
+
verifyCode: [],
|
|
587
|
+
updateCode: [],
|
|
588
|
+
ensureMaxLengthFunctions: [],
|
|
589
|
+
};
|
|
590
|
+
for (const f of fields) {
|
|
591
|
+
result.verifyCode.push(`this.${getEnsureMaxLengthFnName(f)}(item)`);
|
|
592
|
+
result.updateCode.push(`
|
|
593
|
+
if (item.${f.name} !== undefined && existingItem.${f.name} !== item.${f.name}) {
|
|
594
|
+
this.${getEnsureMaxLengthFnName(f)}(item)
|
|
595
|
+
}`);
|
|
596
|
+
result.ensureMaxLengthFunctions.push(`
|
|
443
597
|
/**
|
|
444
598
|
* Utility function that ensures that the ${f.name} field has a max length of ${f.attributes.maxLength}
|
|
445
599
|
*/
|
|
@@ -451,113 +605,128 @@ export class ${meta.data.repositoryClassName} implements Repository<
|
|
|
451
605
|
return
|
|
452
606
|
}
|
|
453
607
|
item.${f.name} = item.${f.name}.substring(0, ${f.attributes.maxLength - 4}) + \`...\`
|
|
454
|
-
}`)
|
|
455
|
-
.join('\n')}
|
|
456
|
-
|
|
457
|
-
${uniqueStringFields
|
|
458
|
-
.map((f) => {
|
|
459
|
-
return `
|
|
460
|
-
/**
|
|
461
|
-
* Utility function that ensures that the ${f.name} field is unique
|
|
462
|
-
*/
|
|
463
|
-
private ${getEnsureUniqueFnName(f)}(item: { ${f.name}?: string }) {
|
|
464
|
-
if (!item.${f.name}) return
|
|
465
|
-
if (!this.uniqueIds.${f.name}.has(item.${f.name})) return
|
|
466
|
-
let counter = 1
|
|
467
|
-
|
|
468
|
-
let ${f.name}: string
|
|
469
|
-
const source${f.name} =${!f.attributes.maxLength ? `item.${f.name}` : `item.${f.name}.substring(0, ${f.attributes.maxLength - 5})`}
|
|
470
|
-
|
|
471
|
-
do {
|
|
472
|
-
${f.name} = \`\${source${f.name}} (\${++counter})\`
|
|
473
|
-
} while (this.uniqueIds.${f.name}.has(${f.name}))
|
|
474
|
-
|
|
475
|
-
this.logger.log(\`${model.typeName} ${f.name} "\${item.${f.name}}" already exists. Renaming to "\${${f.name}}")\`)
|
|
476
|
-
item.${f.name} = ${f.name}
|
|
608
|
+
}`);
|
|
477
609
|
}
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
610
|
+
if (fields.length > 1) {
|
|
611
|
+
result.verifyFunctionComment = `In case a value of the fields ${fields
|
|
612
|
+
.map((f) => f.name)
|
|
613
|
+
.join(', ')} exceeds its max length, it is truncated.\n`;
|
|
614
|
+
}
|
|
615
|
+
else if (fields.length === 1) {
|
|
616
|
+
result.verifyFunctionComment = `In case the value of the field ${fields[0].name} exceeds its max length, it is truncated.\n`;
|
|
617
|
+
}
|
|
618
|
+
return result;
|
|
619
|
+
}
|
|
620
|
+
function getEnsureMaxLengthFnName(field) {
|
|
621
|
+
return `ensureMaxLength${(0, string_1.toPascalCase)(field.name)}`;
|
|
622
|
+
}
|
|
623
|
+
function generateIndexBlocks({ model, imports, }) {
|
|
624
|
+
const indexes = model.attributes.index ? [getIndexDefinition({ fieldNames: model.attributes.index, model })] : [];
|
|
625
|
+
if (indexes.length > 0) {
|
|
626
|
+
imports.addImport({ items: [(0, types_1.toTypeName)('NestedMap')], from: (0, types_1.toPath)('@pxl/common') });
|
|
627
|
+
}
|
|
628
|
+
const result = {
|
|
629
|
+
nestedMapDeclarations: [],
|
|
630
|
+
initCode: [],
|
|
631
|
+
initLogCode: [],
|
|
632
|
+
getterFunctions: [],
|
|
633
|
+
setCode: [],
|
|
634
|
+
removeCode: [],
|
|
635
|
+
};
|
|
636
|
+
for (const i of indexes) {
|
|
637
|
+
result.nestedMapDeclarations.push(`
|
|
638
|
+
protected ${i.name} = new NestedMap<${i.fields.map((f) => f.meta.types.brandedIdType).join(',')}, ${model.typeName}>(
|
|
639
|
+
${i.fields.map((f) => `(x) => x.${f.name}`).join(',')}
|
|
640
|
+
)`);
|
|
641
|
+
result.initCode.push(`this.${i.name}.clear()`);
|
|
642
|
+
result.initLogCode.push(`this.logger.log(\`\${this.${i.name}.size} ${model.typeName} loaded into index ${i.name}\`)`);
|
|
643
|
+
result.getterFunctions.push(`
|
|
644
|
+
public getFrom${indexes.length === 1 ? 'Index' : (0, string_1.toPascalCase)(i.name)}
|
|
645
|
+
({ ${i.fields.map((f) => f.name).join(',')} }: { ${i.fields
|
|
646
|
+
.map((f) => `${f.name} : ${f.meta.types.brandedIdType}`)
|
|
647
|
+
.join(';')} }): ${model.typeName} | null {
|
|
648
|
+
return this.${i.name}.get(${i.fields.map((f) => f.name).join(',')}) ?? null
|
|
649
|
+
}`);
|
|
650
|
+
result.setCode.push(`this.${i.name}.set(item)`);
|
|
651
|
+
result.removeCode.push(`this.${i.name}.delete(item)`);
|
|
652
|
+
}
|
|
653
|
+
return result;
|
|
654
|
+
}
|
|
655
|
+
function getIndexDefinition({ fieldNames, model }) {
|
|
656
|
+
const fields = fieldNames.map((f) => {
|
|
657
|
+
const result = model.fields.find((field) => field.name === f);
|
|
658
|
+
if (!result) {
|
|
659
|
+
throw new Error(`Index field ${f} not found in model ${model.name}`);
|
|
497
660
|
}
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
.
|
|
502
|
-
|
|
503
|
-
|
|
661
|
+
if (result.kind !== 'relation') {
|
|
662
|
+
throw new Error(`Index field ${f} is not a relation in model ${model.name}`);
|
|
663
|
+
}
|
|
664
|
+
const meta = (0, meta_1.getModelMetadata)({ model: result.relationToModel });
|
|
665
|
+
return Object.assign(Object.assign({}, result), { meta });
|
|
666
|
+
});
|
|
667
|
+
const [firstField, ...restFields] = fields;
|
|
668
|
+
if (!restFields || restFields.length === 0) {
|
|
669
|
+
throw new Error(`Index for model ${model.name} must have at least 2 fields!`);
|
|
504
670
|
}
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
671
|
+
return {
|
|
672
|
+
fields,
|
|
673
|
+
name: (0, string_1.toCamelCase)(`${firstField.name}${restFields.map((f) => (0, string_1.toPascalCase)(f.name)).join('')}Index`),
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
function generateRelationsBlocks({ model, imports, }) {
|
|
677
|
+
const relations = model.fields.filter(schema_1.isFieldRelation);
|
|
678
|
+
const result = {
|
|
679
|
+
mapDeclarations: [],
|
|
680
|
+
getterFunctions: [],
|
|
681
|
+
setCode: [],
|
|
682
|
+
removeCode: [],
|
|
683
|
+
};
|
|
684
|
+
for (const r of relations) {
|
|
685
|
+
const fieldMeta = (0, meta_1.getFieldMetadata)({ field: r });
|
|
686
|
+
const relationModelMeta = (0, meta_1.getModelMetadata)({ model: r.relationToModel });
|
|
687
|
+
imports.addImport({
|
|
688
|
+
items: [relationModelMeta.types.brandedIdType],
|
|
689
|
+
from: relationModelMeta.types.importPath,
|
|
690
|
+
});
|
|
691
|
+
result.mapDeclarations.push(`protected ${r.name}Map: Map<${relationModelMeta.types.brandedIdType}, Map<${model.brandedIdType}, ${model.typeName}>> = new Map()`);
|
|
692
|
+
result.getterFunctions.push(`
|
|
693
|
+
/**
|
|
694
|
+
* Function to retrieve all ${(0, string_1.pluralize)(model.name)} that are related to a ${r.name}
|
|
508
695
|
*/
|
|
509
|
-
|
|
510
|
-
this.
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
${relations
|
|
514
|
-
.map((r) => `
|
|
515
|
-
${!r.isRequired ? `if (item.${r.name}) {` : ''}
|
|
516
|
-
const ${r.name}Map = this.${r.name}Map.get(item.${r.name})
|
|
517
|
-
if (${r.name}Map) {
|
|
518
|
-
${r.name}Map.delete(item.id)
|
|
519
|
-
if (${r.name}Map.size === 0) {
|
|
520
|
-
this.${r.name}Map.delete(item.${r.name})
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
${!r.isRequired ? '}' : ''}
|
|
524
|
-
`)
|
|
525
|
-
.join('\n')}
|
|
526
|
-
|
|
527
|
-
${indexes.map((i) => `this.${i.name}.delete(item)`).join('\n')}
|
|
696
|
+
public ${fieldMeta.getByForeignKeyMethodFnName}(id: ${relationModelMeta.types.brandedIdType}): Map<${model.brandedIdType},${model.typeName}> {
|
|
697
|
+
const result = this.${r.name}Map.get(id)
|
|
698
|
+
if (!result) return new Map()
|
|
699
|
+
return result
|
|
528
700
|
}
|
|
529
701
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
? ''
|
|
533
|
-
: `
|
|
534
|
-
/**
|
|
535
|
-
* Utility function that converts a given Database object to a TypeScript model instance
|
|
702
|
+
/**
|
|
703
|
+
* Function to retrieve all ${model.brandedIdType}s that are related to a ${r.name}
|
|
536
704
|
*/
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
return
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
|
|
705
|
+
public ${fieldMeta.getByForeignKeyIdsMethodFnName}(id: ${relationModelMeta.types.brandedIdType}): ${model.brandedIdType}[] {
|
|
706
|
+
const s = this.${r.name}Map.get(id)
|
|
707
|
+
if (!s) return []
|
|
708
|
+
return Array.from(s.keys())
|
|
709
|
+
}`);
|
|
710
|
+
result.setCode.push(`
|
|
711
|
+
${!r.isRequired ? `if (item.${r.name}) {` : ''}
|
|
712
|
+
let ${r.name}Map = this.${r.name}Map.get(item.${r.name})
|
|
713
|
+
if (!${r.name}Map) {
|
|
714
|
+
${r.name}Map = new Map()
|
|
715
|
+
this.${r.name}Map.set(item.${r.name}, ${r.name}Map)
|
|
716
|
+
}
|
|
717
|
+
${r.name}Map.set(item.id, item)
|
|
718
|
+
${!r.isRequired ? `}` : ''}`);
|
|
719
|
+
result.removeCode.push(`
|
|
720
|
+
${!r.isRequired ? `if (item.${r.name}) {` : ''}
|
|
721
|
+
const ${r.name}Map = this.${r.name}Map.get(item.${r.name})
|
|
722
|
+
if (${r.name}Map) {
|
|
723
|
+
${r.name}Map.delete(item.id)
|
|
724
|
+
if (${r.name}Map.size === 0) {
|
|
725
|
+
this.${r.name}Map.delete(item.${r.name})
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
${!r.isRequired ? '}' : ''}
|
|
729
|
+
`);
|
|
730
|
+
}
|
|
731
|
+
return result;
|
|
563
732
|
}
|