@postxl/generator 0.9.1 → 0.10.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, 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
  : `
@@ -99,10 +137,11 @@ export class ${meta.data.repositoryClassName} implements Repository<
99
137
 
100
138
  for (const rawItem of data) {
101
139
  const item = this.${decoder}(rawItem)
102
- this.data.set(item.id, item)
140
+ this.set(item)
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,8 +168,9 @@ 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)) }))
127
172
  return this.init()
173
+ `}
128
174
  }
129
175
 
130
176
  public get(id: ${model.brandedIdType} | null): ${model.typeName} | null {
@@ -142,6 +188,17 @@ export class ${meta.data.repositoryClassName} implements Repository<
142
188
  return Array.from(this.data.values())
143
189
  }
144
190
 
191
+ ${indexes
192
+ .map((i) => `
193
+ public getFrom${indexes.length === 1 ? 'Index' : (0, string_1.toPascalCase)(i.name)}
194
+ ({ ${i.fields.map((f) => f.name).join(',')} }: { ${i.fields
195
+ .map((f) => `${f.name} : ${f.meta.types.brandedIdType}`)
196
+ .join(';')} }): ${model.typeName} | null {
197
+ return this.${i.name}.get(${i.fields.map((f) => f.name).join(',')}) ?? null
198
+ }
199
+ `)
200
+ .join('\n')}
201
+
145
202
  public filter(predicate: (item: ${model.typeName}) => boolean): ${model.typeName}[] {
146
203
  return this.getAllAsArray().filter(predicate)
147
204
  }
@@ -205,7 +262,7 @@ export class ${meta.data.repositoryClassName} implements Repository<
205
262
 
206
263
  ${model.attributes.inMemoryOnly
207
264
  ? `
208
- const newItem = await Promise.resolve({ item, id })
265
+ const newItem = await Promise.resolve({ ...item, id: ${meta.types.toBrandedIdTypeFnName}(id) })
209
266
  `
210
267
  : `
211
268
  const newItem = this.${decoder}(
@@ -398,6 +455,8 @@ export class ${meta.data.repositoryClassName} implements Repository<
398
455
  ${!r.isRequired ? `}` : ''}
399
456
  `)
400
457
  .join('\n')}
458
+
459
+ ${indexes.map((i) => `this.${i.name}.set(item)`).join('\n')}
401
460
  }
402
461
 
403
462
  /**
@@ -420,22 +479,38 @@ export class ${meta.data.repositoryClassName} implements Repository<
420
479
  ${!r.isRequired ? '}' : ''}
421
480
  `)
422
481
  .join('\n')}
482
+
483
+ ${indexes.map((i) => `this.${i.name}.delete(item)`).join('\n')}
423
484
  }
424
485
 
486
+
487
+ ${model.attributes.inMemoryOnly
488
+ ? ''
489
+ : `
425
490
  /**
426
491
  * Utility function that converts a given Database object to a TypeScript model instance
427
492
  */
428
493
  private ${decoder}(item: Pick<DbType, ${[...model.fields.values()]
429
- .map((f) => `'${f.sourceName}'`)
430
- .join(' | ')}>): ${model.typeName} {
494
+ .map((f) => `'${f.sourceName}'`)
495
+ .join(' | ')}>): ${model.typeName} {
431
496
  return ${meta.types.zodDecoderFnName}.parse({
432
497
  ${[...model.fields.values()].map((f) => `${f.name}: item.${f.sourceName}`).join(',\n')}
433
498
  })
434
- }
499
+ }`}
435
500
  }
436
501
  `;
437
502
  }
438
503
  exports.generateRepository = generateRepository;
504
+ /**
505
+ * Generates a mock repository data structure for a given model: same a repository, but in memory only
506
+ */
507
+ function generateMockRepository({ model: modelSource, meta: metaSource, }) {
508
+ // 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
509
+ const meta = Object.assign(Object.assign({}, metaSource), { data: Object.assign(Object.assign({}, metaSource.data), { repositoryClassName: metaSource.data.mockRepositoryClassName, repoFilePath: metaSource.data.mockRepoFilePath }) });
510
+ const model = Object.assign(Object.assign({}, modelSource), { attributes: Object.assign(Object.assign({}, modelSource.attributes), { inMemoryOnly: true }) });
511
+ return generateRepository({ model, meta });
512
+ }
513
+ exports.generateMockRepository = generateMockRepository;
439
514
  function getEnsureUniqueFnName(field) {
440
515
  return `ensureUnique${(0, string_1.toPascalCase)(field.name)}`;
441
516
  }
@@ -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
+ schema: 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.1",
3
+ "version": "0.10.0",
4
4
  "main": "./dist/generator.js",
5
5
  "typings": "./dist/generator.d.ts",
6
6
  "bin": {