@exanderal/stackcraft 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +34 -10
  2. package/dist/create/index.d.ts.map +1 -1
  3. package/dist/create/index.js +12 -1
  4. package/dist/create/index.js.map +1 -1
  5. package/dist/create/scaffold.d.ts.map +1 -1
  6. package/dist/create/scaffold.js +8 -2
  7. package/dist/create/scaffold.js.map +1 -1
  8. package/dist/create/scaffolders/api-nestjs-graphql.d.ts +3 -0
  9. package/dist/create/scaffolders/api-nestjs-graphql.d.ts.map +1 -0
  10. package/dist/create/scaffolders/api-nestjs-graphql.js +31 -0
  11. package/dist/create/scaffolders/api-nestjs-graphql.js.map +1 -0
  12. package/dist/create/types.d.ts +1 -1
  13. package/dist/create/types.d.ts.map +1 -1
  14. package/package.json +1 -1
  15. package/templates/api-nestjs-graphql/.prettierrc +4 -0
  16. package/templates/api-nestjs-graphql/eslint.config.mjs +34 -0
  17. package/templates/api-nestjs-graphql/nest-cli.json +8 -0
  18. package/templates/api-nestjs-graphql/package.json +81 -0
  19. package/templates/api-nestjs-graphql/project.json +21 -0
  20. package/templates/api-nestjs-graphql/src/app.module.ts +21 -0
  21. package/templates/api-nestjs-graphql/src/common/entities/base.entity.ts +21 -0
  22. package/templates/api-nestjs-graphql/src/common/repositories/entity.repository.ts +21 -0
  23. package/templates/api-nestjs-graphql/src/common/repositories/readonly-entity.repository.ts +22 -0
  24. package/templates/api-nestjs-graphql/src/common/services/entity.service.ts +24 -0
  25. package/templates/api-nestjs-graphql/src/common/services/readonly-entity.service.ts +23 -0
  26. package/templates/api-nestjs-graphql/src/main.ts +8 -0
  27. package/templates/api-nestjs-graphql/src/modules/database/database.module.ts +18 -0
  28. package/templates/api-nestjs-graphql/src/modules/health/__tests__/health.controller.spec.ts +20 -0
  29. package/templates/api-nestjs-graphql/src/modules/health/__tests__/health.service.spec.ts +18 -0
  30. package/templates/api-nestjs-graphql/src/modules/health/health.controller.ts +12 -0
  31. package/templates/api-nestjs-graphql/src/modules/health/health.module.ts +9 -0
  32. package/templates/api-nestjs-graphql/src/modules/health/health.service.ts +8 -0
  33. package/templates/api-nestjs-graphql/test/app.e2e-spec.ts +29 -0
  34. package/templates/api-nestjs-graphql/test/jest-e2e.json +9 -0
  35. package/templates/api-nestjs-graphql/tsconfig.build.json +4 -0
  36. package/templates/api-nestjs-graphql/tsconfig.json +21 -0
  37. package/templates/base/package.json +3 -1
  38. package/templates/base/tools/generators/controller/index.js +22 -0
  39. package/templates/base/tools/generators/controller/schema.json +18 -0
  40. package/templates/base/tools/generators/generators.json +11 -1
  41. package/templates/base/tools/generators/module/graphql-files/__fileName__.model.ts__tmpl__ +11 -0
  42. package/templates/base/tools/generators/module/index.js +4 -22
  43. package/templates/base/tools/generators/module/schema.json +4 -5
  44. package/templates/base/tools/generators/resolver/files/__fileName__.resolver.ts__tmpl__ +28 -0
  45. package/templates/base/tools/generators/resolver/index.js +22 -0
  46. package/templates/base/tools/generators/resolver/schema.json +18 -0
  47. /package/templates/base/tools/generators/{module/crud-files → controller/files}/__fileName__.controller.ts__tmpl__ +0 -0
package/README.md CHANGED
@@ -17,7 +17,7 @@ Follow the prompts — you'll have an Nx monorepo with deps installed and ready
17
17
  ```
18
18
  your-project/
19
19
  ├── apps/
20
- │ ├── backend/ # NestJS REST API
20
+ │ ├── backend/ # NestJS REST or GraphQL API
21
21
  │ └── web/ # Vite + React or Next.js
22
22
  ├── packages/ # shared code
23
23
  └── tools/
@@ -26,11 +26,20 @@ your-project/
26
26
 
27
27
  ### Backend (`apps/backend`)
28
28
 
29
+ Choose between REST or GraphQL at setup time — both use the same underlying structure:
30
+
31
+ ```
32
+ src/
33
+ ├── modules/ # domain layer — model, repository, service, module
34
+ ├── api/ # REST controllers (REST only)
35
+ ├── resolvers/ # GraphQL resolvers (GraphQL only)
36
+ └── common/ # shared base classes
37
+ ```
38
+
29
39
  - NestJS with TypeORM
30
40
  - PostgreSQL or MySQL
31
- - Repository/Service abstraction layer (`EntityRepository`, `EntityService`)
32
- - Module-based structure under `src/modules/` — each module owns its model, repository, and service
33
- - HTTP controllers live separately in `src/api/`
41
+ - `EntityRepository` and `EntityService` base classes — extend them for each module
42
+ - UUID primary keys
34
43
 
35
44
  ### Frontend (`apps/web`)
36
45
 
@@ -40,8 +49,6 @@ your-project/
40
49
 
41
50
  ## Running the project
42
51
 
43
- Each app has the same scripts:
44
-
45
52
  ```sh
46
53
  pnpm dev # start all apps in parallel
47
54
  pnpm build # build all apps
@@ -49,7 +56,7 @@ pnpm test # run all tests
49
56
  pnpm lint # lint all apps
50
57
  ```
51
58
 
52
- Or target a specific app:
59
+ Target a specific app:
53
60
 
54
61
  ```sh
55
62
  pnpm nx run backend:dev
@@ -58,13 +65,27 @@ pnpm nx run web:dev
58
65
 
59
66
  ## Code generation
60
67
 
61
- Generate a new backend module:
68
+ Generate a new domain module:
62
69
 
63
70
  ```sh
64
71
  pnpm generate:module --name=trainer
72
+ # add --graphql to include @ObjectType() on the model
73
+ pnpm generate:module --name=trainer --graphql
74
+ ```
75
+
76
+ Generate a REST controller for an existing module:
77
+
78
+ ```sh
79
+ pnpm generate:controller --name=trainer
80
+ # creates apps/backend/src/api/trainer/trainer.controller.ts
65
81
  ```
66
82
 
67
- This creates `apps/backend/src/modules/trainer/` with a model, repository, service, module, and integration test. Add `--crud` to also generate a CRUD controller in `src/api/trainer/`.
83
+ Generate a GraphQL resolver for an existing module:
84
+
85
+ ```sh
86
+ pnpm generate:resolver --name=trainer
87
+ # creates apps/backend/src/resolvers/trainer/trainer.resolver.ts
88
+ ```
68
89
 
69
90
  ## Stack
70
91
 
@@ -75,7 +96,10 @@ This creates `apps/backend/src/modules/trainer/` with a model, repository, servi
75
96
 
76
97
  ## Roadmap
77
98
 
78
- - [ ] NestJS GraphQL + codegen
99
+ - [x] NestJS REST API
100
+ - [x] NestJS GraphQL API
101
+ - [x] Module, controller, and resolver generators
102
+ - [ ] GraphQL codegen pipeline
79
103
  - [ ] Expo mobile
80
104
  - [ ] `stackcraft add` addon system (auth, Supabase, etc.)
81
105
  - [ ] Presets and `--config` for non-interactive use
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/create/index.ts"],"names":[],"mappings":"AAKA,wBAAsB,MAAM,kBAgE3B"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/create/index.ts"],"names":[],"mappings":"AAKA,wBAAsB,MAAM,kBA4E3B"}
@@ -12,6 +12,17 @@ export async function create() {
12
12
  cancel('Cancelled.');
13
13
  process.exit(0);
14
14
  }
15
+ const backend = await select({
16
+ message: 'Backend',
17
+ options: [
18
+ { value: 'nestjs-rest', label: 'NestJS REST', hint: 'REST API with TypeORM' },
19
+ { value: 'nestjs-graphql', label: 'NestJS GraphQL', hint: 'Code-first GraphQL with TypeORM' },
20
+ ],
21
+ });
22
+ if (isCancel(backend)) {
23
+ cancel('Cancelled.');
24
+ process.exit(0);
25
+ }
15
26
  const frontend = await select({
16
27
  message: 'Frontend',
17
28
  options: [
@@ -48,7 +59,7 @@ export async function create() {
48
59
  const config = {
49
60
  projectName: projectName,
50
61
  frontend: frontend,
51
- backend: 'nestjs-rest',
62
+ backend: backend,
52
63
  database: database,
53
64
  packageManager: packageManager,
54
65
  targetDir: resolve(process.cwd(), projectName),
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/create/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/create/index.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"scaffold.d.ts","sourceRoot":"","sources":["../../src/create/scaffold.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAE/C,wBAAsB,QAAQ,CAAC,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,iBAiBlF"}
1
+ {"version":3,"file":"scaffold.d.ts","sourceRoot":"","sources":["../../src/create/scaffold.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAE/C,wBAAsB,QAAQ,CAAC,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,iBAqBlF"}
@@ -1,4 +1,5 @@
1
1
  import { execa } from 'execa';
2
+ import { scaffoldNestjsGraphql } from './scaffolders/api-nestjs-graphql.js';
2
3
  import { scaffoldNestjsRest } from './scaffolders/api-nestjs-rest.js';
3
4
  import { scaffoldBase } from './scaffolders/base.js';
4
5
  import { scaffoldNextjs } from './scaffolders/web-nextjs.js';
@@ -6,8 +7,13 @@ import { scaffoldVite } from './scaffolders/web-vite.js';
6
7
  export async function scaffold(config, onStep) {
7
8
  onStep('Creating workspace...');
8
9
  await scaffoldBase(config);
9
- onStep('Adding API...');
10
- await scaffoldNestjsRest(config);
10
+ onStep('Adding backend...');
11
+ if (config.backend === 'nestjs-graphql') {
12
+ await scaffoldNestjsGraphql(config);
13
+ }
14
+ else {
15
+ await scaffoldNestjsRest(config);
16
+ }
11
17
  if (config.frontend === 'vite') {
12
18
  onStep('Adding Vite + React app...');
13
19
  await scaffoldVite(config);
@@ -1 +1 @@
1
- {"version":3,"file":"scaffold.js","sourceRoot":"","sources":["../../src/create/scaffold.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,OAAO,CAAA;AAC7B,OAAO,EAAE,kBAAkB,EAAE,MAAM,kCAAkC,CAAA;AACrE,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAA;AACpD,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAA;AAC5D,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAA;AAGxD,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,MAAqB,EAAE,MAA6B;IACjF,MAAM,CAAC,uBAAuB,CAAC,CAAA;IAC/B,MAAM,YAAY,CAAC,MAAM,CAAC,CAAA;IAE1B,MAAM,CAAC,eAAe,CAAC,CAAA;IACvB,MAAM,kBAAkB,CAAC,MAAM,CAAC,CAAA;IAEhC,IAAI,MAAM,CAAC,QAAQ,KAAK,MAAM,EAAE,CAAC;QAC/B,MAAM,CAAC,4BAA4B,CAAC,CAAA;QACpC,MAAM,YAAY,CAAC,MAAM,CAAC,CAAA;IAC5B,CAAC;SAAM,IAAI,MAAM,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;QACxC,MAAM,CAAC,uBAAuB,CAAC,CAAA;QAC/B,MAAM,cAAc,CAAC,MAAM,CAAC,CAAA;IAC9B,CAAC;IAED,MAAM,CAAC,4BAA4B,CAAC,CAAA;IACpC,MAAM,KAAK,CAAC,MAAM,CAAC,cAAc,EAAE,CAAC,SAAS,CAAC,EAAE,EAAE,GAAG,EAAE,MAAM,CAAC,SAAS,EAAE,CAAC,CAAA;AAC5E,CAAC"}
1
+ {"version":3,"file":"scaffold.js","sourceRoot":"","sources":["../../src/create/scaffold.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,OAAO,CAAA;AAC7B,OAAO,EAAE,qBAAqB,EAAE,MAAM,qCAAqC,CAAA;AAC3E,OAAO,EAAE,kBAAkB,EAAE,MAAM,kCAAkC,CAAA;AACrE,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAA;AACpD,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAA;AAC5D,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAA;AAGxD,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,MAAqB,EAAE,MAA6B;IACjF,MAAM,CAAC,uBAAuB,CAAC,CAAA;IAC/B,MAAM,YAAY,CAAC,MAAM,CAAC,CAAA;IAE1B,MAAM,CAAC,mBAAmB,CAAC,CAAA;IAC3B,IAAI,MAAM,CAAC,OAAO,KAAK,gBAAgB,EAAE,CAAC;QACxC,MAAM,qBAAqB,CAAC,MAAM,CAAC,CAAA;IACrC,CAAC;SAAM,CAAC;QACN,MAAM,kBAAkB,CAAC,MAAM,CAAC,CAAA;IAClC,CAAC;IAED,IAAI,MAAM,CAAC,QAAQ,KAAK,MAAM,EAAE,CAAC;QAC/B,MAAM,CAAC,4BAA4B,CAAC,CAAA;QACpC,MAAM,YAAY,CAAC,MAAM,CAAC,CAAA;IAC5B,CAAC;SAAM,IAAI,MAAM,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;QACxC,MAAM,CAAC,uBAAuB,CAAC,CAAA;QAC/B,MAAM,cAAc,CAAC,MAAM,CAAC,CAAA;IAC9B,CAAC;IAED,MAAM,CAAC,4BAA4B,CAAC,CAAA;IACpC,MAAM,KAAK,CAAC,MAAM,CAAC,cAAc,EAAE,CAAC,SAAS,CAAC,EAAE,EAAE,GAAG,EAAE,MAAM,CAAC,SAAS,EAAE,CAAC,CAAA;AAC5E,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { ProjectConfig } from '../types.js';
2
+ export declare function scaffoldNestjsGraphql(config: ProjectConfig): Promise<void>;
3
+ //# sourceMappingURL=api-nestjs-graphql.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api-nestjs-graphql.d.ts","sourceRoot":"","sources":["../../../src/create/scaffolders/api-nestjs-graphql.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAWhD,wBAAsB,qBAAqB,CAAC,MAAM,EAAE,aAAa,iBAahE"}
@@ -0,0 +1,31 @@
1
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
+ import { dirname, join } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { copyTemplate } from './utils/copy.js';
5
+ const __dirname = dirname(fileURLToPath(import.meta.url));
6
+ const TEMPLATES_DIR = join(__dirname, '..', '..', '..', 'templates');
7
+ const DB_CONFIG = {
8
+ postgres: { type: 'postgres', port: '5432', driver: 'pg', driverVersion: '^8.0.0', typesPackage: '@types/pg', typesVersion: '^8.0.0' },
9
+ mysql: { type: 'mysql', port: '3306', driver: 'mysql2', driverVersion: '^3.0.0', typesPackage: null, typesVersion: null },
10
+ };
11
+ export async function scaffoldNestjsGraphql(config) {
12
+ const appDir = join(config.targetDir, 'apps', 'backend');
13
+ await mkdir(appDir, { recursive: true });
14
+ const db = DB_CONFIG[config.database];
15
+ await copyTemplate(join(TEMPLATES_DIR, 'api-nestjs-graphql'), appDir, {
16
+ projectName: config.projectName,
17
+ dbType: db.type,
18
+ dbPort: db.port,
19
+ });
20
+ await injectDbDriver(appDir, db);
21
+ }
22
+ async function injectDbDriver(appDir, db) {
23
+ const pkgPath = join(appDir, 'package.json');
24
+ const pkg = JSON.parse(await readFile(pkgPath, 'utf-8'));
25
+ pkg.dependencies[db.driver] = db.driverVersion;
26
+ if (db.typesPackage) {
27
+ pkg.devDependencies[db.typesPackage] = db.typesVersion;
28
+ }
29
+ await writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf-8');
30
+ }
31
+ //# sourceMappingURL=api-nestjs-graphql.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api-nestjs-graphql.js","sourceRoot":"","sources":["../../../src/create/scaffolders/api-nestjs-graphql.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AAC7D,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AACzC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAExC,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAE9C,MAAM,SAAS,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAA;AACzD,MAAM,aAAa,GAAG,IAAI,CAAC,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,WAAW,CAAC,CAAA;AAEpE,MAAM,SAAS,GAAG;IAChB,QAAQ,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,aAAa,EAAE,QAAQ,EAAE,YAAY,EAAE,WAAW,EAAE,YAAY,EAAE,QAAQ,EAAE;IACtI,KAAK,EAAK,EAAE,IAAI,EAAE,OAAO,EAAK,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,aAAa,EAAE,QAAQ,EAAE,YAAY,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE;CACvH,CAAA;AAEV,MAAM,CAAC,KAAK,UAAU,qBAAqB,CAAC,MAAqB;IAC/D,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,CAAC,CAAA;IACxD,MAAM,KAAK,CAAC,MAAM,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;IAExC,MAAM,EAAE,GAAG,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAA;IAErC,MAAM,YAAY,CAAC,IAAI,CAAC,aAAa,EAAE,oBAAoB,CAAC,EAAE,MAAM,EAAE;QACpE,WAAW,EAAE,MAAM,CAAC,WAAW;QAC/B,MAAM,EAAE,EAAE,CAAC,IAAI;QACf,MAAM,EAAE,EAAE,CAAC,IAAI;KAChB,CAAC,CAAA;IAEF,MAAM,cAAc,CAAC,MAAM,EAAE,EAAE,CAAC,CAAA;AAClC,CAAC;AAED,KAAK,UAAU,cAAc,CAAC,MAAc,EAAE,EAA4C;IACxF,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM,EAAE,cAAc,CAAC,CAAA;IAC5C,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAA;IAExD,GAAG,CAAC,YAAY,CAAC,EAAE,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,aAAa,CAAA;IAE9C,IAAI,EAAE,CAAC,YAAY,EAAE,CAAC;QACpB,GAAG,CAAC,eAAe,CAAC,EAAE,CAAC,YAAY,CAAC,GAAG,EAAE,CAAC,YAAY,CAAA;IACxD,CAAC;IAED,MAAM,SAAS,CAAC,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,EAAE,OAAO,CAAC,CAAA;AACxE,CAAC"}
@@ -1,6 +1,6 @@
1
1
  export type PackageManager = 'pnpm' | 'npm';
2
2
  export type Frontend = 'vite' | 'nextjs';
3
- export type Backend = 'nestjs-rest';
3
+ export type Backend = 'nestjs-rest' | 'nestjs-graphql';
4
4
  export type Database = 'postgres' | 'mysql';
5
5
  export interface ProjectConfig {
6
6
  projectName: string;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/create/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,KAAK,CAAA;AAC3C,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,QAAQ,CAAA;AACxC,MAAM,MAAM,OAAO,GAAG,aAAa,CAAA;AACnC,MAAM,MAAM,QAAQ,GAAG,UAAU,GAAG,OAAO,CAAA;AAE3C,MAAM,WAAW,aAAa;IAC5B,WAAW,EAAE,MAAM,CAAA;IACnB,QAAQ,EAAE,QAAQ,CAAA;IAClB,OAAO,EAAE,OAAO,CAAA;IAChB,QAAQ,EAAE,QAAQ,CAAA;IAClB,cAAc,EAAE,cAAc,CAAA;IAC9B,SAAS,EAAE,MAAM,CAAA;CAClB"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/create/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,KAAK,CAAA;AAC3C,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,QAAQ,CAAA;AACxC,MAAM,MAAM,OAAO,GAAG,aAAa,GAAG,gBAAgB,CAAA;AACtD,MAAM,MAAM,QAAQ,GAAG,UAAU,GAAG,OAAO,CAAA;AAE3C,MAAM,WAAW,aAAa;IAC5B,WAAW,EAAE,MAAM,CAAA;IACnB,QAAQ,EAAE,QAAQ,CAAA;IAClB,OAAO,EAAE,OAAO,CAAA;IAChB,QAAQ,EAAE,QAAQ,CAAA;IAClB,cAAc,EAAE,cAAc,CAAA;IAC9B,SAAS,EAAE,MAAM,CAAA;CAClB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exanderal/stackcraft",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "description": "Opinionated full-stack project scaffolding CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,4 @@
1
+ {
2
+ "singleQuote": true,
3
+ "trailingComma": "all"
4
+ }
@@ -0,0 +1,34 @@
1
+ // @ts-check
2
+ import eslint from '@eslint/js';
3
+ import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
4
+ import globals from 'globals';
5
+ import tseslint from 'typescript-eslint';
6
+
7
+ export default tseslint.config(
8
+ {
9
+ ignores: ['eslint.config.mjs'],
10
+ },
11
+ eslint.configs.recommended,
12
+ ...tseslint.configs.recommendedTypeChecked,
13
+ eslintPluginPrettierRecommended,
14
+ {
15
+ languageOptions: {
16
+ globals: {
17
+ ...globals.node,
18
+ ...globals.jest,
19
+ },
20
+ sourceType: 'commonjs',
21
+ parserOptions: {
22
+ projectService: true,
23
+ tsconfigRootDir: import.meta.dirname,
24
+ },
25
+ },
26
+ },
27
+ {
28
+ rules: {
29
+ '@typescript-eslint/no-explicit-any': 'off',
30
+ '@typescript-eslint/no-floating-promises': 'warn',
31
+ '@typescript-eslint/no-unsafe-argument': 'warn'
32
+ },
33
+ },
34
+ );
@@ -0,0 +1,8 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/nest-cli",
3
+ "collection": "@nestjs/schematics",
4
+ "sourceRoot": "src",
5
+ "compilerOptions": {
6
+ "deleteOutDir": true
7
+ }
8
+ }
@@ -0,0 +1,81 @@
1
+ {
2
+ "name": "{{projectName}}-backend",
3
+ "version": "0.0.1",
4
+ "description": "",
5
+ "author": "",
6
+ "private": true,
7
+ "license": "UNLICENSED",
8
+ "scripts": {
9
+ "dev": "nest start --watch",
10
+ "build": "nest build",
11
+ "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
12
+ "start": "nest start",
13
+ "start:dev": "nest start --watch",
14
+ "start:debug": "nest start --debug --watch",
15
+ "start:prod": "node dist/main",
16
+ "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
17
+ "test": "jest",
18
+ "test:watch": "jest --watch",
19
+ "test:cov": "jest --coverage",
20
+ "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
21
+ "test:e2e": "jest --config ./test/jest-e2e.json"
22
+ },
23
+ "dependencies": {
24
+ "@apollo/server": "^4.0.0",
25
+ "@nestjs/apollo": "^13.0.0",
26
+ "@nestjs/common": "^11.0.1",
27
+ "@nestjs/config": "^3.0.0",
28
+ "@nestjs/core": "^11.0.1",
29
+ "@nestjs/graphql": "^13.0.0",
30
+ "@nestjs/platform-express": "^11.0.1",
31
+ "@nestjs/typeorm": "^10.0.0",
32
+ "graphql": "^16.0.0",
33
+ "reflect-metadata": "^0.2.2",
34
+ "rxjs": "^7.8.1",
35
+ "typeorm": "^0.3.0"
36
+ },
37
+ "devDependencies": {
38
+ "@eslint/eslintrc": "^3.2.0",
39
+ "@eslint/js": "^9.18.0",
40
+ "@nestjs/cli": "^11.0.0",
41
+ "@nestjs/schematics": "^11.0.0",
42
+ "@nestjs/testing": "^11.0.1",
43
+ "@swc/cli": "^0.6.0",
44
+ "@swc/core": "^1.10.7",
45
+ "@types/express": "^5.0.0",
46
+ "@types/jest": "^29.5.14",
47
+ "@types/node": "^22.10.7",
48
+ "@types/supertest": "^6.0.2",
49
+ "eslint": "^9.18.0",
50
+ "eslint-config-prettier": "^10.0.1",
51
+ "eslint-plugin-prettier": "^5.2.2",
52
+ "globals": "^16.0.0",
53
+ "jest": "^29.7.0",
54
+ "prettier": "^3.4.2",
55
+ "source-map-support": "^0.5.21",
56
+ "supertest": "^7.0.0",
57
+ "ts-jest": "^29.2.5",
58
+ "ts-loader": "^9.5.2",
59
+ "ts-node": "^10.9.2",
60
+ "tsconfig-paths": "^4.2.0",
61
+ "typescript": "^5.7.3",
62
+ "typescript-eslint": "^8.20.0"
63
+ },
64
+ "jest": {
65
+ "moduleFileExtensions": [
66
+ "js",
67
+ "json",
68
+ "ts"
69
+ ],
70
+ "rootDir": "src",
71
+ "testRegex": ".*\\.spec\\.ts$",
72
+ "transform": {
73
+ "^.+\\.(t|j)s$": "ts-jest"
74
+ },
75
+ "collectCoverageFrom": [
76
+ "**/*.(t|j)s"
77
+ ],
78
+ "coverageDirectory": "../coverage",
79
+ "testEnvironment": "node"
80
+ }
81
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "backend",
3
+ "targets": {
4
+ "dev": {
5
+ "executor": "nx:run-commands",
6
+ "options": { "command": "pnpm run dev", "cwd": "{projectRoot}" }
7
+ },
8
+ "build": {
9
+ "executor": "nx:run-commands",
10
+ "options": { "command": "pnpm run build", "cwd": "{projectRoot}" }
11
+ },
12
+ "lint": {
13
+ "executor": "nx:run-commands",
14
+ "options": { "command": "pnpm run lint", "cwd": "{projectRoot}" }
15
+ },
16
+ "test": {
17
+ "executor": "nx:run-commands",
18
+ "options": { "command": "pnpm run test", "cwd": "{projectRoot}" }
19
+ }
20
+ }
21
+ }
@@ -0,0 +1,21 @@
1
+ import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
2
+ import { Module } from '@nestjs/common';
3
+ import { ConfigModule } from '@nestjs/config';
4
+ import { GraphQLModule } from '@nestjs/graphql';
5
+ import { join } from 'node:path';
6
+ import { DatabaseModule } from './modules/database/database.module';
7
+ import { HealthModule } from './modules/health/health.module';
8
+
9
+ @Module({
10
+ imports: [
11
+ ConfigModule.forRoot({ isGlobal: true }),
12
+ GraphQLModule.forRoot<ApolloDriverConfig>({
13
+ driver: ApolloDriver,
14
+ autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
15
+ sortSchema: true,
16
+ }),
17
+ DatabaseModule,
18
+ HealthModule,
19
+ ],
20
+ })
21
+ export class AppModule {}
@@ -0,0 +1,21 @@
1
+ import { Field, ID, ObjectType } from '@nestjs/graphql';
2
+ import {
3
+ CreateDateColumn,
4
+ PrimaryGeneratedColumn,
5
+ UpdateDateColumn,
6
+ } from 'typeorm';
7
+
8
+ @ObjectType({ isAbstract: true })
9
+ export abstract class BaseEntity {
10
+ @Field(() => ID)
11
+ @PrimaryGeneratedColumn('uuid')
12
+ id: string;
13
+
14
+ @Field()
15
+ @CreateDateColumn()
16
+ createdAt: Date;
17
+
18
+ @Field()
19
+ @UpdateDateColumn()
20
+ updatedAt: Date;
21
+ }
@@ -0,0 +1,21 @@
1
+ import { DeepPartial } from 'typeorm';
2
+ import { BaseEntity } from '../entities/base.entity';
3
+ import { ReadonlyEntityRepository } from './readonly-entity.repository';
4
+
5
+ export abstract class EntityRepository<
6
+ T extends BaseEntity,
7
+ > extends ReadonlyEntityRepository<T> {
8
+ async create(data: DeepPartial<T>): Promise<T> {
9
+ const entity = this.repo.create(data);
10
+ return this.repo.save(entity);
11
+ }
12
+
13
+ async update(id: string, data: DeepPartial<T>): Promise<T> {
14
+ await this.repo.update(id, data as any);
15
+ return this.findById(id) as Promise<T>;
16
+ }
17
+
18
+ async remove(id: string): Promise<void> {
19
+ await this.repo.delete(id);
20
+ }
21
+ }
@@ -0,0 +1,22 @@
1
+ import { FindManyOptions, FindOptionsWhere, In, Repository } from 'typeorm';
2
+ import { BaseEntity } from '../entities/base.entity';
3
+
4
+ export abstract class ReadonlyEntityRepository<T extends BaseEntity> {
5
+ constructor(protected readonly repo: Repository<T>) {}
6
+
7
+ findAll(options?: FindManyOptions<T>): Promise<T[]> {
8
+ return this.repo.find(options);
9
+ }
10
+
11
+ findByIds(ids: string[]): Promise<T[]> {
12
+ return this.repo.findBy({ id: In(ids) } as FindOptionsWhere<T>);
13
+ }
14
+
15
+ findById(id: string): Promise<T | null> {
16
+ return this.repo.findOneBy({ id } as FindOptionsWhere<T>);
17
+ }
18
+
19
+ findOne(where: FindOptionsWhere<T>): Promise<T | null> {
20
+ return this.repo.findOneBy(where);
21
+ }
22
+ }
@@ -0,0 +1,24 @@
1
+ import { DeepPartial } from 'typeorm';
2
+ import { BaseEntity } from '../entities/base.entity';
3
+ import { EntityRepository } from '../repositories/entity.repository';
4
+ import { ReadonlyEntityService } from './readonly-entity.service';
5
+
6
+ export abstract class EntityService<
7
+ T extends BaseEntity,
8
+ > extends ReadonlyEntityService<T> {
9
+ constructor(protected readonly repository: EntityRepository<T>) {
10
+ super(repository);
11
+ }
12
+
13
+ create(data: DeepPartial<T>): Promise<T> {
14
+ return this.repository.create(data);
15
+ }
16
+
17
+ update(id: string, data: DeepPartial<T>): Promise<T> {
18
+ return this.repository.update(id, data);
19
+ }
20
+
21
+ remove(id: string): Promise<void> {
22
+ return this.repository.remove(id);
23
+ }
24
+ }
@@ -0,0 +1,23 @@
1
+ import { FindManyOptions, FindOptionsWhere } from 'typeorm';
2
+ import { BaseEntity } from '../entities/base.entity';
3
+ import { ReadonlyEntityRepository } from '../repositories/readonly-entity.repository';
4
+
5
+ export abstract class ReadonlyEntityService<T extends BaseEntity> {
6
+ constructor(protected readonly repository: ReadonlyEntityRepository<T>) {}
7
+
8
+ findAll(options?: FindManyOptions<T>): Promise<T[]> {
9
+ return this.repository.findAll(options);
10
+ }
11
+
12
+ findByIds(ids: string[]): Promise<T[]> {
13
+ return this.repository.findByIds(ids);
14
+ }
15
+
16
+ findById(id: string): Promise<T | null> {
17
+ return this.repository.findById(id);
18
+ }
19
+
20
+ findOne(where: FindOptionsWhere<T>): Promise<T | null> {
21
+ return this.repository.findOne(where);
22
+ }
23
+ }
@@ -0,0 +1,8 @@
1
+ import { NestFactory } from '@nestjs/core';
2
+ import { AppModule } from './app.module';
3
+
4
+ async function bootstrap() {
5
+ const app = await NestFactory.create(AppModule);
6
+ await app.listen(process.env.PORT ?? 3000);
7
+ }
8
+ bootstrap();
@@ -0,0 +1,18 @@
1
+ import { Module } from '@nestjs/common';
2
+ import { TypeOrmModule } from '@nestjs/typeorm';
3
+
4
+ @Module({
5
+ imports: [
6
+ TypeOrmModule.forRoot({
7
+ type: '{{dbType}}' as 'postgres' | 'mysql',
8
+ host: process.env.DB_HOST ?? 'localhost',
9
+ port: parseInt(process.env.DB_PORT ?? '{{dbPort}}', 10),
10
+ username: process.env.DB_USER ?? 'postgres',
11
+ password: process.env.DB_PASSWORD ?? '',
12
+ database: process.env.DB_NAME ?? '{{projectName}}',
13
+ entities: [],
14
+ synchronize: process.env.NODE_ENV !== 'production',
15
+ }),
16
+ ],
17
+ })
18
+ export class DatabaseModule {}
@@ -0,0 +1,20 @@
1
+ import { Test, TestingModule } from '@nestjs/testing';
2
+ import { HealthController } from './health.controller';
3
+ import { HealthService } from './health.service';
4
+
5
+ describe('HealthController', () => {
6
+ let controller: HealthController;
7
+
8
+ beforeEach(async () => {
9
+ const module: TestingModule = await Test.createTestingModule({
10
+ controllers: [HealthController],
11
+ providers: [HealthService],
12
+ }).compile();
13
+
14
+ controller = module.get<HealthController>(HealthController);
15
+ });
16
+
17
+ it('returns ok status', () => {
18
+ expect(controller.check()).toEqual({ status: 'ok' });
19
+ });
20
+ });
@@ -0,0 +1,18 @@
1
+ import { Test, TestingModule } from '@nestjs/testing';
2
+ import { HealthService } from './health.service';
3
+
4
+ describe('HealthService', () => {
5
+ let service: HealthService;
6
+
7
+ beforeEach(async () => {
8
+ const module: TestingModule = await Test.createTestingModule({
9
+ providers: [HealthService],
10
+ }).compile();
11
+
12
+ service = module.get<HealthService>(HealthService);
13
+ });
14
+
15
+ it('returns ok status', () => {
16
+ expect(service.check()).toEqual({ status: 'ok' });
17
+ });
18
+ });
@@ -0,0 +1,12 @@
1
+ import { Controller, Get } from '@nestjs/common';
2
+ import { HealthService } from './health.service';
3
+
4
+ @Controller('health')
5
+ export class HealthController {
6
+ constructor(private readonly healthService: HealthService) {}
7
+
8
+ @Get()
9
+ check() {
10
+ return this.healthService.check();
11
+ }
12
+ }
@@ -0,0 +1,9 @@
1
+ import { Module } from '@nestjs/common';
2
+ import { HealthController } from './health.controller';
3
+ import { HealthService } from './health.service';
4
+
5
+ @Module({
6
+ controllers: [HealthController],
7
+ providers: [HealthService],
8
+ })
9
+ export class HealthModule {}
@@ -0,0 +1,8 @@
1
+ import { Injectable } from '@nestjs/common';
2
+
3
+ @Injectable()
4
+ export class HealthService {
5
+ check() {
6
+ return { status: 'ok' };
7
+ }
8
+ }
@@ -0,0 +1,29 @@
1
+ import { INestApplication } from '@nestjs/common';
2
+ import { Test, TestingModule } from '@nestjs/testing';
3
+ import * as request from 'supertest';
4
+ import { App } from 'supertest/types';
5
+ import { AppModule } from './../src/app.module';
6
+
7
+ describe('App (e2e)', () => {
8
+ let app: INestApplication<App>;
9
+
10
+ beforeEach(async () => {
11
+ const moduleFixture: TestingModule = await Test.createTestingModule({
12
+ imports: [AppModule],
13
+ }).compile();
14
+
15
+ app = moduleFixture.createNestApplication();
16
+ await app.init();
17
+ });
18
+
19
+ afterEach(async () => {
20
+ await app.close();
21
+ });
22
+
23
+ it('/health (GET)', () => {
24
+ return request(app.getHttpServer())
25
+ .get('/health')
26
+ .expect(200)
27
+ .expect({ status: 'ok' });
28
+ });
29
+ });
@@ -0,0 +1,9 @@
1
+ {
2
+ "moduleFileExtensions": ["js", "json", "ts"],
3
+ "rootDir": ".",
4
+ "testEnvironment": "node",
5
+ "testRegex": ".e2e-spec.ts$",
6
+ "transform": {
7
+ "^.+\\.(t|j)s$": "ts-jest"
8
+ }
9
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
4
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "compilerOptions": {
3
+ "module": "commonjs",
4
+ "declaration": true,
5
+ "removeComments": true,
6
+ "emitDecoratorMetadata": true,
7
+ "experimentalDecorators": true,
8
+ "allowSyntheticDefaultImports": true,
9
+ "target": "ES2023",
10
+ "sourceMap": true,
11
+ "outDir": "./dist",
12
+ "baseUrl": "./",
13
+ "incremental": true,
14
+ "skipLibCheck": true,
15
+ "strictNullChecks": true,
16
+ "forceConsistentCasingInFileNames": true,
17
+ "noImplicitAny": false,
18
+ "strictBindCallApply": false,
19
+ "noFallthroughCasesInSwitch": false
20
+ }
21
+ }
@@ -7,7 +7,9 @@
7
7
  "build": "nx run-many -t build",
8
8
  "test": "nx run-many -t test",
9
9
  "lint": "nx run-many -t lint",
10
- "generate:module": "nx g @local/generators:module"
10
+ "generate:module": "nx g @local/generators:module",
11
+ "generate:resolver": "nx g @local/generators:resolver",
12
+ "generate:controller": "nx g @local/generators:controller"
11
13
  },
12
14
  "devDependencies": {
13
15
  "@nx/devkit": "^20.0.0",
@@ -0,0 +1,22 @@
1
+ const { formatFiles, generateFiles, names } = require('@nx/devkit');
2
+ const { execSync } = require('node:child_process');
3
+ const { join } = require('node:path');
4
+
5
+ module.exports = async function (tree, options) {
6
+ const n = names(options.name);
7
+ const controllerPath = `apps/backend/src/api/${n.fileName}`;
8
+
9
+ generateFiles(tree, join(__dirname, 'files'), controllerPath, { ...n, tmpl: '' });
10
+
11
+ await formatFiles(tree);
12
+
13
+ console.log(`\n✓ Controller created at ${controllerPath}/`);
14
+ console.log(` → Import ${n.className}Module and register ${n.className}Controller in your module\n`);
15
+
16
+ return () => {
17
+ execSync(`npx prettier --write ${controllerPath}`, {
18
+ cwd: tree.root,
19
+ stdio: 'inherit',
20
+ });
21
+ };
22
+ };
@@ -0,0 +1,18 @@
1
+ {
2
+ "$schema": "http://json-schema.org/schema",
3
+ "cli": "nx",
4
+ "id": "controller",
5
+ "title": "Create a REST controller",
6
+ "type": "object",
7
+ "properties": {
8
+ "name": {
9
+ "type": "string",
10
+ "description": "Module name to generate a controller for (e.g. trainer)",
11
+ "$default": {
12
+ "$source": "argv",
13
+ "index": 0
14
+ }
15
+ }
16
+ },
17
+ "required": ["name"]
18
+ }
@@ -3,7 +3,17 @@
3
3
  "module": {
4
4
  "factory": "./module/index",
5
5
  "schema": "./module/schema.json",
6
- "description": "Create a NestJS module"
6
+ "description": "Create a NestJS domain module (model, repository, service)"
7
+ },
8
+ "resolver": {
9
+ "factory": "./resolver/index",
10
+ "schema": "./resolver/schema.json",
11
+ "description": "Create a GraphQL resolver in src/resolvers/"
12
+ },
13
+ "controller": {
14
+ "factory": "./controller/index",
15
+ "schema": "./controller/schema.json",
16
+ "description": "Create a REST controller in src/api/"
7
17
  }
8
18
  }
9
19
  }
@@ -0,0 +1,11 @@
1
+ import { ObjectType } from '@nestjs/graphql';
2
+ import { Entity } from 'typeorm';
3
+ import { BaseEntity } from '../../common/entities/base.entity';
4
+
5
+ @ObjectType()
6
+ @Entity('<%= fileName %>s')
7
+ export class <%= className %>Model extends BaseEntity {
8
+ // @Field()
9
+ // @Column()
10
+ // name: string;
11
+ }
@@ -6,20 +6,10 @@ module.exports = async function (tree, options) {
6
6
  const n = names(options.name);
7
7
  const modulePath = `apps/backend/src/modules/${n.fileName}`;
8
8
 
9
- generateFiles(
10
- tree,
11
- join(__dirname, 'files'),
12
- modulePath,
13
- { ...n, tmpl: '' },
14
- );
9
+ generateFiles(tree, join(__dirname, 'files'), modulePath, { ...n, tmpl: '' });
15
10
 
16
- if (options.crud) {
17
- generateFiles(
18
- tree,
19
- join(__dirname, 'crud-files'),
20
- `apps/backend/src/api/${n.fileName}`,
21
- { ...n, tmpl: '' },
22
- );
11
+ if (options.graphql) {
12
+ generateFiles(tree, join(__dirname, 'graphql-files'), modulePath, { ...n, tmpl: '' });
23
13
  }
24
14
 
25
15
  await formatFiles(tree);
@@ -27,16 +17,8 @@ module.exports = async function (tree, options) {
27
17
  console.log(`\n✓ Module created at ${modulePath}/`);
28
18
  console.log(` → Import ${n.className}Module in app.module.ts\n`);
29
19
 
30
- if (options.crud) {
31
- console.log(`✓ Controller created at apps/backend/src/api/${n.fileName}/`);
32
- console.log(` → Add ${n.className}Controller to a module and import ${n.className}Module there\n`);
33
- }
34
-
35
20
  return () => {
36
- const targets = [modulePath];
37
- if (options.crud) targets.push(`apps/backend/src/api/${n.fileName}`);
38
-
39
- execSync(`npx prettier --write ${targets.join(' ')}`, {
21
+ execSync(`npx prettier --write ${modulePath}`, {
40
22
  cwd: tree.root,
41
23
  stdio: 'inherit',
42
24
  });
@@ -2,7 +2,7 @@
2
2
  "$schema": "http://json-schema.org/schema",
3
3
  "cli": "nx",
4
4
  "id": "module",
5
- "title": "Create a NestJS module",
5
+ "title": "Create a NestJS domain module",
6
6
  "type": "object",
7
7
  "properties": {
8
8
  "name": {
@@ -13,11 +13,10 @@
13
13
  "index": 0
14
14
  }
15
15
  },
16
- "crud": {
16
+ "graphql": {
17
17
  "type": "boolean",
18
- "description": "Generate a CRUD controller in src/api/<name>/",
19
- "default": false,
20
- "x-prompt": "Generate a CRUD controller?"
18
+ "description": "Add @ObjectType() to the model (required for GraphQL resolvers)",
19
+ "default": false
21
20
  }
22
21
  },
23
22
  "required": ["name"]
@@ -0,0 +1,28 @@
1
+ import {
2
+ Args,
3
+ Mutation,
4
+ Query,
5
+ Resolver,
6
+ } from '@nestjs/graphql';
7
+ import { <%= className %>Model } from '../../modules/<%= fileName %>/<%= fileName %>.model';
8
+ import { <%= className %>Service } from '../../modules/<%= fileName %>/<%= fileName %>.service';
9
+
10
+ @Resolver(() => <%= className %>Model)
11
+ export class <%= className %>Resolver {
12
+ constructor(private readonly service: <%= className %>Service) {}
13
+
14
+ @Query(() => [<%= className %>Model])
15
+ <%= propertyName %>s() {
16
+ return this.service.findAll();
17
+ }
18
+
19
+ @Query(() => <%= className %>Model, { nullable: true })
20
+ <%= propertyName %>(@Args('id') id: string) {
21
+ return this.service.findById(id);
22
+ }
23
+
24
+ @Mutation(() => Boolean)
25
+ remove<%= className %>(@Args('id') id: string) {
26
+ return this.service.remove(id).then(() => true);
27
+ }
28
+ }
@@ -0,0 +1,22 @@
1
+ const { formatFiles, generateFiles, names } = require('@nx/devkit');
2
+ const { execSync } = require('node:child_process');
3
+ const { join } = require('node:path');
4
+
5
+ module.exports = async function (tree, options) {
6
+ const n = names(options.name);
7
+ const resolverPath = `apps/backend/src/resolvers/${n.fileName}`;
8
+
9
+ generateFiles(tree, join(__dirname, 'files'), resolverPath, { ...n, tmpl: '' });
10
+
11
+ await formatFiles(tree);
12
+
13
+ console.log(`\n✓ Resolver created at ${resolverPath}/`);
14
+ console.log(` → Import ${n.className}Module and register ${n.className}Resolver in your module\n`);
15
+
16
+ return () => {
17
+ execSync(`npx prettier --write ${resolverPath}`, {
18
+ cwd: tree.root,
19
+ stdio: 'inherit',
20
+ });
21
+ };
22
+ };
@@ -0,0 +1,18 @@
1
+ {
2
+ "$schema": "http://json-schema.org/schema",
3
+ "cli": "nx",
4
+ "id": "resolver",
5
+ "title": "Create a GraphQL resolver",
6
+ "type": "object",
7
+ "properties": {
8
+ "name": {
9
+ "type": "string",
10
+ "description": "Module name to generate a resolver for (e.g. trainer)",
11
+ "$default": {
12
+ "$source": "argv",
13
+ "index": 0
14
+ }
15
+ }
16
+ },
17
+ "required": ["name"]
18
+ }