@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.
@@ -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
- var _a;
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, ...(isGenerated ? [meta.types.toBrandedIdTypeFnName] : [])],
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
- if (!model.attributes.inMemoryOnly) {
71
- imports.addImport({ items: [meta.types.zodDecoderFnName], from: meta.types.importPath });
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
- ${model.attributes.inMemoryOnly
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
- 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
- }
166
-
167
- ${isGenerated ? idIntInitFn : ''}
49
+ ${blocks.default.publicVariableDeclaration}
168
50
 
169
- ${model.defaultField ? defaultValueInitCheckFn : ''}
51
+ ${blocks.id.generateNextIdFunctionName}
170
52
 
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
- public async reInit(data: ${model.typeName}[]): Promise<void> {
179
- ${model.attributes.inMemoryOnly
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
- public async deleteAll(): Promise<void> {
197
- ${model.attributes.inMemoryOnly
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
- ${indexes
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
- private toCreateItem(item: ${model.typeName}) {
244
- ${isGenerated
245
- ? `if (item.${idField.name} > ++this.currentMaxId) {
246
- this.currentMaxId = item.${idField.name}
247
- } else {
248
- item.${idField.name} = this.currentMaxId as ${model.brandedIdType}
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
- ${maxLengthStringFields.map((f) => `this.${getEnsureMaxLengthFnName(f)}(item)`).join('\n')}
106
+ ${blocks.maxLength.verifyCode.join('\n')}
253
107
 
254
- ${uniqueStringFields.map((f) => `this.${getEnsureUniqueFnName(f)}(item)`).join('\n')}
108
+ ${blocks.uniqueStringFields.verifyCode.join('\n')}
255
109
 
256
110
  return {
257
- ${[...model.fields.values()].map((f) => `${f.sourceName}: item.${f.name}`).join(',\n')}
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
- ${model.attributes.inMemoryOnly
261
- ? `
262
- // Non-mocked version is async - so we keep type-compatible signatures for create() and createWithId()
263
- // eslint-disable-next-line @typescript-eslint/require-await, @typescript-eslint/no-unused-vars`
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
- ${model.attributes.inMemoryOnly
285
- ? `
286
- // Non-mocked version is async - so we keep type-compatible signatures for create() and createWithId()
287
- // eslint-disable-next-line @typescript-eslint/require-await, @typescript-eslint/no-unused-vars`
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
- this.set(newItem)
320
- return newItem`}
321
- }
133
+ ${blocks.relations.getterFunctions.join('\n')}
134
+
135
+ ${blocks.maxLength.ensureMaxLengthFunctions.join('\n')}
322
136
 
323
- public async createMany(items: ${model.typeName}[]) {
324
- ${uniqueStringFields.length > 0 || maxLengthStringFields.length > 0
325
- ? `
326
- for (const item of items) {
327
- ${maxLengthStringFields.map((f) => `this.${getEnsureMaxLengthFnName(f)}(item)`).join('\n')}
328
- ${uniqueStringFields.map((f) => `this.${getEnsureUniqueFnName(f)}(item)`).join('\n')}
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
- ${model.attributes.inMemoryOnly
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
- for (const item of items) {
340
- this.set(item)
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
- return items
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
- if (!existingItem) {
352
- throw new Error(\`Could not update ${meta.userFriendlyName} with id \${item.id}. Not found!\`)
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
- ${maxLengthStringFields
357
- .map((f) => {
358
- return `
359
- if (item.${f.name} !== undefined && existingItem.${f.name} !== item.${f.name}) {
360
- this.${getEnsureMaxLengthFnName(f)}(item)
361
- }
362
- `;
363
- })
364
- .join('\n')}
365
- ${uniqueStringFields
366
- .map((f) => {
367
- return `
368
- if (item.${f.name} !== undefined && existingItem.${f.name} !== item.${f.name}) {
369
- this.${getEnsureUniqueFnName(f)}(item)
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
- .join('\n')}
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
- ${model.attributes.inMemoryOnly
376
- ? `
377
- const newItem = await Promise.resolve({ ...existingItem, ...item })
378
- `
379
- : `
380
- const newItem = this.${decoder}(
381
- await this.db.${meta.data.repository.getMethodFnName}.update({
382
- where: {
383
- ${idField.sourceName}: item.${idField.name},
384
- },
385
- data: {
386
- ${[...model.fields.values()]
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
- this.remove(existingItem)
397
- this.set(newItem)
398
-
399
- return newItem
400
- }
401
-
402
- public async delete(id: ${model.brandedIdType}): Promise<void> {
403
- const existingItem = this.get(id)
404
- if (!existingItem) {
405
- throw new Error(\`Could not delete ${model.typeName} with id \${id}. Not found!\`)
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
- ${model.attributes.inMemoryOnly
409
- ? `
410
- await Promise.resolve()
411
- `
412
- : `
413
- await this.db.${meta.data.repository.getMethodFnName}.delete({ where: { ${idField.sourceName}:id } })
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
- this.remove(existingItem)
417
- }
418
-
419
- ${relations
420
- .map((r) => `
421
- /**
422
- * Function to retrieve all ${(0, string_1.pluralize)(model.name)} that are related to a ${r.name}
423
- */
424
- public ${r.fieldMeta.getByForeignKeyMethodFnName}(id: ${r.meta.types.brandedIdType}): Map<${model.brandedIdType},${model.typeName}> {
425
- const result = this.${r.name}Map.get(id)
426
- if (!result) return new Map()
427
- return result
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
- * Function to retrieve all ${model.brandedIdType}s that are related to a ${r.name}
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
- .join('\n')}
440
-
441
- ${maxLengthStringFields
442
- .map((f) => `
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
- .join('\n\n')}
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)
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
- ${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')}
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
- * Function that removes a given item from the internal data store, indexes, etc.
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
- 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')}
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
- ${model.attributes.inMemoryOnly
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
- 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)}`;
560
- }
561
- function getEnsureMaxLengthFnName(field) {
562
- return `ensureMaxLength${(0, string_1.toPascalCase)(field.name)}`;
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
  }