@nestledjs/api 0.0.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/CHANGELOG.md +5 -0
- package/README.md +11 -0
- package/eslint.config.cjs +28 -0
- package/generators.json +69 -0
- package/package.json +21 -0
- package/project.json +47 -0
- package/src/account/files/data-access/src/index.ts__tmpl__ +5 -0
- package/src/account/files/data-access/src/lib/api-account-data-access.module.ts__tmpl__ +10 -0
- package/src/account/files/data-access/src/lib/api-account-data-access.service.ts__tmpl__ +152 -0
- package/src/account/files/data-access/src/lib/dto/account-create-email.input.ts__tmpl__ +9 -0
- package/src/account/files/data-access/src/lib/dto/account-update-password.input.ts__tmpl__ +16 -0
- package/src/account/files/data-access/src/lib/dto/account-update-profile.input.ts__tmpl__ +25 -0
- package/src/account/files/feature/src/index.ts__tmpl__ +1 -0
- package/src/account/files/feature/src/lib/api-account-feature.module.ts__tmpl__ +9 -0
- package/src/account/files/feature/src/lib/api-account-feature.resolver.ts__tmpl__ +83 -0
- package/src/account/generator.spec.ts +71 -0
- package/src/account/generator.ts +20 -0
- package/src/account/schema.d.ts +3 -0
- package/src/account/schema.json +13 -0
- package/src/app/files/src/app.config.ts__tmpl__ +66 -0
- package/src/app/files/src/app.module.ts__tmpl__ +43 -0
- package/src/app/files/src/applogger.middleware.ts__tmpl__ +21 -0
- package/src/app/files/src/main.ts__tmpl__ +33 -0
- package/src/app/files/webpack.config.js__tmpl__ +54 -0
- package/src/app/generator.spec.ts +112 -0
- package/src/app/generator.ts +105 -0
- package/src/app/schema.d.ts +1 -0
- package/src/app/schema.json +9 -0
- package/src/config/files/src/index.ts__tmpl__ +3 -0
- package/src/config/files/src/lib/config.service.ts__tmpl__ +51 -0
- package/src/config/files/src/lib/configuration.ts__tmpl__ +32 -0
- package/src/config/files/src/lib/validation.ts__tmpl__ +21 -0
- package/src/config/generator.spec.ts +47 -0
- package/src/config/generator.ts +16 -0
- package/src/config/schema.d.ts +3 -0
- package/src/config/schema.json +13 -0
- package/src/core/files/data-access/src/index.ts__tmpl__ +5 -0
- package/src/core/files/data-access/src/lib/api-core-data-access.module.ts__tmpl__ +9 -0
- package/src/core/files/data-access/src/lib/api-core-data-access.service.ts__tmpl__ +97 -0
- package/src/core/files/data-access/src/lib/api-core-pub-sub.ts__tmpl__ +37 -0
- package/src/core/files/data-access/src/lib/dto/core-paging.input.ts__tmpl__ +26 -0
- package/src/core/files/data-access/src/lib/dto/multi-select-input.ts__tmpl__ +7 -0
- package/src/core/files/data-access/src/lib/models/core-paging.ts__tmpl__ +19 -0
- package/src/core/files/feature/src/index.ts__tmpl__ +2 -0
- package/src/core/files/feature/src/lib/api-core-feature.controller.ts__tmpl__ +12 -0
- package/src/core/files/feature/src/lib/api-core-feature.module.ts__tmpl__ +86 -0
- package/src/core/files/feature/src/lib/api-core-feature.resolver.ts__tmpl__ +12 -0
- package/src/core/files/feature/src/lib/api-core-feature.service.ts__tmpl__ +55 -0
- package/src/core/files/feature/src/lib/config/configuration.ts__tmpl__ +32 -0
- package/src/core/files/feature/src/lib/config/validation.ts__tmpl__ +25 -0
- package/src/core/files/feature/src/lib/plugins/complexity.plugin.ts__tmpl__ +51 -0
- package/src/core/files/feature/src/lib/plugins/logging.plugin.ts__tmpl__ +17 -0
- package/src/core/files/models/src/index.ts__tmpl__ +1 -0
- package/src/core/files/models/src/lib/generate-models.ts__tmpl__ +294 -0
- package/src/core/files/models/src/lib/models/core-paging.model.ts__tmpl__ +25 -0
- package/src/core/generator.spec.ts +85 -0
- package/src/core/generator.ts +35 -0
- package/src/core/schema.d.ts +3 -0
- package/src/core/schema.json +13 -0
- package/src/custom/generator.spec.ts +75 -0
- package/src/custom/generator.ts +239 -0
- package/src/custom/schema.json +21 -0
- package/src/custom/schema.ts +5 -0
- package/src/extended/generator.spec.ts +95 -0
- package/src/extended/generator.ts +161 -0
- package/src/extended/index.ts +1 -0
- package/src/extended/schema.json +12 -0
- package/src/extended/schema.ts +3 -0
- package/src/generate-crud/files/data-access/src/index.ts__tmpl__ +3 -0
- package/src/generate-crud/files/data-access/src/lib/api-crud-data-access.module.ts__tmpl__ +11 -0
- package/src/generate-crud/files/data-access/src/lib/api-crud-data-access.service.ts__tmpl__ +72 -0
- package/src/generate-crud/files/data-access/src/lib/dto/index.ts__tmpl__ +224 -0
- package/src/generate-crud/files/feature/.gitkeep +0 -0
- package/src/generate-crud/generator.spec.ts +84 -0
- package/src/generate-crud/generator.ts +354 -0
- package/src/generate-crud/schema.json +32 -0
- package/src/generate-crud/schema.ts +8 -0
- package/src/index.ts +13 -0
- package/src/plugin/generator.spec.ts +18 -0
- package/src/plugin/generator.ts +74 -0
- package/src/plugin/schema.json +14 -0
- package/src/plugin/schema.ts +4 -0
- package/src/prisma/files/src/index.ts__tmpl__ +1 -0
- package/src/prisma/files/src/lib/.gitkeep +1 -0
- package/src/prisma/files/src/lib/schemas/schema.prisma__tmpl__ +402 -0
- package/src/prisma/files/src/lib/seed/seed-data/iso-3166-countries.ts__tmpl__ +3239 -0
- package/src/prisma/files/src/lib/seed/seed-data/seed-users.ts__tmpl__ +32 -0
- package/src/prisma/files/src/lib/seed/seed.ts__tmpl__ +64 -0
- package/src/prisma/generator.spec.ts +60 -0
- package/src/prisma/generator.ts +61 -0
- package/src/prisma/schema.d.ts +3 -0
- package/src/prisma/schema.json +13 -0
- package/src/setup/generator.md +49 -0
- package/src/setup/generator.spec.ts +18 -0
- package/src/setup/generator.ts +106 -0
- package/src/setup/schema.json +8 -0
- package/src/smtp-mailer/files/data-access/src/index.ts__tmpl__ +2 -0
- package/src/smtp-mailer/files/data-access/src/lib/api-smtp-mailer-data-access.module.ts__tmpl__ +10 -0
- package/src/smtp-mailer/files/data-access/src/lib/api-smtp-mailer-data-access.service.ts__tmpl__ +61 -0
- package/src/smtp-mailer/generator.spec.ts +41 -0
- package/src/smtp-mailer/generator.ts +14 -0
- package/src/smtp-mailer/schema.d.ts +0 -0
- package/src/smtp-mailer/schema.json +7 -0
- package/src/user/files/data-access/src/index.ts__tmpl__ +5 -0
- package/src/user/files/data-access/src/lib/api-user-data-access.module.ts__tmpl__ +10 -0
- package/src/user/files/data-access/src/lib/api-user-data-access.service.ts__tmpl__ +119 -0
- package/src/user/files/data-access/src/lib/dto/admin-create-user.input.ts__tmpl__ +20 -0
- package/src/user/files/data-access/src/lib/dto/admin-update-user.input.ts__tmpl__ +29 -0
- package/src/user/files/feature/src/index.ts__tmpl__ +1 -0
- package/src/user/files/feature/src/lib/api-user-feature-admin.resolver.ts__tmpl__ +57 -0
- package/src/user/files/feature/src/lib/api-user-feature.module.ts__tmpl__ +10 -0
- package/src/user/files/feature/src/lib/api-user-feature.resolver.ts__tmpl__ +17 -0
- package/src/user/generator.spec.ts +41 -0
- package/src/user/generator.ts +15 -0
- package/src/user/schema.d.ts +0 -0
- package/src/user/schema.json +7 -0
- package/src/utils/files/src/index.ts__tmpl__ +3 -0
- package/src/utils/files/src/lib/decorators/ctx-user.decorator.ts__tmpl__ +6 -0
- package/src/utils/files/src/lib/guards/gql-auth-admin.guard.ts__tmpl__ +39 -0
- package/src/utils/files/src/lib/guards/gql-auth.guard.ts__tmpl__ +11 -0
- package/src/utils/generator.ts +14 -0
- package/src/utils/schema.json +8 -0
- package/src/workspace-setup/generator.md +39 -0
- package/src/workspace-setup/generator.spec.ts +82 -0
- package/src/workspace-setup/generator.ts +49 -0
- package/src/workspace-setup/lib/helpers.ts +142 -0
- package/src/workspace-setup/schema.d.ts +3 -0
- package/src/workspace-setup/schema.json +7 -0
- package/tsconfig.json +16 -0
- package/tsconfig.lib.json +23 -0
- package/tsconfig.spec.json +22 -0
- package/vite.config.mts +37 -0
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
<%# EJS template for generating DTOs %>
|
|
2
|
+
<%
|
|
3
|
+
let gqlImports = new Set(['Field', 'InputType']);
|
|
4
|
+
let usesGraphQLJSON = false;
|
|
5
|
+
let usesInt = false;
|
|
6
|
+
let usesFloat = false;
|
|
7
|
+
let usesID = false;
|
|
8
|
+
let enumNames = new Set();
|
|
9
|
+
|
|
10
|
+
// First pass to determine necessary imports and enums
|
|
11
|
+
for (const model of models) {
|
|
12
|
+
for (const field of model.fields) {
|
|
13
|
+
if (field.type === 'Int') usesInt = true;
|
|
14
|
+
if (field.type === 'Float' || field.type === 'Decimal') usesFloat = true;
|
|
15
|
+
if (field.type === 'Json') usesGraphQLJSON = true;
|
|
16
|
+
|
|
17
|
+
if (field.type === 'ID') { // Explicit ID field
|
|
18
|
+
usesID = true;
|
|
19
|
+
} else if (field.kind === 'object') {
|
|
20
|
+
// Check if a corresponding scalar ID field (e.g., 'userId') exists for this object field (e.g., 'user')
|
|
21
|
+
const correspondingIdFieldName = `${field.name}Id`;
|
|
22
|
+
const hasCorrespondingIdField = model.fields.some(f => f.name === correspondingIdFieldName && f.kind !== 'object');
|
|
23
|
+
if (!hasCorrespondingIdField) {
|
|
24
|
+
// If no pre-existing '...Id' scalar, we'll generate one, so 'ID' import is needed.
|
|
25
|
+
usesID = true;
|
|
26
|
+
}
|
|
27
|
+
} else if (field.kind === 'enum') {
|
|
28
|
+
enumNames.add(field.type);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (usesInt) gqlImports.add('Int');
|
|
34
|
+
if (usesFloat) gqlImports.add('Float');
|
|
35
|
+
if (usesID) gqlImports.add('ID');
|
|
36
|
+
%>
|
|
37
|
+
import { <%= Array.from(gqlImports).join(', ') %> } from '@nestjs/graphql'
|
|
38
|
+
|
|
39
|
+
<% if (enumNames.size > 0) { %>import { <%= Array.from(enumNames).join(', ') %> } from '<%= npmScope %>/api/core/models'
|
|
40
|
+
<% } %><% if (usesGraphQLJSON) { %>import { GraphQLJSON } from 'graphql-type-json'<% } %>
|
|
41
|
+
import { CorePagingInput } from '<%= npmScope %>/api/core/data-access'
|
|
42
|
+
|
|
43
|
+
<% for (const model of models) { %>
|
|
44
|
+
@InputType()
|
|
45
|
+
export class Create<%= model.modelName %>Input {
|
|
46
|
+
<% for (const field of model.fields) { %>
|
|
47
|
+
<%
|
|
48
|
+
let shouldSkipField = false;
|
|
49
|
+
let baseGqlType;
|
|
50
|
+
let tsType;
|
|
51
|
+
let effectiveFieldName = field.name;
|
|
52
|
+
|
|
53
|
+
if (field.kind === 'object') {
|
|
54
|
+
const correspondingIdFieldName = `${field.name}Id`;
|
|
55
|
+
const hasCorrespondingIdField = model.fields.some(f => f.name === correspondingIdFieldName && f.kind !== 'object');
|
|
56
|
+
if (hasCorrespondingIdField) {
|
|
57
|
+
shouldSkipField = true; // Skip this object field; its ID is handled by the existing scalar field.
|
|
58
|
+
} else {
|
|
59
|
+
// No corresponding '...Id' scalar found, so generate an ID field for this relation.
|
|
60
|
+
baseGqlType = 'ID';
|
|
61
|
+
tsType = 'string';
|
|
62
|
+
effectiveFieldName = correspondingIdFieldName;
|
|
63
|
+
}
|
|
64
|
+
} else {
|
|
65
|
+
// Handle non-object (scalar, enum, or explicit ID) fields
|
|
66
|
+
effectiveFieldName = field.name;
|
|
67
|
+
if (field.type === 'String') { baseGqlType = 'String'; tsType = 'string'; }
|
|
68
|
+
else if (field.type === 'Int') { baseGqlType = 'Int'; tsType = 'number'; }
|
|
69
|
+
else if (field.type === 'Float') { baseGqlType = 'Float'; tsType = 'number'; }
|
|
70
|
+
else if (field.type === 'Decimal') { baseGqlType = 'Float'; tsType = 'number'; }
|
|
71
|
+
else if (field.type === 'Boolean') { baseGqlType = 'Boolean'; tsType = 'boolean'; }
|
|
72
|
+
else if (field.type === 'DateTime') { baseGqlType = 'Date'; tsType = 'Date'; }
|
|
73
|
+
else if (field.type === 'Json') { baseGqlType = 'GraphQLJSON'; tsType = 'typeof GraphQLJSON'; }
|
|
74
|
+
else if (field.type === 'ID') { baseGqlType = 'ID'; tsType = 'string'; }
|
|
75
|
+
else if (field.kind === 'enum') {
|
|
76
|
+
baseGqlType = field.type;
|
|
77
|
+
tsType = field.type;
|
|
78
|
+
} else { // Fallback, should ideally not be reached if all model types are known
|
|
79
|
+
baseGqlType = 'String'; tsType = 'string';
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
let fieldDecoratorTypeArg = '';
|
|
84
|
+
let finalTsType = tsType;
|
|
85
|
+
|
|
86
|
+
if (!shouldSkipField) {
|
|
87
|
+
if (field.isList) { // Use original field's isList property
|
|
88
|
+
fieldDecoratorTypeArg = `() => [${baseGqlType}]`; // baseGqlType is 'ID' if transformed from object list
|
|
89
|
+
finalTsType = tsType + '[]'; // tsType is 'string' for ID, so 'string[]'
|
|
90
|
+
} else {
|
|
91
|
+
// For non-list fields (scalars or single objects transformed to ID)
|
|
92
|
+
if (baseGqlType !== 'String' && baseGqlType !== 'Boolean') {
|
|
93
|
+
fieldDecoratorTypeArg = `() => ${baseGqlType}`;
|
|
94
|
+
}
|
|
95
|
+
// finalTsType is already 'tsType', which is correct
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
%>
|
|
99
|
+
<% if (!shouldSkipField) { %>
|
|
100
|
+
@Field(<% if (fieldDecoratorTypeArg) { %><%- fieldDecoratorTypeArg %>, <% } %>{ nullable: true })
|
|
101
|
+
<%= effectiveFieldName %>?: <%= finalTsType %>
|
|
102
|
+
<% } %>
|
|
103
|
+
<% } %>
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
@InputType()
|
|
107
|
+
export class Update<%= model.modelName %>Input {
|
|
108
|
+
<% for (const field of model.fields) { %>
|
|
109
|
+
<%
|
|
110
|
+
let shouldSkipField = false;
|
|
111
|
+
let baseGqlType;
|
|
112
|
+
let tsType;
|
|
113
|
+
let effectiveFieldName = field.name;
|
|
114
|
+
|
|
115
|
+
if (field.kind === 'object') {
|
|
116
|
+
const correspondingIdFieldName = `${field.name}Id`;
|
|
117
|
+
const hasCorrespondingIdField = model.fields.some(f => f.name === correspondingIdFieldName && f.kind !== 'object');
|
|
118
|
+
if (hasCorrespondingIdField) {
|
|
119
|
+
shouldSkipField = true;
|
|
120
|
+
} else {
|
|
121
|
+
baseGqlType = 'ID';
|
|
122
|
+
tsType = 'string';
|
|
123
|
+
effectiveFieldName = correspondingIdFieldName;
|
|
124
|
+
}
|
|
125
|
+
} else {
|
|
126
|
+
effectiveFieldName = field.name;
|
|
127
|
+
if (field.type === 'String') { baseGqlType = 'String'; tsType = 'string'; }
|
|
128
|
+
else if (field.type === 'Int') { baseGqlType = 'Int'; tsType = 'number'; }
|
|
129
|
+
else if (field.type === 'Float') { baseGqlType = 'Float'; tsType = 'number'; }
|
|
130
|
+
else if (field.type === 'Decimal') { baseGqlType = 'Float'; tsType = 'number'; }
|
|
131
|
+
else if (field.type === 'Boolean') { baseGqlType = 'Boolean'; tsType = 'boolean'; }
|
|
132
|
+
else if (field.type === 'DateTime') { baseGqlType = 'Date'; tsType = 'Date'; }
|
|
133
|
+
else if (field.type === 'Json') { baseGqlType = 'GraphQLJSON'; tsType = 'typeof GraphQLJSON'; }
|
|
134
|
+
else if (field.type === 'ID') { baseGqlType = 'ID'; tsType = 'string'; }
|
|
135
|
+
else if (field.kind === 'enum') {
|
|
136
|
+
baseGqlType = field.type;
|
|
137
|
+
tsType = field.type;
|
|
138
|
+
} else {
|
|
139
|
+
baseGqlType = 'String'; tsType = 'string';
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
let fieldDecoratorTypeArg = '';
|
|
144
|
+
let finalTsType = tsType;
|
|
145
|
+
|
|
146
|
+
if (!shouldSkipField) {
|
|
147
|
+
if (field.isList) {
|
|
148
|
+
fieldDecoratorTypeArg = `() => [${baseGqlType}]`;
|
|
149
|
+
finalTsType = tsType + '[]';
|
|
150
|
+
} else {
|
|
151
|
+
if (baseGqlType !== 'String' && baseGqlType !== 'Boolean') {
|
|
152
|
+
fieldDecoratorTypeArg = `() => ${baseGqlType}`;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
%>
|
|
157
|
+
<% if (!shouldSkipField) { %>
|
|
158
|
+
@Field(<% if (fieldDecoratorTypeArg) { %><%- fieldDecoratorTypeArg %>, <% } %>{ nullable: true })
|
|
159
|
+
<%= effectiveFieldName %>?: <%= finalTsType %>
|
|
160
|
+
<% } %>
|
|
161
|
+
<% } %>
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
@InputType()
|
|
165
|
+
export class List<%= model.modelName %>Input extends CorePagingInput {
|
|
166
|
+
<% for (const field of model.fields) { %>
|
|
167
|
+
<%
|
|
168
|
+
let shouldSkipField = false;
|
|
169
|
+
let baseGqlType;
|
|
170
|
+
let tsType;
|
|
171
|
+
let effectiveFieldName = field.name;
|
|
172
|
+
|
|
173
|
+
if (field.kind === 'object') {
|
|
174
|
+
const correspondingIdFieldName = `${field.name}Id`;
|
|
175
|
+
const hasCorrespondingIdField = model.fields.some(f => f.name === correspondingIdFieldName && f.kind !== 'object');
|
|
176
|
+
if (hasCorrespondingIdField) {
|
|
177
|
+
shouldSkipField = true;
|
|
178
|
+
} else {
|
|
179
|
+
// For ListInput, if no direct ...Id field, we create one for filtering by relation ID
|
|
180
|
+
baseGqlType = 'ID';
|
|
181
|
+
tsType = 'string';
|
|
182
|
+
effectiveFieldName = correspondingIdFieldName;
|
|
183
|
+
}
|
|
184
|
+
} else {
|
|
185
|
+
// Handle non-object fields for filtering
|
|
186
|
+
effectiveFieldName = field.name;
|
|
187
|
+
if (field.type === 'String') { baseGqlType = 'String'; tsType = 'string'; }
|
|
188
|
+
else if (field.type === 'Int') { baseGqlType = 'Int'; tsType = 'number'; }
|
|
189
|
+
else if (field.type === 'Float') { baseGqlType = 'Float'; tsType = 'number'; }
|
|
190
|
+
else if (field.type === 'Decimal') { baseGqlType = 'Float'; tsType = 'number'; }
|
|
191
|
+
else if (field.type === 'Boolean') { baseGqlType = 'Boolean'; tsType = 'boolean'; }
|
|
192
|
+
else if (field.type === 'DateTime') { baseGqlType = 'Date'; tsType = 'Date'; }
|
|
193
|
+
else if (field.type === 'Json') { baseGqlType = 'GraphQLJSON'; tsType = 'typeof GraphQLJSON'; }
|
|
194
|
+
else if (field.type === 'ID') { baseGqlType = 'ID'; tsType = 'string'; }
|
|
195
|
+
else if (field.kind === 'enum') {
|
|
196
|
+
baseGqlType = field.type;
|
|
197
|
+
tsType = field.type;
|
|
198
|
+
} else {
|
|
199
|
+
baseGqlType = 'String'; tsType = 'string';
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
let fieldDecoratorTypeArg = '';
|
|
204
|
+
let finalTsType = tsType;
|
|
205
|
+
|
|
206
|
+
if (!shouldSkipField) {
|
|
207
|
+
if (field.isList) {
|
|
208
|
+
fieldDecoratorTypeArg = `() => [${baseGqlType}]`;
|
|
209
|
+
finalTsType = tsType + '[]';
|
|
210
|
+
} else {
|
|
211
|
+
if (baseGqlType !== 'String' && baseGqlType !== 'Boolean') {
|
|
212
|
+
fieldDecoratorTypeArg = `() => ${baseGqlType}`;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
%>
|
|
217
|
+
<% if (!shouldSkipField) { %>
|
|
218
|
+
@Field(<% if (fieldDecoratorTypeArg) { %><%- fieldDecoratorTypeArg %>, <% } %>{ nullable: true })
|
|
219
|
+
<%= effectiveFieldName %>?: <%= finalTsType %>
|
|
220
|
+
<% } %>
|
|
221
|
+
<% } %>
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
<% } %>
|
|
File without changes
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// generators/api/src/generate-crud/generator.spec.ts
|
|
2
|
+
|
|
3
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
4
|
+
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'
|
|
5
|
+
import { GenerateCrudGeneratorDependencies, generateCrudLogic } from './generator'
|
|
6
|
+
import { Tree } from '@nx/devkit'
|
|
7
|
+
|
|
8
|
+
// The mocked DMMF object
|
|
9
|
+
const dmmf = {
|
|
10
|
+
datamodel: {
|
|
11
|
+
models: [
|
|
12
|
+
{
|
|
13
|
+
name: 'User',
|
|
14
|
+
fields: [
|
|
15
|
+
{ name: 'id', type: 'Int', isId: true },
|
|
16
|
+
{ name: 'name', type: 'String', isId: false },
|
|
17
|
+
],
|
|
18
|
+
},
|
|
19
|
+
],
|
|
20
|
+
},
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('generate-crud generator', () => {
|
|
24
|
+
let tree: Tree
|
|
25
|
+
let mockDependencies: GenerateCrudGeneratorDependencies
|
|
26
|
+
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
tree = createTreeWithEmptyWorkspace()
|
|
29
|
+
tree.write('prisma/schema.prisma', 'model User {}')
|
|
30
|
+
|
|
31
|
+
mockDependencies = {
|
|
32
|
+
formatFiles: vi.fn(),
|
|
33
|
+
generateFiles: vi.fn(),
|
|
34
|
+
installPackagesTask: vi.fn(),
|
|
35
|
+
joinPathFragments: vi.fn((...args: string[]) => args.join('/')),
|
|
36
|
+
names: vi.fn((name: string) => ({
|
|
37
|
+
name,
|
|
38
|
+
className: name.charAt(0).toUpperCase() + name.slice(1),
|
|
39
|
+
propertyName: name,
|
|
40
|
+
constantName: name.toUpperCase(),
|
|
41
|
+
fileName: name,
|
|
42
|
+
})),
|
|
43
|
+
getDMMF: vi.fn().mockResolvedValue(dmmf),
|
|
44
|
+
apiLibraryGenerator: vi.fn().mockResolvedValue(undefined),
|
|
45
|
+
getPrismaSchemaPath: vi.fn(() => 'prisma/schema.prisma'),
|
|
46
|
+
readPrismaSchema: vi.fn(
|
|
47
|
+
() => `
|
|
48
|
+
/// @crudAuth: { "create": "public", "readMany": "user" }
|
|
49
|
+
model User {
|
|
50
|
+
id Int @id
|
|
51
|
+
name String
|
|
52
|
+
}
|
|
53
|
+
`,
|
|
54
|
+
),
|
|
55
|
+
getNpmScope: vi.fn(() => 'testscope'),
|
|
56
|
+
// FIX: Add the missing mock for 'pluralize'
|
|
57
|
+
pluralize: vi.fn((name: string) => (name.endsWith('s') ? name : name + 's')),
|
|
58
|
+
}
|
|
59
|
+
vi.clearAllMocks()
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('returns early if no Prisma models are found', async () => {
|
|
63
|
+
mockDependencies.getDMMF = vi.fn().mockResolvedValue({ datamodel: { models: [] } })
|
|
64
|
+
|
|
65
|
+
// The test now correctly calls the exported logic function
|
|
66
|
+
const result = await generateCrudLogic(tree, { name: 'crud' }, mockDependencies)
|
|
67
|
+
|
|
68
|
+
expect(result).toBeUndefined()
|
|
69
|
+
expect(mockDependencies.apiLibraryGenerator).not.toHaveBeenCalled()
|
|
70
|
+
expect(mockDependencies.generateFiles).not.toHaveBeenCalled()
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('generates files and calls utilities for valid models', async () => {
|
|
74
|
+
const callback = await generateCrudLogic(tree, { name: 'crud' }, mockDependencies)
|
|
75
|
+
|
|
76
|
+
expect(mockDependencies.apiLibraryGenerator).toHaveBeenCalled()
|
|
77
|
+
expect(mockDependencies.generateFiles).toHaveBeenCalled()
|
|
78
|
+
expect(mockDependencies.formatFiles).toHaveBeenCalled()
|
|
79
|
+
|
|
80
|
+
expect(typeof callback).toBe('function')
|
|
81
|
+
if (callback) callback()
|
|
82
|
+
expect(mockDependencies.installPackagesTask).toHaveBeenCalled()
|
|
83
|
+
})
|
|
84
|
+
})
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import { formatFiles, generateFiles, installPackagesTask, joinPathFragments, names, Tree } from '@nx/devkit'
|
|
2
|
+
import { getDMMF } from '@prisma/internals'
|
|
3
|
+
import { apiLibraryGenerator, getPrismaSchemaPath, readPrismaSchema } from '@nestledjs/utils'
|
|
4
|
+
import { GenerateCrudGeneratorSchema } from './schema'
|
|
5
|
+
import { getNpmScope } from '@nx/js/src/utils/package-json/get-npm-scope'
|
|
6
|
+
import pluralize from 'pluralize'
|
|
7
|
+
|
|
8
|
+
// STEP 1: DEFINE INTERFACES FOR DATA AND DEPENDENCIES
|
|
9
|
+
interface CrudAuthConfig {
|
|
10
|
+
readOne?: string
|
|
11
|
+
readMany?: string
|
|
12
|
+
count?: string
|
|
13
|
+
create?: string
|
|
14
|
+
update?: string
|
|
15
|
+
delete?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface ModelType {
|
|
19
|
+
name: string
|
|
20
|
+
pluralName: string
|
|
21
|
+
fields: ReadonlyArray<Record<string, unknown> & { name: string; type: string }>
|
|
22
|
+
primaryField: string
|
|
23
|
+
modelName: string
|
|
24
|
+
modelPropertyName: string
|
|
25
|
+
pluralModelName: string
|
|
26
|
+
pluralModelPropertyName: string
|
|
27
|
+
auth?: CrudAuthConfig
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// This interface makes the generator logic testable by defining its external dependencies.
|
|
31
|
+
export interface GenerateCrudGeneratorDependencies {
|
|
32
|
+
formatFiles: typeof formatFiles
|
|
33
|
+
generateFiles: typeof generateFiles
|
|
34
|
+
installPackagesTask: typeof installPackagesTask
|
|
35
|
+
joinPathFragments: typeof joinPathFragments
|
|
36
|
+
names: typeof names
|
|
37
|
+
getDMMF: typeof getDMMF
|
|
38
|
+
apiLibraryGenerator: typeof apiLibraryGenerator
|
|
39
|
+
getPrismaSchemaPath: typeof getPrismaSchemaPath
|
|
40
|
+
readPrismaSchema: typeof readPrismaSchema
|
|
41
|
+
getNpmScope: typeof getNpmScope
|
|
42
|
+
pluralize: typeof pluralize
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// STEP 2: DEFINE PURE HELPER & CONTENT GENERATION FUNCTIONS
|
|
46
|
+
// These functions are side-effect free and can be tested independently.
|
|
47
|
+
|
|
48
|
+
export function parseCrudAuth(comment: string): CrudAuthConfig | null {
|
|
49
|
+
try {
|
|
50
|
+
const match = comment.match(/@crudAuth:\s*(\{.*\})/)
|
|
51
|
+
if (!match) return null
|
|
52
|
+
return JSON.parse(match[1])
|
|
53
|
+
} catch (e) {
|
|
54
|
+
console.error('Error parsing @crudAuth:', e)
|
|
55
|
+
return null
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function getCrudAuthForModel(schema: string, modelName: string): CrudAuthConfig {
|
|
60
|
+
const defaultConfig: CrudAuthConfig = {
|
|
61
|
+
readOne: 'admin',
|
|
62
|
+
readMany: 'admin',
|
|
63
|
+
count: 'admin',
|
|
64
|
+
create: 'admin',
|
|
65
|
+
update: 'admin',
|
|
66
|
+
delete: 'admin',
|
|
67
|
+
}
|
|
68
|
+
const lines = schema.split('\n')
|
|
69
|
+
let modelDoc: string[] = []
|
|
70
|
+
let foundModel = false
|
|
71
|
+
for (const line of lines) {
|
|
72
|
+
const trimmedLine = line.trim()
|
|
73
|
+
if (
|
|
74
|
+
trimmedLine.startsWith(`model ${modelName}`) ||
|
|
75
|
+
trimmedLine.startsWith(`model ${modelName} `) ||
|
|
76
|
+
trimmedLine.startsWith(`model ${modelName}{`)
|
|
77
|
+
) {
|
|
78
|
+
foundModel = true
|
|
79
|
+
break
|
|
80
|
+
} else if (trimmedLine.startsWith('model ')) {
|
|
81
|
+
modelDoc = []
|
|
82
|
+
} else if (trimmedLine.startsWith('///') && !foundModel) {
|
|
83
|
+
modelDoc.push(trimmedLine)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (!foundModel) return defaultConfig
|
|
87
|
+
const authLine = modelDoc.find((line) => line.includes('@crudAuth:'))
|
|
88
|
+
if (!authLine) return defaultConfig
|
|
89
|
+
const config = parseCrudAuth(authLine)
|
|
90
|
+
return config ? { ...defaultConfig, ...config } : defaultConfig
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function getGuardForAuthLevel(level: string): string | null {
|
|
94
|
+
if (!level) return 'GqlAuthAdminGuard'
|
|
95
|
+
level = level.toLowerCase()
|
|
96
|
+
if (level === 'public') return null
|
|
97
|
+
if (level === 'user') return 'GqlAuthGuard'
|
|
98
|
+
if (level === 'admin') return 'GqlAuthAdminGuard'
|
|
99
|
+
const pascalCase = level.charAt(0).toUpperCase() + level.slice(1).toLowerCase()
|
|
100
|
+
return `GqlAuth${pascalCase}Guard`
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function toKebabCase(str: string): string {
|
|
104
|
+
return str.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase()
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function generateResolverContent(model: ModelType, npmScope: string): string {
|
|
108
|
+
const usedGuards = new Set<string>()
|
|
109
|
+
if (model.auth) {
|
|
110
|
+
Object.values(model.auth).forEach((level) => {
|
|
111
|
+
if (level === 'public') return
|
|
112
|
+
const guard = getGuardForAuthLevel(level)
|
|
113
|
+
if (guard) usedGuards.add(guard)
|
|
114
|
+
})
|
|
115
|
+
} else {
|
|
116
|
+
usedGuards.add('GqlAuthAdminGuard')
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const guardImports =
|
|
120
|
+
usedGuards.size > 0 ? `import { ${Array.from(usedGuards).sort().join(', ')} } from '@${npmScope}/api/utils'` : ''
|
|
121
|
+
|
|
122
|
+
const readManyGuardDecorator = model.auth?.readMany ? getGuardForAuthLevel(model.auth.readMany) : 'GqlAuthAdminGuard'
|
|
123
|
+
const countGuardDecorator = model.auth?.count ? getGuardForAuthLevel(model.auth.count) : 'GqlAuthAdminGuard'
|
|
124
|
+
const readOneGuardDecorator = model.auth?.readOne ? getGuardForAuthLevel(model.auth.readOne) : 'GqlAuthAdminGuard'
|
|
125
|
+
const createGuardDecorator = model.auth?.create ? getGuardForAuthLevel(model.auth.create) : 'GqlAuthAdminGuard'
|
|
126
|
+
const updateGuardDecorator = model.auth?.update ? getGuardForAuthLevel(model.auth.update) : 'GqlAuthAdminGuard'
|
|
127
|
+
const deleteGuardDecorator = model.auth?.delete ? getGuardForAuthLevel(model.auth.delete) : 'GqlAuthAdminGuard'
|
|
128
|
+
|
|
129
|
+
const readManyMethodName = model.pluralModelPropertyName
|
|
130
|
+
const countMethodName = `${model.pluralModelPropertyName}Count`
|
|
131
|
+
const readOneMethodName = model.modelPropertyName
|
|
132
|
+
|
|
133
|
+
return `import { Args, Mutation, Query, Resolver, Info } from '@nestjs/graphql'
|
|
134
|
+
import { UseGuards } from '@nestjs/common'
|
|
135
|
+
import type { GraphQLResolveInfo } from 'graphql'
|
|
136
|
+
import { CorePaging } from '@${npmScope}/api/core/data-access'
|
|
137
|
+
import { ApiCrudDataAccessService } from '@${npmScope}/api/generated-crud/data-access'
|
|
138
|
+
import { ${model.modelName} } from '@${npmScope}/api/core/models'
|
|
139
|
+
import { Create${model.modelName}Input, List${model.modelName}Input, Update${
|
|
140
|
+
model.modelName
|
|
141
|
+
}Input } from '@${npmScope}/api/generated-crud/data-access'
|
|
142
|
+
${guardImports}
|
|
143
|
+
|
|
144
|
+
@Resolver(() => ${model.modelName})
|
|
145
|
+
export class Generated${model.modelName}Resolver {
|
|
146
|
+
constructor(private readonly service: ApiCrudDataAccessService) {}
|
|
147
|
+
|
|
148
|
+
@Query(() => [${model.modelName}], { nullable: true })
|
|
149
|
+
${readManyGuardDecorator ? `@UseGuards(${readManyGuardDecorator})` : ''}
|
|
150
|
+
${readManyMethodName}(
|
|
151
|
+
@Info() info: GraphQLResolveInfo,
|
|
152
|
+
@Args({ name: 'input', type: () => List${model.modelName}Input, nullable: true }) input?: List${
|
|
153
|
+
model.modelName
|
|
154
|
+
}Input,
|
|
155
|
+
) {
|
|
156
|
+
return this.service.${readManyMethodName}(info, input)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
@Query(() => CorePaging, { nullable: true })
|
|
160
|
+
${countGuardDecorator ? `@UseGuards(${countGuardDecorator})` : ''}
|
|
161
|
+
${countMethodName}(
|
|
162
|
+
@Args({ name: 'input', type: () => List${model.modelName}Input, nullable: true }) input?: List${
|
|
163
|
+
model.modelName
|
|
164
|
+
}Input,
|
|
165
|
+
) {
|
|
166
|
+
return this.service.${countMethodName}(input)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
@Query(() => ${model.modelName}, { nullable: true })
|
|
170
|
+
${readOneGuardDecorator ? `@UseGuards(${readOneGuardDecorator})` : ''}
|
|
171
|
+
${readOneMethodName}(
|
|
172
|
+
@Info() info: GraphQLResolveInfo,
|
|
173
|
+
@Args('${model.modelPropertyName}Id') ${model.modelPropertyName}Id: string
|
|
174
|
+
) {
|
|
175
|
+
return this.service.${readOneMethodName}(info, ${model.modelPropertyName}Id)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
@Mutation(() => ${model.modelName}, { nullable: true })
|
|
179
|
+
${createGuardDecorator ? `@UseGuards(${createGuardDecorator})` : ''}
|
|
180
|
+
create${model.modelName}(
|
|
181
|
+
@Info() info: GraphQLResolveInfo,
|
|
182
|
+
@Args('input') input: Create${model.modelName}Input,
|
|
183
|
+
) {
|
|
184
|
+
return this.service.create${model.modelName}(info, input)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
@Mutation(() => ${model.modelName}, { nullable: true })
|
|
188
|
+
${updateGuardDecorator ? `@UseGuards(${updateGuardDecorator})` : ''}
|
|
189
|
+
update${model.modelName}(
|
|
190
|
+
@Info() info: GraphQLResolveInfo,
|
|
191
|
+
@Args('${model.modelPropertyName}Id') ${model.modelPropertyName}Id: string,
|
|
192
|
+
@Args('input') input: Update${model.modelName}Input,
|
|
193
|
+
) {
|
|
194
|
+
return this.service.update${model.modelName}(info, ${model.modelPropertyName}Id, input)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
@Mutation(() => ${model.modelName}, { nullable: true })
|
|
198
|
+
${deleteGuardDecorator ? `@UseGuards(${deleteGuardDecorator})` : ''}
|
|
199
|
+
delete${model.modelName}(
|
|
200
|
+
@Args('${model.modelPropertyName}Id') ${model.modelPropertyName}Id: string,
|
|
201
|
+
) {
|
|
202
|
+
return this.service.delete${model.modelName}(${model.modelPropertyName}Id)
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
`
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function generateFeatureModuleContent(models: ModelType[], npmScope: string): string {
|
|
209
|
+
return `import { Module } from '@nestjs/common'\nimport { ApiCrudDataAccessModule } from '@${npmScope}/api/generated-crud/data-access'\n${models
|
|
210
|
+
.map((model) => `import { Generated${model.modelName}Resolver } from './${toKebabCase(model.modelName)}.resolver'`)
|
|
211
|
+
.join('\n')}\n\n@Module({\n imports: [ApiCrudDataAccessModule],\n providers: [${models
|
|
212
|
+
.map((model) => `Generated${model.modelName}Resolver`)
|
|
213
|
+
.join(', ')}],\n})\nexport class ApiGeneratedCrudFeatureModule {}\n`
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function generateFeatureIndexContent(models: ModelType[]): string {
|
|
217
|
+
return `export * from './lib/api-admin-crud-feature.module'\n${models
|
|
218
|
+
.map((model) => `export * from './lib/${toKebabCase(model.modelName)}.resolver'`)
|
|
219
|
+
.join('\n')}\n`
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// STEP 3: DEFINE THE CORE LOGIC FUNCTION
|
|
223
|
+
// This function contains all the generator's logic but uses injected dependencies, making it testable.
|
|
224
|
+
export async function generateCrudLogic(
|
|
225
|
+
tree: Tree,
|
|
226
|
+
schema: GenerateCrudGeneratorSchema,
|
|
227
|
+
dependencies: GenerateCrudGeneratorDependencies,
|
|
228
|
+
) {
|
|
229
|
+
// Helper functions that now use injected dependencies
|
|
230
|
+
async function getAllPrismaModels(tree: Tree): Promise<ModelType[]> {
|
|
231
|
+
const prismaPath = dependencies.getPrismaSchemaPath(tree)
|
|
232
|
+
const prismaSchema = dependencies.readPrismaSchema(tree, prismaPath)
|
|
233
|
+
if (!prismaSchema) {
|
|
234
|
+
console.error(`No Prisma schema found at ${prismaPath}`)
|
|
235
|
+
return []
|
|
236
|
+
}
|
|
237
|
+
try {
|
|
238
|
+
const dmmf = await dependencies.getDMMF({ datamodel: prismaSchema })
|
|
239
|
+
return dmmf.datamodel.models.map((model) => {
|
|
240
|
+
const singularPropertyName = model.name.charAt(0).toLowerCase() + model.name.slice(1)
|
|
241
|
+
const pluralPropertyName = dependencies.pluralize(singularPropertyName)
|
|
242
|
+
const authConfig = getCrudAuthForModel(prismaSchema, model.name)
|
|
243
|
+
return {
|
|
244
|
+
name: model.name,
|
|
245
|
+
pluralName: dependencies.pluralize(model.name),
|
|
246
|
+
fields: model.fields.map((field) => ({ ...field })),
|
|
247
|
+
primaryField: model.fields.find((f) => !f.isId && f.type === 'String')?.name || 'name',
|
|
248
|
+
modelName: model.name,
|
|
249
|
+
modelPropertyName: singularPropertyName,
|
|
250
|
+
pluralModelName: dependencies.pluralize(model.name),
|
|
251
|
+
pluralModelPropertyName: pluralPropertyName,
|
|
252
|
+
auth: authConfig,
|
|
253
|
+
}
|
|
254
|
+
})
|
|
255
|
+
} catch (error) {
|
|
256
|
+
console.error('Error parsing Prisma schema:', error)
|
|
257
|
+
return []
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async function createLibraries(tree: Tree, name: string) {
|
|
262
|
+
const dataAccessLibraryRoot = `libs/api/${name}/data-access`
|
|
263
|
+
const featureLibraryRoot = `libs/api/${name}/feature`
|
|
264
|
+
const dataAccessTemplatePath = dependencies.joinPathFragments(__dirname, './files/data-access')
|
|
265
|
+
const featureTemplatePath = dependencies.joinPathFragments(__dirname, './files/feature')
|
|
266
|
+
await dependencies.apiLibraryGenerator(tree, { name }, dataAccessTemplatePath, 'data-access')
|
|
267
|
+
await dependencies.apiLibraryGenerator(tree, { name }, featureTemplatePath, 'feature')
|
|
268
|
+
return { dataAccessLibraryRoot, featureLibraryRoot }
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
async function generateModelFiles(
|
|
272
|
+
tree: Tree,
|
|
273
|
+
dataAccessLibraryRoot: string,
|
|
274
|
+
featureLibraryRoot: string,
|
|
275
|
+
models: ModelType[],
|
|
276
|
+
name: string,
|
|
277
|
+
) {
|
|
278
|
+
const npmScope = dependencies.getNpmScope(tree)
|
|
279
|
+
const nameObj = dependencies.names(name)
|
|
280
|
+
const substitutions = { ...nameObj, name, models, npmScope: `@${npmScope}`, apiClassName: 'PrismaCrud', tmpl: '' }
|
|
281
|
+
|
|
282
|
+
dependencies.generateFiles(
|
|
283
|
+
tree,
|
|
284
|
+
dependencies.joinPathFragments(__dirname, './files/data-access/src/lib'),
|
|
285
|
+
dependencies.joinPathFragments(dataAccessLibraryRoot, 'src/lib'),
|
|
286
|
+
{ ...substitutions, type: 'data-access' },
|
|
287
|
+
)
|
|
288
|
+
dependencies.generateFiles(
|
|
289
|
+
tree,
|
|
290
|
+
dependencies.joinPathFragments(__dirname, './files/data-access/src'),
|
|
291
|
+
dependencies.joinPathFragments(dataAccessLibraryRoot, 'src'),
|
|
292
|
+
{ ...substitutions, type: 'data-access' },
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
const featureModuleContent = generateFeatureModuleContent(models, npmScope)
|
|
296
|
+
tree.write(
|
|
297
|
+
dependencies.joinPathFragments(featureLibraryRoot, 'src/lib/api-admin-crud-feature.module.ts'),
|
|
298
|
+
featureModuleContent,
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
const featureIndexContent = generateFeatureIndexContent(models)
|
|
302
|
+
tree.write(dependencies.joinPathFragments(featureLibraryRoot, 'src/index.ts'), featureIndexContent)
|
|
303
|
+
|
|
304
|
+
for (const model of models) {
|
|
305
|
+
const resolverFilePath = dependencies.joinPathFragments(
|
|
306
|
+
featureLibraryRoot,
|
|
307
|
+
`src/lib/${toKebabCase(model.modelName)}.resolver.ts`,
|
|
308
|
+
)
|
|
309
|
+
const resolverContent = generateResolverContent(model, npmScope)
|
|
310
|
+
tree.write(resolverFilePath, resolverContent)
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Main Orchestration Logic
|
|
315
|
+
const name = schema.name || 'generated-crud'
|
|
316
|
+
const models = await getAllPrismaModels(tree)
|
|
317
|
+
if (models.length === 0) {
|
|
318
|
+
console.error('No Prisma models found')
|
|
319
|
+
return // Return early for the test case
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const { dataAccessLibraryRoot, featureLibraryRoot } = await createLibraries(tree, name)
|
|
323
|
+
await generateModelFiles(tree, dataAccessLibraryRoot, featureLibraryRoot, models, name)
|
|
324
|
+
await dependencies.formatFiles(tree)
|
|
325
|
+
|
|
326
|
+
return () => {
|
|
327
|
+
dependencies.installPackagesTask(tree)
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// STEP 4: DEFINE THE DEFAULT EXPORT
|
|
332
|
+
// This is what Nx CLI executes. It's a simple wrapper that provides the *real* dependencies to the logic function.
|
|
333
|
+
export default async function (tree: Tree, schema: GenerateCrudGeneratorSchema) {
|
|
334
|
+
const dependencies: GenerateCrudGeneratorDependencies = {
|
|
335
|
+
formatFiles,
|
|
336
|
+
generateFiles,
|
|
337
|
+
installPackagesTask,
|
|
338
|
+
joinPathFragments,
|
|
339
|
+
names,
|
|
340
|
+
getDMMF,
|
|
341
|
+
apiLibraryGenerator,
|
|
342
|
+
getPrismaSchemaPath,
|
|
343
|
+
readPrismaSchema,
|
|
344
|
+
getNpmScope,
|
|
345
|
+
pluralize,
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
try {
|
|
349
|
+
return await generateCrudLogic(tree, schema, dependencies)
|
|
350
|
+
} catch (error) {
|
|
351
|
+
console.error('Error in CRUD generator:', error)
|
|
352
|
+
throw error
|
|
353
|
+
}
|
|
354
|
+
}
|