@postxl/generator 0.24.1 → 0.25.1
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 -5
- package/dist/generators/models/repository.generator.d.ts +11 -1
- package/dist/generators/models/repository.generator.js +621 -455
- 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
|
-
? `
|
|
47
|
+
${blocks.relations.mapDeclarations.join('\n')}
|
|
115
48
|
|
|
116
|
-
|
|
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
|
-
`}
|
|
139
|
-
|
|
140
|
-
public async init() {
|
|
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
|
-
}
|
|
49
|
+
${blocks.default.publicVariableDeclaration}
|
|
166
50
|
|
|
167
|
-
|
|
51
|
+
${blocks.id.generateNextIdFunctionName}
|
|
168
52
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
this.logger.log(\`\${format(this.data.size)} \${pluralize('${model.typeName}', this.data.size)} loaded\`)
|
|
172
|
-
${indexes
|
|
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,503 @@ 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')}
|
|
136
|
+
|
|
137
|
+
${blocks.uniqueStringFields.ensureUniqueFunctions.join('\n\n')}
|
|
322
138
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
${uniqueStringFields.map((f) => `this.${getEnsureUniqueFnName(f)}(item)`).join('\n')}
|
|
329
|
-
}`
|
|
330
|
-
: ''}
|
|
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
|
+
// prettier-ignore
|
|
214
|
+
createCode: `
|
|
215
|
+
// Non-mocked version is async - so we keep type-compatible signatures for create() and createWithId()
|
|
216
|
+
// eslint-disable-next-line @typescript-eslint/require-await, @typescript-eslint/no-unused-vars
|
|
217
|
+
public async create(item: Omit<${model.typeName}, '${idField.name}'> & Partial<{id: ${idField.unbrandedTypeName}}>): Promise<${model.typeName}> {
|
|
218
|
+
const newItem = await Promise.resolve(this.verifyItem(item))
|
|
219
|
+
|
|
220
|
+
this.set(newItem)
|
|
221
|
+
return newItem
|
|
222
|
+
}`,
|
|
223
|
+
createManyCode: `
|
|
224
|
+
public async createMany(items: ${blocks.id.verifyFunctionParameterType}[]) {
|
|
225
|
+
const newItems = items.map((item) => this.verifyItem(item))
|
|
226
|
+
|
|
227
|
+
await Promise.resolve()
|
|
354
228
|
|
|
229
|
+
for (const item of newItems) {
|
|
230
|
+
this.set(item)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return newItems
|
|
234
|
+
}`,
|
|
235
|
+
updateCode: `
|
|
236
|
+
public async update(item: Partial<${model.typeName}> & {
|
|
237
|
+
id: ${model.brandedIdType}
|
|
238
|
+
}): Promise<${model.typeName}> {
|
|
239
|
+
const existingItem = this.get(item.id)
|
|
355
240
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
241
|
+
if (!existingItem) {
|
|
242
|
+
throw new Error(\`Could not update ${meta.userFriendlyName} with id \${item.id}. Not found!\`)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
${blocks.maxLength.updateCode.join('\n')}
|
|
247
|
+
|
|
248
|
+
${blocks.uniqueStringFields.updateCode.join('\n')}
|
|
249
|
+
|
|
250
|
+
const newItem = await Promise.resolve({ ...existingItem, ...item })
|
|
251
|
+
|
|
252
|
+
this.remove(existingItem)
|
|
253
|
+
this.set(newItem)
|
|
254
|
+
|
|
255
|
+
return newItem
|
|
256
|
+
}`,
|
|
257
|
+
deleteCode: `
|
|
258
|
+
public async delete(id: ${model.brandedIdType}): Promise<void> {
|
|
259
|
+
const existingItem = this.get(id)
|
|
260
|
+
if (!existingItem) {
|
|
261
|
+
throw new Error(\`Could not delete ${model.typeName} with id \${id}. Not found!\`)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
await Promise.resolve()
|
|
265
|
+
|
|
266
|
+
this.remove(existingItem)
|
|
267
|
+
}`,
|
|
268
|
+
databaseDecoderCode: ``,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
function generateMainBuildingBlocks_InDatabase({ model, meta, imports, blocks, }) {
|
|
272
|
+
const decoderFunctionName = meta.data.repository.decoderFnName;
|
|
273
|
+
const { idField } = model;
|
|
274
|
+
imports
|
|
275
|
+
.addImport({ items: [meta.types.zodDecoderFnName], from: meta.types.importPath })
|
|
276
|
+
.addImport({
|
|
277
|
+
items: [`DbService`, `${model.sourceName} as DbType`].map(types_1.toTypeName),
|
|
278
|
+
from: (0, types_1.toFileName)('@pxl/db'),
|
|
372
279
|
})
|
|
373
|
-
.
|
|
280
|
+
.addImport({
|
|
281
|
+
items: [`format`, `pluralize`].map(types_1.toTypeName),
|
|
282
|
+
from: (0, types_1.toFileName)('@pxl/common'),
|
|
283
|
+
});
|
|
284
|
+
return {
|
|
285
|
+
constructorCode: `constructor(protected db: DbService) {}`,
|
|
286
|
+
initCode: `
|
|
287
|
+
public async init() {
|
|
288
|
+
this.data.clear()
|
|
289
|
+
|
|
290
|
+
${blocks.uniqueStringFields.clearCode.join('\n')}
|
|
291
|
+
${blocks.default.init.resetCode}
|
|
292
|
+
|
|
293
|
+
${blocks.index.initCode.join('\n')}
|
|
294
|
+
|
|
295
|
+
const data = await this.db.${meta.data.repository.getMethodFnName}.findMany({})
|
|
296
|
+
|
|
297
|
+
for (const rawItem of data) {
|
|
298
|
+
const item = this.${decoderFunctionName}(rawItem)
|
|
299
|
+
this.set(item)
|
|
300
|
+
|
|
301
|
+
${blocks.default.init.setCode}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
${blocks.id.initCode}
|
|
305
|
+
|
|
306
|
+
${blocks.default.init.checkCode}
|
|
307
|
+
|
|
308
|
+
this.logger.log(\`\${format(this.data.size)} \${pluralize('${model.typeName}', this.data.size)} loaded\`)
|
|
309
|
+
${blocks.index.initLogCode.join('\n')}
|
|
310
|
+
}`,
|
|
311
|
+
reInitCode: `
|
|
312
|
+
public async reInit(data: ${model.typeName}[]): Promise<void> {
|
|
313
|
+
if (!this.db.useE2ETestDB) {
|
|
314
|
+
const errorMsg =
|
|
315
|
+
'ReInit() shall only be called in tests using MockRepositories or in DB configured for E2E tests!'
|
|
316
|
+
this.logger.error(errorMsg)
|
|
317
|
+
throw new Error(errorMsg)
|
|
318
|
+
}
|
|
319
|
+
await this.db.runOnlyOnTestDb(() => this.db.${meta.data.repository.getMethodFnName}.createMany({ data: data.map((i) => this.toCreateItem(i)) }))
|
|
320
|
+
return this.init()
|
|
321
|
+
}`,
|
|
322
|
+
deleteAllCode: `
|
|
323
|
+
public async deleteAll(): Promise<void> {
|
|
324
|
+
await this.db.runOnlyOnTestDb(() => this.db.$executeRaw\`DELETE FROM ${model.sourceSchemaName !== undefined ? `"${model.sourceSchemaName}".` : ''}"${model.sourceName}"\`)
|
|
325
|
+
|
|
326
|
+
return this.init()
|
|
327
|
+
}
|
|
328
|
+
`,
|
|
329
|
+
// prettier-ignore
|
|
330
|
+
createCode: `
|
|
331
|
+
public async create(item: Omit<${model.typeName}, '${idField.name}'> & Partial<{ id: ${idField.unbrandedTypeName} }>): Promise<${model.typeName}> {
|
|
332
|
+
const newItem = this.${decoderFunctionName}(
|
|
333
|
+
await this.db.${meta.data.repository.getMethodFnName}.create({
|
|
334
|
+
data: this.toCreateItem(this.verifyItem(item)),
|
|
335
|
+
}),
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
this.set(newItem)
|
|
339
|
+
return newItem
|
|
340
|
+
}`,
|
|
341
|
+
createManyCode: `
|
|
342
|
+
public async createMany(items: ${blocks.id.verifyFunctionParameterType}[]) {
|
|
343
|
+
const newItems = items.map((item) => this.verifyItem(item))
|
|
344
|
+
|
|
345
|
+
await this.db.${meta.data.repository.getMethodFnName}.createMany({ data: newItems.map(i => this.toCreateItem(i)) })
|
|
346
|
+
|
|
347
|
+
for (const item of newItems) {
|
|
348
|
+
this.set(item)
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return newItems
|
|
352
|
+
}`,
|
|
353
|
+
// prettier-ignore
|
|
354
|
+
updateCode: `
|
|
355
|
+
public async update(item: Partial<${model.typeName}> & { id: ${model.brandedIdType}}): Promise<${model.typeName}> {
|
|
356
|
+
const existingItem = this.get(item.id)
|
|
357
|
+
|
|
358
|
+
if (!existingItem) {
|
|
359
|
+
throw new Error(\`Could not update ${meta.userFriendlyName} with id \${item.id}. Not found!\`)
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
${blocks.maxLength.updateCode.join('\n')}
|
|
374
363
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
},
|
|
385
|
-
data: {
|
|
386
|
-
${[...model.fields.values()]
|
|
364
|
+
${blocks.uniqueStringFields.updateCode.join('\n')}
|
|
365
|
+
|
|
366
|
+
const newItem = this.${decoderFunctionName}(
|
|
367
|
+
await this.db.${meta.data.repository.getMethodFnName}.update({
|
|
368
|
+
where: {
|
|
369
|
+
${idField.sourceName}: item.${idField.name},
|
|
370
|
+
},
|
|
371
|
+
data: {
|
|
372
|
+
${[...model.fields.values()]
|
|
387
373
|
.filter((f) => f.kind !== 'id')
|
|
388
374
|
.map((f) => f.isRequired
|
|
389
375
|
? `${f.sourceName}: item.${f.name} ?? existingItem.${f.name}`
|
|
390
376
|
: `${f.sourceName}: item.${f.name}`)
|
|
391
377
|
.join(',\n')}
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
378
|
+
},
|
|
379
|
+
}),
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
this.remove(existingItem)
|
|
383
|
+
this.set(newItem)
|
|
384
|
+
|
|
385
|
+
return newItem
|
|
386
|
+
}`,
|
|
387
|
+
deleteCode: `
|
|
388
|
+
public async delete(id: ${model.brandedIdType}): Promise<void> {
|
|
389
|
+
const existingItem = this.get(id)
|
|
390
|
+
if (!existingItem) {
|
|
391
|
+
throw new Error(\`Could not delete ${model.typeName} with id \${id}. Not found!\`)
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
await this.db.${meta.data.repository.getMethodFnName}.delete({ where: { ${idField.sourceName}:id } })
|
|
395
|
+
|
|
396
|
+
this.remove(existingItem)
|
|
397
|
+
}`,
|
|
398
|
+
databaseDecoderCode: `
|
|
399
|
+
/**
|
|
400
|
+
* Utility function that converts a given Database object to a TypeScript model instance
|
|
401
|
+
*/
|
|
402
|
+
private ${decoderFunctionName}(item: Pick<DbType, ${[...model.fields.values()]
|
|
403
|
+
.map((f) => `'${f.sourceName}'`)
|
|
404
|
+
.join(' | ')}>): ${model.typeName} {
|
|
405
|
+
return ${meta.types.zodDecoderFnName}.parse({
|
|
406
|
+
${[...model.fields.values()].map((f) => `${f.name}: item.${f.sourceName}`).join(',\n')}
|
|
407
|
+
})
|
|
408
|
+
}`,
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
function generateBlocks({ model, meta, imports, }) {
|
|
412
|
+
return {
|
|
413
|
+
id: generateIdBlocks({ model, meta, imports }),
|
|
414
|
+
default: generateDefaultBlocks({ model, meta }),
|
|
415
|
+
uniqueStringFields: generateUniqueFieldsBlocks({ model, meta }),
|
|
416
|
+
maxLength: generateMaxLengthBlocks({ model, meta }),
|
|
417
|
+
index: generateIndexBlocks({ model, meta, imports }),
|
|
418
|
+
relations: generateRelationsBlocks({ model, meta, imports }),
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
function generateIdBlocks({ model, meta, imports, }) {
|
|
422
|
+
const idField = model.idField;
|
|
423
|
+
if (!idField.isGenerated) {
|
|
424
|
+
return generateIdBlocks_NoGeneration({ idField, model, meta });
|
|
406
425
|
}
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
426
|
+
if (idField.schemaType === 'Int') {
|
|
427
|
+
return generateIdBlock_Int({ idField, model, meta, imports });
|
|
428
|
+
}
|
|
429
|
+
if (idField.schemaType === 'String') {
|
|
430
|
+
return generateIdBlock_UUID({ idField, model, meta, imports });
|
|
431
|
+
}
|
|
432
|
+
throw new Error(`Repository block only supports Id generation for number and strings! Found ${idField.schemaType} for model ${model.name} instead!`);
|
|
433
|
+
}
|
|
434
|
+
function generateIdBlocks_NoGeneration({ idField, model, meta, }) {
|
|
435
|
+
return {
|
|
436
|
+
libraryImports: '',
|
|
437
|
+
generateNextIdFunctionName: '',
|
|
438
|
+
initCode: '',
|
|
439
|
+
verifyFunctionComment: `an error is thrown as field has no default setting in schema.`,
|
|
440
|
+
verifyFunctionParameterType: model.typeName,
|
|
441
|
+
verifyCode: `
|
|
442
|
+
if (item.${idField.name} === undefined) {
|
|
443
|
+
throw new Error('Id field ${idField.name} is required!')
|
|
444
|
+
}
|
|
445
|
+
const ${idField.name} = ${meta.types.toBrandedIdTypeFnName}(item.${idField.name})`,
|
|
446
|
+
setCode: '',
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
function generateIdBlock_Int({ idField, model, meta, imports, }) {
|
|
450
|
+
imports.addImport({
|
|
451
|
+
items: [meta.types.toBrandedIdTypeFnName],
|
|
452
|
+
from: meta.types.importPath,
|
|
453
|
+
});
|
|
454
|
+
return {
|
|
455
|
+
libraryImports: '',
|
|
456
|
+
generateNextIdFunctionName: `
|
|
457
|
+
protected currentMaxId = 0
|
|
458
|
+
public generateNextId(): ${model.brandedIdType} {
|
|
459
|
+
return ${meta.types.toBrandedIdTypeFnName}(++this.currentMaxId)
|
|
460
|
+
}`,
|
|
461
|
+
initCode: `this.currentMaxId = (await this.db.${meta.data.repository.getMethodFnName}.aggregate({ _max: { ${idField.sourceName}: true } }))._max.${idField.sourceName} ?? 0`,
|
|
462
|
+
verifyFunctionComment: 'the id is generated by increasing the highest former id and assigned to the item.',
|
|
463
|
+
verifyFunctionParameterType: `(Omit<${model.typeName}, '${idField.name}'> & Partial<{${idField.name}: ${idField.unbrandedTypeName}}>)`,
|
|
464
|
+
verifyCode: `const ${idField.name} = (item.${idField.name} !== undefined) ? ${meta.types.toBrandedIdTypeFnName}(item.${idField.name}) : this.generateNextId()`,
|
|
465
|
+
setCode: `if (item.id > this.currentMaxId) { this.currentMaxId = item.id }`,
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
function generateIdBlock_UUID({ idField, model, meta, imports, }) {
|
|
469
|
+
imports.addImport({
|
|
470
|
+
items: [meta.types.toBrandedIdTypeFnName],
|
|
471
|
+
from: meta.types.importPath,
|
|
472
|
+
});
|
|
473
|
+
return {
|
|
474
|
+
libraryImports: `import { randomUUID } from 'crypto'`,
|
|
475
|
+
generateNextIdFunctionName: `
|
|
476
|
+
public generateNextId(): ${model.brandedIdType} {
|
|
477
|
+
return ${meta.types.toBrandedIdTypeFnName}(randomUUID())
|
|
478
|
+
}`,
|
|
479
|
+
initCode: '',
|
|
480
|
+
verifyFunctionComment: 'a new UUID is generated and assigned to the item.',
|
|
481
|
+
verifyFunctionParameterType: `(Omit<${model.typeName}, '${idField.name}'> & Partial<{${idField.name}: ${idField.unbrandedTypeName}}>)`,
|
|
482
|
+
verifyCode: `const ${idField.name} = (item.${idField.name} !== undefined) ? ${meta.types.toBrandedIdTypeFnName}(item.${idField.name}) : this.generateNextId()`,
|
|
483
|
+
setCode: '',
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
function generateDefaultBlocks({ model, meta }) {
|
|
487
|
+
const defaultField = model.defaultField;
|
|
488
|
+
if (!defaultField) {
|
|
489
|
+
return {
|
|
490
|
+
init: {
|
|
491
|
+
resetCode: '',
|
|
492
|
+
setCode: '',
|
|
493
|
+
checkCode: '',
|
|
494
|
+
},
|
|
495
|
+
publicVariableDeclaration: '',
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
return {
|
|
499
|
+
init: {
|
|
500
|
+
resetCode: `
|
|
501
|
+
// We re-initialize the default value to undefined so the check for exactly one item does not fail upon re-initialization
|
|
502
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-non-null-assertion
|
|
503
|
+
this.defaultValue = undefined!
|
|
504
|
+
`,
|
|
505
|
+
setCode: `
|
|
506
|
+
if (item.${defaultField.name}) {
|
|
507
|
+
if (this.defaultValue) {
|
|
508
|
+
console.warn(\`More than one default ${meta.userFriendlyName} found! \${this.defaultValue.id} and \${item.id}\`)
|
|
509
|
+
}
|
|
510
|
+
this.defaultValue = item
|
|
511
|
+
}`,
|
|
512
|
+
checkCode: `
|
|
513
|
+
if (!this.db.isCLI && !this.defaultValue) {
|
|
514
|
+
throw new Error('No default ${meta.userFriendlyName} found!')
|
|
515
|
+
}`,
|
|
516
|
+
},
|
|
517
|
+
publicVariableDeclaration: `
|
|
518
|
+
// We can safely skip the assignment here as this is done in the init function
|
|
519
|
+
public defaultValue!: ${model.typeName}
|
|
520
|
+
`,
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
function generateUniqueFieldsBlocks({ model }) {
|
|
524
|
+
const fields = model.fields.filter(fields_1.isUniqueStringField);
|
|
525
|
+
const result = {
|
|
526
|
+
mapDeclarations: [],
|
|
527
|
+
clearCode: [],
|
|
528
|
+
verifyFunctionComment: '',
|
|
529
|
+
verifyCode: [],
|
|
530
|
+
updateCode: [],
|
|
531
|
+
ensureUniqueFunctions: [],
|
|
532
|
+
setCode: [],
|
|
533
|
+
removeCode: [],
|
|
534
|
+
};
|
|
535
|
+
for (const f of fields) {
|
|
536
|
+
result.mapDeclarations.push(`'${f.name}': new Map<string, ${model.typeName}>()`);
|
|
537
|
+
result.clearCode.push(`this.uniqueIds.${f.name}.clear()`);
|
|
538
|
+
result.verifyCode.push(`this.${getEnsureUniqueFnName(f)}(item)`);
|
|
539
|
+
result.updateCode.push(`
|
|
540
|
+
if (item.${f.name} !== undefined && existingItem.${f.name} !== item.${f.name}) {
|
|
541
|
+
this.${getEnsureUniqueFnName(f)}(item)
|
|
542
|
+
}`);
|
|
543
|
+
result.ensureUniqueFunctions.push(`
|
|
544
|
+
/**
|
|
545
|
+
* Utility function that ensures that the ${f.name} field is unique
|
|
546
|
+
*/
|
|
547
|
+
private ${getEnsureUniqueFnName(f)}(item: { ${f.name}?: string }) {
|
|
548
|
+
if (!item.${f.name}) return
|
|
549
|
+
if (!this.uniqueIds.${f.name}.has(item.${f.name})) return
|
|
550
|
+
let counter = 1
|
|
415
551
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
552
|
+
let ${f.name}: string
|
|
553
|
+
const source${f.name} =${!f.attributes.maxLength ? `item.${f.name}` : `item.${f.name}.substring(0, ${f.attributes.maxLength - 5})`}
|
|
554
|
+
|
|
555
|
+
do {
|
|
556
|
+
${f.name} = \`\${source${f.name}} (\${++counter})\`
|
|
557
|
+
} while (this.uniqueIds.${f.name}.has(${f.name}))
|
|
558
|
+
|
|
559
|
+
this.logger.log(\`${model.typeName} ${f.name} "\${item.${f.name}}" already exists. Renaming to "\${${f.name}}")\`)
|
|
560
|
+
item.${f.name} = ${f.name}
|
|
561
|
+
}`);
|
|
562
|
+
result.setCode.push(`this.uniqueIds.${f.name}.set(item.${f.name}, item)`);
|
|
563
|
+
result.removeCode.push(`this.uniqueIds.${f.name}.delete(item.${f.name})`);
|
|
428
564
|
}
|
|
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())
|
|
565
|
+
if (fields.length > 1) {
|
|
566
|
+
result.verifyFunctionComment = `In case a value of the fields ${fields
|
|
567
|
+
.map((f) => f.name)
|
|
568
|
+
.join(', ')} is not unique, it is renamed.\n`;
|
|
437
569
|
}
|
|
438
|
-
|
|
439
|
-
.
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
570
|
+
else if (fields.length === 1) {
|
|
571
|
+
result.verifyFunctionComment = `In case the value of the field ${fields[0].name} is not unique, it is renamed.\n`;
|
|
572
|
+
}
|
|
573
|
+
return result;
|
|
574
|
+
}
|
|
575
|
+
function getEnsureUniqueFnName(field) {
|
|
576
|
+
return `ensureUnique${(0, string_1.toPascalCase)(field.name)}`;
|
|
577
|
+
}
|
|
578
|
+
function generateMaxLengthBlocks({ model }) {
|
|
579
|
+
const fields = model.fields.filter(fields_1.isMaxLengthStringField);
|
|
580
|
+
const result = {
|
|
581
|
+
verifyFunctionComment: '',
|
|
582
|
+
verifyCode: [],
|
|
583
|
+
updateCode: [],
|
|
584
|
+
ensureMaxLengthFunctions: [],
|
|
585
|
+
};
|
|
586
|
+
for (const f of fields) {
|
|
587
|
+
result.verifyCode.push(`this.${getEnsureMaxLengthFnName(f)}(item)`);
|
|
588
|
+
result.updateCode.push(`
|
|
589
|
+
if (item.${f.name} !== undefined && existingItem.${f.name} !== item.${f.name}) {
|
|
590
|
+
this.${getEnsureMaxLengthFnName(f)}(item)
|
|
591
|
+
}`);
|
|
592
|
+
result.ensureMaxLengthFunctions.push(`
|
|
443
593
|
/**
|
|
444
594
|
* Utility function that ensures that the ${f.name} field has a max length of ${f.attributes.maxLength}
|
|
445
595
|
*/
|
|
@@ -451,113 +601,129 @@ export class ${meta.data.repositoryClassName} implements Repository<
|
|
|
451
601
|
return
|
|
452
602
|
}
|
|
453
603
|
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}
|
|
604
|
+
}`);
|
|
477
605
|
}
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
/**
|
|
483
|
-
* Function that adds/updates a given item to the internal data store, indexes, etc.
|
|
484
|
-
*/
|
|
485
|
-
private set(item: ${model.typeName}): void {
|
|
486
|
-
${isGenerated ? `if (item.id > this.currentMaxId) { this.currentMaxId = item.id }` : ''}
|
|
487
|
-
this.data.set(item.id, item)
|
|
488
|
-
${uniqueStringFields.map((f) => `this.uniqueIds.${f.name}.set(item.${f.name}, item)`).join('\n')}
|
|
489
|
-
|
|
490
|
-
${relations
|
|
491
|
-
.map((r) => `
|
|
492
|
-
${!r.isRequired ? `if (item.${r.name}) {` : ''}
|
|
493
|
-
let ${r.name}Map = this.${r.name}Map.get(item.${r.name})
|
|
494
|
-
if (!${r.name}Map) {
|
|
495
|
-
${r.name}Map = new Map()
|
|
496
|
-
this.${r.name}Map.set(item.${r.name}, ${r.name}Map)
|
|
497
|
-
}
|
|
498
|
-
${r.name}Map.set(item.id, item)
|
|
499
|
-
${!r.isRequired ? `}` : ''}
|
|
500
|
-
`)
|
|
501
|
-
.join('\n')}
|
|
502
|
-
|
|
503
|
-
${indexes.map((i) => `this.${i.name}.set(item)`).join('\n')}
|
|
606
|
+
if (fields.length > 1) {
|
|
607
|
+
result.verifyFunctionComment = `In case a value of the fields ${fields
|
|
608
|
+
.map((f) => f.name)
|
|
609
|
+
.join(', ')} exceeds its max length, it is truncated.\n`;
|
|
504
610
|
}
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
* Function that removes a given item from the internal data store, indexes, etc.
|
|
508
|
-
*/
|
|
509
|
-
private remove(item: ${model.typeName}): void {
|
|
510
|
-
this.data.delete(item.id)
|
|
511
|
-
${uniqueStringFields.map((f) => `this.uniqueIds.${f.name}.delete(item.${f.name})`).join('\n')}
|
|
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')}
|
|
611
|
+
else if (fields.length === 1) {
|
|
612
|
+
result.verifyFunctionComment = `In case the value of the field ${fields[0].name} exceeds its max length, it is truncated.\n`;
|
|
528
613
|
}
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
${model.attributes.inMemoryOnly
|
|
532
|
-
? ''
|
|
533
|
-
: `
|
|
534
|
-
/**
|
|
535
|
-
* Utility function that converts a given Database object to a TypeScript model instance
|
|
536
|
-
*/
|
|
537
|
-
private ${decoder}(item: Pick<DbType, ${[...model.fields.values()]
|
|
538
|
-
.map((f) => `'${f.sourceName}'`)
|
|
539
|
-
.join(' | ')}>): ${model.typeName} {
|
|
540
|
-
return ${meta.types.zodDecoderFnName}.parse({
|
|
541
|
-
${[...model.fields.values()].map((f) => `${f.name}: item.${f.sourceName}`).join(',\n')}
|
|
542
|
-
})
|
|
543
|
-
}`}
|
|
544
|
-
}
|
|
545
|
-
`;
|
|
546
|
-
}
|
|
547
|
-
exports.generateRepository = generateRepository;
|
|
548
|
-
/**
|
|
549
|
-
* Generates a mock repository data structure for a given model: same a repository, but in memory only
|
|
550
|
-
*/
|
|
551
|
-
function generateMockRepository({ model: modelSource, meta: metaSource, }) {
|
|
552
|
-
// We re-use the repository generator, but we change the meta data to use the mock repository name and the model to be in memory only
|
|
553
|
-
const meta = Object.assign(Object.assign({}, metaSource), { data: Object.assign(Object.assign({}, metaSource.data), { repositoryClassName: metaSource.data.mockRepositoryClassName, repoFilePath: metaSource.data.mockRepoFilePath }) });
|
|
554
|
-
const model = Object.assign(Object.assign({}, modelSource), { attributes: Object.assign(Object.assign({}, modelSource.attributes), { inMemoryOnly: true }) });
|
|
555
|
-
return generateRepository({ model, meta });
|
|
556
|
-
}
|
|
557
|
-
exports.generateMockRepository = generateMockRepository;
|
|
558
|
-
function getEnsureUniqueFnName(field) {
|
|
559
|
-
return `ensureUnique${(0, string_1.toPascalCase)(field.name)}`;
|
|
614
|
+
return result;
|
|
560
615
|
}
|
|
561
616
|
function getEnsureMaxLengthFnName(field) {
|
|
562
617
|
return `ensureMaxLength${(0, string_1.toPascalCase)(field.name)}`;
|
|
563
618
|
}
|
|
619
|
+
function generateIndexBlocks({ model, imports, }) {
|
|
620
|
+
const indexes = model.attributes.index ? [getIndexDefinition({ fieldNames: model.attributes.index, model })] : [];
|
|
621
|
+
if (indexes.length > 0) {
|
|
622
|
+
imports.addImport({ items: [(0, types_1.toTypeName)('NestedMap')], from: (0, types_1.toPath)('@pxl/common') });
|
|
623
|
+
}
|
|
624
|
+
const result = {
|
|
625
|
+
nestedMapDeclarations: [],
|
|
626
|
+
initCode: [],
|
|
627
|
+
initLogCode: [],
|
|
628
|
+
getterFunctions: [],
|
|
629
|
+
setCode: [],
|
|
630
|
+
removeCode: [],
|
|
631
|
+
};
|
|
632
|
+
for (const i of indexes) {
|
|
633
|
+
result.nestedMapDeclarations.push(`
|
|
634
|
+
protected ${i.name} = new NestedMap<${i.fields.map((f) => f.meta.types.brandedIdType).join(',')}, ${model.typeName}>(
|
|
635
|
+
${i.fields.map((f) => `(x) => x.${f.name}`).join(',')}
|
|
636
|
+
)`);
|
|
637
|
+
result.initCode.push(`this.${i.name}.clear()`);
|
|
638
|
+
result.initLogCode.push(`this.logger.log(\`\${this.${i.name}.size} ${model.typeName} loaded into index ${i.name}\`)`);
|
|
639
|
+
result.getterFunctions.push(`
|
|
640
|
+
public getFrom${indexes.length === 1 ? 'Index' : (0, string_1.toPascalCase)(i.name)}
|
|
641
|
+
({ ${i.fields.map((f) => f.name).join(',')} }: { ${i.fields
|
|
642
|
+
.map((f) => `${f.name} : ${f.meta.types.brandedIdType}`)
|
|
643
|
+
.join(';')} }): ${model.typeName} | null {
|
|
644
|
+
return this.${i.name}.get(${i.fields.map((f) => f.name).join(',')}) ?? null
|
|
645
|
+
}`);
|
|
646
|
+
result.setCode.push(`this.${i.name}.set(item)`);
|
|
647
|
+
result.removeCode.push(`this.${i.name}.delete(item)`);
|
|
648
|
+
}
|
|
649
|
+
return result;
|
|
650
|
+
}
|
|
651
|
+
function getIndexDefinition({ fieldNames, model }) {
|
|
652
|
+
const fields = fieldNames.map((f) => {
|
|
653
|
+
const result = model.fields.find((field) => field.name === f);
|
|
654
|
+
if (!result) {
|
|
655
|
+
throw new Error(`Index field ${f} not found in model ${model.name}`);
|
|
656
|
+
}
|
|
657
|
+
if (result.kind !== 'relation') {
|
|
658
|
+
throw new Error(`Index field ${f} is not a relation in model ${model.name}`);
|
|
659
|
+
}
|
|
660
|
+
const meta = (0, meta_1.getModelMetadata)({ model: result.relationToModel });
|
|
661
|
+
return Object.assign(Object.assign({}, result), { meta });
|
|
662
|
+
});
|
|
663
|
+
const [firstField, ...restFields] = fields;
|
|
664
|
+
if (!restFields || restFields.length === 0) {
|
|
665
|
+
throw new Error(`Index for model ${model.name} must have at least 2 fields!`);
|
|
666
|
+
}
|
|
667
|
+
return {
|
|
668
|
+
fields,
|
|
669
|
+
name: (0, string_1.toCamelCase)(`${firstField.name}${restFields.map((f) => (0, string_1.toPascalCase)(f.name)).join('')}Index`),
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
function generateRelationsBlocks({ model, imports, }) {
|
|
673
|
+
const relations = model.fields.filter(schema_1.isFieldRelation);
|
|
674
|
+
const result = {
|
|
675
|
+
mapDeclarations: [],
|
|
676
|
+
getterFunctions: [],
|
|
677
|
+
setCode: [],
|
|
678
|
+
removeCode: [],
|
|
679
|
+
};
|
|
680
|
+
for (const r of relations) {
|
|
681
|
+
const fieldMeta = (0, meta_1.getFieldMetadata)({ field: r });
|
|
682
|
+
const relationModelMeta = (0, meta_1.getModelMetadata)({ model: r.relationToModel });
|
|
683
|
+
imports.addImport({
|
|
684
|
+
items: [relationModelMeta.types.brandedIdType],
|
|
685
|
+
from: relationModelMeta.types.importPath,
|
|
686
|
+
});
|
|
687
|
+
result.mapDeclarations.push(`protected ${r.name}Map: Map<${relationModelMeta.types.brandedIdType}, Map<${model.brandedIdType}, ${model.typeName}>> = new Map()`);
|
|
688
|
+
// prettier-ignore
|
|
689
|
+
result.getterFunctions.push(`
|
|
690
|
+
/**
|
|
691
|
+
* Function to retrieve all ${(0, string_1.pluralize)(model.name)} that are related to a ${r.name}
|
|
692
|
+
*/
|
|
693
|
+
public ${fieldMeta.getByForeignKeyMethodFnName}(id: ${relationModelMeta.types.brandedIdType}): Map<${model.brandedIdType}, ${model.typeName}> {
|
|
694
|
+
const result = this.${r.name}Map.get(id)
|
|
695
|
+
if (!result) return new Map()
|
|
696
|
+
return result
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* Function to retrieve all ${model.brandedIdType}s that are related to a ${r.name}
|
|
701
|
+
*/
|
|
702
|
+
public ${fieldMeta.getByForeignKeyIdsMethodFnName}(id: ${relationModelMeta.types.brandedIdType}): ${model.brandedIdType}[] {
|
|
703
|
+
const s = this.${r.name}Map.get(id)
|
|
704
|
+
if (!s) return []
|
|
705
|
+
return Array.from(s.keys())
|
|
706
|
+
}`);
|
|
707
|
+
result.setCode.push(`
|
|
708
|
+
${!r.isRequired ? `if (item.${r.name}) {` : ''}
|
|
709
|
+
let ${r.name}Map = this.${r.name}Map.get(item.${r.name})
|
|
710
|
+
if (!${r.name}Map) {
|
|
711
|
+
${r.name}Map = new Map()
|
|
712
|
+
this.${r.name}Map.set(item.${r.name}, ${r.name}Map)
|
|
713
|
+
}
|
|
714
|
+
${r.name}Map.set(item.id, item)
|
|
715
|
+
${!r.isRequired ? `}` : ''}`);
|
|
716
|
+
result.removeCode.push(`
|
|
717
|
+
${!r.isRequired ? `if (item.${r.name}) {` : ''}
|
|
718
|
+
const ${r.name}Map = this.${r.name}Map.get(item.${r.name})
|
|
719
|
+
if (${r.name}Map) {
|
|
720
|
+
${r.name}Map.delete(item.id)
|
|
721
|
+
if (${r.name}Map.size === 0) {
|
|
722
|
+
this.${r.name}Map.delete(item.${r.name})
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
${!r.isRequired ? '}' : ''}
|
|
726
|
+
`);
|
|
727
|
+
}
|
|
728
|
+
return result;
|
|
729
|
+
}
|