@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.
@@ -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
- ? `
47
+ ${blocks.relations.mapDeclarations.join('\n')}
115
48
 
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
- `}
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
- ${isGenerated ? idIntInitFn : ''}
51
+ ${blocks.id.generateNextIdFunctionName}
168
52
 
169
- ${model.defaultField ? defaultValueInitCheckFn : ''}
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
- 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,503 @@ 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')}
136
+
137
+ ${blocks.uniqueStringFields.ensureUniqueFunctions.join('\n\n')}
322
138
 
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
- : ''}
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
+ // 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
- ${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
- `;
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
- .join('\n')}
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
- ${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()]
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
- 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!\`)
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
- ${model.attributes.inMemoryOnly
409
- ? `
410
- await Promise.resolve()
411
- `
412
- : `
413
- await this.db.${meta.data.repository.getMethodFnName}.delete({ where: { ${idField.sourceName}:id } })
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
- 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
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
- * 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())
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
- .join('\n')}
440
-
441
- ${maxLengthStringFields
442
- .map((f) => `
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
- .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)
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
+ }