@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,239 @@
|
|
|
1
|
+
import { formatFiles, installPackagesTask, Tree } from '@nx/devkit'
|
|
2
|
+
import { getDMMF } from '@prisma/internals'
|
|
3
|
+
import { addToModules, apiLibraryGenerator, getPrismaSchemaPath, readPrismaSchema } from '@nestledjs/utils'
|
|
4
|
+
import { GenerateCustomGeneratorSchema } from './schema'
|
|
5
|
+
import { execSync } from 'child_process'
|
|
6
|
+
import { getNpmScope } from '@nx/js/src/utils/package-json/get-npm-scope'
|
|
7
|
+
import pluralize from 'pluralize'
|
|
8
|
+
import { join } from 'path'
|
|
9
|
+
|
|
10
|
+
// Group all dependencies into a single object
|
|
11
|
+
const defaultDependencies = {
|
|
12
|
+
formatFiles,
|
|
13
|
+
installPackagesTask,
|
|
14
|
+
getDMMF,
|
|
15
|
+
addToModules,
|
|
16
|
+
apiLibraryGenerator,
|
|
17
|
+
getPrismaSchemaPath,
|
|
18
|
+
readPrismaSchema,
|
|
19
|
+
execSync,
|
|
20
|
+
getNpmScope,
|
|
21
|
+
pluralize,
|
|
22
|
+
join,
|
|
23
|
+
}
|
|
24
|
+
export type CustomGeneratorDependencies = typeof defaultDependencies
|
|
25
|
+
|
|
26
|
+
interface ModelType {
|
|
27
|
+
name: string
|
|
28
|
+
pluralName: string
|
|
29
|
+
fields: ReadonlyArray<Record<string, unknown> & { name: string; type: string }>
|
|
30
|
+
primaryField: string
|
|
31
|
+
modelName: string
|
|
32
|
+
modelPropertyName: string
|
|
33
|
+
pluralModelName: string
|
|
34
|
+
pluralModelPropertyName: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function getAllPrismaModels(tree: Tree, dependencies: CustomGeneratorDependencies): Promise<ModelType[]> {
|
|
38
|
+
const prismaPath = dependencies.getPrismaSchemaPath(tree)
|
|
39
|
+
const prismaSchema = dependencies.readPrismaSchema(tree, prismaPath)
|
|
40
|
+
if (!prismaSchema) {
|
|
41
|
+
console.error(`No Prisma schema found at ${prismaPath}`)
|
|
42
|
+
return []
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const dmmf = await dependencies.getDMMF({ datamodel: prismaSchema })
|
|
47
|
+
return dmmf.datamodel.models.map((model) => {
|
|
48
|
+
const singularPropertyName = model.name.charAt(0).toLowerCase() + model.name.slice(1)
|
|
49
|
+
const pluralPropertyName = dependencies.pluralize(singularPropertyName)
|
|
50
|
+
|
|
51
|
+
// Create a properly typed fields array
|
|
52
|
+
const fields = model.fields.map((field) => ({
|
|
53
|
+
name: field.name,
|
|
54
|
+
type: field.type,
|
|
55
|
+
isId: field.isId,
|
|
56
|
+
isRequired: field.isRequired,
|
|
57
|
+
isList: field.isList,
|
|
58
|
+
isUnique: field.isUnique,
|
|
59
|
+
isReadOnly: field.isReadOnly,
|
|
60
|
+
isGenerated: field.isGenerated,
|
|
61
|
+
isUpdatedAt: field.isUpdatedAt,
|
|
62
|
+
documentation: field.documentation,
|
|
63
|
+
// Include any other properties that might be needed
|
|
64
|
+
...field,
|
|
65
|
+
}))
|
|
66
|
+
|
|
67
|
+
// Create and return the model
|
|
68
|
+
const modelData: ModelType = {
|
|
69
|
+
name: model.name,
|
|
70
|
+
pluralName: dependencies.pluralize(model.name),
|
|
71
|
+
fields,
|
|
72
|
+
primaryField: model.fields.find((f) => !f.isId && f.type === 'String')?.name || 'name',
|
|
73
|
+
modelName: model.name,
|
|
74
|
+
modelPropertyName: singularPropertyName,
|
|
75
|
+
pluralModelName: dependencies.pluralize(model.name),
|
|
76
|
+
pluralModelPropertyName: pluralPropertyName,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return modelData
|
|
80
|
+
})
|
|
81
|
+
} catch (error) {
|
|
82
|
+
console.error('Error parsing Prisma schema:', error)
|
|
83
|
+
return []
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function ensureDirExists(tree: Tree, path: string) {
|
|
88
|
+
if (!tree.exists(path)) {
|
|
89
|
+
// Only create the directory, do not write .gitkeep
|
|
90
|
+
// Directory will be created when a file is written into it
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function toKebabCase(str: string): string {
|
|
95
|
+
return str
|
|
96
|
+
.replace(/([a-z])([A-Z])/g, '$1-$2')
|
|
97
|
+
.replace(/([A-Z])([A-Z][a-z])/g, '$1-$2')
|
|
98
|
+
.toLowerCase()
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function generateCustomFiles(
|
|
102
|
+
tree: Tree,
|
|
103
|
+
customLibraryRoot: string,
|
|
104
|
+
models: ModelType[],
|
|
105
|
+
npmScope: string,
|
|
106
|
+
dependencies: CustomGeneratorDependencies,
|
|
107
|
+
) {
|
|
108
|
+
const defaultDir = dependencies.join(customLibraryRoot, 'src/lib/default')
|
|
109
|
+
const pluginsDir = dependencies.join(customLibraryRoot, 'src/lib/plugins')
|
|
110
|
+
await ensureDirExists(tree, defaultDir)
|
|
111
|
+
await ensureDirExists(tree, pluginsDir)
|
|
112
|
+
// Only write .gitkeep in pluginsDir
|
|
113
|
+
tree.write(dependencies.join(pluginsDir, '.gitkeep'), '')
|
|
114
|
+
|
|
115
|
+
for (const model of models) {
|
|
116
|
+
const kebabModel = toKebabCase(model.modelName)
|
|
117
|
+
const modelFolder = dependencies.join(defaultDir, kebabModel)
|
|
118
|
+
if (tree.exists(modelFolder)) {
|
|
119
|
+
// Skip if model folder already exists
|
|
120
|
+
continue
|
|
121
|
+
}
|
|
122
|
+
await ensureDirExists(tree, modelFolder)
|
|
123
|
+
|
|
124
|
+
// Generate service.ts
|
|
125
|
+
const serviceContent = `import { Injectable } from '@nestjs/common'
|
|
126
|
+
|
|
127
|
+
@Injectable()
|
|
128
|
+
export class ${model.modelName}Service {
|
|
129
|
+
// Empty for now; will override or extend later if needed
|
|
130
|
+
}
|
|
131
|
+
`
|
|
132
|
+
tree.write(dependencies.join(modelFolder, `${kebabModel}.service.ts`), serviceContent)
|
|
133
|
+
|
|
134
|
+
// Generate resolver.ts
|
|
135
|
+
const resolverContent = `
|
|
136
|
+
import { ApiCrudDataAccessService } from '${npmScope}/api/generated-crud/data-access'
|
|
137
|
+
import { Generated${model.modelName}Resolver } from '${npmScope}/api/generated-crud/feature'
|
|
138
|
+
import { Injectable } from '@nestjs/common'
|
|
139
|
+
import { Resolver } from '@nestjs/graphql'
|
|
140
|
+
import { ${model.modelName} } from '${npmScope}/api/core/models'
|
|
141
|
+
|
|
142
|
+
@Resolver(() => ${model.modelName})
|
|
143
|
+
@Injectable()
|
|
144
|
+
export class ${model.modelName}Resolver extends Generated${model.modelName}Resolver {
|
|
145
|
+
constructor(
|
|
146
|
+
// private readonly customService: ${model.modelName}Service,
|
|
147
|
+
generatedService: ApiCrudDataAccessService,
|
|
148
|
+
) {
|
|
149
|
+
super(generatedService)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
`
|
|
153
|
+
tree.write(dependencies.join(modelFolder, `${kebabModel}.resolver.ts`), resolverContent)
|
|
154
|
+
|
|
155
|
+
// Generate module.ts
|
|
156
|
+
const moduleContent = `import { Module } from '@nestjs/common'
|
|
157
|
+
import { ${model.modelName}Service } from './${kebabModel}.service'
|
|
158
|
+
import { ${model.modelName}Resolver } from './${kebabModel}.resolver'
|
|
159
|
+
import { ApiCrudDataAccessModule } from '${npmScope}/api/generated-crud/data-access'
|
|
160
|
+
|
|
161
|
+
@Module({
|
|
162
|
+
imports: [ApiCrudDataAccessModule],
|
|
163
|
+
providers: [${model.modelName}Service, ${model.modelName}Resolver],
|
|
164
|
+
exports: [${model.modelName}Service, ${model.modelName}Resolver],
|
|
165
|
+
})
|
|
166
|
+
export class ${model.modelName}Module {}
|
|
167
|
+
`
|
|
168
|
+
tree.write(dependencies.join(modelFolder, `${kebabModel}.module.ts`), moduleContent)
|
|
169
|
+
|
|
170
|
+
// Add to defaultModules in app.module.ts__tmpl__
|
|
171
|
+
dependencies.addToModules({
|
|
172
|
+
tree,
|
|
173
|
+
modulePath: 'apps/api/src/app.module.ts',
|
|
174
|
+
moduleArrayName: 'defaultModules',
|
|
175
|
+
moduleToAdd: `${model.modelName}Module`,
|
|
176
|
+
importPath: `${npmScope}/api/custom`,
|
|
177
|
+
})
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Update index.ts to export all model modules
|
|
181
|
+
const modelFolders = models.map((m) => toKebabCase(m.modelName))
|
|
182
|
+
const indexContent = modelFolders.map((m) => `export * from './lib/default/${m}/${m}.module'`).join('\n')
|
|
183
|
+
tree.write(dependencies.join(customLibraryRoot, 'src/index.ts'), indexContent)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export async function customGeneratorLogic(
|
|
187
|
+
tree: Tree,
|
|
188
|
+
schema: GenerateCustomGeneratorSchema,
|
|
189
|
+
dependencies: CustomGeneratorDependencies = defaultDependencies,
|
|
190
|
+
) {
|
|
191
|
+
try {
|
|
192
|
+
const name = schema.name || 'custom'
|
|
193
|
+
const customLibraryRoot = schema.directory ? `libs/api/${schema.directory}/${name}` : `libs/api/${name}`
|
|
194
|
+
const projectName = schema.directory ? `api-${schema.directory.replace(/\//g, '-')}-${name}` : `api-${name}`
|
|
195
|
+
|
|
196
|
+
// Overwrite logic
|
|
197
|
+
if (schema.overwrite && tree.exists(customLibraryRoot)) {
|
|
198
|
+
try {
|
|
199
|
+
dependencies.execSync(`nx g @nx/workspace:remove ${projectName} --forceRemove`, {
|
|
200
|
+
stdio: 'inherit',
|
|
201
|
+
cwd: tree.root,
|
|
202
|
+
})
|
|
203
|
+
} catch (error) {
|
|
204
|
+
console.warn(`Failed to remove existing library ${projectName}:`, error)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Use the shared apiLibraryGenerator
|
|
209
|
+
await dependencies.apiLibraryGenerator(tree, { name }, '', undefined, false)
|
|
210
|
+
|
|
211
|
+
await ensureDirExists(tree, dependencies.join(customLibraryRoot, 'src/lib/default'))
|
|
212
|
+
await ensureDirExists(tree, dependencies.join(customLibraryRoot, 'src/lib/plugins'))
|
|
213
|
+
|
|
214
|
+
// Get all Prisma models
|
|
215
|
+
const models = await getAllPrismaModels(tree, dependencies)
|
|
216
|
+
if (models.length === 0) {
|
|
217
|
+
console.error('No Prisma models found')
|
|
218
|
+
return
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Generate custom files per model
|
|
222
|
+
const npmScope = `@${dependencies.getNpmScope(tree)}`
|
|
223
|
+
await generateCustomFiles(tree, customLibraryRoot, models, npmScope, dependencies)
|
|
224
|
+
|
|
225
|
+
// Format files
|
|
226
|
+
await dependencies.formatFiles(tree)
|
|
227
|
+
|
|
228
|
+
return () => {
|
|
229
|
+
dependencies.installPackagesTask(tree)
|
|
230
|
+
}
|
|
231
|
+
} catch (error) {
|
|
232
|
+
console.error('Error in Custom generator:', error)
|
|
233
|
+
throw error
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export default async function (tree: Tree, schema: GenerateCustomGeneratorSchema) {
|
|
238
|
+
return customGeneratorLogic(tree, schema)
|
|
239
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../node_modules/@nx/devkit/schema.d.json",
|
|
3
|
+
"extends": "../../node_modules/@nx/devkit/generators.json",
|
|
4
|
+
"properties": {
|
|
5
|
+
"name": {
|
|
6
|
+
"description": "Name of the custom library to generate",
|
|
7
|
+
"type": "string",
|
|
8
|
+
"default": "custom"
|
|
9
|
+
},
|
|
10
|
+
"directory": {
|
|
11
|
+
"description": "Optional directory to place the library",
|
|
12
|
+
"type": "string"
|
|
13
|
+
},
|
|
14
|
+
"overwrite": {
|
|
15
|
+
"description": "Whether to overwrite existing files",
|
|
16
|
+
"type": "boolean",
|
|
17
|
+
"default": false
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"required": []
|
|
21
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { Tree } from '@nx/devkit'
|
|
2
|
+
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'
|
|
3
|
+
import { vi, describe, it, expect, beforeEach } from 'vitest'
|
|
4
|
+
import { getDMMF } from '@prisma/internals'
|
|
5
|
+
import { getNpmScope } from '@nx/js/src/utils/package-json/get-npm-scope'
|
|
6
|
+
import generator from './generator'
|
|
7
|
+
import { apiLibraryGenerator, getPrismaSchemaPath, readPrismaSchema } from '@nestledjs/utils'
|
|
8
|
+
|
|
9
|
+
// Mock dependencies
|
|
10
|
+
vi.mock('@prisma/internals')
|
|
11
|
+
vi.mock('@nestledjs/utils', () => {
|
|
12
|
+
return {
|
|
13
|
+
apiLibraryGenerator: vi.fn().mockResolvedValue(undefined),
|
|
14
|
+
getPrismaSchemaPath: vi.fn(),
|
|
15
|
+
readPrismaSchema: vi.fn(),
|
|
16
|
+
}
|
|
17
|
+
})
|
|
18
|
+
vi.mock('@nx/js/src/utils/package-json/get-npm-scope')
|
|
19
|
+
|
|
20
|
+
describe('extended-generator', () => {
|
|
21
|
+
let tree: Tree
|
|
22
|
+
|
|
23
|
+
beforeEach(async () => {
|
|
24
|
+
tree = createTreeWithEmptyWorkspace()
|
|
25
|
+
vi.clearAllMocks()
|
|
26
|
+
|
|
27
|
+
vi.mocked(getNpmScope).mockReturnValue('test-scope')
|
|
28
|
+
vi.mocked(getPrismaSchemaPath).mockReturnValue('prisma/schema.prisma')
|
|
29
|
+
vi.mocked(readPrismaSchema).mockReturnValue(`
|
|
30
|
+
model User {
|
|
31
|
+
id Int @id @default(autoincrement())
|
|
32
|
+
name String
|
|
33
|
+
}
|
|
34
|
+
model Post {
|
|
35
|
+
id Int @id @default(autoincrement())
|
|
36
|
+
title String
|
|
37
|
+
}
|
|
38
|
+
`)
|
|
39
|
+
vi.mocked(getDMMF).mockResolvedValue({
|
|
40
|
+
datamodel: {
|
|
41
|
+
models: [
|
|
42
|
+
{
|
|
43
|
+
name: 'User',
|
|
44
|
+
fields: [
|
|
45
|
+
{ name: 'id', type: 'Int', isId: true },
|
|
46
|
+
{ name: 'name', type: 'String' },
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: 'Post',
|
|
51
|
+
fields: [
|
|
52
|
+
{ name: 'id', type: 'Int', isId: true },
|
|
53
|
+
{ name: 'title', type: 'String' },
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
} as any)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('should generate a library for each model with expected files', async () => {
|
|
62
|
+
await generator(tree, { overwrite: true })
|
|
63
|
+
// User library
|
|
64
|
+
expect(apiLibraryGenerator).toHaveBeenCalledWith(
|
|
65
|
+
tree,
|
|
66
|
+
{ name: 'extended-user', overwrite: true },
|
|
67
|
+
'',
|
|
68
|
+
undefined,
|
|
69
|
+
false,
|
|
70
|
+
)
|
|
71
|
+
expect(tree.exists('libs/api/extended/user/src/lib/user.service.ts')).toBe(true)
|
|
72
|
+
expect(tree.exists('libs/api/extended/user/src/lib/user.resolver.ts')).toBe(true)
|
|
73
|
+
expect(tree.exists('libs/api/extended/user/src/lib/user.module.ts')).toBe(true)
|
|
74
|
+
expect(tree.exists('libs/api/extended/user/src/index.ts')).toBe(true)
|
|
75
|
+
// Post library
|
|
76
|
+
expect(apiLibraryGenerator).toHaveBeenCalledWith(
|
|
77
|
+
tree,
|
|
78
|
+
{ name: 'extended-post', overwrite: true },
|
|
79
|
+
'',
|
|
80
|
+
undefined,
|
|
81
|
+
false,
|
|
82
|
+
)
|
|
83
|
+
expect(tree.exists('libs/api/extended/post/src/lib/post.service.ts')).toBe(true)
|
|
84
|
+
expect(tree.exists('libs/api/extended/post/src/lib/post.resolver.ts')).toBe(true)
|
|
85
|
+
expect(tree.exists('libs/api/extended/post/src/lib/post.module.ts')).toBe(true)
|
|
86
|
+
expect(tree.exists('libs/api/extended/post/src/index.ts')).toBe(true)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('should not generate files if no models are found', async () => {
|
|
90
|
+
vi.mocked(getDMMF).mockResolvedValue({ datamodel: { models: [] } } as any)
|
|
91
|
+
await generator(tree, { overwrite: true })
|
|
92
|
+
// No libraries should be created
|
|
93
|
+
expect(apiLibraryGenerator).not.toHaveBeenCalled()
|
|
94
|
+
})
|
|
95
|
+
})
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { formatFiles, installPackagesTask, Tree } from '@nx/devkit'
|
|
2
|
+
import { getDMMF } from '@prisma/internals'
|
|
3
|
+
import { apiLibraryGenerator, getPrismaSchemaPath, readPrismaSchema } from '@nestledjs/utils'
|
|
4
|
+
import { GenerateExtendedGeneratorSchema } from './schema'
|
|
5
|
+
import { getNpmScope } from '@nx/js/src/utils/package-json/get-npm-scope'
|
|
6
|
+
import pluralize from 'pluralize'
|
|
7
|
+
import { join } from 'path'
|
|
8
|
+
|
|
9
|
+
interface ModelType {
|
|
10
|
+
name: string
|
|
11
|
+
pluralName: string
|
|
12
|
+
fields: ReadonlyArray<Record<string, unknown> & { name: string; type: string }>
|
|
13
|
+
primaryField: string
|
|
14
|
+
modelName: string
|
|
15
|
+
modelPropertyName: string
|
|
16
|
+
pluralModelName: string
|
|
17
|
+
pluralModelPropertyName: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function toKebabCase(str: string): string {
|
|
21
|
+
return str
|
|
22
|
+
.replace(/([a-z])([A-Z])/g, '$1-$2')
|
|
23
|
+
.replace(/([A-Z])([A-Z][a-z])/g, '$1-$2')
|
|
24
|
+
.toLowerCase()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function getAllPrismaModels(tree: Tree): Promise<ModelType[]> {
|
|
28
|
+
const prismaPath = getPrismaSchemaPath(tree)
|
|
29
|
+
const prismaSchema = readPrismaSchema(tree, prismaPath)
|
|
30
|
+
if (!prismaSchema) {
|
|
31
|
+
console.error(`No Prisma schema found at ${prismaPath}`)
|
|
32
|
+
return []
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const dmmf = await getDMMF({ datamodel: prismaSchema })
|
|
37
|
+
return dmmf.datamodel.models.map((model) => {
|
|
38
|
+
const singularPropertyName = model.name.charAt(0).toLowerCase() + model.name.slice(1)
|
|
39
|
+
const pluralPropertyName = pluralize(singularPropertyName)
|
|
40
|
+
const fields = model.fields.map((field) => ({
|
|
41
|
+
name: field.name,
|
|
42
|
+
type: field.type,
|
|
43
|
+
isId: field.isId,
|
|
44
|
+
isRequired: field.isRequired,
|
|
45
|
+
isList: field.isList,
|
|
46
|
+
isUnique: field.isUnique,
|
|
47
|
+
isReadOnly: field.isReadOnly,
|
|
48
|
+
isGenerated: field.isGenerated,
|
|
49
|
+
isUpdatedAt: field.isUpdatedAt,
|
|
50
|
+
documentation: field.documentation,
|
|
51
|
+
...field,
|
|
52
|
+
}))
|
|
53
|
+
return {
|
|
54
|
+
name: model.name,
|
|
55
|
+
pluralName: pluralize(model.name),
|
|
56
|
+
fields,
|
|
57
|
+
primaryField: model.fields.find((f) => !f.isId && f.type === 'String')?.name || 'name',
|
|
58
|
+
modelName: model.name,
|
|
59
|
+
modelPropertyName: singularPropertyName,
|
|
60
|
+
pluralModelName: pluralize(model.name),
|
|
61
|
+
pluralModelPropertyName: pluralPropertyName,
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
} catch (error) {
|
|
65
|
+
console.error('Error parsing Prisma schema:', error)
|
|
66
|
+
return []
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function generateModelLibrary(tree: Tree, model: ModelType, npmScope: string, overwrite: boolean) {
|
|
71
|
+
const kebabModel = toKebabCase(model.modelName)
|
|
72
|
+
const libName = kebabModel
|
|
73
|
+
const libRoot = `libs/api/extended/${libName}`
|
|
74
|
+
|
|
75
|
+
// Create the Nx library for this model
|
|
76
|
+
await apiLibraryGenerator(tree, { name: `extended-${libName}`, overwrite }, '', undefined, false)
|
|
77
|
+
|
|
78
|
+
// Generate service.ts
|
|
79
|
+
const serviceContent = `import { Injectable } from '@nestjs/common'
|
|
80
|
+
|
|
81
|
+
@Injectable()
|
|
82
|
+
export class ${model.modelName}Service {
|
|
83
|
+
// Empty for now; will override or extend later if needed
|
|
84
|
+
}
|
|
85
|
+
`
|
|
86
|
+
tree.write(join(libRoot, 'src/lib', `${kebabModel}.service.ts`), serviceContent)
|
|
87
|
+
|
|
88
|
+
// Generate resolver.ts
|
|
89
|
+
const resolverContent = `import { ${model.modelName}Service } from './${kebabModel}.service'
|
|
90
|
+
import { ApiCrudDataAccessService } from '${npmScope}/api/generated-crud/data-access'
|
|
91
|
+
import { Generated${model.modelName}Resolver } from '${npmScope}/api/generated-crud/feature'
|
|
92
|
+
import { Injectable } from '@nestjs/common'
|
|
93
|
+
import { Resolver } from '@nestjs/graphql'
|
|
94
|
+
import { ${model.modelName} } from '${npmScope}/api/core/models'
|
|
95
|
+
|
|
96
|
+
@Resolver(() => ${model.modelName})
|
|
97
|
+
@Injectable()
|
|
98
|
+
export class ${model.modelName}Resolver extends Generated${model.modelName}Resolver {
|
|
99
|
+
constructor(
|
|
100
|
+
private readonly customService: ${model.modelName}Service,
|
|
101
|
+
private readonly generatedService: ApiCrudDataAccessService,
|
|
102
|
+
) {
|
|
103
|
+
super(generatedService)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
`
|
|
107
|
+
tree.write(join(libRoot, 'src/lib', `${kebabModel}.resolver.ts`), resolverContent)
|
|
108
|
+
|
|
109
|
+
// Generate module.ts
|
|
110
|
+
const moduleContent = `import { Module } from '@nestjs/common'
|
|
111
|
+
import { ${model.modelName}Service } from './${kebabModel}.service'
|
|
112
|
+
import { ${model.modelName}Resolver } from './${kebabModel}.resolver'
|
|
113
|
+
|
|
114
|
+
@Module({
|
|
115
|
+
providers: [${model.modelName}Service, ${model.modelName}Resolver],
|
|
116
|
+
exports: [${model.modelName}Service, ${model.modelName}Resolver],
|
|
117
|
+
})
|
|
118
|
+
export class ${model.modelName}Module {}
|
|
119
|
+
`
|
|
120
|
+
tree.write(join(libRoot, 'src/lib', `${kebabModel}.module.ts`), moduleContent)
|
|
121
|
+
|
|
122
|
+
// Update index.ts to export the module
|
|
123
|
+
const indexContent = `export * from './lib/${kebabModel}.module'`
|
|
124
|
+
tree.write(join(libRoot, 'src/index.ts'), indexContent)
|
|
125
|
+
|
|
126
|
+
// Optionally, add to app.module.ts or other registration logic here
|
|
127
|
+
// addToModules({ ... })
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export default async function (tree: Tree, schema: GenerateExtendedGeneratorSchema) {
|
|
131
|
+
try {
|
|
132
|
+
// Ensure the extended folder exists (not an Nx project)
|
|
133
|
+
const extendedRoot = 'libs/api/extended'
|
|
134
|
+
if (!tree.exists(extendedRoot)) {
|
|
135
|
+
tree.write(join(extendedRoot, '.gitkeep'), '')
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Get all Prisma models
|
|
139
|
+
const models = await getAllPrismaModels(tree)
|
|
140
|
+
if (models.length === 0) {
|
|
141
|
+
console.error('No Prisma models found')
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const npmScope = `@${getNpmScope(tree)}`
|
|
146
|
+
const overwrite = !!schema.overwrite
|
|
147
|
+
|
|
148
|
+
// For each model, generate a library and custom files
|
|
149
|
+
for (const model of models) {
|
|
150
|
+
await generateModelLibrary(tree, model, npmScope, overwrite)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
await formatFiles(tree)
|
|
154
|
+
return () => {
|
|
155
|
+
installPackagesTask(tree)
|
|
156
|
+
}
|
|
157
|
+
} catch (error) {
|
|
158
|
+
console.error('Error in Extended generator:', error)
|
|
159
|
+
throw error
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './generator'
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../node_modules/@nx/devkit/schema.d.json",
|
|
3
|
+
"extends": "../../node_modules/@nx/devkit/generators.json",
|
|
4
|
+
"properties": {
|
|
5
|
+
"overwrite": {
|
|
6
|
+
"description": "Whether to overwrite existing files",
|
|
7
|
+
"type": "boolean",
|
|
8
|
+
"default": false
|
|
9
|
+
}
|
|
10
|
+
},
|
|
11
|
+
"required": []
|
|
12
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Module } from '@nestjs/common'
|
|
2
|
+
|
|
3
|
+
import { ApiCrudDataAccessService } from './api-crud-data-access.service'
|
|
4
|
+
import { ApiCoreDataAccessModule } from '<%= npmScope %>/api/core/data-access'
|
|
5
|
+
|
|
6
|
+
@Module({
|
|
7
|
+
imports: [ApiCoreDataAccessModule],
|
|
8
|
+
providers: [ApiCrudDataAccessService],
|
|
9
|
+
exports: [ApiCrudDataAccessService],
|
|
10
|
+
})
|
|
11
|
+
export class ApiCrudDataAccessModule {}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { Injectable } from '@nestjs/common'
|
|
2
|
+
import { ApiCoreDataAccessService } from '<%= npmScope %>/api/core/data-access'
|
|
3
|
+
import { PrismaSelect } from '@paljs/plugins'
|
|
4
|
+
import type { GraphQLResolveInfo } from 'graphql'
|
|
5
|
+
import * as dto from './dto'
|
|
6
|
+
|
|
7
|
+
<% models.forEach((model, idx) => { %>
|
|
8
|
+
<% if (!model.modelName || !model.pluralModelName) { throw new Error('EJS template error: modelName or pluralModelName missing for model at index ' + idx + ': ' + JSON.stringify(model)); } %>
|
|
9
|
+
<% }) %>
|
|
10
|
+
|
|
11
|
+
@Injectable()
|
|
12
|
+
export class ApiCrudDataAccessService {
|
|
13
|
+
constructor(private readonly data: ApiCoreDataAccessService) {}
|
|
14
|
+
|
|
15
|
+
<% for (const model of models) { %>
|
|
16
|
+
async create<%= model.modelName.charAt(0).toUpperCase() + model.modelName.slice(1) %>(info: GraphQLResolveInfo, input: dto.Create<%= model.modelName %>Input) {
|
|
17
|
+
const select = new PrismaSelect(info).value
|
|
18
|
+
return this.data['<%= model.modelPropertyName %>'].create({
|
|
19
|
+
data: input,
|
|
20
|
+
...select,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async <%= (model.pluralModelName === model.modelName ? model.pluralModelName + 'List' : model.pluralModelName).charAt(0).toLowerCase() + (model.pluralModelName === model.modelName ? model.pluralModelName + 'List' : model.pluralModelName).slice(1) %>(info: GraphQLResolveInfo, input?: dto.List<%= model.modelName %>Input) {
|
|
25
|
+
const select = new PrismaSelect(info).value
|
|
26
|
+
return this.data['<%= model.modelPropertyName %>'].findMany({
|
|
27
|
+
...this.data.filter(input),
|
|
28
|
+
...select,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async <%= (model.pluralModelName === model.modelName ? model.pluralModelName + 'List' : model.pluralModelName).charAt(0).toLowerCase() + (model.pluralModelName === model.modelName ? model.pluralModelName + 'List' : model.pluralModelName).slice(1) %>Count(input?: dto.List<%= model.modelName %>Input) {
|
|
33
|
+
const total = await this.data['<%= model.modelPropertyName %>'].count()
|
|
34
|
+
const count = await this.data['<%= model.modelPropertyName %>'].count({
|
|
35
|
+
...this.data.filter(input)
|
|
36
|
+
});
|
|
37
|
+
const take = input?.take || 10
|
|
38
|
+
const skip = input?.skip || 0
|
|
39
|
+
const page = Math.floor(skip / take)
|
|
40
|
+
return {
|
|
41
|
+
take,
|
|
42
|
+
skip,
|
|
43
|
+
page,
|
|
44
|
+
count,
|
|
45
|
+
total,
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async <%= model.modelName.charAt(0).toLowerCase() + model.modelName.slice(1) %>(info: GraphQLResolveInfo, id: string) {
|
|
50
|
+
const select = new PrismaSelect(info).value
|
|
51
|
+
return this.data['<%= model.modelPropertyName %>'].findUnique({
|
|
52
|
+
where: { id },
|
|
53
|
+
...select,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async update<%= model.modelName.charAt(0).toUpperCase() + model.modelName.slice(1) %>(info: GraphQLResolveInfo, id: string, input: dto.Update<%= model.modelName %>Input) {
|
|
58
|
+
const select = new PrismaSelect(info).value
|
|
59
|
+
return this.data['<%= model.modelPropertyName %>'].update({
|
|
60
|
+
where: { id },
|
|
61
|
+
data: input,
|
|
62
|
+
...select,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async delete<%= model.modelName.charAt(0).toUpperCase() + model.modelName.slice(1) %>(id: string) {
|
|
67
|
+
return this.data['<%= model.modelPropertyName %>'].delete({
|
|
68
|
+
where: { id }
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
<% } %>
|
|
72
|
+
}
|