@postxl/generator 0.38.1 → 0.39.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.
Files changed (37) hide show
  1. package/dist/generator.js +6 -0
  2. package/dist/generators/enums/types.generator.js +1 -1
  3. package/dist/generators/indices/businesslogic-actiontypes.generator.js +1 -1
  4. package/dist/generators/indices/businesslogic-update-module.generator.js +1 -1
  5. package/dist/generators/indices/businesslogic-update-service.generator.js +1 -1
  6. package/dist/generators/indices/businesslogic-view-module.generator.js +1 -1
  7. package/dist/generators/indices/businesslogic-view-service.generator.js +1 -1
  8. package/dist/generators/indices/datamock-module.generator.js +5 -5
  9. package/dist/generators/indices/datamocker.generator.js +1 -1
  10. package/dist/generators/indices/datamodule.generator.js +30 -27
  11. package/dist/generators/indices/dataservice.generator.js +2 -2
  12. package/dist/generators/indices/dispatcher-service.generator.js +6 -5
  13. package/dist/generators/indices/emptydatabasemigration.generator.d.ts +2 -0
  14. package/dist/generators/indices/emptydatabasemigration.generator.js +14 -7
  15. package/dist/generators/indices/repositories.generator.js +1 -1
  16. package/dist/generators/indices/seed-migration.generator.js +1 -1
  17. package/dist/generators/indices/seed-service.generator.js +11 -13
  18. package/dist/generators/indices/seed-template-decoder.generator.js +1 -1
  19. package/dist/generators/indices/seeddata-type.generator.js +2 -2
  20. package/dist/generators/indices/selectors.generator.d.ts +7 -0
  21. package/dist/generators/indices/selectors.generator.js +82 -0
  22. package/dist/generators/indices/testdata-service.generator.d.ts +9 -0
  23. package/dist/generators/indices/testdata-service.generator.js +70 -0
  24. package/dist/generators/models/businesslogic-update.generator.js +1 -1
  25. package/dist/generators/models/businesslogic-view.generator.js +1 -1
  26. package/dist/generators/models/react.generator/library.generator.js +4 -0
  27. package/dist/generators/models/react.generator/modals.generator.js +35 -8
  28. package/dist/generators/models/repository.generator.js +110 -1
  29. package/dist/generators/models/route.generator.js +2 -2
  30. package/dist/generators/models/stub.generator.js +1 -1
  31. package/dist/generators/models/types.generator.js +1 -1
  32. package/dist/lib/id-collector.d.ts +43 -0
  33. package/dist/lib/id-collector.js +53 -0
  34. package/dist/lib/meta.d.ts +24 -0
  35. package/dist/lib/meta.js +6 -0
  36. package/dist/lib/schema/schema.d.ts +4 -0
  37. package/package.json +2 -2
package/dist/generator.js CHANGED
@@ -60,7 +60,9 @@ const seed_service_generator_1 = require("./generators/indices/seed-service.gene
60
60
  const seed_template_generator_1 = require("./generators/indices/seed-template.generator");
61
61
  const seed_template_decoder_generator_1 = require("./generators/indices/seed-template-decoder.generator");
62
62
  const seeddata_type_generator_1 = require("./generators/indices/seeddata-type.generator");
63
+ const selectors_generator_1 = require("./generators/indices/selectors.generator");
63
64
  const stubs_generator_1 = require("./generators/indices/stubs.generator");
65
+ const testdata_service_generator_1 = require("./generators/indices/testdata-service.generator");
64
66
  const types_generator_2 = require("./generators/indices/types.generator");
65
67
  const businesslogic_update_generator_1 = require("./generators/models/businesslogic-update.generator");
66
68
  const businesslogic_view_generator_1 = require("./generators/models/businesslogic-view.generator");
@@ -81,6 +83,7 @@ const CONFIG_SCHEMA = zod_1.z
81
83
  pathToTypes: zod_1.z.string().optional(),
82
84
  pathToDataLib: zod_1.z.string().optional(),
83
85
  pathToCypress: zod_1.z.string().optional(),
86
+ pathToE2ELib: zod_1.z.string().optional(),
84
87
  pathToActions: zod_1.z.string().optional(),
85
88
  pathToBusinessLogic: zod_1.z.string().optional(),
86
89
  pathToSeedLib: zod_1.z.string().optional(),
@@ -103,6 +106,7 @@ const CONFIG_SCHEMA = zod_1.z
103
106
  paths: {
104
107
  dataLibPath: (0, types_1.toPath)(s.pathToDataLib || 'repos'),
105
108
  cypressPath: (0, types_1.toPath)(s.pathToCypress || './e2e/cypress/'),
109
+ e2eLibPath: (0, types_1.toPath)(s.pathToE2ELib || './e2e/src/'),
106
110
  actionsPath: (0, types_1.toPath)(s.pathToActions || 'actions'),
107
111
  businessLogicPath: (0, types_1.toPath)(s.pathToBusinessLogic || 'business-logic'),
108
112
  reactFolderPath: (0, types_1.toPath)(s.reactFolderOutput || 'react'),
@@ -211,6 +215,8 @@ function generate({ models, enums, config, prismaClientPath, logger, }) {
211
215
  generated.write(`/${meta.data.dataModuleFilePath}.ts`, (0, datamodule_generator_1.generateDataModule)({ models, meta }));
212
216
  generated.write(`/${meta.data.dataServiceFilePath}.ts`, (0, dataservice_generator_1.generateDataService)({ models, meta }));
213
217
  generated.write(`/${meta.data.dataMockerFilePath}.ts`, (0, datamocker_generator_1.generateDataMocker)({ models, meta }));
218
+ generated.write(`/${meta.data.selectorsFilePath}.ts`, (0, selectors_generator_1.generateSelectors)());
219
+ generated.write(`/${meta.data.testDataServiceFilePath}.ts`, (0, testdata_service_generator_1.generateTestDataService)({ models, meta }));
214
220
  generated.write(`/${meta.data.dataMockerStubIndexFilePath}.ts`, (0, stubs_generator_1.generateDataMockerStubsIndex)({ models, meta }));
215
221
  generated.write(`/${meta.data.repositoriesConstFilePath}.ts`, (0, repositories_generator_1.generateRepositoriesArray)({ models, meta }));
216
222
  generated.write(`/${meta.data.repositoriesIndexFilePath}.ts`, (0, repositories_generator_1.generateRepositoriesIndex)({ models, meta }));
@@ -23,7 +23,7 @@ function generateEnumType({ enumerator, meta, prismaClientPath, }) {
23
23
  }
24
24
  return `'${v}'`;
25
25
  });
26
- return `
26
+ return /* ts */ `
27
27
  /* eslint-disable @typescript-eslint/no-unused-vars */
28
28
  import * as Prisma from '${prismaClientPath}'
29
29
 
@@ -21,7 +21,7 @@ function generateBusinessLogicActionTypes({ models, meta }) {
21
21
  actionsTypeElements.push(modelMeta.businessLogic.update.actionName);
22
22
  actionResultTypeElements.push(`${modelMeta.businessLogic.update.actionModelDiscriminantName}: ${modelMeta.businessLogic.update.actionResultName}`);
23
23
  }
24
- return `
24
+ return /* ts */ `
25
25
  ${imports.generate()}
26
26
 
27
27
  /**
@@ -24,7 +24,7 @@ function generateBusinessLogicUpdateModule({ models, meta }) {
24
24
  providers.push(meta.businessLogic.update.serviceClassName);
25
25
  }
26
26
  const moduleName = meta.businessLogic.update.moduleName;
27
- return `
27
+ return /* ts */ `
28
28
  import { DynamicModule } from '@${meta.config.project}/common'
29
29
 
30
30
  ${imports.generate()}
@@ -20,7 +20,7 @@ function generateBusinessLogicUpdateService({ models, meta, }) {
20
20
  const constructor = mm
21
21
  .map(({ meta }) => `@Inject(forwardRef(() => ${meta.businessLogic.update.serviceClassName})) public readonly ${meta.businessLogic.update.serviceVariableName} :${meta.businessLogic.update.serviceClassName}`)
22
22
  .join(',\n');
23
- return `
23
+ return /* ts */ `
24
24
  import { Inject, Injectable, forwardRef } from '@nestjs/common'
25
25
 
26
26
  ${imports.generate()}
@@ -23,7 +23,7 @@ function generateBusinessLogicViewModule({ models, meta }) {
23
23
  providers.push(meta.businessLogic.view.serviceClassName);
24
24
  }
25
25
  const moduleName = meta.businessLogic.view.moduleName;
26
- return `
26
+ return /* ts */ `
27
27
  import { DynamicModule } from '@${meta.config.project}/common'
28
28
 
29
29
  ${imports.generate()}
@@ -20,7 +20,7 @@ function generateBusinessLogicViewService({ models, meta }) {
20
20
  const constructor = mm
21
21
  .map(({ meta }) => `@Inject(forwardRef(() => ${meta.businessLogic.view.serviceClassName})) public readonly ${meta.businessLogic.view.serviceVariableName} :${meta.businessLogic.view.serviceClassName}`)
22
22
  .join(',\n');
23
- return `
23
+ return /* ts */ `
24
24
  import { Inject, Injectable, forwardRef } from '@nestjs/common'
25
25
 
26
26
  ${imports.generate()}
@@ -36,7 +36,7 @@ function generateDataMockModule({ models, meta }) {
36
36
  [meta.data.dataModuleFilePath]: [Types.toVariableName('DataModule')],
37
37
  [meta.data.dataServiceFilePath]: [Types.toVariableName('DataService')],
38
38
  // we need to import the file directly instead via the normal index file as this would cause a circular dependency else
39
- [meta.actions.actionExecutionInterfaceFilePath]: [meta.actions.actionExecutionMock],
39
+ [meta.actions.importPath]: [meta.actions.actionExecutionMock],
40
40
  });
41
41
  for (const { model, meta } of mm) {
42
42
  imports.addImport({ items: [model.typeName], from: meta.types.importPath });
@@ -55,13 +55,13 @@ function generateDataMockModule({ models, meta }) {
55
55
  }
56
56
  }`)
57
57
  .join(', ');
58
- return `
58
+ return /* ts */ `
59
59
  import { DynamicModule } from '@pxl/common'
60
60
  import { DbModule } from '@${meta.config.project}/db'
61
61
 
62
62
  ${imports.generate()}
63
63
 
64
- export class DataMockModule {
64
+ export class ${meta.data.dataMockModuleName} {
65
65
  static mock(seed?: MockData): DynamicModule {
66
66
  const execution = new MockActionExecution()
67
67
 
@@ -71,7 +71,7 @@ export class DataMockModule {
71
71
  ]
72
72
 
73
73
  const cachedModule = {
74
- module: DataModule,
74
+ module: ${meta.data.moduleName},
75
75
  imports: [DbModule.provideMock()],
76
76
  providers: providers,
77
77
  exports: providers,
@@ -84,7 +84,7 @@ export class DataMockModule {
84
84
  }
85
85
  }
86
86
 
87
- export interface MockData {
87
+ export interface ${meta.data.dataMockDataType} {
88
88
  ${mm.map(({ model, meta }) => `${meta.data.mockDataPropertyName}?: ${model.typeName}[]`).join('\n')}
89
89
  }
90
90
  `;
@@ -71,7 +71,7 @@ function generateAddDataFunction(model, meta) {
71
71
  `;
72
72
  });
73
73
  // TODO: Move the publicly accessible function's name to the metadata definition!
74
- return `
74
+ return /* ts */ `
75
75
  public add${model.typeName}(
76
76
  item?: Partial<Omit<${model.typeName}, '${idField.name}'> & { ${idField.name}: ${idField.unbrandedTypeName} }>
77
77
  ): DataMocker {
@@ -34,6 +34,7 @@ function generateDataModule({ models, meta }) {
34
34
  const mm = models.map((model) => ({ model, meta: (0, meta_1.getModelMetadata)({ model }) }));
35
35
  const imports = imports_1.ImportsGenerator.from(meta.data.dataModuleFilePath).addImports({
36
36
  [meta.data.dataServiceFilePath]: [Types.toVariableName('DataService')],
37
+ [meta.data.dataMockModuleFilePath]: [meta.data.dataMockModuleName],
37
38
  });
38
39
  for (const { meta } of mm) {
39
40
  imports.addImport({
@@ -42,9 +43,12 @@ function generateDataModule({ models, meta }) {
42
43
  });
43
44
  }
44
45
  const moduleName = meta.data.moduleName;
45
- return `
46
+ return /* ts */ `
47
+ import { FactoryProvider } from '@nestjs/common'
48
+
46
49
  import { DynamicModule } from '@${meta.config.project}/common'
47
- import { DbModule } from '@${meta.config.project}/db'
50
+ import { DbModule, DbService } from '@${meta.config.project}/db'
51
+ import { E2EConfig } from '@${meta.config.project}/e2e'
48
52
 
49
53
  ${imports.generate()}
50
54
 
@@ -75,47 +79,46 @@ export class ${moduleName} {
75
79
  /**
76
80
  * The forRoot method should only be called once by the root module.
77
81
  */
78
- static forRoot(): DynamicModule {
82
+ static forRoot(options: E2EConfig): DynamicModule {
79
83
  if (${moduleName}.cachedModule) {
80
84
  throw new Error('${moduleName} is already instantiated, please call .forRoot only once from root...')
81
85
  }
82
86
 
83
- const repositoryProviders = [
84
- ${mm.map(({ meta }) => meta.data.repositoryClassName).join(',')}
85
- ]
86
-
87
- ${moduleName}.cachedModule = {
88
- module: ${moduleName},
89
- global: true,
90
- imports: [DbModule.forRoot()],
91
- providers: [DataService, ...repositoryProviders],
92
- exports: [DataService, ...repositoryProviders],
87
+ if (options.isE2ETest && !options.useDatabaseForE2E) {
88
+ return ${meta.data.dataMockModuleName}.mock()
93
89
  }
94
90
 
95
- return ${moduleName}.cachedModule
96
- }
97
-
98
- static provideE2E(): DynamicModule {
99
- if (${moduleName}.cachedModule) {
100
- console.warn('${moduleName} is already instantiated, skipping...')
101
- return ${moduleName}.cachedModule
91
+ // We need to initialize the user repository right at the beginning,
92
+ // so that we have the rootUser available for the other modules - including
93
+ // the action and seed modules which will ensure that seed data is created.
94
+ const userRepository: FactoryProvider<UserRepository> = {
95
+ provide: UserRepository,
96
+ inject: [DbService],
97
+ useFactory: async (dbService: DbService) => {
98
+ const repository = new UserRepository(dbService)
99
+ await repository.init()
100
+ return repository
101
+ },
102
102
  }
103
103
 
104
- const providers = [
105
- ${mm.map(({ meta }) => meta.data.repositoryClassName).join(',')}
104
+ const repositoryProviders = [
105
+ userRepository,
106
+ ${mm
107
+ .filter((mm) => mm.model.name !== 'User')
108
+ .map(({ meta }) => meta.data.repositoryClassName)
109
+ .join(',')}
106
110
  ]
107
-
111
+
108
112
  ${moduleName}.cachedModule = {
109
113
  module: ${moduleName},
110
114
  global: true,
111
115
  imports: [DbModule.forRoot()],
112
- providers,
113
- exports: providers,
116
+ providers: [DataService, ...repositoryProviders],
117
+ exports: [DataService, ...repositoryProviders],
114
118
  }
115
119
 
116
120
  return ${moduleName}.cachedModule
117
121
  }
118
- }
119
- `;
122
+ }`;
120
123
  }
121
124
  exports.generateDataModule = generateDataModule;
@@ -23,7 +23,7 @@ function generateDataService({ models, meta }) {
23
23
  .map(({ meta }) => `${meta.data.excelExportTableName}: mapValues(await this.${meta.data.dataServiceName}.getAll()),`)
24
24
  .join('\n');
25
25
  const isEmptyChecks = mm.map(({ meta }) => `(await this.${meta.data.dataServiceName}.count()) === 0`).join(' &&');
26
- return `
26
+ return /* ts */ `
27
27
  import { Injectable } from '@nestjs/common'
28
28
 
29
29
  import { mapValues } from '@pxl/common'
@@ -31,7 +31,7 @@ import { mapValues } from '@pxl/common'
31
31
  ${imports.generate()}
32
32
 
33
33
  @Injectable()
34
- export class DataService {
34
+ export class ${meta.data.dataServiceClassName} {
35
35
  constructor(${constructor}) {}
36
36
 
37
37
  public async prepareExcelExport() {
@@ -17,7 +17,7 @@ function generateActionsDispatcherService({ models, meta }) {
17
17
  const modelMeta = (0, meta_1.getModelMetadata)({ model });
18
18
  dataLoader.push(`await this.upload({ name: '${modelMeta.userFriendlyName}', data: data.${modelMeta.seed.constantName}, repo: this.dataService.${modelMeta.data.dataServiceName}, log })`);
19
19
  }
20
- return `
20
+ return /* ts */ `
21
21
  import { Injectable } from '@nestjs/common'
22
22
 
23
23
  import { ExhaustiveSwitchCheck } from '@pxl/common'
@@ -25,7 +25,7 @@ import { DbService } from '@pxl/db'
25
25
 
26
26
  ${imports.generate()}
27
27
 
28
- import { ActionExecution, createActionExecution } from './actionExecution.class'
28
+ import { ActionExecutionFactory, IActionExecution } from './actionExecution'
29
29
  import { Action, ResultOfAction } from './actions.types'
30
30
 
31
31
 
@@ -37,14 +37,15 @@ export class DispatcherService {
37
37
  public seedService: ${meta.seed.serviceClassName} = {} as unknown as any
38
38
  constructor(
39
39
  private readonly updateService: ${meta.businessLogic.update.serviceClassName},
40
- private readonly dbService: DbService
40
+ private readonly dbService: DbService,
41
+ private readonly actionExecutionFactory: ActionExecutionFactory,
41
42
  ) {}
42
43
 
43
44
  public async dispatch<A extends Action>({ action, user }: {
44
45
  action: A;
45
46
  user: ${meta.config.userType}
46
47
  }): Promise<ResultOfAction<A>> {
47
- const execution = await createActionExecution({ action, dbService: this.dbService, user })
48
+ const execution = await this.actionExecutionFactory.create({ action, dbService: this.dbService, user })
48
49
 
49
50
  try {
50
51
  const result = await (this.execute({ action, execution }) as Promise<ResultOfAction<A>>)
@@ -58,7 +59,7 @@ export class DispatcherService {
58
59
  }
59
60
  }
60
61
 
61
- private async execute({ action, execution }: { action: Action; execution: ActionExecution }) {
62
+ private async execute({ action, execution }: { action: Action; execution: IActionExecution }) {
62
63
  switch (action.scope) {
63
64
  ${models
64
65
  .map((model) => {
@@ -2,6 +2,8 @@ import { SchemaMetaData } from '../../lib/meta';
2
2
  import { Model } from '../../lib/schema/schema';
3
3
  /**
4
4
  * Generates a the Prisma migration to create the `emptyDatabase` stored procedure.
5
+ *
6
+ * The routine is used in e2e tests to empty the database before each test.
5
7
  */
6
8
  export declare function generateEmptyDatabaseStoredProcedure({ models }: {
7
9
  models: Model[];
@@ -7,13 +7,20 @@ exports.prismaMigrationExists = exports.deriveEmptyDatabaseMigrationFilePath = e
7
7
  const fs_1 = __importDefault(require("fs"));
8
8
  /**
9
9
  * Generates a the Prisma migration to create the `emptyDatabase` stored procedure.
10
+ *
11
+ * The routine is used in e2e tests to empty the database before each test.
10
12
  */
11
13
  function generateEmptyDatabaseStoredProcedure({ models }) {
12
- const tables = models
13
- .map((model) => `\t${model.sourceSchemaName !== undefined ? `"${model.sourceSchemaName}".` : ''}"${model.sourceName}"`)
14
- .join(',\n');
15
- const useSchemas = models.some((model) => model.sourceSchemaName !== undefined);
16
- const configTableName = useSchemas ? ' "Configuration"."Config"' : '"Config"';
14
+ const modelTables = models
15
+ .map((model) => `\t${model.sourceSchemaName !== undefined ? `"${model.sourceSchemaName}".` : ''}"${model.sourceName}"`);
16
+ // We determine the schema used for all system tables by looking at the User model's schema.
17
+ const userModel = models.find((model) => model.name === 'User');
18
+ if (!userModel) {
19
+ throw new Error('Model definition for "User" could not be found - hence schema for system table cannot be derived!');
20
+ }
21
+ const configSchema = userModel.sourceSchemaName !== undefined ? `"${userModel.sourceSchemaName}".` : '';
22
+ const systemTables = ["Action", "Mutation"].map((table) => `\t${configSchema}"${table}"`);
23
+ const configTableName = `${configSchema}"Config"`;
17
24
  return `
18
25
  CREATE OR REPLACE PROCEDURE "emptyDatabase"() AS $BODY$ BEGIN IF EXISTS (
19
26
  SELECT
@@ -22,8 +29,8 @@ CREATE OR REPLACE PROCEDURE "emptyDatabase"() AS $BODY$ BEGIN IF EXISTS (
22
29
  ) THEN RETURN;
23
30
  END IF;
24
31
  TRUNCATE TABLE
25
- ${tables};
26
- UPDATE ${configTableName} SET "isSeeded"=false;
32
+ ${[...systemTables, ...modelTables].join(',\n')};
33
+
27
34
  END;
28
35
  $BODY$ LANGUAGE plpgsql;
29
36
  `;
@@ -29,7 +29,7 @@ function generateRepositoriesArray({ models, meta }) {
29
29
  from: meta.data.repoFilePath,
30
30
  });
31
31
  }
32
- return `
32
+ return /* ts */ `
33
33
  ${imports.generate()}
34
34
 
35
35
  export const repositories = [${models
@@ -17,7 +17,7 @@ function generateSeedMigration({ models, meta }) {
17
17
  });
18
18
  modelTypes.push(`${modelMeta.seed.constantName}: { create: ${modelMeta.seed.constantName} }`);
19
19
  }
20
- return `
20
+ return /* ts */ `
21
21
  import { createActionSeedData } from '${meta.seed.importPath}'
22
22
  ${imports.generate()}
23
23
 
@@ -17,15 +17,14 @@ function generateSeedService({ models }) {
17
17
  upserts.push(`await this.upsert({ name: '${modelMeta.userFriendlyName}', data: data.${modelMeta.seed.constantName}?.upsert, repo: this.dataService.${modelMeta.data.dataServiceName}, execution })`);
18
18
  deletes.push(`await this.delete({ name: '${modelMeta.userFriendlyName}', data: data.${modelMeta.seed.constantName}?.delete, repo: this.dataService.${modelMeta.data.dataServiceName}, execution })`);
19
19
  }
20
- return `
20
+ return /* ts */ `
21
21
  import { Injectable, Logger } from '@nestjs/common'
22
22
  import { ExhaustiveSwitchCheck, format, pluralize } from '@pxl/common'
23
23
  import { DataService, Repository } from '@pxl/data'
24
24
  import { DbService } from '@pxl/db'
25
25
  import { XlPortService } from '@pxl/xlport'
26
26
 
27
- import { ActionExecution, DispatcherService } from '@pxl/actions'
28
- import { RootUserService } from '@pxl/root-user'
27
+ import { IActionExecution, DispatcherService } from '@pxl/actions'
29
28
 
30
29
  import {
31
30
  ActionPreparation_Seed,
@@ -53,7 +52,6 @@ export class SeedService {
53
52
  private readonly xlPortService: XlPortService,
54
53
  private readonly dbService: DbService,
55
54
  private readonly dispatcherService: DispatcherService,
56
- private readonly rootUserService: RootUserService,
57
55
  ) {
58
56
  dispatcherService.seedService = this
59
57
  }
@@ -79,7 +77,7 @@ export class SeedService {
79
77
  const action = await this.convertToAction(migration)
80
78
 
81
79
  this.logger.log(\`Executing seed migration \${migration.order} - \${migration.name}\`)
82
- await this.dispatcherService.dispatch({ action, user: this.rootUserService.rootUser })
80
+ await this.dispatcherService.dispatch({ action, user: this.dataService.users.rootUser })
83
81
 
84
82
  executed++
85
83
  }
@@ -117,7 +115,7 @@ export class SeedService {
117
115
  /**
118
116
  * Will be call by the dispatcher service to dispatch the given action.
119
117
  */
120
- public async dispatch({ action, execution }: { action: Action_Seed; execution: ActionExecution }) {
118
+ public async dispatch({ action, execution }: { action: Action_Seed; execution: IActionExecution }) {
121
119
  switch (action.type) {
122
120
  case 'data':
123
121
  return this.uploadDataSteps({ steps: action.payload, execution })
@@ -175,7 +173,7 @@ export class SeedService {
175
173
  * Executes the custom logic of the given migration.
176
174
  * To do this, we retrieve the handler from the migration and call it, injecting the data service into the handler.
177
175
  */
178
- private handleCustomLogic({ action, execution }: { action: Action_Seed_CustomLogic; execution: ActionExecution }) {
176
+ private handleCustomLogic({ action, execution }: { action: Action_Seed_CustomLogic; execution: IActionExecution }) {
179
177
  const actionPreparation = SEED_MIGRATIONS.find((m) => m.order === action.order)
180
178
  if (!actionPreparation) {
181
179
  throw new Error(\`Cannot find custom handler for migration with order \${action.order}\`)
@@ -188,7 +186,7 @@ export class SeedService {
188
186
  /**
189
187
  * Executes the data loading step by step
190
188
  */
191
- private async uploadDataSteps({ steps, execution }: { steps: SeedData[]; execution: ActionExecution }) {
189
+ private async uploadDataSteps({ steps, execution }: { steps: SeedData[]; execution: IActionExecution }) {
192
190
  let index = 0
193
191
  for (const step of steps) {
194
192
  this.logger.log(\`Uploading data step \${++index}/\${steps.length}\`)
@@ -196,7 +194,7 @@ export class SeedService {
196
194
  }
197
195
  }
198
196
 
199
- private async uploadData({ data, execution }: { data: SeedData; execution: ActionExecution }) {
197
+ private async uploadData({ data, execution }: { data: SeedData; execution: IActionExecution }) {
200
198
  // NOTE: the order of these calls is important, because of foreign key constraints
201
199
  // The current order is based on the order of the models in the schema
202
200
  // Change the order based on your needs.
@@ -221,7 +219,7 @@ export class SeedService {
221
219
  name: string
222
220
  data: CreateDTO<T, ID>[] | undefined
223
221
  repo: Repository<T, ID>
224
- execution: ActionExecution
222
+ execution: IActionExecution
225
223
  }): Promise<void> {
226
224
  if (!data) {
227
225
  return
@@ -253,7 +251,7 @@ export class SeedService {
253
251
  name: string
254
252
  data: UpdateDTO<T, ID>[] | undefined
255
253
  repo: Repository<T, ID>
256
- execution: ActionExecution
254
+ execution: IActionExecution
257
255
  }): Promise<void> {
258
256
  if (!data) {
259
257
  return
@@ -285,7 +283,7 @@ export class SeedService {
285
283
  name: string
286
284
  data: (CreateDTO<T, ID> | UpdateDTO<T, ID>)[] | undefined
287
285
  repo: Repository<T, ID>
288
- execution: ActionExecution
286
+ execution: IActionExecution
289
287
  }): Promise<void> {
290
288
  if (!data) {
291
289
  return
@@ -317,7 +315,7 @@ export class SeedService {
317
315
  name: string
318
316
  data: ID[] | undefined
319
317
  repo: Repository<T, ID>
320
- execution: ActionExecution
318
+ execution: IActionExecution
321
319
  }): Promise<void> {
322
320
  if (!data) {
323
321
  return
@@ -22,7 +22,7 @@ function generateSeedTemplateDecoder({ models, meta }) {
22
22
  tableDecoders.push(`${modelMeta.seed.excel.tableName}: ${modelMeta.seed.decoder.decoderName}`);
23
23
  renameTransforms.push(`${modelMeta.seed.decoder.dataName}: item['${modelMeta.seed.excel.tableName}']`);
24
24
  }
25
- return `
25
+ return /* ts */ `
26
26
  import * as z from 'zod'
27
27
 
28
28
  ${imports.generate()}
@@ -26,14 +26,14 @@ function generateSeedDataType({ models, meta }) {
26
26
  }`);
27
27
  mockConverters.push(`${modelMeta.seed.constantName}: { create: data.${modelMeta.seed.constantName} }`);
28
28
  }
29
- return `
29
+ return /* ts */ `
30
30
  ${imports.generate()}
31
31
 
32
32
  export type SeedData = {
33
33
  ${modelTypes.join('\n')}
34
34
  }
35
35
 
36
- export function mockDataToCreate(data: MockData): SeedData {
36
+ export function ${meta.seed.mockDataToCreateFunction}(data: ${meta.data.dataMockDataType}): SeedData {
37
37
  return {
38
38
  ${mockConverters.join(',\n')}
39
39
  }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Generates list of component selectors for Cypress tests.
3
+ *
4
+ * Note: This generator does not need the models or meta data passed in.
5
+ * Instead it uses the SelectorCollector singleton that already collected all ids during the generation process of the individual models.
6
+ */
7
+ export declare function generateSelectors(): string;
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.generateSelectors = void 0;
4
+ const id_collector_1 = require("../../lib/id-collector");
5
+ /**
6
+ * List of ids that are not generated but hardcoded across the template.
7
+ */
8
+ const HARDCODED_IDS = [
9
+ 'indexPage-buttons-create',
10
+ 'confirmationModal-buttons-confirm',
11
+ 'confirmationModal-buttons-cancel',
12
+ ];
13
+ /**
14
+ * Generates list of component selectors for Cypress tests.
15
+ *
16
+ * Note: This generator does not need the models or meta data passed in.
17
+ * Instead it uses the SelectorCollector singleton that already collected all ids during the generation process of the individual models.
18
+ */
19
+ function generateSelectors() {
20
+ // SelectorCollector is a static singleton that collects all ids during the generation process. We flush it here to get all ids.
21
+ const collectedIds = id_collector_1.SelectorCollector.flush();
22
+ const object = {};
23
+ const ids = [...HARDCODED_IDS, ...collectedIds];
24
+ ids.sort();
25
+ ids.map((id) => {
26
+ addCypressSelectorToNestedObject({ object, id });
27
+ });
28
+ const selectors = JSON.stringify(object, null, 2);
29
+ return /* ts */ `
30
+ export const SELECTORS =
31
+ ${selectors}
32
+ `;
33
+ }
34
+ exports.generateSelectors = generateSelectors;
35
+ /**
36
+ * Converts an id like `post-create-name` to a nested object like `{post: {create: {name: '[data-cy=post-create-name]'}}}`.
37
+ */
38
+ function addCypressSelectorToNestedObject({ object, id, }) {
39
+ const keys = id.split('-');
40
+ extendObject(object, keys, `[data-cy=${id}]`);
41
+ }
42
+ /**
43
+ * Recursively traverses the keys and adds the value to the object in a nested way.
44
+ * E.g. `{object, keys: ['post', 'create', 'name'], value}` will extend `object` with `{post: {create: {name: value}}}`.
45
+ *
46
+ */
47
+ function extendObject(object, keys, value) {
48
+ const key = keys.shift();
49
+ // no item left -> we are at the end of the recursion
50
+ if (!key) {
51
+ return;
52
+ }
53
+ // create the nested object if it does not exist yet
54
+ object[key] = object[key] || {};
55
+ // In case the object already has a value, we check if it is a string (and not a nested object).
56
+ // If it is a string, it we check if the value is the same as the new value. In this case is is a duplicate and hence fine.
57
+ // However, if it is a string and a different value, it means we have an inconsistent ID structure.
58
+ // E.g. this can happen when we have three entries: "post-create", "post-create-name", "post-create-description".
59
+ // In this case, the "post-create" entry will be a string - and we cannot nest `{name: ..., description: ...}` in it.
60
+ if (key in object && typeof object[key] === 'string') {
61
+ if (keys.length !== 0) {
62
+ throw new Error(`Key ${key} already exists with value: ${object[key]} but we are trying to nest ${keys.join('-')}`);
63
+ }
64
+ if (object[key] !== value) {
65
+ throw new Error(`Duplicate key ${key} with different values: ${object[key]} and ${value}`);
66
+ }
67
+ return;
68
+ }
69
+ // If there are still keys left, we recursively call this function with the nested object and the remaining keys.
70
+ if (keys.length !== 0) {
71
+ extendObject(object[key], keys, value);
72
+ return;
73
+ }
74
+ // If there are no keys left, we need to verify that the object might not already have a nested object.
75
+ // This can happen, e.g. when we have two entries: "post-create-name" and "post-create".
76
+ // In this case, the `post.create` entry will be a nested object`{name: ...}`.
77
+ // If we would set `post.create` to a string, we would lose the nested object.
78
+ if (key in object && typeof object[key] !== 'string' && Object.keys(object[key]).length !== 0) {
79
+ throw new Error(`Key ${key} already has nested object ${JSON.stringify(object[key])} but we are trying to set it to ${value}`);
80
+ }
81
+ object[key] = value;
82
+ }
@@ -0,0 +1,9 @@
1
+ import { SchemaMetaData } from '../../lib/meta';
2
+ import { Model } from '../../lib/schema/schema';
3
+ /**
4
+ * Generates the TestDataService class.
5
+ */
6
+ export declare function generateTestDataService({ models, meta: schemaMeta, }: {
7
+ models: Model[];
8
+ meta: SchemaMetaData;
9
+ }): string;
@@ -0,0 +1,70 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.generateTestDataService = void 0;
4
+ const imports_1 = require("../../lib/imports");
5
+ const meta_1 = require("../../lib/meta");
6
+ const types_1 = require("../../lib/schema/types");
7
+ /**
8
+ * Generates the TestDataService class.
9
+ */
10
+ function generateTestDataService({ models, meta: schemaMeta, }) {
11
+ const imports = imports_1.ImportsGenerator.from(schemaMeta.data.testDataServiceFilePath);
12
+ imports.addImports({
13
+ [schemaMeta.actions.importPath]: [(0, types_1.toClassName)('ActionExecutionFactory')],
14
+ [schemaMeta.data.importPath]: [schemaMeta.data.dataServiceClassName, schemaMeta.data.dataMockDataType],
15
+ [schemaMeta.seed.importPath]: [(0, types_1.toClassName)('createActionSeedData'), schemaMeta.seed.mockDataToCreateFunction],
16
+ });
17
+ const reInitCalls = [];
18
+ const modelMetas = models.map((model) => ({ model, meta: (0, meta_1.getModelMetadata)({ model }) }));
19
+ for (const { model, meta } of modelMetas) {
20
+ if (model.defaultField) {
21
+ imports.addImport({ items: [meta.data.stubGenerationFnName], from: meta.data.importPath });
22
+ const stubDefault = `${meta.data.stubGenerationFnName}({ ${model.defaultField.name}: true })`;
23
+ reInitCalls.push(`await this.dataService.${meta.data.dataServiceName}.reInit({ items: mockData.${meta.seed.constantName}?.create ?? [${stubDefault}], execution: actionExecution })`);
24
+ }
25
+ else {
26
+ reInitCalls.push(`await this.dataService.${meta.data.dataServiceName}.reInit({ items: mockData.${meta.seed.constantName}?.create ?? [], execution: actionExecution })`);
27
+ }
28
+ }
29
+ return /* ts */ `
30
+ import { Injectable, Logger } from '@nestjs/common'
31
+
32
+ import { DbService } from '@pxl/db'
33
+ ${imports.generate()}
34
+
35
+ @Injectable()
36
+ export class TestDataService {
37
+ private logger = new Logger(TestDataService.name)
38
+
39
+ constructor(
40
+ private db: DbService,
41
+ private dataService: ${schemaMeta.data.dataServiceClassName},
42
+ private actionExecutionFactory: ActionExecutionFactory,
43
+ ) {}
44
+
45
+ public async resetTestData(data: MockData) {
46
+ await this.db.emptyDatabase()
47
+
48
+ // We need to init the user repository first so the root user is created
49
+ await this.dataService.users.init()
50
+
51
+ const mockData = ${schemaMeta.seed.mockDataToCreateFunction}(data)
52
+ const action = createActionSeedData({ name: 'E2E', order: 0, data: [] })
53
+ const actionExecution = await this.actionExecutionFactory.create({
54
+ action,
55
+ dbService: this.db,
56
+ user: this.dataService.users.rootUser,
57
+ })
58
+ try {
59
+ ${reInitCalls.join('\n')}
60
+
61
+ await actionExecution.success()
62
+ this.logger.log(\`✅ Reset test data\`)
63
+ } catch (e) {
64
+ await actionExecution.error(e)
65
+ throw e
66
+ }
67
+ }
68
+ }`;
69
+ }
70
+ exports.generateTestDataService = generateTestDataService;
@@ -41,7 +41,7 @@ function generateModelBusinessLogicUpdate({ model, meta }) {
41
41
  `@Inject(forwardRef(() => ${schemaMeta.businessLogic.view.serviceClassName})) private readonly ${viewServiceClassName}: ${schemaMeta.businessLogic.view.serviceClassName}`,
42
42
  ];
43
43
  const methodTypeSignatures = (0, repository_generator_1.getRepositoryMethodsTypeSignatures)({ model, meta });
44
- return `
44
+ return /* ts */ `
45
45
  import { Inject, Injectable, forwardRef } from '@nestjs/common'
46
46
  import { ExhaustiveSwitchCheck } from '@pxl/common'
47
47
 
@@ -92,7 +92,7 @@ function generateModelBusinessLogicView({ model, meta }) {
92
92
  return item
93
93
  }
94
94
  `;
95
- return `
95
+ return /* ts */ `
96
96
  /* eslint-disable @typescript-eslint/no-unused-vars */
97
97
  import { Inject, Injectable, forwardRef } from '@nestjs/common'
98
98
  import { FilterOperator } from '@pxl/common'
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.generateModelLibraryComponents = void 0;
4
+ const id_collector_1 = require("../../../lib/id-collector");
4
5
  const imports_1 = require("../../../lib/imports");
5
6
  /**
6
7
  * Generates components that may be used to list all entries of a given data type.
@@ -10,6 +11,7 @@ const imports_1 = require("../../../lib/imports");
10
11
  */
11
12
  function generateModelLibraryComponents({ model, meta }) {
12
13
  const { react: { context, components }, } = meta;
14
+ const selectorCollector = id_collector_1.SelectorCollector.from(meta.seed.constantName + '-card');
13
15
  const imports = imports_1.ImportsGenerator.from(meta.react.folderPath)
14
16
  .addImport({
15
17
  items: [model.typeName],
@@ -58,11 +60,13 @@ function generateModelLibraryComponents({ model, meta }) {
58
60
  label: 'Edit',
59
61
  icon: 'pencil-on-paper',
60
62
  onClick: () => setIsEditModalOpen(true),
63
+ __cypress_action_selector__: "${selectorCollector.idFor('edit', { typePrefix: 'actions' })}",
61
64
  },
62
65
  {
63
66
  label: 'Delete',
64
67
  icon: 'trash',
65
68
  onClick: () => setIsDeleteModalOpen(true),
69
+ __cypress_action_selector__: "${selectorCollector.idFor('delete', { typePrefix: 'actions' })}",
66
70
  },
67
71
  ]}
68
72
  />
@@ -24,6 +24,7 @@ var __importStar = (this && this.__importStar) || function (mod) {
24
24
  };
25
25
  Object.defineProperty(exports, "__esModule", { value: true });
26
26
  exports.generateDeleteModalModelComponent = exports.generateEditModalModelComponent = exports.generateModelCreateModalComponent = void 0;
27
+ const id_collector_1 = require("../../../lib/id-collector");
27
28
  const imports_1 = require("../../../lib/imports");
28
29
  const meta_1 = require("../../../lib/meta");
29
30
  const fields_1 = require("../../../lib/schema/fields");
@@ -36,6 +37,7 @@ const StringUtils = __importStar(require("../../../lib/utils/string"));
36
37
  function generateModelCreateModalComponent({ model, meta }) {
37
38
  const { fields } = model;
38
39
  const { react: { components: { modals }, }, trpc, } = meta;
40
+ const selectorCollector = id_collector_1.SelectorCollector.from(meta.seed.constantName + '-createModal');
39
41
  return `
40
42
  /* eslint-disable @typescript-eslint/no-unused-vars */
41
43
  ${getFormImports({ model, meta })}
@@ -130,9 +132,10 @@ export const ${modals.createComponentName} = ({ show, onHide }: { show: boolean;
130
132
  fill="fill"
131
133
  onClick={submitForm}
132
134
  loading={isSubmitting}
135
+ __cypress_selector__="${selectorCollector.idFor('submit', { typePrefix: 'buttons' })}"
133
136
  />
134
137
  }>
135
- ${getFormFieldComponents({ model })}
138
+ ${getFormFieldComponents({ model, selectorCollector })}
136
139
  </ModalWithActions>
137
140
  )}
138
141
  </Typed.Formik>
@@ -149,6 +152,7 @@ exports.generateModelCreateModalComponent = generateModelCreateModalComponent;
149
152
  function generateEditModalModelComponent({ model, meta }) {
150
153
  const { fields } = model;
151
154
  const { react: { components }, trpc, } = meta;
155
+ const selectorCollector = id_collector_1.SelectorCollector.from(meta.seed.constantName + '-editModal');
152
156
  return `
153
157
  /* eslint-disable @typescript-eslint/no-unused-vars */
154
158
  ${getFormImports({ model, meta })}
@@ -257,10 +261,11 @@ export const ${components.modals.editComponentName} = ({
257
261
  color="primary"
258
262
  onClick={submitForm}
259
263
  loading={isSubmitting}
264
+ __cypress_selector__="${selectorCollector.idFor('submit', { typePrefix: 'buttons' })}"
260
265
  />
261
266
  }
262
267
  >
263
- ${getFormFieldComponents({ model })}
268
+ ${getFormFieldComponents({ model, selectorCollector })}
264
269
  </ModalWithActions>
265
270
  )}
266
271
  </Typed.Formik>
@@ -528,7 +533,7 @@ function getEditFormikMutationData({ model: { fields } }) {
528
533
  /**
529
534
  * Returns a string containing all the components that should appear in the Formik form for this model.
530
535
  */
531
- function getFormFieldComponents({ model }) {
536
+ function getFormFieldComponents({ model, selectorCollector, }) {
532
537
  var _a;
533
538
  const form = new serializer_1.Serializer();
534
539
  for (const field of model.fields.values()) {
@@ -550,7 +555,11 @@ function getFormFieldComponents({ model }) {
550
555
  form.append(`
551
556
  <div>
552
557
  <Label>${label}</Label>
553
- <Typed.TextField placeholder="Type..." name="${formikFieldName}" />
558
+ <Typed.TextField
559
+ name="${formikFieldName}"
560
+ placeholder="Type..."
561
+ __cypress_field_selector__="${selectorCollector.idFor(field.name, { typePrefix: 'fields' })}"
562
+ />
554
563
  </div>
555
564
  `);
556
565
  break scalar;
@@ -563,7 +572,12 @@ function getFormFieldComponents({ model }) {
563
572
  form.append(`
564
573
  <div>
565
574
  <Label>${label}</Label>
566
- <Typed.NumberField placeholder="2511" name="${formikFieldName}" decimals={${decimals}}/>
575
+ <Typed.NumberField
576
+ name="${formikFieldName}"
577
+ placeholder="2511"
578
+ decimals={${decimals}}
579
+ __cypress_field_selector__="${selectorCollector.idFor(field.name, { typePrefix: 'fields' })}"
580
+ />
567
581
  </div>
568
582
  `);
569
583
  break scalar;
@@ -572,7 +586,11 @@ function getFormFieldComponents({ model }) {
572
586
  form.append(`
573
587
  <div>
574
588
  <Label>Is ${label}</Label>
575
- <Typed.CheckBoxField label="${label}" name="${formikFieldName}" />
589
+ <Typed.CheckBoxField
590
+ name="${formikFieldName}"
591
+ label="${label}"
592
+ __cypress_field_selector__="${selectorCollector.idFor(field.name, { typePrefix: 'fields' })}"
593
+ />
576
594
  </div>
577
595
  `);
578
596
  break scalar;
@@ -593,7 +611,12 @@ function getFormFieldComponents({ model }) {
593
611
  form.append(`
594
612
  <div>
595
613
  <Label>${refMeta.userFriendlyName}</Label>
596
- <Typed.${refMeta.react.components.forms.searchFieldName} name="${formikFieldName}" placeholder="Search..." />
614
+ <Typed.${refMeta.react.components.forms.searchFieldName}
615
+ name="${formikFieldName}"
616
+ placeholder="Search..."
617
+ __cypress_options_selector__="${selectorCollector.idFor(field.name, { typePrefix: 'options' })}"
618
+ __cypress_combobox_selector__="${selectorCollector.idFor(field.name, { typePrefix: 'fields' })}"
619
+ />
597
620
  </div>
598
621
  `);
599
622
  break;
@@ -603,7 +626,11 @@ function getFormFieldComponents({ model }) {
603
626
  form.append(`
604
627
  <div>
605
628
  <Label>${label}</Label>
606
- <Typed.${enumMeta.react.selectFieldName} name="${formikFieldName}" placeholder="Search..." />
629
+ <Typed.${enumMeta.react.selectFieldName}
630
+ name="${formikFieldName}"
631
+ placeholder="Search..."
632
+ __cypress_field_selector__="${selectorCollector.idFor(field.name, { typePrefix: 'fields' })}"
633
+ />
607
634
  </div>
608
635
  `);
609
636
  break;
@@ -55,6 +55,7 @@ function generateRepository({ model, meta }) {
55
55
  model,
56
56
  meta,
57
57
  schemaMeta,
58
+ imports,
58
59
  blocks: {
59
60
  uniqueStringFieldsBlocks,
60
61
  defaultValueBlocks,
@@ -101,6 +102,10 @@ export class ${meta.data.repositoryClassName} implements Repository<${model.type
101
102
 
102
103
  ${indexBlocks.nestedMapDeclarations.join('\n')}
103
104
 
105
+ ${mainBlocks.userRepositorySpecificBlocks.rootUserNameConst}
106
+
107
+ ${mainBlocks.userRepositorySpecificBlocks.getterBlock}
108
+
104
109
  ${mainBlocks.constructorCode}
105
110
 
106
111
  ${mainBlocks.initCode}
@@ -109,6 +114,8 @@ export class ${meta.data.repositoryClassName} implements Repository<${model.type
109
114
 
110
115
  ${mainBlocks.deleteAllCode}
111
116
 
117
+ ${mainBlocks.userRepositorySpecificBlocks.rootUserInitializeBlock}
118
+
112
119
  public async get(id: ${model.brandedIdType} | null): Promise<${model.typeName} | null> {
113
120
  if (id === null) {
114
121
  return Promise.resolve(null)
@@ -205,10 +212,12 @@ exports.generateMockRepository = generateMockRepository;
205
212
  /**
206
213
  * Generates the main building blocks of the repository for in-memory model.
207
214
  */
208
- function _generateMainBuildingBlocks_InMemoryOnly({ model, meta, blocks, }) {
215
+ function _generateMainBuildingBlocks_InMemoryOnly({ model, meta, imports, blocks, }) {
209
216
  const methodTypeSignatures = getRepositoryMethodsTypeSignatures({ model, meta });
217
+ const userRepositorySpecificBlocks = generateUserRepositorySpecificBlocks_InMemoryOnly({ model, meta, imports });
210
218
  return {
211
219
  constructorCode: '',
220
+ userRepositorySpecificBlocks,
212
221
  initCode: `
213
222
  public async init() {
214
223
  this.data.clear()
@@ -218,6 +227,8 @@ function _generateMainBuildingBlocks_InMemoryOnly({ model, meta, blocks, }) {
218
227
 
219
228
  ${blocks.indexBlocks.initCode.join('\n')}
220
229
 
230
+ ${userRepositorySpecificBlocks.initCall}
231
+
221
232
  return Promise.resolve()
222
233
  }`,
223
234
  reInitCode: `
@@ -446,8 +457,10 @@ function generateMainBuildingBlocks_InDatabase({ model, meta, imports, blocks, }
446
457
  .map((part) => `"${part}"`)
447
458
  .join('.');
448
459
  const methodTypeSignatures = getRepositoryMethodsTypeSignatures({ model, meta });
460
+ const userRepositorySpecificBlocks = generateUserRepositorySpecificBlocks_InDatabase({ model, meta, imports });
449
461
  return {
450
462
  constructorCode: `constructor(protected db: DbService) {}`,
463
+ userRepositorySpecificBlocks,
451
464
  initCode: `
452
465
  public async init() {
453
466
  this.data.clear()
@@ -471,6 +484,8 @@ function generateMainBuildingBlocks_InDatabase({ model, meta, imports, blocks, }
471
484
 
472
485
  ${blocks.defaultValueBlocks.init.checkCode}
473
486
 
487
+ ${userRepositorySpecificBlocks.initCall}
488
+
474
489
  this.logger.log(\`\${format(this.data.size)} \${pluralize('${model.typeName}', this.data.size)} loaded\`)
475
490
  ${blocks.indexBlocks.initLogCode.join('\n')}
476
491
  }`,
@@ -741,6 +756,100 @@ function generateMainBuildingBlocks_InDatabase({ model, meta, imports, blocks, }
741
756
  }`,
742
757
  };
743
758
  }
759
+ function generateUserRepositorySpecificBlocks_InDatabase({ model, meta, imports, }) {
760
+ if (model.name !== 'User') {
761
+ return {
762
+ rootUserNameConst: '',
763
+ getterBlock: '',
764
+ initCall: '',
765
+ rootUserInitializeBlock: '',
766
+ };
767
+ }
768
+ imports.addImport({
769
+ from: meta.types.importPath,
770
+ items: [(0, types_1.toTypeName)('UserRole')],
771
+ });
772
+ return {
773
+ rootUserNameConst: `public static ROOT_USER_ID = ${meta.types.toBrandedIdTypeFnName}('root')`,
774
+ getterBlock: `
775
+ // We initialize the root user in the init() function
776
+ private _rootUser!: ${meta.types.typeName}
777
+ public get rootUser(): ${meta.types.typeName} {
778
+ return this._rootUser
779
+ }`,
780
+ initCall: `await this.initializeRootUser()`,
781
+ rootUserInitializeBlock: `
782
+ private async initializeRootUser(): Promise<void> {
783
+ const existingRootUser = await this.get(${meta.data.repositoryClassName}.ROOT_USER_ID)
784
+ if (existingRootUser) {
785
+ this._rootUser = existingRootUser
786
+ return
787
+ }
788
+
789
+ const rawUser = await this.db.user.create({
790
+ data: {
791
+ id: ${meta.data.repositoryClassName}.ROOT_USER_ID,
792
+ name: 'System',
793
+ email: 'system@postxl.com',
794
+ role: UserRole.Admin,
795
+ login: 'not-set',
796
+ familyName: 'System user',
797
+ age: 0,
798
+ countryId: null,
799
+ },
800
+ })
801
+ const newRootUser = this.toUser(rawUser)
802
+ this.set(newRootUser)
803
+ this._rootUser = newRootUser
804
+ }`,
805
+ };
806
+ }
807
+ function generateUserRepositorySpecificBlocks_InMemoryOnly({ model, meta, imports, }) {
808
+ if (model.name !== 'User') {
809
+ return {
810
+ rootUserNameConst: '',
811
+ getterBlock: '',
812
+ initCall: '',
813
+ rootUserInitializeBlock: '',
814
+ };
815
+ }
816
+ imports.addImport({
817
+ from: meta.types.importPath,
818
+ items: [(0, types_1.toTypeName)('UserRole')],
819
+ });
820
+ return {
821
+ rootUserNameConst: `public static ROOT_USER_ID = ${meta.types.toBrandedIdTypeFnName}('root')`,
822
+ getterBlock: `
823
+ // We initialize the root user in the init() function
824
+ private _rootUser!: ${meta.types.typeName}
825
+ public get rootUser(): ${meta.types.typeName} {
826
+ return this._rootUser
827
+ }`,
828
+ initCall: `await this.initializeRootUser()`,
829
+ rootUserInitializeBlock: `
830
+ private async initializeRootUser(): Promise<void> {
831
+ const existingRootUser = await this.get(${meta.data.repositoryClassName}.ROOT_USER_ID)
832
+ if (existingRootUser) {
833
+ this._rootUser = existingRootUser
834
+ return
835
+ }
836
+
837
+ const rawUser = {
838
+ id: ${meta.data.repositoryClassName}.ROOT_USER_ID,
839
+ name: 'System',
840
+ email: 'system@postxl.com',
841
+ role: UserRole.Admin,
842
+ login: 'not-set',
843
+ familyName: 'System user',
844
+ age: 0,
845
+ countryId: null,
846
+ }
847
+ const newRootUser = this.verifyItem (rawUser)
848
+ this.set(newRootUser)
849
+ this._rootUser = newRootUser
850
+ }`,
851
+ };
852
+ }
744
853
  /**
745
854
  * Generates code chunks responsible for verifying the ID validity of a model instance and generating the id
746
855
  * value of a model with auto-generated id.
@@ -31,7 +31,7 @@ function generateRoute({ model, meta }) {
31
31
  meta.businessLogic.update.createActionFunctionNameDeleteMany,
32
32
  ],
33
33
  });
34
- return `
34
+ return /* ts */ `
35
35
  import { z } from 'zod'
36
36
  import { procedure, router } from '../trpc'
37
37
 
@@ -107,7 +107,7 @@ function generateRoutesIndex({ models, meta }) {
107
107
  for (const { meta } of mm) {
108
108
  imports.addImport({ items: [meta.trpc.routerName], from: meta.trpc.routerFilePath });
109
109
  }
110
- return `
110
+ return /* ts */ `
111
111
  ${imports.generate()}
112
112
 
113
113
  /**
@@ -20,7 +20,7 @@ function generateStub({ model, meta }) {
20
20
  modelName: model.name,
21
21
  imports,
22
22
  });
23
- return `
23
+ return /* ts */ `
24
24
  ${imports.generate()}
25
25
 
26
26
  /**
@@ -40,7 +40,7 @@ function generateModelTypes({ model, meta }) {
40
40
  from: schemaMeta.types.dto.path,
41
41
  });
42
42
  const decoderNames = meta.types.zodDecoderFnNames;
43
- return `
43
+ return /* ts */ `
44
44
  import { z } from 'zod'
45
45
 
46
46
  ${imports.generate()}
@@ -0,0 +1,43 @@
1
+ type ElementId = string & {
2
+ __brand: 'ElementId';
3
+ };
4
+ /**
5
+ * The SelectorCollector is used to generate and track HTML element IDs.
6
+ * The idea is that it is instantiated once and then used to generate IDs throughout the generators.
7
+ * After the files are generated, all generated IDs can be serialized to a file.
8
+ *
9
+ * An ID can for instance be something like `post-create-name, `${model.name}-${componentType}-${fieldName}`.
10
+ *
11
+ * Each code generator can instantiate an SelectorCollector - the constructor will require the current model & component type.
12
+ * All generated Ids will be stored in a global static Set so we can serialize them later.
13
+ */
14
+ export declare class SelectorCollector {
15
+ private _prefix;
16
+ /**
17
+ * We store all generated IDs in a global static Set so we can serialize them later.
18
+ */
19
+ private static _ids;
20
+ constructor(_prefix: string);
21
+ /**
22
+ * Creates a new instance of the SelectorCollector.
23
+ */
24
+ static from(prefix: string): SelectorCollector;
25
+ /**
26
+ * Generates a new ID for an element and stores it in the collector
27
+ */
28
+ idFor(
29
+ /**
30
+ * The name of the element, e.g. `age`, `submit`.
31
+ */
32
+ elementName: string, options?: {
33
+ /**
34
+ * Optional prefix for the type of the element, e.g. `field` or `button`.
35
+ */
36
+ typePrefix?: string;
37
+ }): string;
38
+ /**
39
+ * Returns all generated IDs and resets the stored IDs.
40
+ */
41
+ static flush(): ElementId[];
42
+ }
43
+ export {};
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SelectorCollector = void 0;
4
+ /**
5
+ * The SelectorCollector is used to generate and track HTML element IDs.
6
+ * The idea is that it is instantiated once and then used to generate IDs throughout the generators.
7
+ * After the files are generated, all generated IDs can be serialized to a file.
8
+ *
9
+ * An ID can for instance be something like `post-create-name, `${model.name}-${componentType}-${fieldName}`.
10
+ *
11
+ * Each code generator can instantiate an SelectorCollector - the constructor will require the current model & component type.
12
+ * All generated Ids will be stored in a global static Set so we can serialize them later.
13
+ */
14
+ class SelectorCollector {
15
+ constructor(_prefix) {
16
+ this._prefix = _prefix;
17
+ }
18
+ /**
19
+ * Creates a new instance of the SelectorCollector.
20
+ */
21
+ static from(prefix) {
22
+ return new SelectorCollector(prefix);
23
+ }
24
+ /**
25
+ * Generates a new ID for an element and stores it in the collector
26
+ */
27
+ idFor(
28
+ /**
29
+ * The name of the element, e.g. `age`, `submit`.
30
+ */
31
+ elementName, options) {
32
+ const { typePrefix } = options !== null && options !== void 0 ? options : {};
33
+ const id = [this._prefix, typePrefix, elementName].filter((x) => x !== undefined && x !== '').join('-');
34
+ if (SelectorCollector._ids.has(id)) {
35
+ throw new Error(`ID ${id} already exists.`);
36
+ }
37
+ SelectorCollector._ids.add(id);
38
+ return id;
39
+ }
40
+ /**
41
+ * Returns all generated IDs and resets the stored IDs.
42
+ */
43
+ static flush() {
44
+ const result = [...SelectorCollector._ids.values()];
45
+ SelectorCollector._ids.clear();
46
+ return result;
47
+ }
48
+ }
49
+ exports.SelectorCollector = SelectorCollector;
50
+ /**
51
+ * We store all generated IDs in a global static Set so we can serialize them later.
52
+ */
53
+ SelectorCollector._ids = new Set();
@@ -51,10 +51,22 @@ export type SchemaMetaData = {
51
51
  * Path to the file containing the mock data for the database.
52
52
  */
53
53
  dataMockModuleFilePath: Types.Path;
54
+ /**
55
+ * Name of the mock data module class.
56
+ */
57
+ dataMockModuleName: Types.ClassName;
58
+ /**
59
+ * Name of the data mock type/interface.
60
+ */
61
+ dataMockDataType: Types.TypeName;
54
62
  /**
55
63
  * Path to the file containing data service class definitions.
56
64
  */
57
65
  dataServiceFilePath: Types.Path;
66
+ /**
67
+ * Name of the data service class.
68
+ */
69
+ dataServiceClassName: Types.ClassName;
58
70
  /**
59
71
  * Path to the file containing the repository type definition.
60
72
  */
@@ -67,6 +79,14 @@ export type SchemaMetaData = {
67
79
  * Path to the file containing data mocker class definitions.
68
80
  */
69
81
  dataMockerFilePath: Types.Path;
82
+ /**
83
+ * Path to the file containing component selectors for e2e tests.
84
+ */
85
+ selectorsFilePath: Types.Path;
86
+ /**
87
+ * Path to the file containing the testDataService class definition.
88
+ */
89
+ testDataServiceFilePath: Types.Path;
70
90
  /**
71
91
  * Path to the file containing data mocker class definitions.
72
92
  */
@@ -207,6 +227,10 @@ export type SchemaMetaData = {
207
227
  * Path that may be used in the import statement.
208
228
  */
209
229
  importPath: Types.Path;
230
+ /**
231
+ * Name of the function that converts mock data to seed data.
232
+ */
233
+ mockDataToCreateFunction: Types.Fnction;
210
234
  };
211
235
  trpc: {
212
236
  /**
package/dist/lib/meta.js CHANGED
@@ -45,10 +45,15 @@ function getSchemaMetadata({ config }) {
45
45
  importPath: Types.toPath(`@${config.project}/data`),
46
46
  dataModuleFilePath: Types.toPath(`${config.paths.dataLibPath}data.module`),
47
47
  dataMockModuleFilePath: Types.toPath(`${config.paths.dataLibPath}data.mock.module`),
48
+ dataMockModuleName: Types.toClassName(`DataMockModule`),
49
+ dataMockDataType: Types.toTypeName(`MockData`),
48
50
  dataServiceFilePath: Types.toPath(`${config.paths.dataLibPath}data.service`),
51
+ dataServiceClassName: Types.toClassName(`DataService`),
49
52
  repositoryTypeFilePath: Types.toPath(`${config.paths.dataLibPath}repository.type`),
50
53
  repositoryTypeName: Types.toTypeName(`Repository`),
51
54
  dataMockerFilePath: Types.toPath(`${config.paths.cypressPath}support/data-mocker.class`),
55
+ selectorsFilePath: Types.toPath(`${config.paths.cypressPath}support/selectors`),
56
+ testDataServiceFilePath: Types.toPath(`${config.paths.e2eLibPath}test-data.service`),
52
57
  dataMockerStubImportPath: Types.toPath(`${config.paths.cypressPath}support/stubs`),
53
58
  dataMockerStubIndexFilePath: Types.toPath(`${config.paths.cypressPath}support/stubs/index`),
54
59
  repositoriesConstFilePath: Types.toPath(`${config.paths.dataLibPath}repositories/repositories`),
@@ -94,6 +99,7 @@ function getSchemaMetadata({ config }) {
94
99
  serviceFilePath: Types.toPath(`${config.paths.seedLibPath}seed.service`),
95
100
  serviceClassName: Types.toClassName(`SeedService`),
96
101
  importPath: Types.toPath(`@${config.project}/seed`),
102
+ mockDataToCreateFunction: Types.toFunction(`mockDataToCreate`),
97
103
  },
98
104
  types: {
99
105
  indexFilePath: Types.toPath(`${config.paths.modelTypeDefinitionsPath}index`),
@@ -63,6 +63,10 @@ export type SchemaConfig = {
63
63
  * Path to the directory containing Cypress project.
64
64
  */
65
65
  cypressPath: Types.Path;
66
+ /**
67
+ * Path to the directory containing e2e tests.
68
+ */
69
+ e2eLibPath: Types.Path;
66
70
  /**
67
71
  * Path to the directory containing actions.
68
72
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@postxl/generator",
3
- "version": "0.38.1",
3
+ "version": "0.39.0",
4
4
  "main": "./dist/generator.js",
5
5
  "typings": "./dist/generator.d.ts",
6
6
  "bin": {
@@ -34,7 +34,7 @@
34
34
  "@types/eslint": "^8.44.7",
35
35
  "@types/jest": "^29.5.0",
36
36
  "@types/node": "18.15.10",
37
- "jest": "29.5.0",
37
+ "jest": "29.7.0",
38
38
  "prisma": "5.2.0",
39
39
  "ts-jest": "29.0.5",
40
40
  "ts-node": "10.9.1",