@postxl/generator 0.56.8 → 0.57.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/generator.js CHANGED
@@ -42,6 +42,7 @@ const lock_1 = require("@postxl/lock");
42
42
  const react_generator_1 = require("./generators/enums/react.generator");
43
43
  const types_generator_1 = require("./generators/enums/types.generator");
44
44
  const businesslogic_actiontypes_generator_1 = require("./generators/indices/businesslogic-actiontypes.generator");
45
+ const businesslogic_update_clonecontext_generator_1 = require("./generators/indices/businesslogic-update-clonecontext.generator");
45
46
  const businesslogic_update_index_generator_1 = require("./generators/indices/businesslogic-update-index.generator");
46
47
  const businesslogic_update_module_generator_1 = require("./generators/indices/businesslogic-update-module.generator");
47
48
  const businesslogic_update_service_generator_1 = require("./generators/indices/businesslogic-update-service.generator");
@@ -67,6 +68,7 @@ const selectors_generator_1 = require("./generators/indices/selectors.generator"
67
68
  const stubs_generator_1 = require("./generators/indices/stubs.generator");
68
69
  const testdata_service_generator_1 = require("./generators/indices/testdata-service.generator");
69
70
  const types_generator_2 = require("./generators/indices/types.generator");
71
+ const admin_page_generator_1 = require("./generators/models/admin.page.generator");
70
72
  const businesslogic_update_generator_1 = require("./generators/models/businesslogic-update.generator");
71
73
  const businesslogic_view_generator_1 = require("./generators/models/businesslogic-view.generator");
72
74
  const importexport_decoder_generator_1 = require("./generators/models/importexport-decoder.generator");
@@ -98,6 +100,7 @@ const CONFIG_SCHEMA = zod_1.z
98
100
  trpcRoutesFolder: zod_1.z.string().optional(),
99
101
  reactFolderOutput: zod_1.z.string().optional(),
100
102
  prismaMigrationsFolder: zod_1.z.string().optional(),
103
+ pathToAdminPages: zod_1.z.string().optional(),
101
104
  randomSeed: zod_1.z
102
105
  .string()
103
106
  .optional()
@@ -123,6 +126,7 @@ const CONFIG_SCHEMA = zod_1.z
123
126
  seedDataPath: (0, types_1.toPath)(s.pathToSeedData || './backend/seed-data/src/'),
124
127
  seedLibPath: (0, types_1.toPath)(s.pathToSeedLib || './backend/libs/seed/src/'),
125
128
  trpcRoutesFolderPath: (0, types_1.toPath)(s.trpcRoutesFolder || './backend/libs/trpc/src/routes/'),
129
+ adminPagesFolderPath: (0, types_1.toPath)(s.pathToAdminPages || './web/src/pages/admin/'),
126
130
  },
127
131
  randomSeed: s.randomSeed,
128
132
  force: s.force,
@@ -191,6 +195,8 @@ function generate({ models, enums, config, prismaClientPath, logger, }) {
191
195
  generated.write(`/${meta.trpc.routerFilePath}.ts`, (0, route_generator_1.generateRoute)({ model, meta }));
192
196
  // React
193
197
  yield generated.copy((0, react_generator_2.generateReactComponentsForModel)({ model, meta }), meta.react.folderPath);
198
+ // Admin
199
+ generated.write(`/${meta.admin.filePath}.tsx`, (0, admin_page_generator_1.generateAdminPage)({ meta }));
194
200
  logger.log(`- ${model.name} processed`);
195
201
  }
196
202
  // Generate Enums
@@ -228,6 +234,7 @@ function generate({ models, enums, config, prismaClientPath, logger, }) {
228
234
  generated.write(`/${meta.businessLogic.view.moduleFilePath}.ts`, (0, businesslogic_view_module_generator_1.generateBusinessLogicViewModule)({ models, meta }));
229
235
  generated.write(`/${meta.businessLogic.view.serviceFilePath}.ts`, (0, businesslogic_view_service_generator_1.generateBusinessLogicViewService)({ models, meta }));
230
236
  generated.write(`/${meta.businessLogic.update.indexFilePath}.ts`, (0, businesslogic_update_index_generator_1.generateBusinessLogicUpdateIndex)({ models, meta }));
237
+ generated.write(`/${meta.businessLogic.update.cloneContextFilePath}.ts`, (0, businesslogic_update_clonecontext_generator_1.generateBusinessLogicCloneContext)({ models, meta }));
231
238
  generated.write(`/${meta.businessLogic.update.moduleFilePath}.ts`, (0, businesslogic_update_module_generator_1.generateBusinessLogicUpdateModule)({ models, meta }));
232
239
  generated.write(`/${meta.businessLogic.update.serviceFilePath}.ts`, (0, businesslogic_update_service_generator_1.generateBusinessLogicUpdateService)({ models, meta }));
233
240
  generated.write(`/${meta.businessLogic.update.actionTypesFilePath}.ts`, (0, businesslogic_actiontypes_generator_1.generateBusinessLogicActionTypes)({ models, meta }));
@@ -0,0 +1,9 @@
1
+ import { SchemaMetaData } from '../../lib/meta';
2
+ import { Model } from '../../lib/schema/schema';
3
+ /**
4
+ * Generates the clone context for the business logic update service.
5
+ */
6
+ export declare function generateBusinessLogicCloneContext({ models, meta: schemaMeta, }: {
7
+ models: Model[];
8
+ meta: SchemaMetaData;
9
+ }): string;
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.generateBusinessLogicCloneContext = void 0;
4
+ const imports_1 = require("../../lib/imports");
5
+ const meta_1 = require("../../lib/meta");
6
+ /**
7
+ * Generates the clone context for the business logic update service.
8
+ */
9
+ function generateBusinessLogicCloneContext({ models, meta: schemaMeta, }) {
10
+ const imports = imports_1.ImportsGenerator.from(schemaMeta.businessLogic.update.cloneContextFilePath);
11
+ const typeDefinitions = [];
12
+ const mapAssignments = [];
13
+ for (const model of models) {
14
+ const modelMeta = (0, meta_1.getModelMetadata)({ model });
15
+ imports.addImports({
16
+ [schemaMeta.types.importPath]: [modelMeta.types.brandedIdType, modelMeta.types.typeName],
17
+ });
18
+ typeDefinitions.push(`${modelMeta.businessLogic.update.cloneContextMap}: Map<${modelMeta.types.brandedIdType}, ${modelMeta.types.typeName}>`);
19
+ mapAssignments.push(`${modelMeta.businessLogic.update.cloneContextMap}: new Map()`);
20
+ }
21
+ return /* ts */ `
22
+ ${imports.generate()}
23
+
24
+ /**
25
+ * The context that is passed in a clone action.
26
+ *
27
+ * This context is used to track which items have already been cloned and link them to the cloned item.
28
+ */
29
+ export type CloneContext = {
30
+ ${typeDefinitions.join('\n')}
31
+ }
32
+
33
+ export function createCloneContext(): CloneContext {
34
+ return {
35
+ ${mapAssignments.join(',\n')}
36
+ }
37
+ }`;
38
+ }
39
+ exports.generateBusinessLogicCloneContext = generateBusinessLogicCloneContext;
@@ -15,6 +15,7 @@ function generateBusinessLogicUpdateIndex({ models, meta }) {
15
15
  const meta = (0, meta_1.getModelMetadata)({ model });
16
16
  exports.exportSelectionFromPath(meta.businessLogic.update.serviceFilePath, [
17
17
  meta.businessLogic.update.serviceClassName,
18
+ meta.businessLogic.update.decoders.name,
18
19
  ]);
19
20
  }
20
21
  return exports.generate();
@@ -0,0 +1,7 @@
1
+ import { ModelMetaData } from '../../lib/meta';
2
+ /**
3
+ * returns an admin page for a given model.
4
+ */
5
+ export declare function generateAdminPage({ meta }: {
6
+ meta: ModelMetaData;
7
+ }): string;
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.generateAdminPage = void 0;
4
+ const imports_1 = require("../../lib/imports");
5
+ /**
6
+ * returns an admin page for a given model.
7
+ */
8
+ function generateAdminPage({ meta }) {
9
+ const { react } = meta;
10
+ const imports = imports_1.ImportsGenerator.from(meta.admin.filePath).addImport({
11
+ items: [react.components.libraryComponentName, react.components.modals.createComponentName],
12
+ from: meta.react.importPath,
13
+ });
14
+ return `
15
+ import styled from 'styled-components'
16
+
17
+ import { ActionWrapper, ActionsBarWrapper, Search, Spacer } from '@components/atoms/ActionsBar'
18
+ import { Button } from '@components/atoms/Button'
19
+ import { Headline } from '@components/atoms/Headline'
20
+
21
+ ${imports.generate()}
22
+
23
+ import { Content, Layout } from '@components/shared/Layout'
24
+ import { t } from '@i18n/translation'
25
+ import { useState } from 'react'
26
+
27
+ export default function Admin${meta.internalSingularNameCapitalized}Page() {
28
+ const [query, setQuery] = useState('')
29
+ const [isCreateModalOpen, setIsCreateModalOpen] = useState(false)
30
+
31
+ return (
32
+ <Layout>
33
+ <Header>
34
+ <Headline label="${meta.userFriendlyName}" />
35
+ <ActionsBarWrapper>
36
+ <Search
37
+ key="Search"
38
+ placeholder="Search ${meta.userFriendlyNamePlural}"
39
+ icon="magnifying-glass"
40
+ value={query}
41
+ onChange={(e) => setQuery(e.target.value)}
42
+ />
43
+
44
+ <Spacer key="Spacer" />
45
+
46
+ <ActionWrapper key="GlobalFilter">
47
+ <Button label={t['Create']} icon="plus" fill="fill" __cypress_selector__="indexPage-buttons-create" onClick={() => setIsCreateModalOpen(true)}/>
48
+ </ActionWrapper>
49
+ </ActionsBarWrapper>
50
+ </Header>
51
+
52
+ <Content>
53
+ <${meta.react.components.libraryComponentName} />
54
+ </Content>
55
+
56
+ <${meta.react.components.modals.createComponentName}
57
+ show={isCreateModalOpen}
58
+ onHide={() => setIsCreateModalOpen(false)}
59
+ />
60
+ </Layout>
61
+ )
62
+ }
63
+
64
+ const Header = styled.header\`
65
+ display: flex;
66
+ flex-direction: column;
67
+ align-items: stretch;
68
+
69
+ gap: var(--headline-spacing);
70
+ \`
71
+ `;
72
+ }
73
+ exports.generateAdminPage = generateAdminPage;
@@ -41,10 +41,18 @@ function generateModelBusinessLogicUpdate({ model, meta }) {
41
41
  iExecution: schemaMeta.actions.execution.interface,
42
42
  typeName: meta.types.typeName,
43
43
  brandedId: model.brandedIdType,
44
- createType: `Create${meta.internalSingularNameCapitalized}`,
45
- updateType: `Update${meta.internalSingularNameCapitalized}`,
46
- upsertType: `Upsert${meta.internalSingularNameCapitalized}`,
47
- cloneType: `Clone${meta.internalSingularNameCapitalized}`,
44
+ decoders: {
45
+ name: meta.businessLogic.update.decoders.name,
46
+ createType: `Create${meta.internalSingularNameCapitalized}`,
47
+ updateType: `Update${meta.internalSingularNameCapitalized}`,
48
+ upsertType: `Upsert${meta.internalSingularNameCapitalized}`,
49
+ cloneType: `Clone${meta.internalSingularNameCapitalized}`,
50
+ },
51
+ cloneContext: {
52
+ type: schemaMeta.businessLogic.update.cloneContextType,
53
+ createMethod: schemaMeta.businessLogic.update.cloneContextCreateMethod,
54
+ map: meta.businessLogic.update.cloneContextMap,
55
+ },
48
56
  };
49
57
  const imports = imports_1.ImportsGenerator.from(meta.businessLogic.update.serviceFilePath);
50
58
  imports.addImports({
@@ -58,6 +66,10 @@ function generateModelBusinessLogicUpdate({ model, meta }) {
58
66
  [meta.businessLogic.view.serviceFilePath]: [meta.businessLogic.view.serviceClassName],
59
67
  [schemaMeta.actions.importPath]: [schemaMeta.actions.execution.interface, schemaMeta.actions.dispatcher.definition],
60
68
  [schemaMeta.businessLogic.update.serviceFilePath]: schemaMeta.businessLogic.update.serviceClassName,
69
+ [schemaMeta.businessLogic.update.cloneContextFilePath]: [
70
+ schemaMeta.businessLogic.update.cloneContextType,
71
+ schemaMeta.businessLogic.update.cloneContextCreateMethod,
72
+ ],
61
73
  [schemaMeta.businessLogic.view.serviceFilePath]: schemaMeta.businessLogic.view.serviceClassName,
62
74
  });
63
75
  for (const relation of (0, fields_1.getRelationFields)(model)) {
@@ -85,7 +97,7 @@ function generateModelBusinessLogicUpdate({ model, meta }) {
85
97
  `@Inject(forwardRef(() => ${schemaMeta.businessLogic.update.serviceClassName})) private readonly updateService: ${schemaMeta.businessLogic.update.serviceClassName}`,
86
98
  `@Inject(forwardRef(() => ${schemaMeta.businessLogic.view.serviceClassName})) private readonly viewService: ${schemaMeta.businessLogic.view.serviceClassName}`,
87
99
  ];
88
- const { zodCreateObject, zodUpdateObject, zodUpsertObject, zodCloneObject } = meta.businessLogic.update;
100
+ const decoders = meta.businessLogic.update.decoders;
89
101
  const { view, update } = meta.businessLogic;
90
102
  /* prettier-ignore */
91
103
  return /* ts */ `
@@ -99,37 +111,37 @@ function generateModelBusinessLogicUpdate({ model, meta }) {
99
111
  export type Actions = {
100
112
  ${(0, jsdoc_1.toJsDocComment)([`Creates a new ${meta.userFriendlyName} and returns it.`])}
101
113
  create: {
102
- payload: ${m.createType}
114
+ payload: ${m.decoders.createType}
103
115
  result: ${m.typeName}
104
116
  }
105
117
 
106
118
  ${(0, jsdoc_1.toJsDocComment)([`Creates multiple new ${meta.userFriendlyNamePlural} and returns them.`])}
107
119
  createMany: {
108
- payload: ${m.createType}[]
120
+ payload: ${m.decoders.createType}[]
109
121
  result: ${m.typeName}[]
110
122
  }
111
123
 
112
124
  ${(0, jsdoc_1.toJsDocComment)([`Updates a ${meta.userFriendlyName} and returns it.`])}
113
125
  update: {
114
- payload: ${m.updateType}
126
+ payload: ${m.decoders.updateType}
115
127
  result: ${m.typeName}
116
128
  }
117
129
 
118
130
  ${(0, jsdoc_1.toJsDocComment)([`Updates multiple ${meta.userFriendlyNamePlural} and returns them.`])}
119
131
  updateMany: {
120
- payload: ${m.updateType}[]
132
+ payload: ${m.decoders.updateType}[]
121
133
  result: ${m.typeName}[]
122
134
  }
123
135
 
124
136
  ${(0, jsdoc_1.toJsDocComment)([`Creates or updates a ${meta.userFriendlyName} and returns it.`])}
125
137
  upsert: {
126
- payload: ${m.upsertType}
138
+ payload: ${m.decoders.upsertType}
127
139
  result: ${m.typeName}
128
140
  }
129
141
 
130
142
  ${(0, jsdoc_1.toJsDocComment)([`Creates or updates multiple ${meta.userFriendlyNamePlural} and returns them.`])}
131
143
  upsertMany: {
132
- payload: ${m.upsertType}[]
144
+ payload: ${m.decoders.upsertType}[]
133
145
  result: ${m.typeName}[]
134
146
  }
135
147
 
@@ -147,7 +159,7 @@ function generateModelBusinessLogicUpdate({ model, meta }) {
147
159
 
148
160
  ${(0, jsdoc_1.toJsDocComment)([`Clones a ${meta.userFriendlyName} and returns the clone.`])}
149
161
  clone: {
150
- payload: ${m.cloneType}
162
+ payload: ${m.decoders.cloneType}
151
163
  result: ${m.typeName}
152
164
  }
153
165
  }
@@ -155,46 +167,53 @@ function generateModelBusinessLogicUpdate({ model, meta }) {
155
167
  /**
156
168
  * Zod decoder for validating the create input of a ${meta.userFriendlyName}.
157
169
  */
158
- export const ${zodCreateObject} = z.object({
170
+ export const ${decoders.create} = z.object({
159
171
  ${model.fields
160
172
  .filter((f) => !f.attributes.isReadonly)
161
173
  .map((f) => `${f.name}: z.${(0, zod_1.getZodDecoderDefinition)({ field: f })}`)
162
174
  .join(',')}
163
175
  })
164
176
 
165
- type ${m.createType} = z.infer<typeof ${zodCreateObject}>
177
+ type ${m.decoders.createType} = z.infer<typeof ${decoders.create}>
166
178
 
167
179
  /**
168
180
  * Zod decoder for validating the update input of a ${meta.userFriendlyName} .
169
181
  */
170
- export const ${zodUpdateObject} = z.object({
182
+ export const ${decoders.update} = z.object({
171
183
  ${model.fields
172
184
  .filter((f) => !f.attributes.isReadonly || f.kind === 'id')
173
185
  .map((f) => `${f.name}: z.${(0, zod_1.getZodDecoderDefinition)({ field: f, allowAnyOptionalField: f.kind !== 'id' })}`)
174
186
  .join(',')}
175
187
  })
176
188
 
177
- type ${m.updateType} = z.infer<typeof ${zodUpdateObject}>
189
+ type ${m.decoders.updateType} = z.infer<typeof ${decoders.update}>
178
190
 
179
191
  /**
180
192
  * Zod decoder for validating the upsert input of a ${meta.userFriendlyName} .
181
193
  */
182
- export const ${zodUpsertObject} = z.union([${zodUpdateObject}, ${zodCreateObject}])
194
+ export const ${decoders.upsert} = z.union([${decoders.update}, ${decoders.create}])
183
195
 
184
- type ${m.upsertType} = z.infer<typeof ${zodUpsertObject}>
196
+ type ${m.decoders.upsertType} = z.infer<typeof ${decoders.upsert}>
185
197
 
186
198
  /**
187
199
  * Zod decoder for validating the clone input of a ${meta.userFriendlyName} .
188
200
  */
189
- export const ${zodCloneObject} = z.object({
201
+ export const ${decoders.clone} = z.object({
190
202
  ${model.fields
191
203
  .filter((f) => !f.attributes.isReadonly || f.kind === "id")
192
204
  .map((f) => `${f.name}: z.${(0, zod_1.getZodDecoderDefinition)({ field: f, allowAnyOptionalField: f.kind !== 'id' })}`)
193
205
  .join(',')}
194
206
  })
195
207
 
196
- type ${m.cloneType} = z.infer<typeof ${zodCloneObject}>
208
+ type ${m.decoders.cloneType} = z.infer<typeof ${decoders.clone}>
197
209
 
210
+ export const ${decoders.name} = {
211
+ create: ${decoders.create},
212
+ update: ${decoders.update},
213
+ upsert: ${decoders.upsert},
214
+ clone: ${decoders.clone},
215
+ }
216
+
198
217
  export type ${update.serviceInterfaceName} = ${schemaMeta.actions.dispatcher.definition}<Actions>
199
218
 
200
219
  @Injectable()
@@ -210,32 +229,32 @@ function generateModelBusinessLogicUpdate({ model, meta }) {
210
229
  }
211
230
 
212
231
  ${(0, jsdoc_1.toJsDocComment)([`Creates a new ${meta.userFriendlyName} and returns it.`])}
213
- public async create({ data, execution }: { data: ${m.createType}; execution: ${m.iExecution} }): Promise<${m.typeName}> {
232
+ public async create({ data, execution }: { data: ${m.decoders.createType}; execution: ${m.iExecution} }): Promise<${m.typeName}> {
214
233
  return this.data.create({ item: data, execution })
215
234
  }
216
235
 
217
236
  ${(0, jsdoc_1.toJsDocComment)([`Creates multiple new ${meta.userFriendlyNamePlural} and returns them.`])}
218
- public async createMany({ data, execution }: { data: ${m.createType}[]; execution: ${m.iExecution} }): Promise<${m.typeName}[]> {
237
+ public async createMany({ data, execution }: { data: ${m.decoders.createType}[]; execution: ${m.iExecution} }): Promise<${m.typeName}[]> {
219
238
  return this.data.createMany({ items: data, execution })
220
239
  }
221
240
 
222
241
  ${(0, jsdoc_1.toJsDocComment)([`Updates a ${meta.userFriendlyName} and returns it.`])}
223
- public async update({ data, execution }: { data: ${m.updateType}; execution: ${m.iExecution} }): Promise<${m.typeName}> {
242
+ public async update({ data, execution }: { data: ${m.decoders.updateType}; execution: ${m.iExecution} }): Promise<${m.typeName}> {
224
243
  return this.data.update({ item: data, execution })
225
244
  }
226
245
 
227
246
  ${(0, jsdoc_1.toJsDocComment)([`Updates multiple ${meta.userFriendlyNamePlural} and returns them.`])}
228
- public async updateMany({ data, execution }: { data: ${m.updateType}[]; execution: ${m.iExecution} }): Promise<${m.typeName}[]> {
247
+ public async updateMany({ data, execution }: { data: ${m.decoders.updateType}[]; execution: ${m.iExecution} }): Promise<${m.typeName}[]> {
229
248
  return this.data.updateMany({ items: data, execution })
230
249
  }
231
250
 
232
251
  ${(0, jsdoc_1.toJsDocComment)([`Creates or updates a ${meta.userFriendlyName} and returns it.`])}
233
- public async upsert({ data, execution }: { data: ${m.upsertType}; execution: ${m.iExecution} }): Promise<${m.typeName}> {
252
+ public async upsert({ data, execution }: { data: ${m.decoders.upsertType}; execution: ${m.iExecution} }): Promise<${m.typeName}> {
234
253
  return this.data.upsert({ item: data, execution })
235
254
  }
236
255
 
237
256
  ${(0, jsdoc_1.toJsDocComment)([`Creates or updates multiple ${meta.userFriendlyNamePlural} and returns them.`])}
238
- public async upsertMany({ data, execution }: { data: ${m.upsertType}[]; execution: ${m.iExecution} }): Promise<${m.typeName}[]> {
257
+ public async upsertMany({ data, execution }: { data: ${m.decoders.upsertType}[]; execution: ${m.iExecution} }): Promise<${m.typeName}[]> {
239
258
  return this.data.upsertMany({ items: data, execution })
240
259
  }
241
260
 
@@ -330,6 +349,10 @@ function generateCloneMethod({ model, meta, m }) {
330
349
  const backReferenceCloning = [];
331
350
  const backReferenceNames = [];
332
351
  for (const { referencingField, referencingModel } of model.references) {
352
+ // We only clone back references that are marked as cloneWithParent.
353
+ if (!referencingField.attributes.cloneWithParent) {
354
+ continue;
355
+ }
333
356
  const refModelMeta = (0, meta_1.getModelMetadata)({ model: referencingModel });
334
357
  const refFieldMeta = (0, meta_1.getFieldMetadata)({ field: referencingField });
335
358
  backReferenceNames.push(`${refModelMeta.userFriendlyNamePlural}.${referencingField.name}`);
@@ -337,15 +360,22 @@ function generateCloneMethod({ model, meta, m }) {
337
360
  backReferenceCloning.push(`
338
361
  // ${referencingModel.name}.${referencingField.name}
339
362
  for (const childId of await this.viewService.${view.serviceVariableName}.data.${refFieldMeta.getByForeignKeyIdsMethodFnName}(id)) {
340
- await this.updateService.${update.serviceVariableName}.clone({ data: { id: childId, ${referencingField.name}: clone.id }, execution })
363
+ await this.updateService.${update.serviceVariableName}.clone({ data: { id: childId, ${referencingField.name}: clone.id }, execution, context })
341
364
  }
342
365
  `);
343
366
  }
344
367
  return `
345
368
  ${(0, jsdoc_1.toJsDocComment)([`Creates a new ${meta.userFriendlyName} deep clone and returns it.`])}
346
369
  public async clone(
347
- { data: { id, ...data }, execution }: { data: ${m.cloneType}; execution: ${m.iExecution} }
370
+ { data: { id, ...data }, execution, context = ${m.cloneContext.createMethod}() }:
371
+ { data: ${m.decoders.cloneType}; execution: ${m.iExecution}; context?: ${m.cloneContext.type} }
348
372
  ): Promise<${m.typeName}> {
373
+ const alreadyCloned = context.${m.cloneContext.map}.get(id)
374
+ if (alreadyCloned) {
375
+ // We already cloned this item before, so we return the cloned item.
376
+ return alreadyCloned
377
+ }
378
+
349
379
  const source = await this.view.get(id)
350
380
  if (!source) {
351
381
  throw new Error(\`${meta.userFriendlyName} with id \${id} not found\`)
@@ -353,6 +383,8 @@ function generateCloneMethod({ model, meta, m }) {
353
383
 
354
384
  const clone = await this.data.create({ item: { ...omitId(source), ...data }, execution })
355
385
 
386
+ context.${m.cloneContext.map}.set(id, clone)
387
+
356
388
  ${backReferenceCloning.join('\n')}
357
389
 
358
390
  return clone
@@ -12,14 +12,14 @@ function generateRoute({ model, meta }) {
12
12
  const defaultValueMethod = `
13
13
  getDefault: procedure.query(({ ctx }) => ctx.view.${meta.data.dataServiceName}.${dataRepositoryVariableName}.defaultValue),
14
14
  `;
15
- const { zodCreateObject, zodUpdateObject, zodUpsertObject, zodCloneObject } = meta.businessLogic.update;
15
+ const decoders = meta.businessLogic.update.decoders;
16
16
  const imports = imports_1.ImportsGenerator.from(meta.trpc.routerFilePath).addImports({
17
17
  [meta.types.importPath]: [
18
18
  (0, types_1.toAnnotatedTypeName)(model.typeName),
19
19
  meta.types.toBrandedIdTypeFnName,
20
20
  meta.types.zodDecoderFnNames.id,
21
21
  ],
22
- [meta.businessLogic.update.serviceFilePath]: [zodCreateObject, zodUpdateObject, zodUpsertObject, zodCloneObject],
22
+ [meta.businessLogic.update.importPath]: [decoders.name],
23
23
  });
24
24
  return /* ts */ `
25
25
  import { z } from 'zod'
@@ -61,27 +61,27 @@ export const ${meta.trpc.routerName} = router({
61
61
  }),
62
62
 
63
63
  create: procedure
64
- .input(${zodCreateObject})
64
+ .input(${decoders.name}.create)
65
65
  .mutation(({ input, ctx }) => ctx.dispatch({scope: "${scopeName}", type: "create", payload: input})),
66
66
 
67
67
  createMany: procedure
68
- .input(z.array(${zodCreateObject}))
68
+ .input(z.array(${decoders.name}.create))
69
69
  .mutation(({ input, ctx }) => ctx.dispatch({scope: "${scopeName}", type: "createMany", payload: input})),
70
70
 
71
71
  update: procedure
72
- .input(${zodUpdateObject})
72
+ .input(${decoders.name}.update)
73
73
  .mutation(({ input, ctx }) => ctx.dispatch({scope: "${scopeName}", type: "update", payload: input})),
74
74
 
75
75
  updateMany: procedure
76
- .input(z.array(${zodUpdateObject}))
76
+ .input(z.array(${decoders.name}.update))
77
77
  .mutation(({ input, ctx }) => ctx.dispatch({scope: "${scopeName}", type: "updateMany", payload: input})),
78
78
 
79
79
  upsert: procedure
80
- .input(${zodUpsertObject})
80
+ .input(${decoders.name}.upsert)
81
81
  .mutation(({ input, ctx }) => ctx.dispatch({scope: "${scopeName}", type: "upsert", payload: input})),
82
82
 
83
83
  upsertMany: procedure
84
- .input(z.array(${zodUpsertObject}))
84
+ .input(z.array(${decoders.name}.upsert))
85
85
  .mutation(({ input, ctx }) => ctx.dispatch({scope: "${scopeName}", type: "upsertMany", payload: input})),
86
86
 
87
87
  delete: procedure
@@ -93,7 +93,7 @@ export const ${meta.trpc.routerName} = router({
93
93
  .mutation(({ input, ctx }) => ctx.dispatch({scope: "${scopeName}", type: "deleteMany", payload: input})),
94
94
 
95
95
  clone: procedure
96
- .input(${zodCloneObject})
96
+ .input(${decoders.name}.clone)
97
97
  .mutation(({ input, ctx }) => ctx.dispatch({scope: "${scopeName}", type: "clone", payload: input})),
98
98
  })
99
99
  `;
@@ -90,6 +90,11 @@ export type FieldAttributes = {
90
90
  * The field that should be used as the default label for the model.
91
91
  */
92
92
  isLabel?: boolean;
93
+ /**
94
+ * Schema tag: `@@CloneWithParent()`
95
+ * If set, the all models that reference the parent that was cloned should also be cloned.
96
+ */
97
+ cloneWithParent?: boolean;
93
98
  };
94
99
  export type EnumAttributes = {
95
100
  /**
@@ -568,6 +568,18 @@ export type SchemaMetaData = {
568
568
  * Path to the file containing the overall action types
569
569
  */
570
570
  actionTypesFilePath: Types.FilePath;
571
+ /**
572
+ * Path to the file containing the clone context
573
+ */
574
+ cloneContextFilePath: Types.FilePath;
575
+ /**
576
+ * The name of the clone context type
577
+ */
578
+ cloneContextType: Types.TypeName;
579
+ /**
580
+ * The name of the function that creates a clone context
581
+ */
582
+ cloneContextCreateMethod: Types.FunctionName;
571
583
  };
572
584
  };
573
585
  seedData: {
@@ -788,11 +800,11 @@ export type ModelMetaData = {
788
800
  */
789
801
  className: Types.ClassName;
790
802
  /**
791
- * The name of the function that decodes a source (database) object to a fully typed object, e.g. `toAggregation`.
803
+ * The name of the function that decodes a source (database) object to a fully typed object (e.g. `toAggregation`.)
792
804
  */
793
805
  decoderFnName: Types.FunctionName;
794
806
  /**
795
- * The name of the method that should be used to get objects from the database, e.g. `aggregations`.
807
+ * The name of the method that should be used to get objects from the database (e.g. `aggregations`.)
796
808
  */
797
809
  getMethodFnName: Types.FunctionName;
798
810
  };
@@ -834,7 +846,7 @@ export type ModelMetaData = {
834
846
  */
835
847
  exportDataTypeName: Types.TypeName;
836
848
  /**
837
- * Name of the function that adds an instance of this model to the exporter, e.g. `addAggregation`.
849
+ * Name of the function that adds an instance of this model to the exporter (e.g. `addAggregation`.)
838
850
  */
839
851
  exportAddFunctionName: Types.FunctionName;
840
852
  /**
@@ -858,23 +870,23 @@ export type ModelMetaData = {
858
870
  */
859
871
  filePath: Types.FilePath;
860
872
  /**
861
- * Name of the type that represents the model in Excel import/export, e.g. `Aggregation_EncodedExcelData`.
873
+ * Name of the type that represents the model in Excel import/export (e.g. `Aggregation_EncodedExcelData`.)
862
874
  */
863
875
  encodedExcelType: Types.TypeName;
864
876
  /**
865
- * Name of the decoder function that represents the Excel table import type, e.g. `aggregationExcelTableDecoder`.
877
+ * Name of the decoder function that represents the Excel table import type (e.g. `aggregationExcelTableDecoder`.)
866
878
  */
867
879
  tableDecoder: Types.FunctionName;
868
880
  /**
869
- * Name of the function that converts a model item to the Excel format, e.g. `encodeAggregation`.
881
+ * Name of the function that converts a model item to the Excel format (e.g. `encodeAggregation`.)
870
882
  */
871
883
  itemEncoderFunctionName: Types.FunctionName;
872
884
  /**
873
- * Name of the function that converts an array of model items to the Excel format, e.g. `encodeAggregations`.
885
+ * Name of the function that converts an array of model items to the Excel format (e.g. `encodeAggregations`.)
874
886
  */
875
887
  arrayEncoderFunctionName: Types.FunctionName;
876
888
  /**
877
- * Name of the array in the decoded data containing all entities of this model, e.g. `aggregations`.
889
+ * Name of the array in the decoded data containing all entities of this model (e.g. `aggregations`.)
878
890
  */
879
891
  decodedModelArrayName: Types.VariableName;
880
892
  };
@@ -897,6 +909,10 @@ export type ModelMetaData = {
897
909
  * The definitions for the view service of a model
898
910
  */
899
911
  view: {
912
+ /**
913
+ * Path that may be used to import the types of this model.
914
+ */
915
+ importPath: Types.BackendModulePath;
900
916
  /**
901
917
  * The name by which the model's view class is exposed in the viewService/context. (e.g. aggregations)
902
918
  */
@@ -918,6 +934,10 @@ export type ModelMetaData = {
918
934
  * The definitions for the update service of a model
919
935
  */
920
936
  update: {
937
+ /**
938
+ * Path that may be used to import the types of this model.
939
+ */
940
+ importPath: Types.BackendModulePath;
921
941
  /**
922
942
  * The name by which the model's update class is exposed in the updateService/context. (e.g. AggregationUpdateService)
923
943
  */
@@ -942,22 +962,32 @@ export type ModelMetaData = {
942
962
  * The name of the model used as a discriminant for the action execution. (e.g. `aggregation`)
943
963
  */
944
964
  actionModelDiscriminantName: Types.VariableName;
965
+ decoders: {
966
+ /**
967
+ * The name of the variable that holds the decoders for the model. (e.g. `aggregationDecoders`)
968
+ */
969
+ name: Types.VariableName;
970
+ /**
971
+ * The name of the function that decodes a Create object to a fully typed object (e.g. `aggregationCreateDecoder`.)
972
+ */
973
+ create: Types.FunctionName;
974
+ /**
975
+ * The name of the function that decodes an Update object to a fully typed object (e.g. `aggregationUpdateDecoder`.)
976
+ */
977
+ update: Types.FunctionName;
978
+ /**
979
+ * The name of the function that decodes an Upsert object to a fully typed object (e.g. `aggregationUpsertDecoder`.)
980
+ */
981
+ upsert: Types.FunctionName;
982
+ /**
983
+ * The name of the function that clones an object (e.g. `aggregationCloneDecoder`.)
984
+ */
985
+ clone: Types.FunctionName;
986
+ };
945
987
  /**
946
- * The name of the function that decodes a Create object to a fully typed object, e.g. `aggregationCreateDecoder`.
947
- */
948
- zodCreateObject: Types.FunctionName;
949
- /**
950
- * The name of the function that decodes an Update object to a fully typed object, e.g. `aggregationUpdateDecoder`.
951
- */
952
- zodUpdateObject: Types.FunctionName;
953
- /**
954
- * The name of the function that decodes an Upsert object to a fully typed object, e.g. `aggregationUpsertDecoder`.
955
- */
956
- zodUpsertObject: Types.FunctionName;
957
- /**
958
- * The name of the function that clones an object, e.g. `aggregationCloneDecoder`.
988
+ * The name of the map variable in the clone context that holds the cloned items of this model (e.g. `aggregation`)
959
989
  */
960
- zodCloneObject: Types.FunctionName;
990
+ cloneContextMap: Types.VariableName;
961
991
  };
962
992
  /**
963
993
  * Name by which the business logic service exposes the data service.
@@ -993,6 +1023,10 @@ export type ModelMetaData = {
993
1023
  * The path to the folder that contains React components for this model.
994
1024
  */
995
1025
  folderPath: Types.FilePath;
1026
+ /**
1027
+ * Globally accessible import path for the model's React components.
1028
+ */
1029
+ importPath: Types.FilePath;
996
1030
  context: {
997
1031
  /**
998
1032
  * Name of the function that should be used as React hook (e.g. `useAggregationContext`).
@@ -1087,6 +1121,15 @@ export type ModelMetaData = {
1087
1121
  reactQueryMethod: Types.FunctionName;
1088
1122
  };
1089
1123
  };
1124
+ /**
1125
+ * Properties provided by the `admin.page` generators.
1126
+ */
1127
+ admin: {
1128
+ /**
1129
+ * The absolute file path of the admin page for this model.
1130
+ */
1131
+ filePath: Types.FilePath;
1132
+ };
1090
1133
  /**
1091
1134
  * Properties provided by the `types` generators.
1092
1135
  */
@@ -1100,7 +1143,7 @@ export type ModelMetaData = {
1100
1143
  */
1101
1144
  importPath: Types.BackendModulePath;
1102
1145
  /**
1103
- * The name of the type that represents a branded ID, e.g. `AggregationId`.
1146
+ * The name of the type that represents a branded ID (e.g. `AggregationId`.)
1104
1147
  */
1105
1148
  brandedIdType: Types.TypeName;
1106
1149
  /**
@@ -1110,11 +1153,11 @@ export type ModelMetaData = {
1110
1153
  toBrandedIdTypeFnName: Types.FunctionName;
1111
1154
  zodDecoderFnNames: {
1112
1155
  /**
1113
- * The name of the function that decodes a scalar value to a branded ID type, e.g. `aggregationIdDecoder`.
1156
+ * The name of the function that decodes a scalar value to a branded ID type (e.g. `aggregationIdDecoder`.)
1114
1157
  */
1115
1158
  id: Types.FunctionName;
1116
1159
  /**
1117
- * The name of the function that decodes a source (database) object to a fully typed object, e.g. `aggregationDatabaseDecoder`.
1160
+ * The name of the function that decodes a source (database) object to a fully typed object (e.g. `aggregationDatabaseDecoder`.)
1118
1161
  */
1119
1162
  fromDatabase: Types.FunctionName;
1120
1163
  };
@@ -1123,15 +1166,15 @@ export type ModelMetaData = {
1123
1166
  */
1124
1167
  dto: {
1125
1168
  /**
1126
- * The name of the type that represents a DTO for creating a new object, e.g. `AggregationCreateDTO`.
1169
+ * The name of the type that represents a DTO for creating a new object (e.g. `AggregationCreateDTO`.)
1127
1170
  */
1128
1171
  create: Types.TypeName;
1129
1172
  /**
1130
- * The name of the type that represents a DTO for updating an existing object, e.g. `AggregationUpdateDTO`.
1173
+ * The name of the type that represents a DTO for updating an existing object (e.g. `AggregationUpdateDTO`.)
1131
1174
  */
1132
1175
  update: Types.TypeName;
1133
1176
  /**
1134
- * The name of the type that represents a DTO for upserting an existing object, e.g. `AggregationUpsertDTO`.
1177
+ * The name of the type that represents a DTO for upserting an existing object (e.g. `AggregationUpsertDTO`.)
1135
1178
  */
1136
1179
  upsert: Types.TypeName;
1137
1180
  };
@@ -1140,17 +1183,17 @@ export type ModelMetaData = {
1140
1183
  */
1141
1184
  typeDefFileName: Types.FileName;
1142
1185
  /**
1143
- * The name of the type that represents a fully typed, flat object, e.g. `Aggregation`.
1186
+ * The name of the type that represents a fully typed, flat object (e.g. `Aggregation`.)
1144
1187
  * This type only refers to related types by their branded ID.
1145
1188
  */
1146
1189
  typeName: Types.TypeName;
1147
1190
  /**
1148
- * The name of the type that represents a fully typed object, e.g. `AggregationFull`.
1191
+ * The name of the type that represents a fully typed object (e.g. `AggregationFull`.)
1149
1192
  * This type refers to relations by linking to the (flat) types.
1150
1193
  */
1151
1194
  linkedTypeName: Types.TypeName;
1152
1195
  /**
1153
- * The name of the type that represents a source (i.e. database) object, e.g. `Aggregation`.
1196
+ * The name of the type that represents a source (i.e. database) object (e.g. `Aggregation`.)
1154
1197
  */
1155
1198
  sourceType: Types.TypeName;
1156
1199
  };
@@ -1167,15 +1210,15 @@ export type FieldMetaData = {
1167
1210
  */
1168
1211
  tsFieldName: Types.VariableName;
1169
1212
  /**
1170
- * The name of the method that should be used to get all child objects for a given item, e.g. `getItemsForAggregation`.
1213
+ * The name of the method that should be used to get all child objects for a given item (e.g. `getItemsForAggregation`.)
1171
1214
  */
1172
1215
  getByForeignKeyMethodFnName: Types.FunctionName;
1173
1216
  /**
1174
- * The name of the method that should be used to get all child Ids for a given item, e.g. `getIdsForAggregation`.
1217
+ * The name of the method that should be used to get all child Ids for a given item (e.g. `getIdsForAggregation`.)
1175
1218
  */
1176
1219
  getByForeignKeyIdsMethodFnName: Types.FunctionName;
1177
1220
  /**
1178
- * The name of the column in the seed Excel table, e.g. `Aggregation`.
1221
+ * The name of the column in the seed Excel table (e.g. `Aggregation`.)
1179
1222
  */
1180
1223
  excelColumnName: string;
1181
1224
  };
@@ -1216,7 +1259,7 @@ export type EnumMetaData = {
1216
1259
  */
1217
1260
  membersMap: Types.VariableName;
1218
1261
  /**
1219
- * Relative path to the file that contains the enum's types, e.g. `./aggregation.type`.
1262
+ * Relative path to the file that contains the enum's types (e.g. `./aggregation.type`.)
1220
1263
  */
1221
1264
  filePath: Types.FilePath;
1222
1265
  /**
package/dist/lib/meta.js CHANGED
@@ -119,6 +119,9 @@ function getSchemaMetadata({ config }) {
119
119
  serviceClassName: Types.toClassName(`UpdateService`),
120
120
  serviceFilePath: Types.toPath(`${config.paths.businessLogicPath}update/update.service`),
121
121
  actionTypesFilePath: Types.toPath(`${config.paths.businessLogicPath}update/actions`),
122
+ cloneContextFilePath: Types.toPath(`${config.paths.businessLogicPath}update/clone.context`),
123
+ cloneContextType: Types.toTypeName(`CloneContext`),
124
+ cloneContextCreateMethod: Types.toFunctionName(`createCloneContext`),
122
125
  },
123
126
  },
124
127
  data: {
@@ -331,22 +334,28 @@ function getModelMetadata({ model }) {
331
334
  scopeName: Types.toVariableName(`${camelCase}`),
332
335
  importPath: Types.toBackendModulePath(`@backend/business-logic`),
333
336
  view: {
337
+ importPath: Types.toBackendModulePath(`@backend/business-logic/view`),
334
338
  serviceClassName: Types.toClassName(`${PascalCase}ViewService`),
335
339
  serviceVariableName: Types.toVariableName(`${uncapitalizedPlural}`),
336
340
  serviceFileName: Types.toFileName(`${camelCase}.view.service`),
337
341
  serviceFilePath: Types.toPath(`${config.paths.businessLogicPath}view/${camelCase}.view.service`),
338
342
  },
339
343
  update: {
344
+ importPath: Types.toBackendModulePath(`@backend/business-logic/update`),
340
345
  serviceClassName: Types.toClassName(`${PascalCase}UpdateService`),
341
346
  serviceInterfaceName: Types.toTypeName(`I${PascalCase}UpdateService`),
342
347
  serviceVariableName: Types.toVariableName(`${uncapitalizedPlural}`),
343
348
  serviceFileName: Types.toFileName(`${camelCase}.update.service`),
344
349
  serviceFilePath: Types.toPath(`${config.paths.businessLogicPath}update/${camelCase}.update.service`),
345
350
  actionModelDiscriminantName: Types.toVariableName(`${camelCase}`),
346
- zodCreateObject: Types.toFunctionName(`${camelCase}CreateDecoder`),
347
- zodUpdateObject: Types.toFunctionName(`${camelCase}UpdateDecoder`),
348
- zodUpsertObject: Types.toFunctionName(`${camelCase}UpsertDecoder`),
349
- zodCloneObject: Types.toFunctionName(`${camelCase}CloneDecoder`),
351
+ decoders: {
352
+ name: Types.toVariableName(`${camelCase}Decoders`),
353
+ create: Types.toFunctionName(`${camelCase}CreateDecoder`),
354
+ update: Types.toFunctionName(`${camelCase}UpdateDecoder`),
355
+ upsert: Types.toFunctionName(`${camelCase}UpsertDecoder`),
356
+ clone: Types.toFunctionName(`${camelCase}CloneDecoder`),
357
+ },
358
+ cloneContextMap: Types.toVariableName(`${camelCase}`),
350
359
  },
351
360
  dataRepositoryVariableName: Types.toVariableName(`data`),
352
361
  },
@@ -358,6 +367,7 @@ function getModelMetadata({ model }) {
358
367
  react: {
359
368
  folderName: Types.toFolderName(`${PascalCase}`),
360
369
  folderPath: Types.toPath(`${config.paths.reactFolderPath}models/${PascalCase}/`),
370
+ importPath: Types.toPath(`@components/models/${PascalCase}`),
361
371
  context: {
362
372
  hookFnName: Types.toFunctionName(`use${PascalCase}Context`),
363
373
  instanceGetterHookFnName: Types.toFunctionName(`use${PascalCase}`),
@@ -402,6 +412,9 @@ function getModelMetadata({ model }) {
402
412
  reactQueryMethod: Types.toFunctionName(`${uncapitalizedPlural}.delete`),
403
413
  },
404
414
  },
415
+ admin: {
416
+ filePath: Types.toPath(`${config.paths.adminPagesFolderPath}/${uncapitalized}`),
417
+ },
405
418
  types: {
406
419
  importPath: Types.toBackendModulePath(`@backend/types`),
407
420
  filePath: Types.toPath(`${config.paths.modelTypeDefinitionsPath}${camelCase}.type`),
@@ -87,6 +87,10 @@ export type SchemaConfig = {
87
87
  * Path to the directory containing trpc routes.
88
88
  */
89
89
  trpcRoutesFolderPath: Types.FilePath;
90
+ /**
91
+ * Path to the directory containing admin pages.
92
+ */
93
+ adminPagesFolderPath: Types.FilePath;
90
94
  };
91
95
  /**
92
96
  * Whether the generator should overwrite existing files.
@@ -144,9 +144,10 @@ function getFieldAttributes(field) {
144
144
  .or(zod_1.default.string().transform((s) => parseInt(s, 10)))
145
145
  .optional(),
146
146
  readonly: blankStringBooleanDecoder,
147
+ cloneWithParent: blankStringBooleanDecoder,
147
148
  })
148
149
  .transform((obj) => {
149
- var _a;
150
+ var _a, _b;
150
151
  if (isPrismaIgnored && !obj.ignore) {
151
152
  throw new Error(`Field ${field.name} is ignored by Prisma, but is missing the "ignore" PostXL attribute!`);
152
153
  }
@@ -160,6 +161,7 @@ function getFieldAttributes(field) {
160
161
  isReadonly: obj.readonly || field.isGenerated || field.isUpdatedAt || field.name === 'createdAt' || field.isId,
161
162
  isUpdatedAt: (_a = field.isUpdatedAt) !== null && _a !== void 0 ? _a : false,
162
163
  isCreatedAt: field.name === 'createdAt',
164
+ cloneWithParent: (_b = obj.cloneWithParent) !== null && _b !== void 0 ? _b : false,
163
165
  };
164
166
  });
165
167
  const result = decoder.safeParse(attributes);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@postxl/generator",
3
- "version": "0.56.8",
3
+ "version": "0.57.1",
4
4
  "main": "./dist/generator.js",
5
5
  "typings": "./dist/generator.d.ts",
6
6
  "bin": {