@postxl/generator 0.72.0 → 0.73.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 +14 -0
- package/dist/generators/models/export/encoder.generator.js +13 -4
- package/dist/generators/models/import/decoder.generator.js +18 -8
- package/dist/generators/models/react/context.generator.js +6 -10
- package/dist/generators/models/react/modals.generator.js +1 -1
- package/dist/generators/models/view/service.generator.js +64 -192
- package/dist/lib/attributes.d.ts +13 -2
- package/dist/lib/meta.js +1 -1
- package/dist/lib/schema/schema.d.ts +4 -4
- package/dist/lib/schema/types.d.ts +10 -0
- package/dist/lib/schema/types.js +5 -1
- package/dist/lib/utils/error.js +0 -1
- package/dist/prisma/attributes.js +6 -4
- package/dist/prisma/parse.js +28 -29
- package/package.json +1 -2
package/dist/generator.js
CHANGED
|
@@ -404,6 +404,20 @@ function generate(_a) {
|
|
|
404
404
|
}
|
|
405
405
|
}
|
|
406
406
|
yield lock.writeToProjectRoot(root, { dryRun: false });
|
|
407
|
+
// NOTE: Detached mode tells whether any of the UTF-8 files were detached.
|
|
408
|
+
// We use this in CI template tests.
|
|
409
|
+
const isNoDetachedMode = process.env.POSTXL_NO_DETACHED_MODE === 'true';
|
|
410
|
+
if (isNoDetachedMode) {
|
|
411
|
+
console.log(`No detached mode enabled. Checking that all files are managed!`);
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
if (isNoDetachedMode) {
|
|
415
|
+
const detachedFiles = results.filter((result) => { var _a; return result.status === 'skip' && !Buffer.isBuffer((_a = result.disk) === null || _a === void 0 ? void 0 : _a.content); });
|
|
416
|
+
if (detachedFiles.length > 0) {
|
|
417
|
+
console.log(`Detached files found: ${detachedFiles.map((f) => f.path).join(', ')}`);
|
|
418
|
+
}
|
|
419
|
+
process.exit(1);
|
|
420
|
+
}
|
|
407
421
|
// NOTE: Lastly we generate the log of the changes.
|
|
408
422
|
const log = lock_1.ConsoleUtils.getFilesChangelog(results.map((result) => {
|
|
409
423
|
if (result.status === 'write') {
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.generateModelExportEncoder = void 0;
|
|
4
4
|
const imports_1 = require("../../../lib/imports");
|
|
5
|
+
const meta_1 = require("../../../lib/meta");
|
|
5
6
|
const types_1 = require("../../../lib/schema/types");
|
|
6
7
|
/**
|
|
7
8
|
* Creates an encoder for the Seed Excel template.
|
|
@@ -13,15 +14,13 @@ function generateModelExportEncoder({ model, meta }) {
|
|
|
13
14
|
[meta.import.decoder.location.import]: [meta.import.decoder.rowDecoderName],
|
|
14
15
|
});
|
|
15
16
|
const { userFriendlyName: userFriendly, userFriendlyNamePlural: userFriendlyPlural } = meta;
|
|
17
|
+
const fieldEncoders = generateFieldEncoders({ model });
|
|
16
18
|
/* prettier-ignore */
|
|
17
19
|
return `
|
|
18
20
|
import * as z from 'zod'
|
|
19
21
|
|
|
20
|
-
import { capitalizeKeys } from '@postxl/runtime'
|
|
21
|
-
|
|
22
22
|
${imports.generate()}
|
|
23
23
|
|
|
24
|
-
|
|
25
24
|
/**
|
|
26
25
|
* The type for rows in the ${userFriendly} table
|
|
27
26
|
*/
|
|
@@ -31,7 +30,9 @@ function generateModelExportEncoder({ model, meta }) {
|
|
|
31
30
|
/**
|
|
32
31
|
* Converts a ${userFriendly} to an Excel row
|
|
33
32
|
*/
|
|
34
|
-
export const ${itemEncoderFunctionName} = (item: ${model.typeName}): ${encodedExcelType} =>
|
|
33
|
+
export const ${itemEncoderFunctionName} = (item: ${model.typeName}): ${encodedExcelType} => ({
|
|
34
|
+
${fieldEncoders.join(',\n')}
|
|
35
|
+
})
|
|
35
36
|
|
|
36
37
|
/**
|
|
37
38
|
* Converts a list of ${userFriendlyPlural} to an Excel table
|
|
@@ -40,3 +41,11 @@ function generateModelExportEncoder({ model, meta }) {
|
|
|
40
41
|
`;
|
|
41
42
|
}
|
|
42
43
|
exports.generateModelExportEncoder = generateModelExportEncoder;
|
|
44
|
+
function generateFieldEncoders({ model }) {
|
|
45
|
+
const fieldEncoders = [];
|
|
46
|
+
for (const field of model.fields) {
|
|
47
|
+
const fieldMeta = (0, meta_1.getFieldMetadata)({ field });
|
|
48
|
+
fieldEncoders.push(`"${fieldMeta.excelColumnName}": item.${field.name}`);
|
|
49
|
+
}
|
|
50
|
+
return fieldEncoders;
|
|
51
|
+
}
|
|
@@ -17,11 +17,10 @@ function generateModelImportDecoder({ model, meta }) {
|
|
|
17
17
|
});
|
|
18
18
|
const { userFriendlyName: userFriendly, internalSingularNameCapitalized: singularCapitalized } = meta;
|
|
19
19
|
const { fieldDecoders, blankFieldDecoders } = generateFieldDecoders({ model, meta, imports });
|
|
20
|
+
const fieldTransformers = generateFieldTransformers({ model });
|
|
20
21
|
return `
|
|
21
22
|
import * as z from 'zod'
|
|
22
23
|
|
|
23
|
-
import { uncapitalizeKeys } from '@postxl/runtime'
|
|
24
|
-
|
|
25
24
|
${imports.generate()}
|
|
26
25
|
|
|
27
26
|
/**
|
|
@@ -52,7 +51,10 @@ const blank${singularCapitalized}RowDecoder = z
|
|
|
52
51
|
export const ${tableImportDecoder} = z
|
|
53
52
|
.array(
|
|
54
53
|
blank${singularCapitalized}RowDecoder
|
|
55
|
-
.or(${meta.import.decoder.rowDecoderName}
|
|
54
|
+
.or(${meta.import.decoder.rowDecoderName}
|
|
55
|
+
.transform(item => ({
|
|
56
|
+
${fieldTransformers.join(',\n')}
|
|
57
|
+
})))
|
|
56
58
|
)
|
|
57
59
|
.transform((items) => items.filter(Boolean) as ${model.typeName}[])`;
|
|
58
60
|
}
|
|
@@ -63,11 +65,11 @@ function generateFieldDecoders({ model, meta, imports, }) {
|
|
|
63
65
|
for (const field of model.fields) {
|
|
64
66
|
const fieldMeta = (0, meta_1.getFieldMetadata)({ field });
|
|
65
67
|
const optionalModifier = field.attributes.isUpdatedAt || field.attributes.isCreatedAt ? '.optional()' : '';
|
|
66
|
-
blankFieldDecoders.push(
|
|
68
|
+
blankFieldDecoders.push(`'${fieldMeta.excelColumnName}': excelNullOrBlankDecoder${optionalModifier}`);
|
|
67
69
|
switch (field.kind) {
|
|
68
70
|
case 'id': {
|
|
69
71
|
imports.addImport({ items: [meta.types.toBrandedIdTypeFnName], from: meta.types.importPath });
|
|
70
|
-
fieldDecoders.push(
|
|
72
|
+
fieldDecoders.push(`'${fieldMeta.excelColumnName}': ${toExcelDecoder({
|
|
71
73
|
tsTypeName: field.unbrandedTypeName,
|
|
72
74
|
nullable: false,
|
|
73
75
|
imports,
|
|
@@ -75,7 +77,7 @@ function generateFieldDecoders({ model, meta, imports, }) {
|
|
|
75
77
|
break;
|
|
76
78
|
}
|
|
77
79
|
case 'scalar': {
|
|
78
|
-
fieldDecoders.push(
|
|
80
|
+
fieldDecoders.push(`'${fieldMeta.excelColumnName}': ${toExcelDecoder({
|
|
79
81
|
tsTypeName: field.tsTypeName,
|
|
80
82
|
nullable: !field.isRequired,
|
|
81
83
|
imports,
|
|
@@ -86,7 +88,7 @@ function generateFieldDecoders({ model, meta, imports, }) {
|
|
|
86
88
|
const refModel = field.relationToModel;
|
|
87
89
|
const refMeta = (0, meta_1.getModelMetadata)({ model: refModel });
|
|
88
90
|
imports.addImport({ items: [refMeta.types.toBrandedIdTypeFnName], from: refMeta.types.importPath });
|
|
89
|
-
fieldDecoders.push(
|
|
91
|
+
fieldDecoders.push(`'${fieldMeta.excelColumnName}': ${toExcelDecoder({
|
|
90
92
|
tsTypeName: field.unbrandedTypeName,
|
|
91
93
|
nullable: !field.isRequired,
|
|
92
94
|
imports,
|
|
@@ -98,7 +100,7 @@ function generateFieldDecoders({ model, meta, imports, }) {
|
|
|
98
100
|
break;
|
|
99
101
|
}
|
|
100
102
|
case 'enum': {
|
|
101
|
-
fieldDecoders.push(
|
|
103
|
+
fieldDecoders.push(`'${fieldMeta.excelColumnName}': z.enum([
|
|
102
104
|
${field.enumerator.values.map((v) => `'${v}'`).join(', ')}
|
|
103
105
|
])${field.isRequired ? '' : '.nullable()'}${optionalModifier}`);
|
|
104
106
|
break;
|
|
@@ -136,3 +138,11 @@ function toExcelDecoder({ tsTypeName, nullable, imports, }) {
|
|
|
136
138
|
throw new Error('Unknown type');
|
|
137
139
|
}
|
|
138
140
|
}
|
|
141
|
+
function generateFieldTransformers({ model }) {
|
|
142
|
+
const fieldTransformers = [];
|
|
143
|
+
for (const field of model.fields) {
|
|
144
|
+
const fieldMeta = (0, meta_1.getFieldMetadata)({ field });
|
|
145
|
+
fieldTransformers.push(`${field.name}: item['${fieldMeta.excelColumnName}']`);
|
|
146
|
+
}
|
|
147
|
+
return fieldTransformers;
|
|
148
|
+
}
|
|
@@ -2,23 +2,19 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.generateModelContext = void 0;
|
|
4
4
|
const imports_1 = require("../../../lib/imports");
|
|
5
|
-
const string_1 = require("../../../lib/utils/string");
|
|
6
5
|
/**
|
|
7
6
|
* Utility component that generates the definition of the React context for a given model.
|
|
8
7
|
*/
|
|
9
8
|
function generateModelContext({ model, meta }) {
|
|
10
|
-
const queryName = (0, string_1.toCamelCase)(model.name);
|
|
11
9
|
const imports = imports_1.ImportsGenerator.from(meta.react.folderPath).addTypeImport({
|
|
12
|
-
items: [model.typeName, model.brandedIdType],
|
|
13
10
|
from: meta.types.importPath,
|
|
11
|
+
items: [model.typeName, model.brandedIdType],
|
|
14
12
|
});
|
|
15
13
|
const resultMapTypeDefinition = `Map<${model.brandedIdType}, ${model.typeName}>`;
|
|
16
14
|
return `
|
|
17
|
-
|
|
18
|
-
import React, { useMemo } from 'react'
|
|
15
|
+
import { useMemo } from 'react'
|
|
19
16
|
|
|
20
17
|
import { trpc } from '@lib/trpc'
|
|
21
|
-
import { filterMap, mapMap } from '@postxl/runtime'
|
|
22
18
|
|
|
23
19
|
${imports.generate()}
|
|
24
20
|
|
|
@@ -32,17 +28,17 @@ type ProviderHookType = {
|
|
|
32
28
|
* A React utility hook to access the model data in the generated forms.
|
|
33
29
|
*/
|
|
34
30
|
export const ${meta.react.context.hookFnName} = (): ProviderHookType => {
|
|
35
|
-
const
|
|
31
|
+
const dataQuery = trpc.${meta.trpc.getMap.reactQueryMethod}.useQuery()
|
|
36
32
|
|
|
37
33
|
const value = useMemo<ProviderHookType>(() => {
|
|
38
|
-
const data: ${resultMapTypeDefinition} =
|
|
34
|
+
const data: ${resultMapTypeDefinition} = dataQuery.data ?? new ${resultMapTypeDefinition}()
|
|
39
35
|
|
|
40
36
|
return {
|
|
41
|
-
ready:
|
|
37
|
+
ready: dataQuery.isLoading === false,
|
|
42
38
|
map: data,
|
|
43
39
|
list: Array.from(data.values())
|
|
44
40
|
}
|
|
45
|
-
}, [
|
|
41
|
+
}, [dataQuery.isLoading, dataQuery.data])
|
|
46
42
|
|
|
47
43
|
return value
|
|
48
44
|
}
|
|
@@ -574,7 +574,7 @@ function getFormFieldComponents({ model, testIdCollector, }) {
|
|
|
574
574
|
continue;
|
|
575
575
|
}
|
|
576
576
|
const formikFieldName = getFormikFieldName(field.name);
|
|
577
|
-
const label =
|
|
577
|
+
const label = field.attributes.label;
|
|
578
578
|
switch (field.kind) {
|
|
579
579
|
case 'id': {
|
|
580
580
|
// NOTE: We never show the ID field in the form even if it's in the type signature of the form input.
|
|
@@ -5,9 +5,7 @@ const imports_1 = require("../../../lib/imports");
|
|
|
5
5
|
const meta_1 = require("../../../lib/meta");
|
|
6
6
|
const fields_1 = require("../../../lib/schema/fields");
|
|
7
7
|
const types_1 = require("../../../lib/schema/types");
|
|
8
|
-
const types_2 = require("../../../lib/types");
|
|
9
8
|
const ast_1 = require("../../../lib/utils/ast");
|
|
10
|
-
const jsdoc_1 = require("../../../lib/utils/jsdoc");
|
|
11
9
|
/**
|
|
12
10
|
* Generates view business logic for a given model.
|
|
13
11
|
* The view logic exposes all information and links of a model. See template's readme for more info.
|
|
@@ -20,91 +18,64 @@ function generateModelBusinessLogicView({ model, meta }) {
|
|
|
20
18
|
[meta.types.importPath]: [(0, types_1.toAnnotatedTypeName)(model.brandedIdType), (0, types_1.toAnnotatedTypeName)(meta.types.typeName)],
|
|
21
19
|
[schemaMeta.view.serviceLocation.path]: schemaMeta.view.serviceClassName,
|
|
22
20
|
});
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
for (const relation of (0, fields_1.getRelationFields)(model)) {
|
|
38
|
-
const refModel = relation.relationToModel;
|
|
39
|
-
const refMeta = (0, meta_1.getModelMetadata)({ model: refModel });
|
|
40
|
-
const variableGetter = `await this.${viewServiceClassName}.${refMeta.view.serviceVariableName}.get(itemRaw.${relation.name})`;
|
|
41
|
-
const variablePresenceCheck = `
|
|
42
|
-
if (!${relation.relationFieldName}) {
|
|
43
|
-
throw new Error(\`Could not find ${refMeta.types.typeName} with id \${itemRaw.${relation.name}} for ${model.typeName}.${relation.name}!\`)
|
|
44
|
-
}
|
|
45
|
-
`;
|
|
46
|
-
const relationVariableName = relation.relationFieldName;
|
|
47
|
-
const relationVariableDefinition = relation.isRequired
|
|
48
|
-
? `${variableGetter};${variablePresenceCheck}`
|
|
49
|
-
: `itemRaw.${relation.name} !== null ? ${variableGetter} : null`;
|
|
50
|
-
variables.set(relation.name, {
|
|
51
|
-
variableName: relationVariableName,
|
|
52
|
-
variableDefinition: `const ${relationVariableName} = ${relationVariableDefinition}`,
|
|
53
|
-
});
|
|
54
|
-
if (relation.relationToModel.typeName !== model.typeName) {
|
|
55
|
-
imports.addImport({ from: refMeta.types.importPath, items: [refMeta.types.toBrandedIdTypeFnName] });
|
|
56
|
-
imports.addTypeImport({ from: refMeta.types.importPath, items: [refModel.brandedIdType, refMeta.types.typeName] });
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
const hasLinkedItems = variables.size > 0;
|
|
60
|
-
if (hasLinkedItems) {
|
|
61
|
-
// NOTE: If we need to generate the linked item type, we need to import the enum types.
|
|
62
|
-
for (const enumField of (0, fields_1.getEnumFields)(model)) {
|
|
63
|
-
const enumMeta = (0, meta_1.getEnumMetadata)(enumField);
|
|
64
|
-
imports.addTypeImport({ from: enumMeta.types.importPath, items: [enumField.typeName] });
|
|
21
|
+
const compareFnBlock = (0, ast_1.createSwitchStatement)({
|
|
22
|
+
field: 'field',
|
|
23
|
+
cases: (0, fields_1.getScalarFields)({ fields: model.fields, tsTypeName: 'string' }).map((f) => ({
|
|
24
|
+
match: `"${f.name}"`,
|
|
25
|
+
block: `return (a[field] || '').localeCompare(b[field] || '')`,
|
|
26
|
+
})),
|
|
27
|
+
defaultBlock: 'return 0',
|
|
28
|
+
});
|
|
29
|
+
const stringFieldFilters = (0, fields_1.getScalarFields)({ fields: model.fields, tsTypeName: 'string' }).map((f) => {
|
|
30
|
+
return {
|
|
31
|
+
match: `"${f.name}"`,
|
|
32
|
+
block: `
|
|
33
|
+
if (typeof value !== 'string') {
|
|
34
|
+
return false
|
|
65
35
|
}
|
|
66
|
-
}
|
|
67
|
-
const linkedItemsGetterFn = `
|
|
68
|
-
/**
|
|
69
|
-
* Returns the linked ${meta.userFriendlyName} with the given id or null if it does not exist.
|
|
70
|
-
* Linked: The ${meta.userFriendlyName} contains the linked (raw) items themselves, not only the ids.
|
|
71
|
-
*/
|
|
72
|
-
public async getLinkedItem(id: ${model.brandedIdType}): Promise<${meta.types.linkedTypeName} | null> {
|
|
73
|
-
const itemRaw = await this.data.get(id)
|
|
74
|
-
if (!itemRaw) {
|
|
75
|
-
return null
|
|
76
|
-
}
|
|
77
36
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
37
|
+
switch (operator) {
|
|
38
|
+
case 'contains': {
|
|
39
|
+
return (item[field] || '').toLowerCase().includes(value.toLowerCase())
|
|
40
|
+
}
|
|
41
|
+
default: {
|
|
42
|
+
return false
|
|
43
|
+
}
|
|
85
44
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
45
|
+
`,
|
|
46
|
+
};
|
|
47
|
+
});
|
|
48
|
+
const numberFieldsFilters = (0, fields_1.getScalarFields)({ fields: model.fields, tsTypeName: 'number' }).map((f) => {
|
|
49
|
+
return {
|
|
50
|
+
match: `"${f.name}"`,
|
|
51
|
+
block: `
|
|
52
|
+
const toCompare = item[field]
|
|
53
|
+
if (typeof value !== 'number' || toCompare === null) {
|
|
54
|
+
return false
|
|
89
55
|
}
|
|
90
|
-
return `${linkedRel.variableName}`;
|
|
91
|
-
})
|
|
92
|
-
.join(',\n')}
|
|
93
|
-
}
|
|
94
56
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
57
|
+
switch (operator) {
|
|
58
|
+
case 'eq': {
|
|
59
|
+
return item[field] === value
|
|
60
|
+
}
|
|
61
|
+
case 'gt': {
|
|
62
|
+
return toCompare > value
|
|
63
|
+
}
|
|
64
|
+
case 'lt': {
|
|
65
|
+
return toCompare < value
|
|
66
|
+
}
|
|
67
|
+
default: {
|
|
68
|
+
return false
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
`,
|
|
72
|
+
};
|
|
73
|
+
});
|
|
74
|
+
const filterFnBlock = (0, ast_1.createSwitchStatement)({
|
|
75
|
+
field: 'field',
|
|
76
|
+
cases: [...stringFieldFilters, ...numberFieldsFilters],
|
|
77
|
+
defaultBlock: 'return false',
|
|
78
|
+
});
|
|
108
79
|
return /* ts */ `
|
|
109
80
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
110
81
|
import z from 'zod'
|
|
@@ -113,8 +84,6 @@ import { Inject, Injectable, forwardRef } from '@nestjs/common'
|
|
|
113
84
|
|
|
114
85
|
${imports.generate()}
|
|
115
86
|
|
|
116
|
-
${hasLinkedItems ? linkedTypeDefinition : ''}
|
|
117
|
-
|
|
118
87
|
/**
|
|
119
88
|
* Cursor decoder.
|
|
120
89
|
*/
|
|
@@ -144,7 +113,11 @@ type FilterOperator = z.infer<typeof ${meta.view.filterOperatorDecoder}>
|
|
|
144
113
|
|
|
145
114
|
@Injectable()
|
|
146
115
|
export class ${meta.view.serviceClassName} {
|
|
147
|
-
constructor(
|
|
116
|
+
constructor(
|
|
117
|
+
public readonly data: ${meta.data.repository.className},
|
|
118
|
+
@Inject(forwardRef(() => ${schemaMeta.view.serviceClassName}))
|
|
119
|
+
private readonly viewService: ${schemaMeta.view.serviceClassName}
|
|
120
|
+
) {}
|
|
148
121
|
|
|
149
122
|
/**
|
|
150
123
|
* Returns the raw ${meta.userFriendlyName} with the given id or null if it does not exist.
|
|
@@ -154,8 +127,6 @@ export class ${meta.view.serviceClassName} {
|
|
|
154
127
|
return this.data.get(id)
|
|
155
128
|
}
|
|
156
129
|
|
|
157
|
-
${hasLinkedItems ? linkedItemsGetterFn : ''}
|
|
158
|
-
|
|
159
130
|
/**
|
|
160
131
|
* Returns a map of all ${meta.userFriendlyNamePlural}.
|
|
161
132
|
*/
|
|
@@ -192,68 +163,13 @@ export class ${meta.view.serviceClassName} {
|
|
|
192
163
|
|
|
193
164
|
// Utility Functions
|
|
194
165
|
|
|
195
|
-
${_createModelCompareFn({ model })}
|
|
196
|
-
|
|
197
|
-
${_createModelFilterFn({ model })}
|
|
198
|
-
`;
|
|
199
|
-
}
|
|
200
|
-
exports.generateModelBusinessLogicView = generateModelBusinessLogicView;
|
|
201
166
|
/**
|
|
202
|
-
*
|
|
203
|
-
* of a model using a given operator on a given field.
|
|
167
|
+
* Compares two ${model.typeName} instances by the given field.
|
|
204
168
|
*/
|
|
205
|
-
function
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
match: `"${f.name}"`,
|
|
209
|
-
block: `
|
|
210
|
-
if (typeof value !== 'string') {
|
|
211
|
-
return false
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
switch (operator) {
|
|
215
|
-
case 'contains': {
|
|
216
|
-
return (item[field] || '').toLowerCase().includes(value.toLowerCase())
|
|
217
|
-
}
|
|
218
|
-
default: {
|
|
219
|
-
return false
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
`,
|
|
223
|
-
};
|
|
224
|
-
});
|
|
225
|
-
const numberFieldsFilters = (0, fields_1.getScalarFields)({ fields: model.fields, tsTypeName: 'number' }).map((f) => {
|
|
226
|
-
return {
|
|
227
|
-
match: `"${f.name}"`,
|
|
228
|
-
block: `
|
|
229
|
-
const toCompare = item[field]
|
|
230
|
-
if (typeof value !== 'number' || toCompare === null) {
|
|
231
|
-
return false
|
|
232
|
-
}
|
|
169
|
+
function compare(a: ${model.typeName}, b: ${model.typeName}, field: keyof ${model.typeName}) {
|
|
170
|
+
${compareFnBlock}
|
|
171
|
+
}
|
|
233
172
|
|
|
234
|
-
switch (operator) {
|
|
235
|
-
case 'eq': {
|
|
236
|
-
return item[field] === value
|
|
237
|
-
}
|
|
238
|
-
case 'gt': {
|
|
239
|
-
return toCompare > value
|
|
240
|
-
}
|
|
241
|
-
case 'lt': {
|
|
242
|
-
return toCompare < value
|
|
243
|
-
}
|
|
244
|
-
default: {
|
|
245
|
-
return false
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
`,
|
|
249
|
-
};
|
|
250
|
-
});
|
|
251
|
-
const fnBlock = (0, ast_1.createSwitchStatement)({
|
|
252
|
-
field: 'field',
|
|
253
|
-
cases: [...stringFieldFilters, ...numberFieldsFilters],
|
|
254
|
-
defaultBlock: 'return false',
|
|
255
|
-
});
|
|
256
|
-
return `
|
|
257
173
|
/**
|
|
258
174
|
* Filters the given ${model.typeName} by the given field, using provided operator and value.
|
|
259
175
|
*/
|
|
@@ -263,52 +179,8 @@ function filterFn(
|
|
|
263
179
|
operator: FilterOperator,
|
|
264
180
|
value: string | number
|
|
265
181
|
): boolean {
|
|
266
|
-
${
|
|
182
|
+
${filterFnBlock}
|
|
267
183
|
}
|
|
268
184
|
`;
|
|
269
185
|
}
|
|
270
|
-
|
|
271
|
-
* Returns a utility compare function that lets you compare two instances of a given model
|
|
272
|
-
* by a given field.
|
|
273
|
-
*/
|
|
274
|
-
function _createModelCompareFn({ model }) {
|
|
275
|
-
const stringFieldComparators = (0, fields_1.getScalarFields)({ fields: model.fields, tsTypeName: 'string' }).map((f) => {
|
|
276
|
-
return {
|
|
277
|
-
match: `"${f.name}"`,
|
|
278
|
-
block: `
|
|
279
|
-
return (a[field] || '').localeCompare(b[field] || '')
|
|
280
|
-
`,
|
|
281
|
-
};
|
|
282
|
-
});
|
|
283
|
-
const fnBlock = (0, ast_1.createSwitchStatement)({
|
|
284
|
-
field: 'field',
|
|
285
|
-
cases: [...stringFieldComparators],
|
|
286
|
-
defaultBlock: 'return 0',
|
|
287
|
-
});
|
|
288
|
-
return `
|
|
289
|
-
/**
|
|
290
|
-
* Compares two ${model.typeName} instances by the given field.
|
|
291
|
-
*/
|
|
292
|
-
function compare(a: ${model.typeName}, b: ${model.typeName}, field: keyof ${model.typeName}) {
|
|
293
|
-
${fnBlock}
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
`;
|
|
297
|
-
}
|
|
298
|
-
/**
|
|
299
|
-
* Converts a field to a TypeScript type definition with linked fields.
|
|
300
|
-
*/
|
|
301
|
-
function getLinkedFieldType(f) {
|
|
302
|
-
switch (f.kind) {
|
|
303
|
-
case 'enum':
|
|
304
|
-
return `${f.name}: ${f.typeName}`;
|
|
305
|
-
case 'relation':
|
|
306
|
-
return `${f.relationFieldName}: ${f.relationToModel.typeName}`;
|
|
307
|
-
case 'id':
|
|
308
|
-
return `${f.name}: ${f.model.brandedIdType}`;
|
|
309
|
-
case 'scalar':
|
|
310
|
-
return `${f.name}: ${f.tsTypeName}`;
|
|
311
|
-
default:
|
|
312
|
-
throw new types_2.ExhaustiveSwitchCheck(f);
|
|
313
|
-
}
|
|
314
|
-
}
|
|
186
|
+
exports.generateModelBusinessLogicView = generateModelBusinessLogicView;
|
package/dist/lib/attributes.d.ts
CHANGED
|
@@ -81,10 +81,21 @@ export type FieldAttributes = {
|
|
|
81
81
|
*/
|
|
82
82
|
maxLength?: number;
|
|
83
83
|
/**
|
|
84
|
-
* Schema tag: `@@
|
|
84
|
+
* Schema tag: `@@IsNameField()`
|
|
85
85
|
* The field that should be used as the default label for the model.
|
|
86
|
+
*
|
|
87
|
+
* If this is set, the repository will verify that exactly one record has this field set to true - and use it as the default label.
|
|
88
|
+
*
|
|
89
|
+
* If this attribute is set, this field will be used in the code/UI as the field that specifies the model's name. If not provided, we check for the existence of the`name` field. If that is not found, we use the `id` field as the name field.
|
|
90
|
+
*/
|
|
91
|
+
isNameField?: boolean;
|
|
92
|
+
/**
|
|
93
|
+
* Schema tag: `@@Label("Label")`
|
|
94
|
+
* The human readable label/name of the field.
|
|
95
|
+
*
|
|
96
|
+
* If not provided, we use the field name.
|
|
86
97
|
*/
|
|
87
|
-
|
|
98
|
+
label: string;
|
|
88
99
|
};
|
|
89
100
|
export type EnumAttributes = {
|
|
90
101
|
/**
|
package/dist/lib/meta.js
CHANGED
|
@@ -402,7 +402,7 @@ function getFieldMetadata({ field }) {
|
|
|
402
402
|
tsFieldName: Types.toVariableName((0, string_1.toCamelCase)(field.name)),
|
|
403
403
|
getByForeignKeyMethodFnName: Types.toFunctionName(`getItemsFor${PascalCase}`),
|
|
404
404
|
getByForeignKeyIdsMethodFnName: Types.toFunctionName(`getIdsFor${PascalCase}`),
|
|
405
|
-
excelColumnName:
|
|
405
|
+
excelColumnName: field.attributes.label,
|
|
406
406
|
};
|
|
407
407
|
}
|
|
408
408
|
exports.getFieldMetadata = getFieldMetadata;
|
|
@@ -284,9 +284,9 @@ export type FieldScalar = Prettify<Omit<FieldCore, 'name'> & {
|
|
|
284
284
|
export type FieldId = Prettify<Omit<FieldCore, 'name'> & {
|
|
285
285
|
kind: 'id';
|
|
286
286
|
/**
|
|
287
|
-
* Name of the field and variable (
|
|
287
|
+
* Name of the field and variable (must be `id`)
|
|
288
288
|
*/
|
|
289
|
-
name: Types.
|
|
289
|
+
name: Types.IdFieldName;
|
|
290
290
|
/**
|
|
291
291
|
* Model that this ID field identifies.
|
|
292
292
|
*/
|
|
@@ -314,9 +314,9 @@ export type FieldId = Prettify<Omit<FieldCore, 'name'> & {
|
|
|
314
314
|
export declare const isFieldId: (field: Field) => field is Prettify<Omit<FieldCore, "name"> & {
|
|
315
315
|
kind: 'id';
|
|
316
316
|
/**
|
|
317
|
-
* Name of the field and variable (
|
|
317
|
+
* Name of the field and variable (must be `id`)
|
|
318
318
|
*/
|
|
319
|
-
name: Types.
|
|
319
|
+
name: Types.IdFieldName;
|
|
320
320
|
/**
|
|
321
321
|
* Model that this ID field identifies.
|
|
322
322
|
*/
|
|
@@ -71,6 +71,16 @@ export declare const isAnnotatedTypeName: (t: string | AnnotatedTypeName) => t i
|
|
|
71
71
|
export type FieldName = string & {
|
|
72
72
|
readonly ___type: 'FieldName';
|
|
73
73
|
};
|
|
74
|
+
/**
|
|
75
|
+
* Brands a raw string to an IdFieldName identifier.
|
|
76
|
+
*/
|
|
77
|
+
export declare const ID_FIELD_NAME: IdFieldName;
|
|
78
|
+
/**
|
|
79
|
+
* The name of the id field. Must be `id`.
|
|
80
|
+
*/
|
|
81
|
+
export type IdFieldName = 'id' & {
|
|
82
|
+
readonly ___type: 'FieldName';
|
|
83
|
+
};
|
|
74
84
|
/**
|
|
75
85
|
* Brands a raw string to a FieldName identifier.
|
|
76
86
|
*/
|
package/dist/lib/schema/types.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
// MARK: - Generated content related types
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
-
exports.toPackageName = exports.toPath = exports.toBackendModulePath = exports.toModuleLocation = exports.toFolderName = exports.toFileName = exports.toVariableName = exports.toFunctionName = exports.toEnumName = exports.toFieldName = exports.isAnnotatedTypeName = exports.toAnnotatedTypeName = exports.toTypeName = exports.toModelName = exports.toDiscriminantName = exports.toClassName = void 0;
|
|
4
|
+
exports.toPackageName = exports.toPath = exports.toBackendModulePath = exports.toModuleLocation = exports.toFolderName = exports.toFileName = exports.toVariableName = exports.toFunctionName = exports.toEnumName = exports.toFieldName = exports.ID_FIELD_NAME = exports.isAnnotatedTypeName = exports.toAnnotatedTypeName = exports.toTypeName = exports.toModelName = exports.toDiscriminantName = exports.toClassName = void 0;
|
|
5
5
|
/**
|
|
6
6
|
* Converts a raw string to a branded ClassName.
|
|
7
7
|
*/
|
|
@@ -35,6 +35,10 @@ exports.toAnnotatedTypeName = toAnnotatedTypeName;
|
|
|
35
35
|
*/
|
|
36
36
|
const isAnnotatedTypeName = (t) => typeof t === 'object' && t.kind === 'TypeName';
|
|
37
37
|
exports.isAnnotatedTypeName = isAnnotatedTypeName;
|
|
38
|
+
/**
|
|
39
|
+
* Brands a raw string to an IdFieldName identifier.
|
|
40
|
+
*/
|
|
41
|
+
exports.ID_FIELD_NAME = 'id';
|
|
38
42
|
/**
|
|
39
43
|
* Brands a raw string to a FieldName identifier.
|
|
40
44
|
*/
|
package/dist/lib/utils/error.js
CHANGED
|
@@ -34,7 +34,6 @@ function extractNestedErrors(value) {
|
|
|
34
34
|
* As a fallback, it returns a JSON string representation of the error.
|
|
35
35
|
*/
|
|
36
36
|
function formatZodError(e) {
|
|
37
|
-
console.log(e);
|
|
38
37
|
const formatted = e.format();
|
|
39
38
|
if (typeof formatted === 'string') {
|
|
40
39
|
return formatted;
|
|
@@ -138,7 +138,8 @@ function getFieldAttributes(field) {
|
|
|
138
138
|
ignore: blankStringBooleanDecoder,
|
|
139
139
|
description: zod_1.default.string().optional(),
|
|
140
140
|
isDefault: blankStringBooleanDecoder,
|
|
141
|
-
|
|
141
|
+
isNameField: blankStringBooleanDecoder,
|
|
142
|
+
label: zod_1.default.string().optional(),
|
|
142
143
|
example: examplesDecoder,
|
|
143
144
|
examples: examplesDecoder,
|
|
144
145
|
maxLength: zod_1.default
|
|
@@ -148,7 +149,7 @@ function getFieldAttributes(field) {
|
|
|
148
149
|
readonly: blankStringBooleanDecoder,
|
|
149
150
|
})
|
|
150
151
|
.transform((obj) => {
|
|
151
|
-
var _a;
|
|
152
|
+
var _a, _b;
|
|
152
153
|
if (isPrismaIgnored && !obj.ignore) {
|
|
153
154
|
throw new Error(`Field ${field.name} is ignored by Prisma, but is missing the "ignore" PostXL attribute!`);
|
|
154
155
|
}
|
|
@@ -156,11 +157,12 @@ function getFieldAttributes(field) {
|
|
|
156
157
|
ignore: obj.ignore,
|
|
157
158
|
description: obj.description,
|
|
158
159
|
isDefaultField: obj.isDefault,
|
|
159
|
-
|
|
160
|
+
isNameField: obj.isNameField,
|
|
161
|
+
label: (_a = obj.label) !== null && _a !== void 0 ? _a : (0, string_1.toPascalCase)(field.name),
|
|
160
162
|
examples: obj.examples || obj.example,
|
|
161
163
|
maxLength: obj.maxLength,
|
|
162
164
|
isReadonly: obj.readonly || field.isGenerated || field.isUpdatedAt || field.name === 'createdAt' || field.isId,
|
|
163
|
-
isUpdatedAt: (
|
|
165
|
+
isUpdatedAt: (_b = field.isUpdatedAt) !== null && _b !== void 0 ? _b : false,
|
|
164
166
|
isCreatedAt: field.name === 'createdAt',
|
|
165
167
|
};
|
|
166
168
|
});
|
package/dist/prisma/parse.js
CHANGED
|
@@ -226,7 +226,7 @@ function parseModel({ dmmfModel, enums, models, config, }) {
|
|
|
226
226
|
}
|
|
227
227
|
if (dmmfField.isId) {
|
|
228
228
|
const isGeneratedField = isAutoIncrementField(dmmfField) || isUUIDField(dmmfField);
|
|
229
|
-
const _field = Object.assign(Object.assign({ kind: 'id' }, shared), { isUnique: isUniqueField(dmmfField), isGenerated: isGeneratedField, unbrandedTypeName: getTsTypeForId(dmmfField), model: core });
|
|
229
|
+
const _field = Object.assign(Object.assign({ kind: 'id' }, shared), { name: Types.ID_FIELD_NAME, isUnique: isUniqueField(dmmfField), isGenerated: isGeneratedField, unbrandedTypeName: getTsTypeForId(dmmfField), model: core });
|
|
230
230
|
return _field;
|
|
231
231
|
}
|
|
232
232
|
if (dmmfField.kind === 'scalar') {
|
|
@@ -257,7 +257,7 @@ function parseModel({ dmmfModel, enums, models, config, }) {
|
|
|
257
257
|
(0, error_1.throwError)(`${fieldName} is not scalar, enum nor relation.`);
|
|
258
258
|
})
|
|
259
259
|
.filter((field) => !isFieldIgnored({ field }));
|
|
260
|
-
const { idField, defaultField, nameField, createdAtField, updatedAtField } =
|
|
260
|
+
const { idField, defaultField, nameField, createdAtField, updatedAtField } = getTaggedFields({ fields, model: core });
|
|
261
261
|
return Object.assign(Object.assign({}, core), { idField,
|
|
262
262
|
defaultField,
|
|
263
263
|
nameField,
|
|
@@ -268,61 +268,60 @@ function parseModel({ dmmfModel, enums, models, config, }) {
|
|
|
268
268
|
references: [] });
|
|
269
269
|
}
|
|
270
270
|
/**
|
|
271
|
-
*
|
|
271
|
+
* Returns special fields that are tagged with special attributes.
|
|
272
272
|
*/
|
|
273
|
-
function
|
|
273
|
+
function getTaggedFields({ fields, model: { name }, }) {
|
|
274
274
|
var _a;
|
|
275
275
|
let idField = undefined;
|
|
276
276
|
let createdAtField = undefined;
|
|
277
277
|
let updatedAtField = undefined;
|
|
278
|
-
let labelField = undefined;
|
|
279
278
|
let nameField = undefined;
|
|
279
|
+
let isNameField = undefined;
|
|
280
280
|
let defaultField = undefined;
|
|
281
281
|
for (const field of fields) {
|
|
282
282
|
const fieldName = (0, logger_1.highlight)(`${name}.${field.name}`);
|
|
283
283
|
switch (field.kind) {
|
|
284
|
+
case 'id': {
|
|
285
|
+
if (idField) {
|
|
286
|
+
(0, error_1.throwError)(`${fieldName} has multiple id fields - ${idField.name} and ${field.name}`);
|
|
287
|
+
}
|
|
288
|
+
idField = field;
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
284
291
|
case 'scalar':
|
|
285
292
|
if (field.name === 'name') {
|
|
286
293
|
nameField = field;
|
|
287
294
|
}
|
|
295
|
+
if (field.attributes.isNameField) {
|
|
296
|
+
if (isNameField != null) {
|
|
297
|
+
(0, error_1.throwError)(`${fieldName} has multiple name fields - ${isNameField.name} and ${field.name}`);
|
|
298
|
+
}
|
|
299
|
+
isNameField = field;
|
|
300
|
+
}
|
|
288
301
|
if (field.attributes.isCreatedAt) {
|
|
289
|
-
if (createdAtField) {
|
|
290
|
-
(0, error_1.throwError)(`${fieldName} has multiple createdAt fields`);
|
|
302
|
+
if (createdAtField != null) {
|
|
303
|
+
(0, error_1.throwError)(`${fieldName} has multiple createdAt fields - ${createdAtField.name} and ${field.name}`);
|
|
291
304
|
}
|
|
292
305
|
createdAtField = field;
|
|
293
306
|
}
|
|
294
307
|
if (field.attributes.isUpdatedAt) {
|
|
295
|
-
if (updatedAtField) {
|
|
296
|
-
(0, error_1.throwError)(`${fieldName} has multiple updatedAt fields`);
|
|
308
|
+
if (updatedAtField != null) {
|
|
309
|
+
(0, error_1.throwError)(`${fieldName} has multiple updatedAt fields - ${updatedAtField.name} and ${field.name}`);
|
|
297
310
|
}
|
|
298
311
|
updatedAtField = field;
|
|
299
312
|
}
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
313
|
+
if (field.attributes.isDefaultField) {
|
|
314
|
+
if (defaultField != null) {
|
|
315
|
+
(0, error_1.throwError)(`${fieldName} has multiple default fields - ${defaultField.name} and ${field.name}`);
|
|
316
|
+
}
|
|
317
|
+
defaultField = field;
|
|
304
318
|
}
|
|
305
|
-
idField = field;
|
|
306
319
|
break;
|
|
307
320
|
case 'relation':
|
|
308
321
|
break;
|
|
309
322
|
case 'enum':
|
|
310
323
|
break;
|
|
311
324
|
}
|
|
312
|
-
//handle default case
|
|
313
|
-
if (field.attributes.isDefaultField && field.kind === 'scalar') {
|
|
314
|
-
if (defaultField !== undefined) {
|
|
315
|
-
(0, error_1.throwError)(`${fieldName} has multiple default fields`);
|
|
316
|
-
}
|
|
317
|
-
defaultField = field;
|
|
318
|
-
}
|
|
319
|
-
//handle name field
|
|
320
|
-
if (field.attributes.isLabel) {
|
|
321
|
-
if (labelField !== undefined) {
|
|
322
|
-
(0, error_1.throwError)(`${fieldName} has multiple name fields`);
|
|
323
|
-
}
|
|
324
|
-
labelField = field;
|
|
325
|
-
}
|
|
326
325
|
}
|
|
327
326
|
if (!idField) {
|
|
328
327
|
(0, error_1.throwError)(`Model ${(0, logger_1.highlight)(name)} does not have an id field`);
|
|
@@ -330,7 +329,7 @@ function validateFields({ fields, model: { name } }) {
|
|
|
330
329
|
return {
|
|
331
330
|
idField,
|
|
332
331
|
defaultField,
|
|
333
|
-
nameField: (_a =
|
|
332
|
+
nameField: (_a = isNameField !== null && isNameField !== void 0 ? isNameField : nameField) !== null && _a !== void 0 ? _a : idField,
|
|
334
333
|
createdAtField,
|
|
335
334
|
updatedAtField,
|
|
336
335
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@postxl/generator",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.73.1",
|
|
4
4
|
"main": "./dist/generator.js",
|
|
5
5
|
"typings": "./dist/generator.d.ts",
|
|
6
6
|
"bin": {
|
|
@@ -41,7 +41,6 @@
|
|
|
41
41
|
"prisma": "5.8.1"
|
|
42
42
|
},
|
|
43
43
|
"scripts": {
|
|
44
|
-
"test:generators": "./scripts/test-generators.sh",
|
|
45
44
|
"test:setup": "./scripts/test-setup.sh",
|
|
46
45
|
"test:jest": "jest",
|
|
47
46
|
"test:watch": "jest --watch",
|