@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.
Files changed (27) hide show
  1. package/LICENSE +24 -0
  2. package/bin/api-generator.js +5 -0
  3. package/lib/apiGenerator.d.ts +1 -0
  4. package/lib/apiGenerator.js +7 -0
  5. package/lib/commands/generate/generate-command.d.ts +2 -0
  6. package/lib/commands/generate/generate-command.js +45 -0
  7. package/lib/commands/generate/generateCrud/generate-crud.d.ts +24 -0
  8. package/lib/commands/generate/generateCrud/generate-crud.js +1212 -0
  9. package/lib/commands/generate/generateCrudInput/generate-crud-input.d.ts +11 -0
  10. package/lib/commands/generate/generateCrudInput/generate-crud-input.js +463 -0
  11. package/lib/commands/generate/generateCrudSingle/generate-crud-single.d.ts +4 -0
  12. package/lib/commands/generate/generateCrudSingle/generate-crud-single.js +153 -0
  13. package/lib/commands/generate/utils/build-name-variants.d.ts +10 -0
  14. package/lib/commands/generate/utils/build-name-variants.js +25 -0
  15. package/lib/commands/generate/utils/constants.d.ts +1 -0
  16. package/lib/commands/generate/utils/constants.js +4 -0
  17. package/lib/commands/generate/utils/generate-imports-code.d.ts +5 -0
  18. package/lib/commands/generate/utils/generate-imports-code.js +32 -0
  19. package/lib/commands/generate/utils/test-helper.d.ts +5 -0
  20. package/lib/commands/generate/utils/test-helper.js +72 -0
  21. package/lib/commands/generate/utils/ts-morph-helper.d.ts +10 -0
  22. package/lib/commands/generate/utils/ts-morph-helper.js +191 -0
  23. package/lib/commands/generate/utils/write-generated-file.d.ts +1 -0
  24. package/lib/commands/generate/utils/write-generated-file.js +69 -0
  25. package/lib/commands/generate/utils/write-generated-files.d.ts +8 -0
  26. package/lib/commands/generate/utils/write-generated-files.js +20 -0
  27. 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
+ };