@postxl/generator 0.9.2 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/generator.js CHANGED
@@ -151,6 +151,7 @@ const generate = ({ models, enums, config, prismaClientPath, logger, }) => __awa
151
151
  if (!config.disableGenerators.data) {
152
152
  generated.write(`/${meta.data.stubFilePath}.ts`, (0, stub_generator_1.generateStub)({ model, meta }));
153
153
  generated.write(`/${meta.data.repoFilePath}.ts`, (0, repository_generator_1.generateRepository)({ model, meta }));
154
+ generated.write(`/${meta.data.mockRepoFilePath}.ts`, (0, repository_generator_1.generateMockRepository)({ model, meta }));
154
155
  }
155
156
  // Routes
156
157
  if (!config.disableGenerators.trpc) {
@@ -47,22 +47,25 @@ function generateDataMockModule({ models, meta }) {
47
47
  });
48
48
  for (const { model, meta } of mm) {
49
49
  imports.addImport({ items: [model.typeName], from: meta.types.importPath });
50
- imports.addImport({ items: [meta.data.repositoryClassName], from: meta.data.importPath });
51
- imports.addImport({ items: [meta.seed.constantName], from: meta.seed.importPath });
50
+ imports.addImport({ items: [meta.data.repositoryClassName], from: meta.data.repoFilePath });
51
+ imports.addImport({ items: [meta.data.mockRepositoryClassName], from: meta.data.mockRepoFilePath });
52
52
  }
53
53
  const providers = mm
54
- .map(({ meta, model }) => `provideMockRepository<
55
- ${model.typeName},
56
- ${model.idField.unbrandedTypeName},
57
- ${model.typeName}
58
- >(${meta.data.repositoryClassName}, ${meta.seed.constantName})`)
54
+ .map(({ meta, model }) => `{
55
+ provide: ${meta.data.repositoryClassName},
56
+ useFactory: async () => {
57
+ const repository = new ${meta.data.mockRepositoryClassName}()
58
+ if (!!seed && !!seed.${meta.seed.constantName}) {
59
+ await repository.reInit(seed.${meta.seed.constantName})
60
+ }
61
+ return repository
62
+ }
63
+ }`)
59
64
  .join(', ');
60
65
  return `
61
66
  import { DynamicModule } from '@nestjs/common'
62
67
  import { DbModule } from '@${meta.config.project}/db'
63
68
 
64
- import { provideMockRepository } from './mock.repository'
65
-
66
69
  ${imports.generate()}
67
70
 
68
71
  export class DataMockModule {
@@ -77,7 +80,7 @@ export class DataMockModule {
77
80
  }
78
81
 
79
82
  // eslint-disable-next-line @typescript-eslint/require-await, @typescript-eslint/no-unused-vars
80
- static async mock(): Promise<DynamicModule> {
83
+ static async mock(seed?: MockData): Promise<DynamicModule> {
81
84
  const providers = [
82
85
  DataService,
83
86
  TestDataService,
@@ -7,3 +7,10 @@ export declare function generateRepository({ model, meta }: {
7
7
  model: Model;
8
8
  meta: ModelMetaData;
9
9
  }): string;
10
+ /**
11
+ * Generates a mock repository data structure for a given model: same a repository, but in memory only
12
+ */
13
+ export declare function generateMockRepository({ model: modelSource, meta: metaSource, }: {
14
+ model: Model;
15
+ meta: ModelMetaData;
16
+ }): string;
@@ -1,11 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.generateRepository = void 0;
3
+ exports.generateMockRepository = exports.generateRepository = void 0;
4
4
  const fields_1 = require("../../lib/schema/fields");
5
5
  const string_1 = require("../../lib/utils/string");
6
6
  const schema_1 = require("../../lib/schema/schema");
7
7
  const meta_1 = require("../../lib/meta");
8
8
  const imports_1 = require("../../lib/imports");
9
+ const types_1 = require("../../lib/schema/types");
9
10
  /**
10
11
  * Generates repository data structure for a given model.
11
12
  */
@@ -18,6 +19,28 @@ function generateRepository({ model, meta }) {
18
19
  const relations = fields
19
20
  .filter(schema_1.isFieldRelation)
20
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)] : [];
21
44
  const defaultValueInitFn = `
22
45
  if (item.${(_a = model.defaultField) === null || _a === void 0 ? void 0 : _a.name}) {
23
46
  if (this.defaultValue) {
@@ -38,7 +61,11 @@ function generateRepository({ model, meta }) {
38
61
  items: [model.typeName, model.brandedIdType, meta.types.toBrandedIdTypeFnName],
39
62
  from: meta.types.importPath,
40
63
  })
41
- .addImport({ items: [meta.types.zodDecoderFnName], from: meta.types.importPath });
64
+ .addImport({ items: [meta.types.zodDecoderFnName], from: meta.types.importPath })
65
+ .addImport({
66
+ items: [(0, types_1.toClassName)('Repository')],
67
+ from: (0, types_1.toFileName)(model.schemaConfig.paths.dataLibPath + 'repository.type'),
68
+ });
42
69
  relations.forEach((r) => {
43
70
  imports.addImport({
44
71
  items: [r.meta.types.brandedIdType],
@@ -48,9 +75,7 @@ function generateRepository({ model, meta }) {
48
75
  return `
49
76
  import { Injectable, Logger } from '@nestjs/common'
50
77
  import { DbService, ${model.sourceName} as DbType } from '@${model.schemaConfig.project}/db'
51
- import { format, pluralize } from '@pxl/common'
52
-
53
- import { Repository } from '../repository.type'
78
+ import { format, pluralize ${indexes.length === 0 ? '' : ', NestedMap'} } from '@pxl/common'
54
79
 
55
80
  ${imports.generate()}
56
81
 
@@ -84,13 +109,26 @@ export class ${meta.data.repositoryClassName} implements Repository<
84
109
  ${uniqueStringFields.map((f) => `'${f.name}': new Map<string, ${model.typeName}>()`).join(',\n')}
85
110
  }
86
111
 
112
+ ${indexes
113
+ .map((i) => `
114
+ protected ${i.name} = new NestedMap<${i.fields.map((f) => f.meta.types.brandedIdType).join(',')}, ${model.typeName}>(
115
+ ${i.fields.map((f) => `(x) => x.${f.name}`).join(',')}
116
+ )
117
+ `)
118
+ .join('\n')}
119
+ ${model.attributes.inMemoryOnly
120
+ ? ''
121
+ : `
87
122
  constructor(${model.attributes.inMemoryOnly ? '' : 'protected '}db: DbService) {}
88
-
123
+ `}
124
+
89
125
  public async init() {
90
126
  this.data.clear()
91
127
 
92
- ${uniqueStringFields.map((f) => `this.uniqueIds.${f.name}.clear()\n`)}
128
+ ${uniqueStringFields.map((f) => `this.uniqueIds.${f.name}.clear()`).join('\n')}
93
129
 
130
+ ${indexes.map((i) => `this.${i.name}.clear()`).join('\n')}
131
+
94
132
  ${model.attributes.inMemoryOnly
95
133
  ? 'return Promise.resolve()'
96
134
  : `
@@ -103,6 +141,7 @@ export class ${meta.data.repositoryClassName} implements Repository<
103
141
 
104
142
  ${model.defaultField ? defaultValueInitFn : ''}
105
143
  ${uniqueStringFields.map((f) => `this.uniqueIds.${f.name}.set(item.${f.name}, item)`).join('\n')}
144
+ ${indexes.map((i) => `this.${i.name}.set(item)`).join('\n')}
106
145
  }
107
146
 
108
147
  ${isGenerated ? idIntInitFn : ''}
@@ -110,12 +149,18 @@ export class ${meta.data.repositoryClassName} implements Repository<
110
149
  ${model.defaultField ? defaultValueInitCheckFn : ''}
111
150
 
112
151
  this.logger.log(\`\${format(this.data.size)} \${pluralize('${model.typeName}', this.data.size)} loaded\`)
152
+ ${indexes
153
+ .map((i) => `this.logger.log(\`\${this.${i.name}.size} ${model.typeName} loaded into index ${i.name}\`)`)
154
+ .join('\n')}
113
155
  `}
114
156
  }
115
157
 
116
158
  public async reInit(data: ${model.typeName}[]): Promise<void> {
117
159
  ${model.attributes.inMemoryOnly
118
- ? 'return Promise.resolve()'
160
+ ? `
161
+ await this.init()
162
+ await this.createMany(data)
163
+ `
119
164
  : `
120
165
  if (!this.db.useE2ETestDB) {
121
166
  const errorMsg =
@@ -123,7 +168,17 @@ export class ${meta.data.repositoryClassName} implements Repository<
123
168
  this.logger.error(errorMsg)
124
169
  throw new Error(errorMsg)
125
170
  }
126
- await this.db.runOnlyOnTestDb(() => this.db.${meta.data.repository.getMethodFnName}.createMany({ data: data.map((i) => this.toCreateItem(i)) }))`}
171
+ await this.db.runOnlyOnTestDb(() => this.db.${meta.data.repository.getMethodFnName}.createMany({ data: data.map((i) => this.toCreateItem(i)) }))
172
+ return this.init()
173
+ `}
174
+ }
175
+
176
+ public async deleteAll(): Promise<void> {
177
+ ${model.attributes.inMemoryOnly
178
+ ? ''
179
+ : `
180
+ await this.db.runOnlyOnTestDb(() => this.db.$executeRaw\`DELETE FROM ${model.sourceSchemaName !== undefined ? `"${model.sourceSchemaName}".` : ''}"${model.sourceName}"\`)
181
+ `}
127
182
  return this.init()
128
183
  }
129
184
 
@@ -142,6 +197,17 @@ export class ${meta.data.repositoryClassName} implements Repository<
142
197
  return Array.from(this.data.values())
143
198
  }
144
199
 
200
+ ${indexes
201
+ .map((i) => `
202
+ public getFrom${indexes.length === 1 ? 'Index' : (0, string_1.toPascalCase)(i.name)}
203
+ ({ ${i.fields.map((f) => f.name).join(',')} }: { ${i.fields
204
+ .map((f) => `${f.name} : ${f.meta.types.brandedIdType}`)
205
+ .join(';')} }): ${model.typeName} | null {
206
+ return this.${i.name}.get(${i.fields.map((f) => f.name).join(',')}) ?? null
207
+ }
208
+ `)
209
+ .join('\n')}
210
+
145
211
  public filter(predicate: (item: ${model.typeName}) => boolean): ${model.typeName}[] {
146
212
  return this.getAllAsArray().filter(predicate)
147
213
  }
@@ -205,7 +271,7 @@ export class ${meta.data.repositoryClassName} implements Repository<
205
271
 
206
272
  ${model.attributes.inMemoryOnly
207
273
  ? `
208
- const newItem = await Promise.resolve({ item, id })
274
+ const newItem = await Promise.resolve({ ...item, id: ${meta.types.toBrandedIdTypeFnName}(id) })
209
275
  `
210
276
  : `
211
277
  const newItem = this.${decoder}(
@@ -398,6 +464,8 @@ export class ${meta.data.repositoryClassName} implements Repository<
398
464
  ${!r.isRequired ? `}` : ''}
399
465
  `)
400
466
  .join('\n')}
467
+
468
+ ${indexes.map((i) => `this.${i.name}.set(item)`).join('\n')}
401
469
  }
402
470
 
403
471
  /**
@@ -420,22 +488,38 @@ export class ${meta.data.repositoryClassName} implements Repository<
420
488
  ${!r.isRequired ? '}' : ''}
421
489
  `)
422
490
  .join('\n')}
491
+
492
+ ${indexes.map((i) => `this.${i.name}.delete(item)`).join('\n')}
423
493
  }
424
494
 
495
+
496
+ ${model.attributes.inMemoryOnly
497
+ ? ''
498
+ : `
425
499
  /**
426
500
  * Utility function that converts a given Database object to a TypeScript model instance
427
501
  */
428
502
  private ${decoder}(item: Pick<DbType, ${[...model.fields.values()]
429
- .map((f) => `'${f.sourceName}'`)
430
- .join(' | ')}>): ${model.typeName} {
503
+ .map((f) => `'${f.sourceName}'`)
504
+ .join(' | ')}>): ${model.typeName} {
431
505
  return ${meta.types.zodDecoderFnName}.parse({
432
506
  ${[...model.fields.values()].map((f) => `${f.name}: item.${f.sourceName}`).join(',\n')}
433
507
  })
434
- }
508
+ }`}
435
509
  }
436
510
  `;
437
511
  }
438
512
  exports.generateRepository = generateRepository;
513
+ /**
514
+ * Generates a mock repository data structure for a given model: same a repository, but in memory only
515
+ */
516
+ function generateMockRepository({ model: modelSource, meta: metaSource, }) {
517
+ // 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
518
+ const meta = Object.assign(Object.assign({}, metaSource), { data: Object.assign(Object.assign({}, metaSource.data), { repositoryClassName: metaSource.data.mockRepositoryClassName, repoFilePath: metaSource.data.mockRepoFilePath }) });
519
+ const model = Object.assign(Object.assign({}, modelSource), { attributes: Object.assign(Object.assign({}, modelSource.attributes), { inMemoryOnly: true }) });
520
+ return generateRepository({ model, meta });
521
+ }
522
+ exports.generateMockRepository = generateMockRepository;
439
523
  function getEnsureUniqueFnName(field) {
440
524
  return `ensureUnique${(0, string_1.toPascalCase)(field.name)}`;
441
525
  }
@@ -33,6 +33,11 @@ export type ModelAttributes = {
33
33
  * Whether the model should be stored in the database or only in memory.
34
34
  */
35
35
  inMemoryOnly: boolean;
36
+ /**
37
+ * Schema tag: ´@@Index()`
38
+ * Creates an index on the given fields.
39
+ */
40
+ index?: string[];
36
41
  };
37
42
  export type FieldAttributes = {
38
43
  /**
@@ -125,10 +125,22 @@ export type ModelMetaData = {
125
125
  * Path to the file containing the repository definition.
126
126
  */
127
127
  repoFilePath: Types.Path;
128
+ /**
129
+ * Name of the file containing the mock repository definition for this model.
130
+ */
131
+ mockRepoFileName: Types.FileName;
132
+ /**
133
+ * Path to the file containing the mock repository definition.
134
+ */
135
+ mockRepoFilePath: Types.Path;
128
136
  /**
129
137
  * The name of the class for the repository definition of this model (e.g. AggregationRepository).
130
138
  */
131
139
  repositoryClassName: Types.ClassName;
140
+ /**
141
+ * The name of the class for the in-memory mock repository definition of this model (e.g. MockAggregationRepository).
142
+ */
143
+ mockRepositoryClassName: Types.ClassName;
132
144
  /**
133
145
  * The name by which the repository is exposed in the dataService/context. (e.g. aggregations)
134
146
  */
package/dist/lib/meta.js CHANGED
@@ -71,10 +71,13 @@ function getModelMetadata({ model }) {
71
71
  defaultStubConstantName: Types.toVariableName(`${camelCase}DefaultStub`),
72
72
  repoFileName: Types.toFileName(`${camelCase}.repository`),
73
73
  repoFilePath: Types.toPath(`${config.paths.dataLibPath}repositories/${camelCase}.repository`),
74
+ mockRepoFileName: Types.toFileName(`${camelCase}.mock.repository`),
75
+ mockRepoFilePath: Types.toPath(`${config.paths.dataLibPath}repositories/mock/${camelCase}.mock.repository`),
74
76
  stubFilePath: Types.toPath(`${config.paths.dataLibPath}stubs/${camelCase}.stub`),
75
77
  importPath: Types.toPath(`@${config.project}/data`),
76
78
  stubGenerationFnName: Types.toFunction(`stub${PascalCase}`),
77
79
  repositoryClassName: Types.toClassName(`${PascalCase}Repository`),
80
+ mockRepositoryClassName: Types.toClassName(`Mock${PascalCase}Repository`),
78
81
  dataServiceName: Types.toVariableName(`${uncapitalizedPlural}`),
79
82
  excelExportTableName: `${pluralized}`,
80
83
  repository: {
@@ -1,8 +1,12 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.getFieldAttributes = exports.getModelAttributes = exports.parseArgumentToStringOrStringArray = exports.parseAttributesFromDocumentation = void 0;
4
7
  const remeda_1 = require("remeda");
5
8
  const string_1 = require("../lib/utils/string");
9
+ const zod_1 = __importDefault(require("zod"));
6
10
  /**
7
11
  * Parses attributes from a given string using provided prefix.
8
12
  */
@@ -43,20 +47,48 @@ function parseArgumentToStringOrStringArray(str) {
43
47
  throw new Error(`Could not parse attribute argument: ${str}`);
44
48
  }
45
49
  exports.parseArgumentToStringOrStringArray = parseArgumentToStringOrStringArray;
50
+ const blankStringBooleanDecoder = zod_1.default
51
+ .string()
52
+ .transform((str) => true)
53
+ .or(zod_1.default.boolean())
54
+ .optional()
55
+ .default(false);
46
56
  /**
47
57
  * Returns attribute information for a given model.
48
58
  */
49
59
  function getModelAttributes(model) {
50
60
  const attributes = parseAttributesFromDocumentation(model);
51
- return {
52
- ignore: Object.hasOwn(attributes, 'ignore'),
53
- skipUpdate: Object.hasOwn(attributes, 'skipUpdate') || Object.hasOwn(attributes, 'customUpdate'),
54
- skipCreate: Object.hasOwn(attributes, 'skipCreate') || Object.hasOwn(attributes, 'customCreate'),
55
- skipDelete: Object.hasOwn(attributes, 'skipDelete') || Object.hasOwn(attributes, 'customDelete'),
56
- inMemoryOnly: Object.hasOwn(attributes, 'inMemoryOnly'),
57
- description: attributes.description && (0, remeda_1.isString)(attributes.description) ? attributes.description : undefined,
58
- databaseSchema: attributes.schema && (0, remeda_1.isString)(attributes.schema) ? attributes.schema : undefined,
59
- };
61
+ const decoder = zod_1.default
62
+ .object({
63
+ ignore: blankStringBooleanDecoder,
64
+ skipUpdate: blankStringBooleanDecoder,
65
+ customUpdate: blankStringBooleanDecoder,
66
+ skipCreate: blankStringBooleanDecoder,
67
+ customCreate: blankStringBooleanDecoder,
68
+ skipDelete: blankStringBooleanDecoder,
69
+ customDelete: blankStringBooleanDecoder,
70
+ inMemoryOnly: blankStringBooleanDecoder,
71
+ description: zod_1.default.string().optional(),
72
+ schema: zod_1.default.string().optional(),
73
+ index: zod_1.default.array(zod_1.default.string()).optional(),
74
+ })
75
+ .transform((obj) => ({
76
+ ignore: obj.ignore,
77
+ skipUpdate: obj.skipUpdate || obj.customUpdate,
78
+ skipCreate: obj.skipCreate || obj.customCreate,
79
+ skipDelete: obj.skipDelete || obj.customDelete,
80
+ inMemoryOnly: obj.inMemoryOnly,
81
+ description: obj.description,
82
+ databaseSchema: obj.schema,
83
+ index: obj.index,
84
+ }));
85
+ const result = decoder.safeParse(attributes);
86
+ if (!result.success) {
87
+ throw new Error(`Model ${model.name} has invalid model attributes: ${result.error}`);
88
+ }
89
+ if (result.data.description === 'The calculated metric value for a dataset.') {
90
+ }
91
+ return result.data;
60
92
  }
61
93
  exports.getModelAttributes = getModelAttributes;
62
94
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@postxl/generator",
3
- "version": "0.9.2",
3
+ "version": "0.11.0",
4
4
  "main": "./dist/generator.js",
5
5
  "typings": "./dist/generator.d.ts",
6
6
  "bin": {