@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.
Files changed (132) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/README.md +11 -0
  3. package/eslint.config.cjs +28 -0
  4. package/generators.json +69 -0
  5. package/package.json +21 -0
  6. package/project.json +47 -0
  7. package/src/account/files/data-access/src/index.ts__tmpl__ +5 -0
  8. package/src/account/files/data-access/src/lib/api-account-data-access.module.ts__tmpl__ +10 -0
  9. package/src/account/files/data-access/src/lib/api-account-data-access.service.ts__tmpl__ +152 -0
  10. package/src/account/files/data-access/src/lib/dto/account-create-email.input.ts__tmpl__ +9 -0
  11. package/src/account/files/data-access/src/lib/dto/account-update-password.input.ts__tmpl__ +16 -0
  12. package/src/account/files/data-access/src/lib/dto/account-update-profile.input.ts__tmpl__ +25 -0
  13. package/src/account/files/feature/src/index.ts__tmpl__ +1 -0
  14. package/src/account/files/feature/src/lib/api-account-feature.module.ts__tmpl__ +9 -0
  15. package/src/account/files/feature/src/lib/api-account-feature.resolver.ts__tmpl__ +83 -0
  16. package/src/account/generator.spec.ts +71 -0
  17. package/src/account/generator.ts +20 -0
  18. package/src/account/schema.d.ts +3 -0
  19. package/src/account/schema.json +13 -0
  20. package/src/app/files/src/app.config.ts__tmpl__ +66 -0
  21. package/src/app/files/src/app.module.ts__tmpl__ +43 -0
  22. package/src/app/files/src/applogger.middleware.ts__tmpl__ +21 -0
  23. package/src/app/files/src/main.ts__tmpl__ +33 -0
  24. package/src/app/files/webpack.config.js__tmpl__ +54 -0
  25. package/src/app/generator.spec.ts +112 -0
  26. package/src/app/generator.ts +105 -0
  27. package/src/app/schema.d.ts +1 -0
  28. package/src/app/schema.json +9 -0
  29. package/src/config/files/src/index.ts__tmpl__ +3 -0
  30. package/src/config/files/src/lib/config.service.ts__tmpl__ +51 -0
  31. package/src/config/files/src/lib/configuration.ts__tmpl__ +32 -0
  32. package/src/config/files/src/lib/validation.ts__tmpl__ +21 -0
  33. package/src/config/generator.spec.ts +47 -0
  34. package/src/config/generator.ts +16 -0
  35. package/src/config/schema.d.ts +3 -0
  36. package/src/config/schema.json +13 -0
  37. package/src/core/files/data-access/src/index.ts__tmpl__ +5 -0
  38. package/src/core/files/data-access/src/lib/api-core-data-access.module.ts__tmpl__ +9 -0
  39. package/src/core/files/data-access/src/lib/api-core-data-access.service.ts__tmpl__ +97 -0
  40. package/src/core/files/data-access/src/lib/api-core-pub-sub.ts__tmpl__ +37 -0
  41. package/src/core/files/data-access/src/lib/dto/core-paging.input.ts__tmpl__ +26 -0
  42. package/src/core/files/data-access/src/lib/dto/multi-select-input.ts__tmpl__ +7 -0
  43. package/src/core/files/data-access/src/lib/models/core-paging.ts__tmpl__ +19 -0
  44. package/src/core/files/feature/src/index.ts__tmpl__ +2 -0
  45. package/src/core/files/feature/src/lib/api-core-feature.controller.ts__tmpl__ +12 -0
  46. package/src/core/files/feature/src/lib/api-core-feature.module.ts__tmpl__ +86 -0
  47. package/src/core/files/feature/src/lib/api-core-feature.resolver.ts__tmpl__ +12 -0
  48. package/src/core/files/feature/src/lib/api-core-feature.service.ts__tmpl__ +55 -0
  49. package/src/core/files/feature/src/lib/config/configuration.ts__tmpl__ +32 -0
  50. package/src/core/files/feature/src/lib/config/validation.ts__tmpl__ +25 -0
  51. package/src/core/files/feature/src/lib/plugins/complexity.plugin.ts__tmpl__ +51 -0
  52. package/src/core/files/feature/src/lib/plugins/logging.plugin.ts__tmpl__ +17 -0
  53. package/src/core/files/models/src/index.ts__tmpl__ +1 -0
  54. package/src/core/files/models/src/lib/generate-models.ts__tmpl__ +294 -0
  55. package/src/core/files/models/src/lib/models/core-paging.model.ts__tmpl__ +25 -0
  56. package/src/core/generator.spec.ts +85 -0
  57. package/src/core/generator.ts +35 -0
  58. package/src/core/schema.d.ts +3 -0
  59. package/src/core/schema.json +13 -0
  60. package/src/custom/generator.spec.ts +75 -0
  61. package/src/custom/generator.ts +239 -0
  62. package/src/custom/schema.json +21 -0
  63. package/src/custom/schema.ts +5 -0
  64. package/src/extended/generator.spec.ts +95 -0
  65. package/src/extended/generator.ts +161 -0
  66. package/src/extended/index.ts +1 -0
  67. package/src/extended/schema.json +12 -0
  68. package/src/extended/schema.ts +3 -0
  69. package/src/generate-crud/files/data-access/src/index.ts__tmpl__ +3 -0
  70. package/src/generate-crud/files/data-access/src/lib/api-crud-data-access.module.ts__tmpl__ +11 -0
  71. package/src/generate-crud/files/data-access/src/lib/api-crud-data-access.service.ts__tmpl__ +72 -0
  72. package/src/generate-crud/files/data-access/src/lib/dto/index.ts__tmpl__ +224 -0
  73. package/src/generate-crud/files/feature/.gitkeep +0 -0
  74. package/src/generate-crud/generator.spec.ts +84 -0
  75. package/src/generate-crud/generator.ts +354 -0
  76. package/src/generate-crud/schema.json +32 -0
  77. package/src/generate-crud/schema.ts +8 -0
  78. package/src/index.ts +13 -0
  79. package/src/plugin/generator.spec.ts +18 -0
  80. package/src/plugin/generator.ts +74 -0
  81. package/src/plugin/schema.json +14 -0
  82. package/src/plugin/schema.ts +4 -0
  83. package/src/prisma/files/src/index.ts__tmpl__ +1 -0
  84. package/src/prisma/files/src/lib/.gitkeep +1 -0
  85. package/src/prisma/files/src/lib/schemas/schema.prisma__tmpl__ +402 -0
  86. package/src/prisma/files/src/lib/seed/seed-data/iso-3166-countries.ts__tmpl__ +3239 -0
  87. package/src/prisma/files/src/lib/seed/seed-data/seed-users.ts__tmpl__ +32 -0
  88. package/src/prisma/files/src/lib/seed/seed.ts__tmpl__ +64 -0
  89. package/src/prisma/generator.spec.ts +60 -0
  90. package/src/prisma/generator.ts +61 -0
  91. package/src/prisma/schema.d.ts +3 -0
  92. package/src/prisma/schema.json +13 -0
  93. package/src/setup/generator.md +49 -0
  94. package/src/setup/generator.spec.ts +18 -0
  95. package/src/setup/generator.ts +106 -0
  96. package/src/setup/schema.json +8 -0
  97. package/src/smtp-mailer/files/data-access/src/index.ts__tmpl__ +2 -0
  98. package/src/smtp-mailer/files/data-access/src/lib/api-smtp-mailer-data-access.module.ts__tmpl__ +10 -0
  99. package/src/smtp-mailer/files/data-access/src/lib/api-smtp-mailer-data-access.service.ts__tmpl__ +61 -0
  100. package/src/smtp-mailer/generator.spec.ts +41 -0
  101. package/src/smtp-mailer/generator.ts +14 -0
  102. package/src/smtp-mailer/schema.d.ts +0 -0
  103. package/src/smtp-mailer/schema.json +7 -0
  104. package/src/user/files/data-access/src/index.ts__tmpl__ +5 -0
  105. package/src/user/files/data-access/src/lib/api-user-data-access.module.ts__tmpl__ +10 -0
  106. package/src/user/files/data-access/src/lib/api-user-data-access.service.ts__tmpl__ +119 -0
  107. package/src/user/files/data-access/src/lib/dto/admin-create-user.input.ts__tmpl__ +20 -0
  108. package/src/user/files/data-access/src/lib/dto/admin-update-user.input.ts__tmpl__ +29 -0
  109. package/src/user/files/feature/src/index.ts__tmpl__ +1 -0
  110. package/src/user/files/feature/src/lib/api-user-feature-admin.resolver.ts__tmpl__ +57 -0
  111. package/src/user/files/feature/src/lib/api-user-feature.module.ts__tmpl__ +10 -0
  112. package/src/user/files/feature/src/lib/api-user-feature.resolver.ts__tmpl__ +17 -0
  113. package/src/user/generator.spec.ts +41 -0
  114. package/src/user/generator.ts +15 -0
  115. package/src/user/schema.d.ts +0 -0
  116. package/src/user/schema.json +7 -0
  117. package/src/utils/files/src/index.ts__tmpl__ +3 -0
  118. package/src/utils/files/src/lib/decorators/ctx-user.decorator.ts__tmpl__ +6 -0
  119. package/src/utils/files/src/lib/guards/gql-auth-admin.guard.ts__tmpl__ +39 -0
  120. package/src/utils/files/src/lib/guards/gql-auth.guard.ts__tmpl__ +11 -0
  121. package/src/utils/generator.ts +14 -0
  122. package/src/utils/schema.json +8 -0
  123. package/src/workspace-setup/generator.md +39 -0
  124. package/src/workspace-setup/generator.spec.ts +82 -0
  125. package/src/workspace-setup/generator.ts +49 -0
  126. package/src/workspace-setup/lib/helpers.ts +142 -0
  127. package/src/workspace-setup/schema.d.ts +3 -0
  128. package/src/workspace-setup/schema.json +7 -0
  129. package/tsconfig.json +16 -0
  130. package/tsconfig.lib.json +23 -0
  131. package/tsconfig.spec.json +22 -0
  132. 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,5 @@
1
+ export interface GenerateCustomGeneratorSchema {
2
+ name?: string;
3
+ directory?: string;
4
+ overwrite?: boolean;
5
+ }
@@ -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,3 @@
1
+ export interface GenerateExtendedGeneratorSchema {
2
+ overwrite?: boolean;
3
+ }
@@ -0,0 +1,3 @@
1
+ export * from './lib/api-crud-data-access.module'
2
+ export * from './lib/api-crud-data-access.service'
3
+ export * from './lib/dto'
@@ -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
+ }