@comet/api-generator 8.0.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +24 -0
- package/bin/api-generator.js +5 -0
- package/lib/apiGenerator.d.ts +1 -0
- package/lib/apiGenerator.js +7 -0
- package/lib/commands/generate/generate-command.d.ts +2 -0
- package/lib/commands/generate/generate-command.js +45 -0
- package/lib/commands/generate/generateCrud/generate-crud.d.ts +24 -0
- package/lib/commands/generate/generateCrud/generate-crud.js +1212 -0
- package/lib/commands/generate/generateCrudInput/generate-crud-input.d.ts +11 -0
- package/lib/commands/generate/generateCrudInput/generate-crud-input.js +463 -0
- package/lib/commands/generate/generateCrudSingle/generate-crud-single.d.ts +4 -0
- package/lib/commands/generate/generateCrudSingle/generate-crud-single.js +153 -0
- package/lib/commands/generate/utils/build-name-variants.d.ts +10 -0
- package/lib/commands/generate/utils/build-name-variants.js +25 -0
- package/lib/commands/generate/utils/constants.d.ts +1 -0
- package/lib/commands/generate/utils/constants.js +4 -0
- package/lib/commands/generate/utils/generate-imports-code.d.ts +5 -0
- package/lib/commands/generate/utils/generate-imports-code.js +32 -0
- package/lib/commands/generate/utils/test-helper.d.ts +5 -0
- package/lib/commands/generate/utils/test-helper.js +72 -0
- package/lib/commands/generate/utils/ts-morph-helper.d.ts +10 -0
- package/lib/commands/generate/utils/ts-morph-helper.js +191 -0
- package/lib/commands/generate/utils/write-generated-file.d.ts +1 -0
- package/lib/commands/generate/utils/write-generated-file.js +69 -0
- package/lib/commands/generate/utils/write-generated-files.d.ts +8 -0
- package/lib/commands/generate/utils/write-generated-files.js +20 -0
- package/package.json +65 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type EntityMetadata } from "@mikro-orm/postgresql";
|
|
2
|
+
import { type GeneratedFile } from "../utils/write-generated-files";
|
|
3
|
+
export declare function generateCrudInput(generatorOptions: {
|
|
4
|
+
targetDirectory: string;
|
|
5
|
+
}, metadata: EntityMetadata<any>, options?: {
|
|
6
|
+
nested: boolean;
|
|
7
|
+
fileName?: string;
|
|
8
|
+
className?: string;
|
|
9
|
+
excludeFields: string[];
|
|
10
|
+
generateUpdateInput?: boolean;
|
|
11
|
+
}): Promise<GeneratedFile[]>;
|
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.generateCrudInput = generateCrudInput;
|
|
13
|
+
const cms_api_1 = require("@comet/cms-api");
|
|
14
|
+
const class_validator_1 = require("class-validator");
|
|
15
|
+
const generate_crud_1 = require("../generateCrud/generate-crud");
|
|
16
|
+
const build_name_variants_1 = require("../utils/build-name-variants");
|
|
17
|
+
const constants_1 = require("../utils/constants");
|
|
18
|
+
const generate_imports_code_1 = require("../utils/generate-imports-code");
|
|
19
|
+
const ts_morph_helper_1 = require("../utils/ts-morph-helper");
|
|
20
|
+
function tsCodeRecordToString(object) {
|
|
21
|
+
const filteredEntries = Object.entries(object).filter(([key, value]) => value !== undefined);
|
|
22
|
+
if (filteredEntries.length == 0)
|
|
23
|
+
return "";
|
|
24
|
+
return `{${filteredEntries.map(([key, value]) => `${key}: ${value},`).join("\n")}}`;
|
|
25
|
+
}
|
|
26
|
+
function findReferenceTargetType(targetMeta, referencedColumnName) {
|
|
27
|
+
const referencedColumnProp = targetMeta === null || targetMeta === void 0 ? void 0 : targetMeta.props.find((p) => p.name == referencedColumnName);
|
|
28
|
+
if (!referencedColumnProp)
|
|
29
|
+
throw new Error("referencedColumnProp not found");
|
|
30
|
+
if (referencedColumnProp.type == "uuid") {
|
|
31
|
+
return "uuid";
|
|
32
|
+
}
|
|
33
|
+
else if (referencedColumnProp.type == "string") {
|
|
34
|
+
return "string";
|
|
35
|
+
}
|
|
36
|
+
else if (referencedColumnProp.type == "integer" || referencedColumnProp.type == "int") {
|
|
37
|
+
return "integer";
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function generateCrudInput(generatorOptions_1, metadata_1) {
|
|
44
|
+
return __awaiter(this, arguments, void 0, function* (generatorOptions,
|
|
45
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
46
|
+
metadata, options = {
|
|
47
|
+
nested: false,
|
|
48
|
+
excludeFields: [],
|
|
49
|
+
generateUpdateInput: true,
|
|
50
|
+
}) {
|
|
51
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
|
|
52
|
+
const generatedFiles = [];
|
|
53
|
+
const { dedicatedResolverArgProps } = (0, generate_crud_1.buildOptions)(metadata, generatorOptions);
|
|
54
|
+
const props = metadata.props
|
|
55
|
+
.filter((prop) => {
|
|
56
|
+
return !prop.embedded;
|
|
57
|
+
})
|
|
58
|
+
.filter((prop) => {
|
|
59
|
+
return (0, cms_api_1.hasCrudFieldFeature)(metadata.class, prop.name, "input");
|
|
60
|
+
})
|
|
61
|
+
.filter((prop) => {
|
|
62
|
+
//filter out props that are dedicatedResolverArgProps
|
|
63
|
+
return !dedicatedResolverArgProps.some((dedicatedResolverArgProp) => dedicatedResolverArgProp.name === prop.name);
|
|
64
|
+
})
|
|
65
|
+
.filter((prop) => !options.excludeFields.includes(prop.name));
|
|
66
|
+
let fieldsOut = "";
|
|
67
|
+
const imports = [
|
|
68
|
+
{ name: "IsSlug", importPath: "@comet/cms-api" },
|
|
69
|
+
{ name: "RootBlockInputScalar", importPath: "@comet/cms-api" },
|
|
70
|
+
{ name: "IsNullable", importPath: "@comet/cms-api" },
|
|
71
|
+
{ name: "PartialType", importPath: "@comet/cms-api" },
|
|
72
|
+
{ name: "BlockInputInterface", importPath: "@comet/cms-api" },
|
|
73
|
+
{ name: "isBlockInputInterface", importPath: "@comet/cms-api" },
|
|
74
|
+
];
|
|
75
|
+
for (const prop of props) {
|
|
76
|
+
let type = prop.type;
|
|
77
|
+
const fieldName = prop.name;
|
|
78
|
+
const definedDecorators = (0, ts_morph_helper_1.morphTsProperty)(prop.name, metadata).getDecorators();
|
|
79
|
+
const decorators = [];
|
|
80
|
+
let isOptional = prop.nullable;
|
|
81
|
+
if (prop.name != "position") {
|
|
82
|
+
if (!prop.nullable) {
|
|
83
|
+
decorators.push("@IsNotEmpty()");
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
decorators.push("@IsNullable()");
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (["id", "createdAt", "updatedAt", "scope"].includes(prop.name)) {
|
|
90
|
+
//skip those (TODO find a non-magic solution?)
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
else if (prop.name == "position") {
|
|
94
|
+
const initializer = (_a = (0, ts_morph_helper_1.morphTsProperty)(prop.name, metadata).getInitializer()) === null || _a === void 0 ? void 0 : _a.getText();
|
|
95
|
+
const defaultValue = initializer == "undefined" || initializer == "null" ? "null" : initializer;
|
|
96
|
+
const fieldOptions = tsCodeRecordToString({ nullable: "true", defaultValue });
|
|
97
|
+
isOptional = true;
|
|
98
|
+
decorators.push(`@IsOptional()`);
|
|
99
|
+
decorators.push(`@Min(1)`);
|
|
100
|
+
decorators.push("@IsInt()");
|
|
101
|
+
decorators.push(`@Field(() => Int, ${fieldOptions})`);
|
|
102
|
+
type = "number";
|
|
103
|
+
}
|
|
104
|
+
else if (prop.enum) {
|
|
105
|
+
const initializer = (_b = (0, ts_morph_helper_1.morphTsProperty)(prop.name, metadata).getInitializer()) === null || _b === void 0 ? void 0 : _b.getText();
|
|
106
|
+
const defaultValue = prop.nullable && (initializer == "undefined" || initializer == "null" || initializer === undefined) ? "null" : initializer;
|
|
107
|
+
const fieldOptions = tsCodeRecordToString({ nullable: prop.nullable ? "true" : undefined, defaultValue });
|
|
108
|
+
const enumName = (0, ts_morph_helper_1.findEnumName)(prop.name, metadata);
|
|
109
|
+
const importPath = (0, ts_morph_helper_1.findEnumImportPath)(enumName, `${generatorOptions.targetDirectory}/dto`, metadata);
|
|
110
|
+
imports.push({ name: enumName, importPath });
|
|
111
|
+
decorators.push(`@IsEnum(${enumName})`);
|
|
112
|
+
decorators.push(`@Field(() => ${enumName}, ${fieldOptions})`);
|
|
113
|
+
type = enumName;
|
|
114
|
+
}
|
|
115
|
+
else if (prop.type === "EnumArrayType") {
|
|
116
|
+
if (prop.nullable) {
|
|
117
|
+
console.warn(`${prop.name}: Nullable enum arrays are not supported`);
|
|
118
|
+
}
|
|
119
|
+
decorators.length = 0; //remove @IsNotEmpty
|
|
120
|
+
const initializer = (_c = (0, ts_morph_helper_1.morphTsProperty)(prop.name, metadata).getInitializer()) === null || _c === void 0 ? void 0 : _c.getText();
|
|
121
|
+
const fieldOptions = tsCodeRecordToString({ defaultValue: initializer });
|
|
122
|
+
const enumName = (0, ts_morph_helper_1.findEnumName)(prop.name, metadata);
|
|
123
|
+
const importPath = (0, ts_morph_helper_1.findEnumImportPath)(enumName, `${generatorOptions.targetDirectory}/dto`, metadata);
|
|
124
|
+
imports.push({ name: enumName, importPath });
|
|
125
|
+
decorators.push(`@IsEnum(${enumName}, { each: true })`);
|
|
126
|
+
decorators.push(`@Field(() => [${enumName}], ${fieldOptions})`);
|
|
127
|
+
type = `${enumName}[]`;
|
|
128
|
+
}
|
|
129
|
+
else if (prop.type === "string" || prop.type === "text") {
|
|
130
|
+
const initializer = (_d = (0, ts_morph_helper_1.morphTsProperty)(prop.name, metadata).getInitializer()) === null || _d === void 0 ? void 0 : _d.getText();
|
|
131
|
+
const defaultValue = prop.nullable && (initializer == "undefined" || initializer == "null" || initializer === undefined) ? "null" : initializer;
|
|
132
|
+
const fieldOptions = tsCodeRecordToString({ nullable: prop.nullable ? "true" : undefined, defaultValue });
|
|
133
|
+
decorators.push("@IsString()");
|
|
134
|
+
if (prop.name.startsWith("scope_")) {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
else if (prop.name === "slug") {
|
|
138
|
+
//TODO find a non-magic solution
|
|
139
|
+
decorators.push("@IsSlug()");
|
|
140
|
+
}
|
|
141
|
+
decorators.push(`@Field(${fieldOptions})`);
|
|
142
|
+
type = "string";
|
|
143
|
+
}
|
|
144
|
+
else if (prop.type === "DecimalType" || prop.type == "BigIntType" || prop.type === "number") {
|
|
145
|
+
const initializer = (_e = (0, ts_morph_helper_1.morphTsProperty)(prop.name, metadata).getInitializer()) === null || _e === void 0 ? void 0 : _e.getText();
|
|
146
|
+
const defaultValue = prop.nullable && (initializer == "undefined" || initializer == "null" || initializer === undefined) ? "null" : initializer;
|
|
147
|
+
const fieldOptions = tsCodeRecordToString({ nullable: prop.nullable ? "true" : undefined, defaultValue });
|
|
148
|
+
if (constants_1.integerTypes.includes(prop.columnTypes[0])) {
|
|
149
|
+
decorators.push("@IsInt()");
|
|
150
|
+
decorators.push(`@Field(() => Int, ${fieldOptions})`);
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
decorators.push("@IsNumber()");
|
|
154
|
+
decorators.push(`@Field(${fieldOptions})`);
|
|
155
|
+
}
|
|
156
|
+
type = "number";
|
|
157
|
+
}
|
|
158
|
+
else if (prop.type === "DateType") {
|
|
159
|
+
// ISO Date without time
|
|
160
|
+
const initializer = (_f = (0, ts_morph_helper_1.morphTsProperty)(prop.name, metadata).getInitializer()) === null || _f === void 0 ? void 0 : _f.getText();
|
|
161
|
+
const defaultValue = prop.nullable && (initializer == "undefined" || initializer == "null" || initializer === undefined) ? "null" : initializer;
|
|
162
|
+
const fieldOptions = tsCodeRecordToString({ nullable: prop.nullable ? "true" : undefined, defaultValue });
|
|
163
|
+
decorators.push("@IsDate()");
|
|
164
|
+
decorators.push(`@Field(() => GraphQLDate, ${fieldOptions})`);
|
|
165
|
+
type = "Date";
|
|
166
|
+
}
|
|
167
|
+
else if (prop.type === "Date") {
|
|
168
|
+
// DateTime
|
|
169
|
+
const initializer = (_g = (0, ts_morph_helper_1.morphTsProperty)(prop.name, metadata).getInitializer()) === null || _g === void 0 ? void 0 : _g.getText();
|
|
170
|
+
const defaultValue = prop.nullable && (initializer == "undefined" || initializer == "null" || initializer === undefined) ? "null" : initializer;
|
|
171
|
+
const fieldOptions = tsCodeRecordToString({ nullable: prop.nullable ? "true" : undefined, defaultValue });
|
|
172
|
+
decorators.push("@IsDate()");
|
|
173
|
+
decorators.push(`@Field(${fieldOptions})`);
|
|
174
|
+
type = "Date";
|
|
175
|
+
}
|
|
176
|
+
else if (prop.type === "BooleanType" || prop.type === "boolean") {
|
|
177
|
+
const initializer = (_h = (0, ts_morph_helper_1.morphTsProperty)(prop.name, metadata).getInitializer()) === null || _h === void 0 ? void 0 : _h.getText();
|
|
178
|
+
const defaultValue = prop.nullable && (initializer == "undefined" || initializer == "null" || initializer === undefined) ? "null" : initializer;
|
|
179
|
+
const fieldOptions = tsCodeRecordToString({ nullable: prop.nullable ? "true" : undefined, defaultValue });
|
|
180
|
+
decorators.push("@IsBoolean()");
|
|
181
|
+
decorators.push(`@Field(${fieldOptions})`);
|
|
182
|
+
type = "boolean";
|
|
183
|
+
}
|
|
184
|
+
else if (prop.type === "RootBlockType") {
|
|
185
|
+
const blockName = (0, ts_morph_helper_1.findBlockName)(prop.name, metadata);
|
|
186
|
+
const importPath = (0, ts_morph_helper_1.findBlockImportPath)(blockName, `${generatorOptions.targetDirectory}/dto`, metadata);
|
|
187
|
+
imports.push({ name: blockName, importPath });
|
|
188
|
+
decorators.push(`@Field(() => RootBlockInputScalar(${blockName})${prop.nullable ? ", { nullable: true }" : ""})`);
|
|
189
|
+
decorators.push(`@Transform(({ value }) => (isBlockInputInterface(value) ? value : ${blockName}.blockInputFactory(value)), { toClassOnly: true })`);
|
|
190
|
+
decorators.push("@ValidateNested()");
|
|
191
|
+
type = "BlockInputInterface";
|
|
192
|
+
}
|
|
193
|
+
else if (prop.kind == "m:1") {
|
|
194
|
+
const initializer = (_j = (0, ts_morph_helper_1.morphTsProperty)(prop.name, metadata).getInitializer()) === null || _j === void 0 ? void 0 : _j.getText();
|
|
195
|
+
const defaultValueNull = prop.nullable && (initializer == "undefined" || initializer == "null" || initializer === undefined);
|
|
196
|
+
const fieldOptions = tsCodeRecordToString({
|
|
197
|
+
nullable: prop.nullable ? "true" : undefined,
|
|
198
|
+
defaultValue: defaultValueNull ? "null" : undefined,
|
|
199
|
+
});
|
|
200
|
+
decorators.push(`@Field(() => ID, ${fieldOptions})`);
|
|
201
|
+
if (prop.referencedColumnNames.length > 1) {
|
|
202
|
+
console.warn(`${prop.name}: Composite keys are not supported`);
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
const refType = findReferenceTargetType(prop.targetMeta, prop.referencedColumnNames[0]);
|
|
206
|
+
if (refType == "uuid") {
|
|
207
|
+
type = "string";
|
|
208
|
+
decorators.push("@IsUUID()");
|
|
209
|
+
}
|
|
210
|
+
else if (refType == "string") {
|
|
211
|
+
type = "string";
|
|
212
|
+
decorators.push("@IsString()");
|
|
213
|
+
}
|
|
214
|
+
else if (refType == "integer") {
|
|
215
|
+
type = "number";
|
|
216
|
+
decorators.push("@Transform(({ value }) => (value ? parseInt(value) : null))");
|
|
217
|
+
decorators.push("@IsInt()");
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
console.warn(`${prop.name}: Unsupported referenced type`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
else if (prop.kind == "1:m") {
|
|
224
|
+
if (prop.orphanRemoval) {
|
|
225
|
+
//if orphanRemoval is enabled, we need to generate a nested input type
|
|
226
|
+
decorators.length = 0;
|
|
227
|
+
if (!prop.targetMeta)
|
|
228
|
+
throw new Error("No targetMeta");
|
|
229
|
+
const inputNameClassName = `${metadata.className}Nested${prop.targetMeta.className}Input`;
|
|
230
|
+
{
|
|
231
|
+
const excludeFields = prop.targetMeta.props.filter((p) => p.kind == "m:1" && p.targetMeta == metadata).map((p) => p.name);
|
|
232
|
+
const { fileNameSingular } = (0, build_name_variants_1.buildNameVariants)(metadata);
|
|
233
|
+
const { fileNameSingular: targetFileNameSingular } = (0, build_name_variants_1.buildNameVariants)(prop.targetMeta);
|
|
234
|
+
const fileName = `dto/${fileNameSingular}-nested-${targetFileNameSingular}.input.ts`;
|
|
235
|
+
const nestedInputFiles = yield generateCrudInput(generatorOptions, prop.targetMeta, {
|
|
236
|
+
nested: true,
|
|
237
|
+
fileName,
|
|
238
|
+
className: inputNameClassName,
|
|
239
|
+
excludeFields,
|
|
240
|
+
});
|
|
241
|
+
generatedFiles.push(...nestedInputFiles);
|
|
242
|
+
imports.push({
|
|
243
|
+
name: inputNameClassName,
|
|
244
|
+
importPath: nestedInputFiles[0].name.replace(/^dto/, ".").replace(/\.ts$/, ""),
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
decorators.push(`@Field(() => [${inputNameClassName}], {${prop.nullable ? "nullable: true" : "defaultValue: []"}})`);
|
|
248
|
+
decorators.push(`@IsArray()`);
|
|
249
|
+
decorators.push(`@Type(() => ${inputNameClassName})`);
|
|
250
|
+
type = `${inputNameClassName}[]`;
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
//if orphanRemoval is disabled, we reference the id in input
|
|
254
|
+
decorators.length = 0;
|
|
255
|
+
decorators.push(`@Field(() => [ID], {${prop.nullable ? "nullable: true" : "defaultValue: []"}})`);
|
|
256
|
+
decorators.push(`@IsArray()`);
|
|
257
|
+
if (prop.referencedColumnNames.length > 1) {
|
|
258
|
+
console.warn(`${prop.name}: Composite keys are not supported`);
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
const refType = findReferenceTargetType(prop.targetMeta, prop.referencedColumnNames[0]);
|
|
262
|
+
if (refType == "uuid") {
|
|
263
|
+
type = "string[]";
|
|
264
|
+
decorators.push("@IsUUID(undefined, { each: true })");
|
|
265
|
+
}
|
|
266
|
+
else if (refType == "string") {
|
|
267
|
+
type = "string[]";
|
|
268
|
+
decorators.push("@IsString({ each: true })");
|
|
269
|
+
}
|
|
270
|
+
else if (refType == "integer") {
|
|
271
|
+
type = "number[]";
|
|
272
|
+
decorators.push("@Transform(({ value }) => value.map((id: string) => parseInt(id)))");
|
|
273
|
+
decorators.push("@IsInt({ each: true })");
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
console.warn(`${prop.name}: Unsupported referenced type`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
else if (prop.kind == "m:n") {
|
|
281
|
+
decorators.length = 0;
|
|
282
|
+
decorators.push(`@Field(() => [ID], {${prop.nullable ? "nullable" : "defaultValue: []"}})`);
|
|
283
|
+
decorators.push(`@IsArray()`);
|
|
284
|
+
if (prop.referencedColumnNames.length > 1) {
|
|
285
|
+
console.warn(`${prop.name}: Composite keys are not supported`);
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
const refType = findReferenceTargetType(prop.targetMeta, prop.referencedColumnNames[0]);
|
|
289
|
+
if (refType == "uuid") {
|
|
290
|
+
type = "string[]";
|
|
291
|
+
decorators.push("@IsUUID(undefined, { each: true })");
|
|
292
|
+
}
|
|
293
|
+
else if (refType == "string") {
|
|
294
|
+
type = "string[]";
|
|
295
|
+
decorators.push("@IsString({ each: true })");
|
|
296
|
+
}
|
|
297
|
+
else if (refType == "integer") {
|
|
298
|
+
type = "number[]";
|
|
299
|
+
decorators.push("@Transform(({ value }) => value.map((id: string) => parseInt(id)))");
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
console.warn(`${prop.name}: Unsupported referenced type`);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
else if (prop.kind == "1:1") {
|
|
306
|
+
if (!prop.targetMeta)
|
|
307
|
+
throw new Error("No targetMeta");
|
|
308
|
+
const inputNameClassName = `${metadata.className}Nested${prop.targetMeta.className}Input`;
|
|
309
|
+
{
|
|
310
|
+
const excludeFields = prop.targetMeta.props.filter((p) => p.kind == "1:1" && p.targetMeta == metadata).map((p) => p.name);
|
|
311
|
+
const { fileNameSingular } = (0, build_name_variants_1.buildNameVariants)(metadata);
|
|
312
|
+
const { fileNameSingular: targetFileNameSingular } = (0, build_name_variants_1.buildNameVariants)(prop.targetMeta);
|
|
313
|
+
const fileName = `dto/${fileNameSingular}-nested-${targetFileNameSingular}.input.ts`;
|
|
314
|
+
const nestedInputFiles = yield generateCrudInput(generatorOptions, prop.targetMeta, {
|
|
315
|
+
nested: true,
|
|
316
|
+
fileName,
|
|
317
|
+
className: inputNameClassName,
|
|
318
|
+
excludeFields,
|
|
319
|
+
});
|
|
320
|
+
generatedFiles.push(...nestedInputFiles);
|
|
321
|
+
imports.push({
|
|
322
|
+
name: inputNameClassName,
|
|
323
|
+
importPath: nestedInputFiles[nestedInputFiles.length - 1].name.replace(/^dto/, ".").replace(/\.ts$/, ""),
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
decorators.push(`@Field(() => ${inputNameClassName}${prop.nullable ? ", { nullable: true }" : ""})`);
|
|
327
|
+
decorators.push(`@Type(() => ${inputNameClassName})`);
|
|
328
|
+
decorators.push("@ValidateNested()");
|
|
329
|
+
type = `${inputNameClassName}`;
|
|
330
|
+
}
|
|
331
|
+
else if (prop.type == "JsonType" || prop.embeddable || prop.type == "ArrayType") {
|
|
332
|
+
const tsProp = (0, ts_morph_helper_1.morphTsProperty)(prop.name, metadata);
|
|
333
|
+
let tsType = tsProp.getType();
|
|
334
|
+
if (tsType.isUnion() && tsType.getUnionTypes().length == 2 && tsType.getUnionTypes()[0].getText() == "undefined") {
|
|
335
|
+
// undefinded | type (or prop?: type) -> type
|
|
336
|
+
tsType = tsType.getUnionTypes()[1];
|
|
337
|
+
}
|
|
338
|
+
type = tsType.getText(tsProp);
|
|
339
|
+
if (tsType.isArray()) {
|
|
340
|
+
const initializer = (_k = tsProp.getInitializer()) === null || _k === void 0 ? void 0 : _k.getText();
|
|
341
|
+
const defaultValue = prop.nullable && (initializer == "undefined" || initializer == "null" || initializer === undefined)
|
|
342
|
+
? "null"
|
|
343
|
+
: initializer == "[]"
|
|
344
|
+
? "[]"
|
|
345
|
+
: undefined;
|
|
346
|
+
const fieldOptions = tsCodeRecordToString({
|
|
347
|
+
nullable: prop.nullable ? "true" : undefined,
|
|
348
|
+
defaultValue: defaultValue,
|
|
349
|
+
});
|
|
350
|
+
decorators.push(`@IsArray()`);
|
|
351
|
+
if (type == "string[]") {
|
|
352
|
+
decorators.push(`@Field(() => [String], ${fieldOptions})`);
|
|
353
|
+
decorators.push("@IsString({ each: true })");
|
|
354
|
+
}
|
|
355
|
+
else if (type == "number[]") {
|
|
356
|
+
decorators.push(`@Field(() => [Number], ${fieldOptions})`);
|
|
357
|
+
decorators.push("@IsNumber({ each: true })");
|
|
358
|
+
}
|
|
359
|
+
else if (type == "boolean[]") {
|
|
360
|
+
decorators.push(`@Field(() => [Boolean], ${fieldOptions})`);
|
|
361
|
+
decorators.push("@IsBoolean({ each: true })");
|
|
362
|
+
}
|
|
363
|
+
else if (tsType.getArrayElementTypeOrThrow().isClass()) {
|
|
364
|
+
const nestedClassName = tsType.getArrayElementTypeOrThrow().getText(tsProp);
|
|
365
|
+
const importPath = (0, ts_morph_helper_1.findInputClassImportPath)(nestedClassName, `${generatorOptions.targetDirectory}/dto`, metadata);
|
|
366
|
+
imports.push({ name: nestedClassName, importPath });
|
|
367
|
+
decorators.push(`@ValidateNested()`);
|
|
368
|
+
decorators.push(`@Type(() => ${nestedClassName})`);
|
|
369
|
+
decorators.push(`@Field(() => [${nestedClassName}], ${fieldOptions})`);
|
|
370
|
+
}
|
|
371
|
+
else {
|
|
372
|
+
decorators.push(`@Field(() => [GraphQLJSONObject], ${fieldOptions}) // Warning: this input is not validated properly`);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
else if (tsType.isClass()) {
|
|
376
|
+
const nestedClassName = tsType.getText(tsProp);
|
|
377
|
+
const importPath = (0, ts_morph_helper_1.findInputClassImportPath)(nestedClassName, `${generatorOptions.targetDirectory}/dto`, metadata);
|
|
378
|
+
imports.push({ name: nestedClassName, importPath });
|
|
379
|
+
decorators.push(`@ValidateNested()`);
|
|
380
|
+
decorators.push(`@Type(() => ${nestedClassName})`);
|
|
381
|
+
decorators.push(`@Field(() => ${nestedClassName}${prop.nullable ? ", { nullable: true }" : ""})`);
|
|
382
|
+
}
|
|
383
|
+
else {
|
|
384
|
+
decorators.push(`@Field(() => GraphQLJSONObject${prop.nullable ? ", { nullable: true }" : ""}) // Warning: this input is not validated properly`);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
else if (prop.type == "uuid") {
|
|
388
|
+
const initializer = (_l = (0, ts_morph_helper_1.morphTsProperty)(prop.name, metadata).getInitializer()) === null || _l === void 0 ? void 0 : _l.getText();
|
|
389
|
+
const defaultValueNull = prop.nullable && (initializer == "undefined" || initializer == "null" || initializer === undefined);
|
|
390
|
+
const fieldOptions = tsCodeRecordToString({
|
|
391
|
+
nullable: prop.nullable ? "true" : undefined,
|
|
392
|
+
defaultValue: defaultValueNull ? "null" : undefined,
|
|
393
|
+
});
|
|
394
|
+
decorators.push(`@Field(() => ID, ${fieldOptions})`);
|
|
395
|
+
decorators.push("@IsUUID()");
|
|
396
|
+
type = "string";
|
|
397
|
+
}
|
|
398
|
+
else {
|
|
399
|
+
console.warn(`${prop.name}: unsupported type ${type}`);
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
const classValidatorValidators = (0, class_validator_1.getMetadataStorage)().getTargetValidationMetadatas(metadata.class, prop.name, false, false, undefined);
|
|
403
|
+
for (const validator of classValidatorValidators) {
|
|
404
|
+
if (validator.propertyName !== prop.name)
|
|
405
|
+
continue;
|
|
406
|
+
const constraints = (0, class_validator_1.getMetadataStorage)().getTargetValidatorConstraints(validator.constraintCls);
|
|
407
|
+
for (const constraint of constraints) {
|
|
408
|
+
const decorator = definedDecorators.find((decorator) => {
|
|
409
|
+
return (
|
|
410
|
+
// ignore casing since class validator is inconsistent with casing
|
|
411
|
+
decorator.getName().toUpperCase() === constraint.name.toUpperCase() ||
|
|
412
|
+
// some class validator decorators have a prefix "Is" but not in the constraint name
|
|
413
|
+
`Is${decorator.getName()}`.toUpperCase() === constraint.name.toUpperCase());
|
|
414
|
+
});
|
|
415
|
+
if (decorator) {
|
|
416
|
+
const importPath = (0, ts_morph_helper_1.findValidatorImportPath)(decorator.getName(), generatorOptions, metadata);
|
|
417
|
+
if (importPath) {
|
|
418
|
+
imports.push({ name: decorator.getName(), importPath });
|
|
419
|
+
if (!decorators.includes(decorator.getText())) {
|
|
420
|
+
decorators.unshift(decorator.getText());
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
else {
|
|
425
|
+
console.warn(`Decorator import for constraint ${constraint.name} not found`);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
fieldsOut += `${decorators.join("\n")}
|
|
430
|
+
${fieldName}${isOptional ? "?" : ""}: ${type};
|
|
431
|
+
|
|
432
|
+
`;
|
|
433
|
+
}
|
|
434
|
+
const className = (_m = options.className) !== null && _m !== void 0 ? _m : `${metadata.className}Input`;
|
|
435
|
+
const inputOut = `import { Field, InputType, ID, Int } from "@nestjs/graphql";
|
|
436
|
+
import { Transform, Type } from "class-transformer";
|
|
437
|
+
import { IsString, IsNotEmpty, ValidateNested, IsNumber, IsBoolean, IsDate, IsOptional, IsEnum, IsUUID, IsArray, IsInt, Min } from "class-validator";
|
|
438
|
+
import { GraphQLJSONObject } from "graphql-scalars";
|
|
439
|
+
import { GraphQLDate } from "graphql-scalars";
|
|
440
|
+
${(0, generate_imports_code_1.generateImportsCode)(imports)}
|
|
441
|
+
|
|
442
|
+
@InputType()
|
|
443
|
+
export class ${className} {
|
|
444
|
+
${fieldsOut}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
${options.generateUpdateInput && !options.nested
|
|
448
|
+
? `
|
|
449
|
+
@InputType()
|
|
450
|
+
export class ${className.replace(/Input$/, "")}UpdateInput extends PartialType(${className}) {}
|
|
451
|
+
`
|
|
452
|
+
: ""}
|
|
453
|
+
`;
|
|
454
|
+
const { fileNameSingular } = (0, build_name_variants_1.buildNameVariants)(metadata);
|
|
455
|
+
const fileName = (_o = options.fileName) !== null && _o !== void 0 ? _o : `dto/${fileNameSingular}.input.ts`;
|
|
456
|
+
generatedFiles.push({
|
|
457
|
+
name: fileName,
|
|
458
|
+
content: inputOut,
|
|
459
|
+
type: "input",
|
|
460
|
+
});
|
|
461
|
+
return generatedFiles;
|
|
462
|
+
});
|
|
463
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type CrudSingleGeneratorOptions } from "@comet/cms-api";
|
|
2
|
+
import { type EntityMetadata } from "@mikro-orm/postgresql";
|
|
3
|
+
import { type GeneratedFile } from "../utils/write-generated-files";
|
|
4
|
+
export declare function generateCrudSingle(generatorOptions: CrudSingleGeneratorOptions, metadata: EntityMetadata<any>): Promise<GeneratedFile[]>;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
36
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
37
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
38
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
39
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
40
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
41
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
|
+
exports.generateCrudSingle = generateCrudSingle;
|
|
46
|
+
const cms_api_1 = require("@comet/cms-api");
|
|
47
|
+
const path = __importStar(require("path"));
|
|
48
|
+
const generate_crud_input_1 = require("../generateCrudInput/generate-crud-input");
|
|
49
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
50
|
+
function generateCrudSingle(generatorOptions, metadata) {
|
|
51
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
52
|
+
const classNameSingular = metadata.className;
|
|
53
|
+
const classNamePlural = !metadata.className.endsWith("s") ? `${metadata.className}s` : metadata.className;
|
|
54
|
+
const instanceNameSingular = classNameSingular[0].toLocaleLowerCase() + classNameSingular.slice(1);
|
|
55
|
+
const instanceNamePlural = classNamePlural[0].toLocaleLowerCase() + classNamePlural.slice(1);
|
|
56
|
+
const fileNameSingular = instanceNameSingular.replace(/[A-Z]/g, (i) => `-${i.toLocaleLowerCase()}`);
|
|
57
|
+
const fileNamePlural = instanceNamePlural.replace(/[A-Z]/g, (i) => `-${i.toLocaleLowerCase()}`);
|
|
58
|
+
if (!generatorOptions.requiredPermission)
|
|
59
|
+
generatorOptions.requiredPermission = [instanceNamePlural];
|
|
60
|
+
function generateCrudResolver() {
|
|
61
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
62
|
+
const generatedFiles = [];
|
|
63
|
+
const scopeProp = metadata.props.find((prop) => prop.name == "scope");
|
|
64
|
+
if (scopeProp && !scopeProp.targetMeta)
|
|
65
|
+
throw new Error("Scope prop has no targetMeta");
|
|
66
|
+
const blockProps = metadata.props.filter((prop) => {
|
|
67
|
+
return (0, cms_api_1.hasCrudFieldFeature)(metadata.class, prop.name, "input") && prop.type === "RootBlockType";
|
|
68
|
+
});
|
|
69
|
+
const serviceOut = `import { ObjectQuery } from "@mikro-orm/postgresql";
|
|
70
|
+
import { InjectRepository } from "@mikro-orm/nestjs";
|
|
71
|
+
import { EntityRepository } from "@mikro-orm/postgresql";
|
|
72
|
+
import { Injectable } from "@nestjs/common";
|
|
73
|
+
import { ${metadata.className} } from "${path.relative(generatorOptions.targetDirectory, metadata.path).replace(/\.ts$/, "")}";
|
|
74
|
+
|
|
75
|
+
@Injectable()
|
|
76
|
+
export class ${classNamePlural}Service {
|
|
77
|
+
|
|
78
|
+
}
|
|
79
|
+
`;
|
|
80
|
+
generatedFiles.push({ name: `${fileNamePlural}.service.ts`, content: serviceOut, type: "service" });
|
|
81
|
+
const resolverOut = `import { InjectRepository } from "@mikro-orm/nestjs";
|
|
82
|
+
import { EntityRepository, EntityManager } from "@mikro-orm/postgresql";
|
|
83
|
+
import { FindOptions } from "@mikro-orm/postgresql";
|
|
84
|
+
import { Args, ID, Mutation, Query, Resolver } from "@nestjs/graphql";
|
|
85
|
+
import { RequiredPermission, SortDirection, validateNotModified } from "@comet/cms-api";
|
|
86
|
+
|
|
87
|
+
import { ${metadata.className} } from "${path.relative(generatorOptions.targetDirectory, metadata.path).replace(/\.ts$/, "")}";
|
|
88
|
+
${scopeProp && scopeProp.targetMeta
|
|
89
|
+
? `import { ${scopeProp.targetMeta.className} } from "${path
|
|
90
|
+
.relative(generatorOptions.targetDirectory, scopeProp.targetMeta.path)
|
|
91
|
+
.replace(/\.ts$/, "")}";`
|
|
92
|
+
: ""}
|
|
93
|
+
import { ${classNamePlural}Service } from "./${fileNamePlural}.service";
|
|
94
|
+
import { ${classNameSingular}Input } from "./dto/${fileNameSingular}.input";
|
|
95
|
+
import { Paginated${classNamePlural} } from "./dto/paginated-${fileNamePlural}";
|
|
96
|
+
|
|
97
|
+
@Resolver(() => ${metadata.className})
|
|
98
|
+
@RequiredPermission(${JSON.stringify(generatorOptions.requiredPermission)}${!scopeProp ? `, { skipScopeCheck: true }` : ""})
|
|
99
|
+
export class ${classNameSingular}Resolver {
|
|
100
|
+
constructor(
|
|
101
|
+
private readonly entityManager: EntityManager,
|
|
102
|
+
private readonly ${instanceNamePlural}Service: ${classNamePlural}Service,
|
|
103
|
+
@InjectRepository(${metadata.className}) private readonly repository: EntityRepository<${metadata.className}>
|
|
104
|
+
) {}
|
|
105
|
+
|
|
106
|
+
@Query(() => ${metadata.className}, { nullable: true })
|
|
107
|
+
async ${instanceNameSingular}(
|
|
108
|
+
${scopeProp ? `@Args("scope", { type: () => ${scopeProp.type} }) scope: ${scopeProp.type},` : ""}
|
|
109
|
+
): Promise<${metadata.className} | null> {
|
|
110
|
+
const ${instanceNamePlural} = await this.repository.find({${scopeProp ? `scope` : ""}});
|
|
111
|
+
if (${instanceNamePlural}.length > 1) {
|
|
112
|
+
throw new Error("There must be only one ${instanceNameSingular}");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return ${instanceNamePlural}.length > 0 ? ${instanceNamePlural}[0] : null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
@Mutation(() => ${metadata.className})
|
|
119
|
+
async save${classNameSingular}(
|
|
120
|
+
${scopeProp ? `@Args("scope", { type: () => ${scopeProp.type} }) scope: ${scopeProp.type},` : ""}
|
|
121
|
+
@Args("input", { type: () => ${classNameSingular}Input }) input: ${classNameSingular}Input
|
|
122
|
+
): Promise<${metadata.className}> {
|
|
123
|
+
let ${instanceNameSingular} = await this.repository.findOne({${scopeProp ? `scope` : ""}});
|
|
124
|
+
|
|
125
|
+
if (!${instanceNameSingular}) {
|
|
126
|
+
${instanceNameSingular} = this.repository.create({
|
|
127
|
+
...input,
|
|
128
|
+
${blockProps.length ? `${blockProps.map((prop) => `${prop.name}: input.${prop.name}.transformToBlockData()`).join(", ")}, ` : ""}
|
|
129
|
+
${scopeProp ? `scope,` : ""}
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
${instanceNameSingular}.assign({
|
|
134
|
+
...input,
|
|
135
|
+
${blockProps.length ? `${blockProps.map((prop) => `${prop.name}: input.${prop.name}.transformToBlockData()`).join(", ")}, ` : ""}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
await this.entityManager.flush();
|
|
139
|
+
|
|
140
|
+
return ${instanceNameSingular};
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
`;
|
|
144
|
+
generatedFiles.push({ name: `${fileNameSingular}.resolver.ts`, content: resolverOut, type: "resolver" });
|
|
145
|
+
return generatedFiles;
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
return [
|
|
149
|
+
...(yield (0, generate_crud_input_1.generateCrudInput)(generatorOptions, metadata, { nested: false, excludeFields: [], generateUpdateInput: false })),
|
|
150
|
+
...(yield generateCrudResolver()),
|
|
151
|
+
];
|
|
152
|
+
});
|
|
153
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type EntityMetadata } from "@mikro-orm/postgresql";
|
|
2
|
+
export declare function classNameToInstanceName(className: string): string;
|
|
3
|
+
export declare function buildNameVariants(metadata: EntityMetadata<any>): {
|
|
4
|
+
classNameSingular: string;
|
|
5
|
+
classNamePlural: string;
|
|
6
|
+
instanceNameSingular: string;
|
|
7
|
+
instanceNamePlural: string;
|
|
8
|
+
fileNameSingular: string;
|
|
9
|
+
fileNamePlural: string;
|
|
10
|
+
};
|