@appxdigital/appx-core 0.1.122 → 0.1.124

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 (40) hide show
  1. package/CHANGELOG.md +50 -1
  2. package/README.md +35 -3
  3. package/dist/appx-core.module.d.ts +1 -0
  4. package/dist/appx-core.module.d.ts.map +1 -1
  5. package/dist/appx-core.module.js +1 -0
  6. package/dist/common/bigint-serializer.d.ts +1 -0
  7. package/dist/common/bigint-serializer.d.ts.map +1 -0
  8. package/dist/common/bigint-serializer.js +18 -0
  9. package/dist/common/decorators/scalar-field.decorator.d.ts +22 -0
  10. package/dist/common/decorators/scalar-field.decorator.d.ts.map +1 -0
  11. package/dist/common/decorators/scalar-field.decorator.js +103 -0
  12. package/dist/config/generate-all.js +22 -55
  13. package/dist/config/generate-controllers.d.ts +2 -1
  14. package/dist/config/generate-controllers.d.ts.map +1 -1
  15. package/dist/config/generate-controllers.js +7 -22
  16. package/dist/config/generate-dtos.d.ts +14 -1
  17. package/dist/config/generate-dtos.d.ts.map +1 -1
  18. package/dist/config/generate-dtos.js +85 -28
  19. package/dist/config/generate-models.d.ts +3 -0
  20. package/dist/config/generate-models.d.ts.map +1 -0
  21. package/dist/config/generate-models.js +107 -0
  22. package/dist/config/generate-modules.d.ts +30 -1
  23. package/dist/config/generate-modules.d.ts.map +1 -1
  24. package/dist/config/generate-modules.js +145 -86
  25. package/dist/config/generate-resolvers.d.ts +7 -1
  26. package/dist/config/generate-resolvers.d.ts.map +1 -1
  27. package/dist/config/generate-resolvers.js +10 -17
  28. package/dist/config/generate-services.d.ts +2 -1
  29. package/dist/config/generate-services.d.ts.map +1 -1
  30. package/dist/config/generate-services.js +7 -16
  31. package/dist/config/utils.d.ts +53 -1
  32. package/dist/config/utils.d.ts.map +1 -1
  33. package/dist/config/utils.js +124 -11
  34. package/dist/index.d.ts +1 -0
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +1 -0
  37. package/docs/README.md +1 -0
  38. package/docs/dtos.md +13 -1
  39. package/docs/generate.md +57 -0
  40. package/package.json +4 -2
@@ -23,10 +23,9 @@ var __importStar = (this && this.__importStar) || function (mod) {
23
23
  return result;
24
24
  };
25
25
  Object.defineProperty(exports, "__esModule", { value: true });
26
- const fs = __importStar(require("fs"));
26
+ exports.scaffoldResolver = void 0;
27
27
  const path = __importStar(require("path"));
28
28
  const utils_1 = require("./utils");
29
- const modelsPath = path.join(process.cwd(), 'src/generated');
30
29
  const outputPath = path.join(process.cwd(), 'src/modules');
31
30
  /**
32
31
  * Generic resolver template for a given model
@@ -68,20 +67,14 @@ export class ${model}Resolver extends ${model}GenericResolver {
68
67
  }
69
68
  `;
70
69
  /**
71
- * Generate resolvers for each model
70
+ * Scaffold the (read-only) GraphQL resolver file for a single model (once;
71
+ * never overwritten). Imports the prisma-nestjs-graphql artifacts under
72
+ * `src/generated/<folder>/`, so the deploy-safe pass (`prisma generate`) must
73
+ * have produced them first.
72
74
  */
73
- fs.readdirSync(modelsPath).forEach((folder) => {
74
- if (utils_1.IGNORE_FOLDERS.includes(folder))
75
- return;
76
- const modelName = (0, utils_1.kebabToPascalCase)(folder);
77
- const modelOutputPath = path.join(outputPath, folder);
78
- if (!fs.existsSync(modelOutputPath)) {
79
- fs.mkdirSync(modelOutputPath, { recursive: true });
80
- console.log(`Folder for model ${modelName} created.`);
81
- }
82
- else {
83
- console.log(`Folder for model ${modelName} already exists, skipping creation.`);
84
- }
85
- const resolverPath = path.join(modelOutputPath, `${folder}.resolver.ts`);
75
+ function scaffoldResolver(modelName) {
76
+ const folder = (0, utils_1.modelFolder)(modelName);
77
+ const resolverPath = path.join(outputPath, folder, `${folder}.resolver.ts`);
86
78
  (0, utils_1.createFileIfNotExists)(resolverPath, genericResolverTemplate(modelName, folder));
87
- });
79
+ }
80
+ exports.scaffoldResolver = scaffoldResolver;
@@ -1,2 +1,3 @@
1
- export {};
1
+ /** Scaffold the service file for a single model (once; never overwritten). */
2
+ export declare function scaffoldService(modelName: string): void;
2
3
  //# sourceMappingURL=generate-services.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"generate-services.d.ts","sourceRoot":"","sources":["../../src/config/generate-services.ts"],"names":[],"mappings":""}
1
+ {"version":3,"file":"generate-services.d.ts","sourceRoot":"","sources":["../../src/config/generate-services.ts"],"names":[],"mappings":"AAwBA,8EAA8E;AAC9E,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAIvD"}
@@ -23,10 +23,9 @@ var __importStar = (this && this.__importStar) || function (mod) {
23
23
  return result;
24
24
  };
25
25
  Object.defineProperty(exports, "__esModule", { value: true });
26
- const fs = __importStar(require("fs"));
26
+ exports.scaffoldService = void 0;
27
27
  const path = __importStar(require("path"));
28
28
  const utils_1 = require("./utils");
29
- const modelsPath = path.join(process.cwd(), 'src/generated');
30
29
  const servicesOutputPath = path.join(process.cwd(), 'src/modules');
31
30
  /**
32
31
  * Generic service template for a given model
@@ -45,18 +44,10 @@ export class ${model}Service extends CoreService<${model}> {
45
44
  // Override methods or add custom logic as needed
46
45
  }
47
46
  `;
48
- /**
49
- * Generate services for each model
50
- */
51
- fs.readdirSync(modelsPath).forEach((folder) => {
52
- if (utils_1.IGNORE_FOLDERS.includes(folder))
53
- return;
54
- const modelName = (0, utils_1.kebabToPascalCase)(folder);
55
- const modelOutputPath = path.join(servicesOutputPath, folder);
56
- if (!fs.existsSync(modelOutputPath)) {
57
- fs.mkdirSync(modelOutputPath, { recursive: true });
58
- console.log(`Folder for model ${modelName} created.`);
59
- }
60
- const servicePath = path.join(modelOutputPath, `${folder}.service.ts`);
47
+ /** Scaffold the service file for a single model (once; never overwritten). */
48
+ function scaffoldService(modelName) {
49
+ const folder = (0, utils_1.modelFolder)(modelName);
50
+ const servicePath = path.join(servicesOutputPath, folder, `${folder}.service.ts`);
61
51
  (0, utils_1.createFileIfNotExists)(servicePath, genericServiceTemplate(modelName));
62
- });
52
+ }
53
+ exports.scaffoldService = scaffoldService;
@@ -1,3 +1,18 @@
1
+ /**
2
+ * Run a project-local CLI binary (e.g. `prisma`) from the PROJECT's node_modules.
3
+ *
4
+ * The `appx-core` CLI is normally installed **globally**, so the project's
5
+ * `node_modules/.bin` is not on PATH — a bare `prisma generate` would resolve to
6
+ * a global Prisma (a different version than the project's, or nothing at all).
7
+ * We resolve the executable inside the project and prepend its `.bin` to PATH so
8
+ * that anything the tool spawns — Prisma's generator binaries such as
9
+ * `prisma-nestjs-graphql` — also resolves from the project.
10
+ *
11
+ * Throws with an actionable message rather than silently falling back to a
12
+ * global install, since generating against the wrong Prisma version is exactly
13
+ * the failure this avoids.
14
+ */
15
+ export declare const runProjectBin: (bin: string, args: string, cwd?: string) => void;
1
16
  export declare const kebabToPascalCase: (str: string) => string;
2
17
  export declare const pascalToKebabCase: (str: string) => string;
3
18
  export declare const createFileIfNotExists: (filePath: string, content: string) => void;
@@ -14,5 +29,42 @@ export declare const GENERATED_BANNER: string;
14
29
  * directories as needed.
15
30
  */
16
31
  export declare const writeGeneratedFile: (filePath: string, content: string) => void;
17
- export declare const IGNORE_FOLDERS: string[];
32
+ /**
33
+ * Framework-owned models. Excluded from model discovery: no generic CRUD module,
34
+ * controller, service, resolver, or DTO is scaffolded for them. `Session` /
35
+ * `UserRefreshToken` are framework plumbing; `User` is owned by the auth module
36
+ * (generic User CRUD would be a mass-assignment surface, and `/auth/*` already
37
+ * covers user endpoints), so it is hidden from the module wizard entirely.
38
+ */
39
+ export declare const FRAMEWORK_MODELS: string[];
40
+ /**
41
+ * The kebab-case folder / file basename for a model. Matches the folder naming
42
+ * of the prisma-nestjs-graphql generator output under `src/generated/`, so the
43
+ * scaffolded resolver's imports (`../../generated/<folder>/<folder>.model`)
44
+ * resolve. E.g. `ProjectMember` -> `project-member`, `TypeSample` -> `type-sample`.
45
+ */
46
+ export declare const modelFolder: (modelName: string) => string;
47
+ /**
48
+ * Loads the datamodel from the consumer project's generated Prisma client and
49
+ * returns the discoverable (non-framework-owned) models. This is the single
50
+ * source of truth for "what models exist" — independent of whatever folders the
51
+ * GraphQL plugin happened to emit. Requires `@prisma/client` lazily so callers
52
+ * can run `prisma generate` first and pick up a fresh schema in the same process.
53
+ */
54
+ export declare const loadAllModels: () => any[];
55
+ export declare const loadModels: () => any[];
56
+ /** True if a module exists at the model's canonical folder under src/modules. */
57
+ export declare const moduleExists: (modelName: string) => boolean;
58
+ /**
59
+ * The set of model names already served by existing module source — detected by
60
+ * the `CoreController<Model>` / `CoreService<Model>` type argument. Pure (takes
61
+ * file contents) so it is unit-testable. This catches a module that serves a
62
+ * model under a NON-canonical folder name (e.g. a hand-written pluralised
63
+ * `wearable-connections/` module owning the `WearableConnection` model), which a
64
+ * folder-name check (`moduleExists`) would miss — preventing the wizard from
65
+ * scaffolding a duplicate module + controller on a second route.
66
+ */
67
+ export declare const modelsServedBySource: (contents: string[]) => Set<string>;
68
+ /** Scans src/modules for the models already served by an existing module. */
69
+ export declare const ownedModels: () => Set<string>;
18
70
  //# sourceMappingURL=utils.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/config/utils.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,iBAAiB,QAAS,MAAM,WAK5C,CAAC;AAEF,eAAO,MAAM,iBAAiB,QAAS,MAAM,WAK5C,CAAC;AAEF,eAAO,MAAM,qBAAqB,aAAc,MAAM,WAAW,MAAM,SAStE,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,QAGoD,CAAC;AAElF;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,aAAc,MAAM,WAAW,MAAM,SAInE,CAAC;AAEF,eAAO,MAAM,cAAc,UAS1B,CAAC"}
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/config/utils.ts"],"names":[],"mappings":"AAuBA;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,aAAa,QAAS,MAAM,QAAQ,MAAM,QAAO,MAAM,KAAmB,IAatF,CAAC;AAEF,eAAO,MAAM,iBAAiB,QAAS,MAAM,WAK5C,CAAC;AAEF,eAAO,MAAM,iBAAiB,QAAS,MAAM,WAK5C,CAAC;AAEF,eAAO,MAAM,qBAAqB,aAAc,MAAM,WAAW,MAAM,SAStE,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,QAGoD,CAAC;AAElF;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,aAAc,MAAM,WAAW,MAAM,SAInE,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,gBAAgB,UAA0C,CAAC;AAExE;;;;;GAKG;AACH,eAAO,MAAM,WAAW,cAAe,MAAM,KAAG,MAAsC,CAAC;AAEvF;;;;;;GAMG;AACH,eAAO,MAAM,aAAa,QAAO,GAAG,EAInC,CAAC;AAEF,eAAO,MAAM,UAAU,QAAO,GAAG,EAAuE,CAAC;AAEzG,iFAAiF;AACjF,eAAO,MAAM,YAAY,cAAe,MAAM,KAAG,OAGhD,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB,aAAc,MAAM,EAAE,KAAG,IAAI,MAAM,CAQnE,CAAC;AAEF,6EAA6E;AAC7E,eAAO,MAAM,WAAW,QAAO,IAAI,MAAM,CAYxC,CAAC"}
@@ -23,9 +23,57 @@ var __importStar = (this && this.__importStar) || function (mod) {
23
23
  return result;
24
24
  };
25
25
  Object.defineProperty(exports, "__esModule", { value: true });
26
- exports.IGNORE_FOLDERS = exports.writeGeneratedFile = exports.GENERATED_BANNER = exports.createFileIfNotExists = exports.pascalToKebabCase = exports.kebabToPascalCase = void 0;
26
+ exports.ownedModels = exports.modelsServedBySource = exports.moduleExists = exports.loadModels = exports.loadAllModels = exports.modelFolder = exports.FRAMEWORK_MODELS = exports.writeGeneratedFile = exports.GENERATED_BANNER = exports.createFileIfNotExists = exports.pascalToKebabCase = exports.kebabToPascalCase = exports.runProjectBin = void 0;
27
27
  const fs = __importStar(require("fs"));
28
28
  const path = __importStar(require("path"));
29
+ const child_process_1 = require("child_process");
30
+ /**
31
+ * Locate a binary inside the project's own `node_modules/.bin`, walking up from
32
+ * `cwd` so hoisted / monorepo layouts resolve too. Returns null when the project
33
+ * doesn't provide it (i.e. only a global install would be found on PATH).
34
+ */
35
+ const findProjectBin = (bin, cwd) => {
36
+ const name = process.platform === 'win32' ? `${bin}.cmd` : bin;
37
+ let dir = path.resolve(cwd);
38
+ // eslint-disable-next-line no-constant-condition
39
+ while (true) {
40
+ const binDir = path.join(dir, 'node_modules', '.bin');
41
+ const exe = path.join(binDir, name);
42
+ if (fs.existsSync(exe))
43
+ return { exe, binDir };
44
+ const parent = path.dirname(dir);
45
+ if (parent === dir)
46
+ return null;
47
+ dir = parent;
48
+ }
49
+ };
50
+ /**
51
+ * Run a project-local CLI binary (e.g. `prisma`) from the PROJECT's node_modules.
52
+ *
53
+ * The `appx-core` CLI is normally installed **globally**, so the project's
54
+ * `node_modules/.bin` is not on PATH — a bare `prisma generate` would resolve to
55
+ * a global Prisma (a different version than the project's, or nothing at all).
56
+ * We resolve the executable inside the project and prepend its `.bin` to PATH so
57
+ * that anything the tool spawns — Prisma's generator binaries such as
58
+ * `prisma-nestjs-graphql` — also resolves from the project.
59
+ *
60
+ * Throws with an actionable message rather than silently falling back to a
61
+ * global install, since generating against the wrong Prisma version is exactly
62
+ * the failure this avoids.
63
+ */
64
+ const runProjectBin = (bin, args, cwd = process.cwd()) => {
65
+ const found = findProjectBin(bin, cwd);
66
+ if (!found) {
67
+ throw new Error(`Could not find "${bin}" in this project's node_modules. ` +
68
+ `Run "npm install" in the project root and try again.`);
69
+ }
70
+ (0, child_process_1.execSync)(`"${found.exe}" ${args}`.trim(), {
71
+ cwd,
72
+ stdio: 'inherit',
73
+ env: { ...process.env, PATH: `${found.binDir}${path.delimiter}${process.env.PATH ?? ''}` },
74
+ });
75
+ };
76
+ exports.runProjectBin = runProjectBin;
29
77
  const kebabToPascalCase = (str) => {
30
78
  return str
31
79
  .split("-")
@@ -72,13 +120,78 @@ const writeGeneratedFile = (filePath, content) => {
72
120
  console.log(`Generated ${filePath}`);
73
121
  };
74
122
  exports.writeGeneratedFile = writeGeneratedFile;
75
- exports.IGNORE_FOLDERS = [
76
- 'prisma',
77
- 'schema.gql',
78
- 'session',
79
- 'user-refresh-token',
80
- // Generated DTOs live under src/generated/dto/. The model generators scan
81
- // src/generated's top-level dirs as model names, so this must be skipped or
82
- // they emit a bogus `dto` module.
83
- 'dto',
84
- ];
123
+ /**
124
+ * Framework-owned models. Excluded from model discovery: no generic CRUD module,
125
+ * controller, service, resolver, or DTO is scaffolded for them. `Session` /
126
+ * `UserRefreshToken` are framework plumbing; `User` is owned by the auth module
127
+ * (generic User CRUD would be a mass-assignment surface, and `/auth/*` already
128
+ * covers user endpoints), so it is hidden from the module wizard entirely.
129
+ */
130
+ exports.FRAMEWORK_MODELS = ['Session', 'UserRefreshToken', 'User'];
131
+ /**
132
+ * The kebab-case folder / file basename for a model. Matches the folder naming
133
+ * of the prisma-nestjs-graphql generator output under `src/generated/`, so the
134
+ * scaffolded resolver's imports (`../../generated/<folder>/<folder>.model`)
135
+ * resolve. E.g. `ProjectMember` -> `project-member`, `TypeSample` -> `type-sample`.
136
+ */
137
+ const modelFolder = (modelName) => (0, exports.pascalToKebabCase)(modelName);
138
+ exports.modelFolder = modelFolder;
139
+ /**
140
+ * Loads the datamodel from the consumer project's generated Prisma client and
141
+ * returns the discoverable (non-framework-owned) models. This is the single
142
+ * source of truth for "what models exist" — independent of whatever folders the
143
+ * GraphQL plugin happened to emit. Requires `@prisma/client` lazily so callers
144
+ * can run `prisma generate` first and pick up a fresh schema in the same process.
145
+ */
146
+ const loadAllModels = () => {
147
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
148
+ const { Prisma } = require(path.join(process.cwd(), 'node_modules', '@prisma/client'));
149
+ return Prisma.dmmf.datamodel.models;
150
+ };
151
+ exports.loadAllModels = loadAllModels;
152
+ const loadModels = () => (0, exports.loadAllModels)().filter((m) => !exports.FRAMEWORK_MODELS.includes(m.name));
153
+ exports.loadModels = loadModels;
154
+ /** True if a module exists at the model's canonical folder under src/modules. */
155
+ const moduleExists = (modelName) => {
156
+ const folder = (0, exports.modelFolder)(modelName);
157
+ return fs.existsSync(path.join(process.cwd(), 'src/modules', folder, `${folder}.module.ts`));
158
+ };
159
+ exports.moduleExists = moduleExists;
160
+ /**
161
+ * The set of model names already served by existing module source — detected by
162
+ * the `CoreController<Model>` / `CoreService<Model>` type argument. Pure (takes
163
+ * file contents) so it is unit-testable. This catches a module that serves a
164
+ * model under a NON-canonical folder name (e.g. a hand-written pluralised
165
+ * `wearable-connections/` module owning the `WearableConnection` model), which a
166
+ * folder-name check (`moduleExists`) would miss — preventing the wizard from
167
+ * scaffolding a duplicate module + controller on a second route.
168
+ */
169
+ const modelsServedBySource = (contents) => {
170
+ const owned = new Set();
171
+ const re = /Core(?:Controller|Service)<\s*([A-Za-z0-9_]+)\s*>/g;
172
+ for (const content of contents) {
173
+ let m;
174
+ while ((m = re.exec(content)) !== null)
175
+ owned.add(m[1]);
176
+ }
177
+ return owned;
178
+ };
179
+ exports.modelsServedBySource = modelsServedBySource;
180
+ /** Scans src/modules for the models already served by an existing module. */
181
+ const ownedModels = () => {
182
+ const root = path.join(process.cwd(), 'src/modules');
183
+ if (!fs.existsSync(root))
184
+ return new Set();
185
+ const contents = [];
186
+ for (const dir of fs.readdirSync(root)) {
187
+ const dirPath = path.join(root, dir);
188
+ if (!fs.statSync(dirPath).isDirectory())
189
+ continue;
190
+ for (const file of fs.readdirSync(dirPath)) {
191
+ if (file.endsWith('.ts'))
192
+ contents.push(fs.readFileSync(path.join(dirPath, file), 'utf8'));
193
+ }
194
+ }
195
+ return (0, exports.modelsServedBySource)(contents);
196
+ };
197
+ exports.ownedModels = ownedModels;
package/dist/index.d.ts CHANGED
@@ -23,6 +23,7 @@ export * from './common/guards/rbac.guard';
23
23
  export * from './common/guards/user-population.guard';
24
24
  export * from './modules/common.module';
25
25
  export * from './common/interceptors/prisma.interceptor';
26
+ export * from './common/decorators/scalar-field.decorator';
26
27
  export * from './common/config/permissionsConfigTypes';
27
28
  export * from './common/config/adminConfigType';
28
29
  export * from './common/config/permissions.config.provider';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,4BAA4B,CAAC;AAC3C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,gCAAgC,CAAC;AAC/C,cAAc,yBAAyB,CAAC;AACxC,cAAc,0BAA0B,CAAC;AACzC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,iCAAiC,CAAC;AAChD,cAAc,4BAA4B,CAAC;AAI3C,cAAc,gCAAgC,CAAC;AAC/C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,iCAAiC,CAAC;AAChD,cAAc,6BAA6B,CAAC;AAC5C,cAAc,qCAAqC,CAAC;AACpD,cAAc,+BAA+B,CAAC;AAC9C,cAAc,iCAAiC,CAAC;AAChD,cAAc,6BAA6B,CAAC;AAC5C,cAAc,uCAAuC,CAAC;AACtD,cAAc,oCAAoC,CAAC;AACnD,cAAc,2CAA2C,CAAC;AAC1D,cAAc,2CAA2C,CAAC;AAC1D,cAAc,sCAAsC,CAAC;AACrD,cAAc,4BAA4B,CAAC;AAC3C,cAAc,uCAAuC,CAAC;AACtD,cAAc,yBAAyB,CAAC;AACxC,cAAc,0CAA0C,CAAC;AACzD,cAAc,wCAAwC,CAAC;AACvD,cAAc,iCAAiC,CAAC;AAChD,cAAc,6CAA6C,CAAC;AAC5D,cAAc,oBAAoB,CAAC;AACnC,cAAc,mCAAmC,CAAC;AAClD,cAAc,2CAA2C,CAAC;AAC1D,cAAc,wCAAwC,CAAC;AACvD,cAAc,+CAA+C,CAAC;AAC9D,cAAc,uCAAuC,CAAC;AACtD,cAAc,oCAAoC,CAAC;AACnD,cAAc,0CAA0C,CAAC;AACzD,cAAc,oCAAoC,CAAC;AACnD,cAAc,4BAA4B,CAAC;AAC3C,cAAc,wCAAwC,CAAC;AACvD,cAAc,oCAAoC,CAAC;AACnD,cAAc,+BAA+B,CAAC;AAC9C,cAAc,qCAAqC,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,4BAA4B,CAAC;AAC3C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,gCAAgC,CAAC;AAC/C,cAAc,yBAAyB,CAAC;AACxC,cAAc,0BAA0B,CAAC;AACzC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,iCAAiC,CAAC;AAChD,cAAc,4BAA4B,CAAC;AAI3C,cAAc,gCAAgC,CAAC;AAC/C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,iCAAiC,CAAC;AAChD,cAAc,6BAA6B,CAAC;AAC5C,cAAc,qCAAqC,CAAC;AACpD,cAAc,+BAA+B,CAAC;AAC9C,cAAc,iCAAiC,CAAC;AAChD,cAAc,6BAA6B,CAAC;AAC5C,cAAc,uCAAuC,CAAC;AACtD,cAAc,oCAAoC,CAAC;AACnD,cAAc,2CAA2C,CAAC;AAC1D,cAAc,2CAA2C,CAAC;AAC1D,cAAc,sCAAsC,CAAC;AACrD,cAAc,4BAA4B,CAAC;AAC3C,cAAc,uCAAuC,CAAC;AACtD,cAAc,yBAAyB,CAAC;AACxC,cAAc,0CAA0C,CAAC;AACzD,cAAc,4CAA4C,CAAC;AAC3D,cAAc,wCAAwC,CAAC;AACvD,cAAc,iCAAiC,CAAC;AAChD,cAAc,6CAA6C,CAAC;AAC5D,cAAc,oBAAoB,CAAC;AACnC,cAAc,mCAAmC,CAAC;AAClD,cAAc,2CAA2C,CAAC;AAC1D,cAAc,wCAAwC,CAAC;AACvD,cAAc,+CAA+C,CAAC;AAC9D,cAAc,uCAAuC,CAAC;AACtD,cAAc,oCAAoC,CAAC;AACnD,cAAc,0CAA0C,CAAC;AACzD,cAAc,oCAAoC,CAAC;AACnD,cAAc,4BAA4B,CAAC;AAC3C,cAAc,wCAAwC,CAAC;AACvD,cAAc,oCAAoC,CAAC;AACnD,cAAc,+BAA+B,CAAC;AAC9C,cAAc,qCAAqC,CAAC"}
package/dist/index.js CHANGED
@@ -42,6 +42,7 @@ __exportStar(require("./common/guards/rbac.guard"), exports);
42
42
  __exportStar(require("./common/guards/user-population.guard"), exports);
43
43
  __exportStar(require("./modules/common.module"), exports);
44
44
  __exportStar(require("./common/interceptors/prisma.interceptor"), exports);
45
+ __exportStar(require("./common/decorators/scalar-field.decorator"), exports);
45
46
  __exportStar(require("./common/config/permissionsConfigTypes"), exports);
46
47
  __exportStar(require("./common/config/adminConfigType"), exports);
47
48
  __exportStar(require("./common/config/permissions.config.provider"), exports);
package/docs/README.md CHANGED
@@ -4,6 +4,7 @@ Authoritative usage documentation for `@appxdigital/appx-core`. **This folder is
4
4
 
5
5
  ## Contents
6
6
 
7
+ - **[generate.md](./generate.md)** — the two code-generation commands: `generate` (deploy-safe — regenerates `src/generated/**` only, no code mutation) and `generate models` (the wizard that scaffolds + registers CRUD modules for chosen models). When to use each, and why not every table needs a module.
7
8
  - **[permissions.md](./permissions.md)** — the authorization model: the `@Permission` and `@ExposeModels` decorators, the `permissions.config.ts` shape, roles/actions, row-level `conditions`, field-level control, action fallbacks, and the `GUEST` (unauthenticated) flow.
8
9
  - **[dtos.md](./dtos.md)** — the generated CRUD validation DTOs: their two-file structure, **how to remove a field** (`OmitType` / `@NoWrite`), how to add validation, and why nested relation writes don't belong on generic CRUD.
9
10
  - **[data-access.md](./data-access.md)** — how you talk to the database: the Prisma proxy (`prisma.model.*`), how ABAC filtering and field omission are enforced on every call, the redirected single-record methods, relation behaviour, bypass hatches, and transactions.
package/docs/dtos.md CHANGED
@@ -34,7 +34,19 @@ A scalar field is **included** unless it is:
34
34
  - annotated `/// @NoWrite`,
35
35
  - annotated `/// @Role(none)`.
36
36
 
37
- **Relation navigation fields are excluded**; their scalar foreign keys are kept. Types map to validators: enums → `@IsEnum`, scalars → `@IsString`/`@IsInt`/`@IsBoolean`/`@IsNumber`/`@IsDateString`, `Json`/`Bytes` → `@Allow()`.
37
+ **Relation navigation fields are excluded**; their scalar foreign keys are kept. Each field is typed to match its **Prisma model type** so a controller override type-checks, and validated accordingly:
38
+
39
+ | Prisma type | DTO type | Decorator | Wire input |
40
+ |---|---|---|---|
41
+ | `String` / enum | `string` / enum | `@IsString` / `@IsEnum` | string |
42
+ | `Int` / `Float` | `number` | `@IsInt` / `@IsNumber` | number |
43
+ | `Boolean` | `boolean` | `@IsBoolean` | boolean |
44
+ | `DateTime` | `Date` | `@Type(() => Date)` | ISO-8601 string |
45
+ | `Decimal` | `Prisma.Decimal` | `@DecimalField()` | string **or** number |
46
+ | `BigInt` | `bigint` | `@BigIntField()` | string **or** number |
47
+ | `Json` / `Bytes` | `any` | `@Allow()` | any |
48
+
49
+ `@DecimalField()` / `@BigIntField()` accept a string or number and coerce/validate it (invalid → `400`); prefer strings for `Decimal` (money) and large `BigInt` values to avoid JSON float-precision loss. A `BigInt` column serialises as a **string** in JSON responses (JSON has no bigint). A `Decimal` column with the GraphQL generator requires the `prisma-graphql-type-decimal` peer dependency (npm auto-installs it).
38
50
 
39
51
  > Because relations are excluded, generic CRUD endpoints accept **flat** writes (scalars + FK ids), not nested relation writes. See [Relations & nested writes](#relations--nested-writes) below.
40
52
 
@@ -0,0 +1,57 @@
1
+ # Code generation — `generate` and `generate models`
2
+
3
+ AppX Core generates code from your Prisma schema through **two commands with different responsibilities**. The split exists so that keeping generated types in sync (safe to automate) is separate from scaffolding CRUD modules (which writes code you own and edits `app.module.ts`).
4
+
5
+ | Command | What it does | Writes | Safe in CI? |
6
+ |---|---|---|---|
7
+ | `appx-core generate` | Regenerate the Prisma client, GraphQL types, and DTO **base** classes | `src/generated/**` only (gitignored) | **Yes** — idempotent, no code mutation |
8
+ | `appx-core generate models` | Scaffold + register CRUD modules for chosen models | `src/modules/**` and `src/app.module.ts` | No — run during development |
9
+
10
+ ## `appx-core generate` — the deploy-safe pass
11
+
12
+ ```bash
13
+ appx-core generate
14
+ ```
15
+
16
+ Runs `prisma generate` (which emits both the Prisma client and the prisma-nestjs-graphql artifacts) and regenerates the DTO base classes under `src/generated/dto/**`. It **never** writes `src/modules/**` and **never** edits `src/app.module.ts`, so it is safe to run in CI, a `postinstall`, or a predeploy step, and safe to re-run any number of times.
17
+
18
+ Run it **every time you change the Prisma schema** — it refreshes the client, GraphQL types, and DTO bases in one step (no separate `prisma generate` needed). Because everything it writes lives under the gitignored `src/generated/`, it never produces a diff in your committed code.
19
+
20
+ ## `appx-core generate models` — the module wizard
21
+
22
+ ```bash
23
+ appx-core generate models # interactive picker
24
+ appx-core generate models Habit Log # named models (Pascal or kebab)
25
+ appx-core generate models --all # every generatable model
26
+ ```
27
+
28
+ For each selected model it scaffolds the hand-owned files — module, service, controller, resolver, and the DTO subclass — **once** (never overwriting your edits), registers the module in `app.module.ts`, and then runs the deploy-safe pass so the new imports resolve.
29
+
30
+ The interactive picker lists **only models that don't already have a module**. If there are none, it prints `No modules available to generate`.
31
+
32
+ ### You don't need a module for every table
33
+
34
+ Generated CRUD supports **permission-gated nested writes** (`create` / `connect`), so you can expose a parent resource and write related rows through it. Scaffold modules only for the resources you actually serve over HTTP/GraphQL — not mechanically for every table.
35
+
36
+ ### `User` is framework-owned
37
+
38
+ The `User` model is never scaffolded. Authentication already serves user endpoints under `/auth/*`, and a generic `POST/PUT /users` CRUD is a mass-assignment surface. If you genuinely need generic user CRUD, write it by hand — but prefer a dedicated, permission-scoped endpoint.
39
+
40
+ ### Ownership is by model, not folder name
41
+
42
+ Whether a model "already has a module" is determined by the model it serves (`CoreController<Model>` / `CoreService<Model>`), not by a folder-name convention. So a hand-written module under a non-canonical folder (e.g. a pluralised `wearable-connections/` serving `WearableConnection`) is recognised, and the wizard will **not** scaffold a duplicate.
43
+
44
+ ## Typical workflow
45
+
46
+ ```bash
47
+ # 1. Edit prisma/schema.prisma, then:
48
+ npx prisma migrate dev # apply the schema change
49
+ appx-core generate # refresh client / GraphQL / DTO bases (safe)
50
+ appx-core generate models # expose the new table(s) as CRUD (pick them)
51
+ ```
52
+
53
+ In CI / deploy, run **only** `appx-core generate` — it will never touch your committed code.
54
+
55
+ ## GraphQL
56
+
57
+ Scaffolded modules include a **read-only** GraphQL resolver (query + aggregate; no mutations). See [graphql.md](./graphql.md). Per-module GraphQL toggling is planned for a later release.
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@appxdigital/appx-core",
3
- "version": "0.1.122",
3
+ "version": "0.1.124",
4
4
  "description": "Appx Core is a library that provides a set of tools to help you build your application faster.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "scripts": {
8
8
  "build": "tsc",
9
- "build:cli": "npm --prefix cli ci",
9
+ "build:cli": "node scripts/sync-cli-core-version.mjs && npm --prefix cli ci",
10
10
  "build:all": "npm run build && npm run build:cli",
11
11
  "db-pull": "npx prisma db pull && prisma-case-format --table-case pascal --field-case camel --enum-case pascal --file prisma/schema.prisma",
12
12
  "test": "jest",
@@ -46,6 +46,7 @@
46
46
  "helmet": "^8.3.0",
47
47
  "jest": "^29.7.0",
48
48
  "prettier": "^3.8.3",
49
+ "prisma-graphql-type-decimal": "^3.0.0",
49
50
  "rimraf": "^6.1.2",
50
51
  "supertest": "^7.2.2",
51
52
  "testcontainers": "^10.28.0",
@@ -94,6 +95,7 @@
94
95
  "passport-local": "~1.0.0",
95
96
  "prisma": "~6.5.0",
96
97
  "prisma-case-format": "~2.2.1",
98
+ "prisma-graphql-type-decimal": "^3.0.0",
97
99
  "prisma-nestjs-graphql": "21.1.1",
98
100
  "reflect-metadata": "~0.2.0",
99
101
  "rxjs": "~7.8.1"