@appxdigital/appx-core 0.1.119 → 0.1.121-beta.4

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 (34) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +564 -533
  3. package/dist/backoffice/appx-core-admin.module.d.ts.map +1 -1
  4. package/dist/backoffice/appx-core-admin.module.js +3 -3
  5. package/dist/common/interceptors/prisma.interceptor.d.ts.map +1 -1
  6. package/dist/common/interceptors/prisma.interceptor.js +18 -6
  7. package/dist/common/setup/core-config.d.ts +66 -0
  8. package/dist/common/setup/core-config.d.ts.map +1 -0
  9. package/dist/common/setup/core-config.js +67 -0
  10. package/dist/common/setup/setup-core-security.d.ts +62 -0
  11. package/dist/common/setup/setup-core-security.d.ts.map +1 -0
  12. package/dist/common/setup/setup-core-security.js +40 -0
  13. package/dist/config/generate-all.js +4 -0
  14. package/dist/config/generate-controllers.js +19 -2
  15. package/dist/config/generate-dtos.d.ts +2 -0
  16. package/dist/config/generate-dtos.d.ts.map +1 -0
  17. package/dist/config/generate-dtos.js +199 -0
  18. package/dist/config/utils.d.ts +14 -0
  19. package/dist/config/utils.d.ts.map +1 -1
  20. package/dist/config/utils.js +35 -2
  21. package/dist/index.d.ts +2 -0
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +2 -0
  24. package/dist/modules/auth/auth.controller.d.ts.map +1 -1
  25. package/dist/modules/auth/auth.controller.js +6 -2
  26. package/dist/modules/auth/auth.service.d.ts.map +1 -1
  27. package/dist/modules/core/core.controller.d.ts.map +1 -1
  28. package/dist/modules/core/core.controller.js +12 -5
  29. package/dist/modules/user/user.service.d.ts.map +1 -1
  30. package/dist/modules/user/user.service.js +7 -3
  31. package/dist/prisma/prisma.service.d.ts +45 -0
  32. package/dist/prisma/prisma.service.d.ts.map +1 -1
  33. package/dist/prisma/prisma.service.js +208 -3
  34. package/package.json +108 -86
@@ -1 +1 @@
1
- {"version":3,"file":"appx-core-admin.module.d.ts","sourceRoot":"","sources":["../../src/backoffice/appx-core-admin.module.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,aAAa,EAAiB,MAAM,gBAAgB,CAAC;AAQ7D,OAAO,EAAC,eAAe,EAAC,MAAM,kCAAkC,CAAC;AAIjE,OAAO,EAAC,qBAAqB,EAAC,MAAM,yCAAyC,CAAC;AAE9E,qBAEa,mBAAmB;IAC5B,MAAM,CAAC,OAAO,CAAC,MAAM,EAAE,eAAe,EAAE,gBAAgB,EAAE,qBAAqB,GAAG,OAAO,CAAC,aAAa,CAAC;CAG3G"}
1
+ {"version":3,"file":"appx-core-admin.module.d.ts","sourceRoot":"","sources":["../../src/backoffice/appx-core-admin.module.ts"],"names":[],"mappings":"AAGA,OAAO,EAAC,aAAa,EAAiB,MAAM,gBAAgB,CAAC;AAQ7D,OAAO,EAAC,eAAe,EAAC,MAAM,kCAAkC,CAAC;AAIjE,OAAO,EAAC,qBAAqB,EAAC,MAAM,yCAAyC,CAAC;AAE9E,qBAEa,mBAAmB;IAC5B,MAAM,CAAC,OAAO,CAAC,MAAM,EAAE,eAAe,EAAE,gBAAgB,EAAE,qBAAqB,GAAG,OAAO,CAAC,aAAa,CAAC;CAG3G"}
@@ -28,8 +28,11 @@ var __importStar = (this && this.__importStar) || function (mod) {
28
28
  __setModuleDefault(result, mod);
29
29
  return result;
30
30
  };
31
+ var _a;
31
32
  Object.defineProperty(exports, "__esModule", { value: true });
32
33
  exports.AppxCoreAdminModule = void 0;
34
+ // Set tmp dir to avoid dot ".adminjs" since it is not served by default on Express 5
35
+ (_a = process.env).ADMIN_JS_TMP_DIR ?? (_a.ADMIN_JS_TMP_DIR = 'tmp/adminjs');
33
36
  const common_1 = require("@nestjs/common");
34
37
  const utils_1 = require("./utils");
35
38
  const fs_1 = require("fs");
@@ -51,9 +54,6 @@ function lowerCase(name) {
51
54
  return name.substring(0, 1).toLowerCase() + name.substring(1);
52
55
  }
53
56
  async function createAdminJsModule(adminConfig, permissionConfig) {
54
- var _a;
55
- // Set tmp dir to avoid dot ".adminjs" since it is not served by default on Express 5
56
- (_a = process.env).ADMIN_JS_TMP_DIR ?? (_a.ADMIN_JS_TMP_DIR = 'tmp/adminjs');
57
57
  // Due to AdminJS only allowing ESM now, we need to use dynamic imports to load the modules, this function can be found within src/backoffice/utils.ts
58
58
  const { default: AdminJS } = await (0, utils_1.dynamicImport)('adminjs');
59
59
  const { Database, Resource, convertParam } = await (0, utils_1.dynamicImport)('@adminjs/prisma');
@@ -1 +1 @@
1
- {"version":3,"file":"prisma.interceptor.d.ts","sourceRoot":"","sources":["../../../src/common/interceptors/prisma.interceptor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,WAAW,EAAE,gBAAgB,EAAc,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAC3F,OAAO,EAAoB,aAAa,EAAC,MAAM,6BAA6B,CAAC;AAC7E,OAAO,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AACvC,OAAO,EAAC,aAAa,EAAC,MAAM,gBAAgB,CAAC;AAG7C,OAAO,EAAC,UAAU,EAAa,MAAM,MAAM,CAAC;AAK5C,qBACa,iBAAkB,YAAW,eAAe;IAIjD,OAAO,CAAC,QAAQ,CAAC,aAAa;IAC9B,OAAO,CAAC,SAAS;IACjB,OAAO,CAAC,aAAa;IALzB,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAAS;gBAG1B,aAAa,EAAE,aAAa,EACrC,SAAS,EAAE,SAAS,EACpB,aAAa,EAAE,aAAa;IAKxC,SAAS,CAAC,OAAO,EAAE,gBAAgB,EAAE,IAAI,EAAE,WAAW,GAAG,UAAU,CAAC,GAAG,CAAC;CAoE3E"}
1
+ {"version":3,"file":"prisma.interceptor.d.ts","sourceRoot":"","sources":["../../../src/common/interceptors/prisma.interceptor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,WAAW,EAAE,gBAAgB,EAAc,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAC3F,OAAO,EAAoB,aAAa,EAAC,MAAM,6BAA6B,CAAC;AAC7E,OAAO,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AACvC,OAAO,EAAC,aAAa,EAAC,MAAM,gBAAgB,CAAC;AAG7C,OAAO,EAAC,UAAU,EAAa,MAAM,MAAM,CAAC;AAK5C,qBACa,iBAAkB,YAAW,eAAe;IAIjD,OAAO,CAAC,QAAQ,CAAC,aAAa;IAC9B,OAAO,CAAC,SAAS;IACjB,OAAO,CAAC,aAAa;IALzB,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAAS;gBAG1B,aAAa,EAAE,aAAa,EACrC,SAAS,EAAE,SAAS,EACpB,aAAa,EAAE,aAAa;IAKxC,SAAS,CAAC,OAAO,EAAE,gBAAgB,EAAE,IAAI,EAAE,WAAW,GAAG,UAAU,CAAC,GAAG,CAAC;CA+E3E"}
@@ -39,31 +39,43 @@ let PrismaInterceptor = exports.PrismaInterceptor = class PrismaInterceptor {
39
39
  }, () => {
40
40
  const useTransaction = this.reflector.get('useTransaction', context.getHandler()) ?? this.defaultUseTransaction === 'true';
41
41
  if (useTransaction) {
42
+ // Run the handler inside the transaction and CAPTURE its result,
43
+ // but do not emit the response yet: the client must not see a
44
+ // success response until the transaction has actually committed.
45
+ // Emitting inside the transaction (before it commits) races the
46
+ // commit — an immediate read-after-write can miss the row.
47
+ let handlerResult;
42
48
  this.prismaService
43
49
  .$transaction(async (transactionClient) => {
44
50
  nestjs_request_context_1.RequestContext.currentContext.req.prisma = transactionClient;
45
- await new Promise((resolve, reject) => {
51
+ handlerResult = await new Promise((resolve, reject) => {
52
+ let captured;
46
53
  next
47
54
  .handle()
48
- .pipe((0, operators_1.tap)(() => { }), (0, operators_1.catchError)((err) => {
55
+ .pipe((0, operators_1.catchError)((err) => {
49
56
  const handledError = (0, error_handler_1.handleError)(err);
50
57
  reject(handledError);
51
58
  return (0, rxjs_1.throwError)(() => handledError);
52
59
  }))
53
60
  .subscribe({
54
- next: (result) => observer.next(result),
61
+ next: (result) => {
62
+ captured = result;
63
+ },
55
64
  complete: () => {
56
65
  delete nestjs_request_context_1.RequestContext.currentContext.req.prisma;
57
- observer.complete();
58
- resolve(null);
66
+ resolve(captured);
59
67
  },
60
68
  error: (err) => {
61
69
  delete nestjs_request_context_1.RequestContext.currentContext.req.prisma;
62
- observer.error(err);
63
70
  reject(err);
64
71
  },
65
72
  });
66
73
  });
74
+ })
75
+ .then(() => {
76
+ // The transaction has committed — now it is safe to respond.
77
+ observer.next(handlerResult);
78
+ observer.complete();
67
79
  })
68
80
  .catch((err) => {
69
81
  delete nestjs_request_context_1.RequestContext.currentContext.req.prisma;
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Boot-time configuration helpers used by the scaffold's `main.ts` /
3
+ * `app.module.ts`. They fail fast on insecure or ambiguous configuration
4
+ * instead of silently falling back.
5
+ */
6
+ /** Minimum length we require of a session signing secret. */
7
+ export declare const MIN_SESSION_SECRET_LENGTH = 32;
8
+ export interface CoreSessionOptionsInput {
9
+ /** The session signing secret. Required — no fallback. */
10
+ secret?: string;
11
+ /** The express-session store (e.g. `CorePrismaSessionStore`). */
12
+ store: any;
13
+ /** Cookie name. */
14
+ cookieName?: string;
15
+ /** Session TTL in seconds (drives cookie `maxAge`). Default 86400. */
16
+ ttlSeconds?: number;
17
+ /**
18
+ * Force the `Secure` cookie flag. Defaults to `true` in production
19
+ * (`NODE_ENV === 'production'`), `false` otherwise so local HTTP dev works.
20
+ */
21
+ secure?: boolean;
22
+ /** `SameSite` policy. Default `'lax'`. */
23
+ sameSite?: boolean | 'lax' | 'strict' | 'none';
24
+ }
25
+ /**
26
+ * Builds hardened `express-session` options.
27
+ *
28
+ * - **:** throws if `secret` is missing or shorter than
29
+ * {@link MIN_SESSION_SECRET_LENGTH}. There is no hardcoded fallback — a
30
+ * missing secret must stop the boot, never silently sign sessions with a
31
+ * public constant.
32
+ * - **:** sets `cookie: { httpOnly, secure, sameSite, maxAge }` so session
33
+ * cookies are not readable by JS, are `Secure` in production, and carry a
34
+ * `SameSite` policy.
35
+ *
36
+ * Pass the result straight to `session(...)`.
37
+ */
38
+ export declare function buildCoreSessionOptions(input: CoreSessionOptionsInput): {
39
+ secret: string;
40
+ resave: boolean;
41
+ saveUninitialized: boolean;
42
+ name: string | undefined;
43
+ store: any;
44
+ cookie: {
45
+ httpOnly: boolean;
46
+ secure: boolean;
47
+ sameSite: boolean | "none" | "lax" | "strict";
48
+ maxAge: number;
49
+ };
50
+ };
51
+ /**
52
+ * Resolves the env file paths for `ConfigModule.forRoot({ envFilePath })`.
53
+ *
54
+ * Returns `['.env.<NODE_ENV>', '.env']`. `ConfigModule` gives the **first**
55
+ * existing file precedence, so environment-specific values in `.env.<NODE_ENV>`
56
+ * win, and a plain `.env` (what `appx-core create` generates) is loaded as a
57
+ * base/fallback. Without the `.env` fallback a freshly scaffolded project —
58
+ * which ships only `.env` — would load nothing and then fail the * `SESSION_SECRET` check at boot.
59
+ *
60
+ * **:** throws if `NODE_ENV` is unset instead of silently defaulting to
61
+ * `development` — a production deploy that forgets `NODE_ENV` must fail fast
62
+ * rather than load `.env.development` (dev DB creds, loose CORS, insecure
63
+ * cookies). Set `NODE_ENV` explicitly (`production` / `development` / `test`).
64
+ */
65
+ export declare function coreEnvFilePath(): string[];
66
+ //# sourceMappingURL=core-config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"core-config.d.ts","sourceRoot":"","sources":["../../../src/common/setup/core-config.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,6DAA6D;AAC7D,eAAO,MAAM,yBAAyB,KAAK,CAAC;AAE5C,MAAM,WAAW,uBAAuB;IACpC,0DAA0D;IAC1D,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,iEAAiE;IACjE,KAAK,EAAE,GAAG,CAAC;IACX,mBAAmB;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,sEAAsE;IACtE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,0CAA0C;IAC1C,QAAQ,CAAC,EAAE,OAAO,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;CAClD;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,uBAAuB;;;;;;;;;;;;EAuBrE;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,IAAI,MAAM,EAAE,CAS1C"}
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+ /**
3
+ * Boot-time configuration helpers used by the scaffold's `main.ts` /
4
+ * `app.module.ts`. They fail fast on insecure or ambiguous configuration
5
+ * instead of silently falling back.
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.coreEnvFilePath = exports.buildCoreSessionOptions = exports.MIN_SESSION_SECRET_LENGTH = void 0;
9
+ /** Minimum length we require of a session signing secret. */
10
+ exports.MIN_SESSION_SECRET_LENGTH = 32;
11
+ /**
12
+ * Builds hardened `express-session` options.
13
+ *
14
+ * - **:** throws if `secret` is missing or shorter than
15
+ * {@link MIN_SESSION_SECRET_LENGTH}. There is no hardcoded fallback — a
16
+ * missing secret must stop the boot, never silently sign sessions with a
17
+ * public constant.
18
+ * - **:** sets `cookie: { httpOnly, secure, sameSite, maxAge }` so session
19
+ * cookies are not readable by JS, are `Secure` in production, and carry a
20
+ * `SameSite` policy.
21
+ *
22
+ * Pass the result straight to `session(...)`.
23
+ */
24
+ function buildCoreSessionOptions(input) {
25
+ const { secret, store, cookieName, ttlSeconds = 86400, secure, sameSite = 'lax' } = input;
26
+ if (!secret || secret.length < exports.MIN_SESSION_SECRET_LENGTH) {
27
+ throw new Error(`SESSION_SECRET must be set and at least ${exports.MIN_SESSION_SECRET_LENGTH} characters. ` +
28
+ 'Refusing to start with a missing or weak session secret.');
29
+ }
30
+ return {
31
+ secret,
32
+ resave: false,
33
+ saveUninitialized: false,
34
+ name: cookieName,
35
+ store,
36
+ cookie: {
37
+ httpOnly: true,
38
+ secure: secure ?? process.env.NODE_ENV === 'production',
39
+ sameSite,
40
+ maxAge: ttlSeconds * 1000,
41
+ },
42
+ };
43
+ }
44
+ exports.buildCoreSessionOptions = buildCoreSessionOptions;
45
+ /**
46
+ * Resolves the env file paths for `ConfigModule.forRoot({ envFilePath })`.
47
+ *
48
+ * Returns `['.env.<NODE_ENV>', '.env']`. `ConfigModule` gives the **first**
49
+ * existing file precedence, so environment-specific values in `.env.<NODE_ENV>`
50
+ * win, and a plain `.env` (what `appx-core create` generates) is loaded as a
51
+ * base/fallback. Without the `.env` fallback a freshly scaffolded project —
52
+ * which ships only `.env` — would load nothing and then fail the * `SESSION_SECRET` check at boot.
53
+ *
54
+ * **:** throws if `NODE_ENV` is unset instead of silently defaulting to
55
+ * `development` — a production deploy that forgets `NODE_ENV` must fail fast
56
+ * rather than load `.env.development` (dev DB creds, loose CORS, insecure
57
+ * cookies). Set `NODE_ENV` explicitly (`production` / `development` / `test`).
58
+ */
59
+ function coreEnvFilePath() {
60
+ const env = process.env.NODE_ENV;
61
+ if (!env) {
62
+ throw new Error('NODE_ENV must be explicitly set (e.g. "production" or "development"). ' +
63
+ 'Refusing to default to development.');
64
+ }
65
+ return [`.env.${env}`, '.env'];
66
+ }
67
+ exports.coreEnvFilePath = coreEnvFilePath;
@@ -0,0 +1,62 @@
1
+ import { INestApplication, ValidationPipeOptions } from '@nestjs/common';
2
+ /**
3
+ * Minimal CORS options accepted by {@link setupCoreSecurity}. Mirrors the
4
+ * subset of Nest's `CorsOptions` that most apps set; extra keys are passed
5
+ * through to `app.enableCors` untouched. Declared locally to avoid a fragile
6
+ * deep import into Nest internals.
7
+ */
8
+ export interface CoreCorsOptions {
9
+ origin?: string | string[] | boolean | RegExp | (string | RegExp)[];
10
+ credentials?: boolean;
11
+ [key: string]: any;
12
+ }
13
+ /**
14
+ * Options for {@link setupCoreSecurity}. Each concern can be disabled by
15
+ * passing `false`, or tuned by passing an options object that is merged over
16
+ * the secure defaults.
17
+ */
18
+ export interface CoreSecurityOptions {
19
+ /**
20
+ * Global `ValidationPipe`. Secure default:
21
+ * `{ transform: true, whitelist: true, forbidNonWhitelisted: true }`.
22
+ *
23
+ * `whitelist` strips body properties that carry no validation decorator;
24
+ * `forbidNonWhitelisted` turns such properties into a `400` instead of
25
+ * silently dropping them. Combined with DTOs that declare every accepted
26
+ * field, this rejects unknown body properties on `POST /auth/register`
27
+ * and every other class-validated request body.
28
+ *
29
+ * Pass `false` to skip (you must then register your own pipe), or an
30
+ * options object to override individual flags.
31
+ */
32
+ validationPipe?: ValidationPipeOptions | false;
33
+ /**
34
+ * CORS. Default: `{ origin: 'http://localhost:3000', credentials: true }`
35
+ * — the scaffold's historical default. Read `origin` from configuration
36
+ * (e.g. a `CORS_ORIGIN` env var) and pass it here so the allowed origin is
37
+ * deployment-configurable rather than hardcoded.
38
+ *
39
+ * Guardrail: `origin: '*'` together with `credentials: true` throws at
40
+ * setup — that combination is rejected by browsers and is a common footgun.
41
+ *
42
+ * Pass `false` to skip (you manage CORS yourself).
43
+ */
44
+ cors?: CoreCorsOptions | false;
45
+ /**
46
+ * Security headers via Helmet: `Content-Security-Policy`,
47
+ * `X-Content-Type-Options`, `Strict-Transport-Security`, frame-ancestors, etc.
48
+ * Default: enabled with Helmet's defaults. Pass an options object to tune
49
+ * (e.g. a custom CSP for the AdminJS UI), or `false` to skip.
50
+ */
51
+ helmet?: Record<string, any> | false;
52
+ }
53
+ /**
54
+ * Applies AppX Core's baseline HTTP hardening to a Nest application.
55
+ *
56
+ * Call once in `main.ts`, after `NestFactory.create(...)` and before
57
+ * `app.listen(...)`. New scaffolds call it for you; existing projects should
58
+ * add the call when upgrading — see the CHANGELOG migration note for the
59
+ * version that introduced each concern.
60
+ */
61
+ export declare function setupCoreSecurity(app: INestApplication, options?: CoreSecurityOptions): void;
62
+ //# sourceMappingURL=setup-core-security.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"setup-core-security.d.ts","sourceRoot":"","sources":["../../../src/common/setup/setup-core-security.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,gBAAgB,EAAkB,qBAAqB,EAAC,MAAM,gBAAgB,CAAC;AAEvF;;;;;GAKG;AACH,MAAM,WAAW,eAAe;IAC5B,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,OAAO,GAAG,MAAM,GAAG,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,CAAC;IACpE,WAAW,CAAC,EAAE,OAAO,CAAC;IAEtB,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;CACtB;AAED;;;;GAIG;AACH,MAAM,WAAW,mBAAmB;IAChC;;;;;;;;;;;;OAYG;IACH,cAAc,CAAC,EAAE,qBAAqB,GAAG,KAAK,CAAC;IAE/C;;;;;;;;;;OAUG;IACH,IAAI,CAAC,EAAE,eAAe,GAAG,KAAK,CAAC;IAE/B;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,KAAK,CAAC;CACxC;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAC7B,GAAG,EAAE,gBAAgB,EACrB,OAAO,GAAE,mBAAwB,GAClC,IAAI,CAkCN"}
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.setupCoreSecurity = void 0;
4
+ const common_1 = require("@nestjs/common");
5
+ /**
6
+ * Applies AppX Core's baseline HTTP hardening to a Nest application.
7
+ *
8
+ * Call once in `main.ts`, after `NestFactory.create(...)` and before
9
+ * `app.listen(...)`. New scaffolds call it for you; existing projects should
10
+ * add the call when upgrading — see the CHANGELOG migration note for the
11
+ * version that introduced each concern.
12
+ */
13
+ function setupCoreSecurity(app, options = {}) {
14
+ if (options.validationPipe !== false) {
15
+ app.useGlobalPipes(new common_1.ValidationPipe({
16
+ transform: true,
17
+ whitelist: true,
18
+ forbidNonWhitelisted: true,
19
+ ...(options.validationPipe ?? {}),
20
+ }));
21
+ }
22
+ if (options.cors !== false) {
23
+ const cors = options.cors ?? {};
24
+ const origin = cors.origin ?? 'http://localhost:3000';
25
+ const credentials = cors.credentials ?? true;
26
+ if (credentials && origin === '*') {
27
+ throw new Error("setupCoreSecurity: CORS origin '*' is incompatible with credentials:true — " +
28
+ 'browsers reject it and it exposes credentialed endpoints to every origin. ' +
29
+ 'Set an explicit origin (e.g. via a CORS_ORIGIN env var) or disable credentials.');
30
+ }
31
+ app.enableCors({ ...cors, origin, credentials });
32
+ }
33
+ if (options.helmet !== false) {
34
+ // Lazy require: helmet is a peer dependency, only needed when enabled.
35
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
36
+ const helmet = require('helmet');
37
+ app.use(typeof options.helmet === 'object' ? helmet(options.helmet) : helmet());
38
+ }
39
+ }
40
+ exports.setupCoreSecurity = setupCoreSecurity;
@@ -43,6 +43,10 @@ console.log('Generating Services...');
43
43
  (0, child_process_1.execSync)(`node ${path.join(scriptsDir, 'generate-services.js')}`, {
44
44
  stdio: 'inherit',
45
45
  });
46
+ console.log('Generating DTOs...');
47
+ (0, child_process_1.execSync)(`node ${path.join(scriptsDir, 'generate-dtos.js')}`, {
48
+ stdio: 'inherit',
49
+ });
46
50
  console.log('Generating Controllers...');
47
51
  (0, child_process_1.execSync)(`node ${path.join(scriptsDir, 'generate-controllers.js')}`, {
48
52
  stdio: 'inherit',
@@ -34,10 +34,12 @@ const modulesOutputPath = path.join(process.cwd(), 'src/modules');
34
34
  * @param folder
35
35
  */
36
36
  const genericControllerTemplate = (model, folder) => `
37
- import { Controller } from '@nestjs/common';
37
+ import { Body, Controller, Param, Post, Put } from '@nestjs/common';
38
38
  import { ${model}Service } from './${folder}.service';
39
39
  import { ${model} } from '@prisma/client';
40
- import { CoreController } from '@appxdigital/appx-core';
40
+ import { CoreController, Permission } from '@appxdigital/appx-core';
41
+ import { Create${model}Dto } from './dto/create-${folder}.dto';
42
+ import { Update${model}Dto } from './dto/update-${folder}.dto';
41
43
 
42
44
  @Controller('${model.toLowerCase()}s')
43
45
  export class ${model}Controller extends CoreController<${model}> {
@@ -48,6 +50,21 @@ export class ${model}Controller extends CoreController<${model}> {
48
50
  static get entityName(): string {
49
51
  return '${model}';
50
52
  }
53
+
54
+ // Overridden so the @Body() type is a concrete DTO — this is what lets
55
+ // ValidationPipe (whitelist) strip unknown fields.
56
+ // Custom validation goes in the DTO subclass, not here.
57
+ @Post()
58
+ @Permission('create')
59
+ async create(@Body() data: Create${model}Dto) {
60
+ return super.create(data as any);
61
+ }
62
+
63
+ @Put(':id')
64
+ @Permission('updateMany')
65
+ async update(@Param('id') id: string, @Body() data: Update${model}Dto) {
66
+ return super.update(id, data as any);
67
+ }
51
68
  }
52
69
  `;
53
70
  /**
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=generate-dtos.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"generate-dtos.d.ts","sourceRoot":"","sources":["../../src/config/generate-dtos.ts"],"names":[],"mappings":""}
@@ -0,0 +1,199 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ const path = __importStar(require("path"));
27
+ const utils_1 = require("./utils");
28
+ /**
29
+ * DTO generator.
30
+ *
31
+ * Generated CRUD controllers type `@Body()` as a generic, so NestJS's
32
+ * ValidationPipe has no concrete class to validate against and unknown fields
33
+ * are accepted. This generator emits a class-validator DTO per model+action so
34
+ * the controller can type its body concretely.
35
+ *
36
+ * Two-file, schema-is-source-of-truth model:
37
+ * - `src/generated/dto/<model>/create-<model>.generated.dto.ts` — the base
38
+ * class, derived from the Prisma schema, ALWAYS overwritten (gitignored).
39
+ * - `src/modules/<model>/dto/create-<model>.dto.ts` — a hand-owned subclass,
40
+ * generated ONCE, where the developer adds custom validation. Never
41
+ * overwritten. The controller imports this subclass.
42
+ *
43
+ * A scalar field is writable (appears in the DTO) unless it is:
44
+ * - the primary key (`@id`),
45
+ * - a server-managed timestamp (`@default(now())` or `@updatedAt`),
46
+ * - annotated `/// @NoWrite`, or
47
+ * - annotated `/// @Role(none)` (readable by nobody ⇒ not CRUD-writable).
48
+ * Relation navigation fields are excluded; their scalar foreign keys are kept.
49
+ */
50
+ const cwd = process.cwd();
51
+ // Read the consumer's generated Prisma client for the DMMF (authoritative
52
+ // field metadata incl. `documentation`, which carries /// @Role / @NoWrite).
53
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
54
+ const { Prisma } = require(path.join(cwd, 'node_modules', '@prisma/client'));
55
+ const generatedDtoRoot = path.join(cwd, 'src/generated/dto');
56
+ const modulesRoot = path.join(cwd, 'src/modules');
57
+ function isNoWrite(doc) {
58
+ if (!doc)
59
+ return false;
60
+ if (/@NoWrite\b/i.test(doc))
61
+ return true;
62
+ // @Role(none): readable by nobody ⇒ treat as not CRUD-writable.
63
+ const roleMatch = doc.match(/@Role\((.*?)\)/);
64
+ if (roleMatch) {
65
+ const roles = roleMatch[1].split(',').map((r) => r.trim().toLowerCase());
66
+ if (roles.includes('none'))
67
+ return true;
68
+ }
69
+ return false;
70
+ }
71
+ function isServerManagedTimestamp(field) {
72
+ if (field.isUpdatedAt)
73
+ return true;
74
+ const def = field.default;
75
+ return !!def && typeof def === 'object' && def.name === 'now';
76
+ }
77
+ /** Decision for a scalar/enum field: base validator, TS type, optional enum arg. */
78
+ function mapField(field) {
79
+ if (field.kind === 'enum') {
80
+ return { validator: 'IsEnum', tsType: field.type, enumArg: field.type };
81
+ }
82
+ switch (field.type) {
83
+ case 'String':
84
+ return { validator: 'IsString', tsType: 'string' };
85
+ case 'Boolean':
86
+ return { validator: 'IsBoolean', tsType: 'boolean' };
87
+ case 'Int':
88
+ case 'BigInt':
89
+ return { validator: 'IsInt', tsType: 'number' };
90
+ case 'Float':
91
+ case 'Decimal':
92
+ return { validator: 'IsNumber', tsType: 'number' };
93
+ case 'DateTime':
94
+ return { validator: 'IsDateString', tsType: 'string' };
95
+ default:
96
+ // Json, Bytes, and anything unrecognised: keep the field (so it isn't
97
+ // stripped by whitelist) without a strict type validator.
98
+ return { validator: 'Allow', tsType: 'any' };
99
+ }
100
+ }
101
+ function writableFields(model) {
102
+ return model.fields.filter((f) => {
103
+ if (f.kind === 'object')
104
+ return false; // relation navigation
105
+ if (f.isId)
106
+ return false;
107
+ if (isServerManagedTimestamp(f))
108
+ return false;
109
+ if (isNoWrite(f.documentation))
110
+ return false;
111
+ return true;
112
+ });
113
+ }
114
+ /** Renders a base DTO class body + its import block. */
115
+ function renderBaseDto(className, fields, mode) {
116
+ const validatorImports = new Set();
117
+ const enumImports = new Set();
118
+ const lines = [];
119
+ for (const field of fields) {
120
+ const m = mapField(field);
121
+ validatorImports.add(m.validator);
122
+ if (m.enumArg)
123
+ enumImports.add(m.enumArg);
124
+ // update: everything optional. create: optional if not required or has a default.
125
+ const optional = mode === 'update' || !field.isRequired || field.hasDefaultValue;
126
+ const isList = field.isList;
127
+ const tsType = isList ? `${m.tsType}[]` : m.tsType;
128
+ // Compose the validator decorator. @Allow takes no options; for other
129
+ // validators on a list field, add `{ each: true }` (+ @IsArray).
130
+ let validatorDecorator;
131
+ if (m.validator === 'Allow') {
132
+ validatorDecorator = '@Allow()';
133
+ }
134
+ else {
135
+ const args = [];
136
+ if (m.enumArg)
137
+ args.push(m.enumArg);
138
+ if (isList)
139
+ args.push('{ each: true }');
140
+ validatorDecorator = `@${m.validator}(${args.join(', ')})`;
141
+ }
142
+ const decoratorLines = [];
143
+ if (optional) {
144
+ validatorImports.add('IsOptional');
145
+ decoratorLines.push(' @IsOptional()');
146
+ }
147
+ if (isList && m.validator !== 'Allow') {
148
+ validatorImports.add('IsArray');
149
+ decoratorLines.push(' @IsArray()');
150
+ }
151
+ decoratorLines.push(` ${validatorDecorator}`);
152
+ lines.push(decoratorLines.join('\n'));
153
+ lines.push(` ${field.name}${optional ? '?' : '!'}: ${tsType};`);
154
+ lines.push('');
155
+ }
156
+ const importLines = [];
157
+ if (validatorImports.size > 0) {
158
+ importLines.push(`import { ${[...validatorImports].sort().join(', ')} } from 'class-validator';`);
159
+ }
160
+ if (enumImports.size > 0) {
161
+ importLines.push(`import { ${[...enumImports].sort().join(', ')} } from '@prisma/client';`);
162
+ }
163
+ return (utils_1.GENERATED_BANNER +
164
+ '\n' +
165
+ (importLines.length ? importLines.join('\n') + '\n\n' : '') +
166
+ `export class ${className} {\n` +
167
+ (lines.length ? lines.map((l) => l).join('\n') : '') +
168
+ `}\n`);
169
+ }
170
+ /** Renders the hand-owned subclass (generated once, then customizable). */
171
+ function renderSubclassDto(subclass, baseClass, importPath) {
172
+ return (`import { ${baseClass} } from '${importPath}';\n\n` +
173
+ `/**\n` +
174
+ ` * Hand-owned DTO for the CRUD endpoint. Add custom class-validator rules\n` +
175
+ ` * here; this file is generated once and never overwritten by \`appx generate\`.\n` +
176
+ ` * The base class is regenerated from the Prisma schema.\n` +
177
+ ` */\n` +
178
+ `export class ${subclass} extends ${baseClass} {}\n`);
179
+ }
180
+ for (const model of Prisma.dmmf.datamodel.models) {
181
+ const modelName = model.name; // Pascal, e.g. ProjectMember
182
+ const folder = (0, utils_1.pascalToKebabCase)(modelName); // kebab, e.g. project-member
183
+ if (utils_1.IGNORE_FOLDERS.includes(folder))
184
+ continue;
185
+ const fields = writableFields(model);
186
+ for (const mode of ['create', 'update']) {
187
+ const capMode = mode.charAt(0).toUpperCase() + mode.slice(1);
188
+ const baseClass = `${capMode}${modelName}GeneratedDto`;
189
+ const subclass = `${capMode}${modelName}Dto`;
190
+ // Base (overwritten, gitignored).
191
+ const basePath = path.join(generatedDtoRoot, folder, `${mode}-${folder}.generated.dto.ts`);
192
+ (0, utils_1.writeGeneratedFile)(basePath, renderBaseDto(baseClass, fields, mode));
193
+ // Subclass (once, hand-owned, committed).
194
+ const subPath = path.join(modulesRoot, folder, 'dto', `${mode}-${folder}.dto.ts`);
195
+ const importPath = `../../../generated/dto/${folder}/${mode}-${folder}.generated.dto`;
196
+ (0, utils_1.createFileIfNotExists)(subPath, renderSubclassDto(subclass, baseClass, importPath));
197
+ }
198
+ }
199
+ console.log('DTOs generated.');
@@ -1,4 +1,18 @@
1
1
  export declare const kebabToPascalCase: (str: string) => string;
2
+ export declare const pascalToKebabCase: (str: string) => string;
2
3
  export declare const createFileIfNotExists: (filePath: string, content: string) => void;
4
+ /**
5
+ * Banner stamped on every fully-generated file. These files live under
6
+ * `src/generated/` (gitignored) and are OVERWRITTEN on every `appx generate`.
7
+ * Never hand-edit them — customize the hand-owned subclass under
8
+ * `src/modules/<model>/dto/` instead.
9
+ */
10
+ export declare const GENERATED_BANNER: string;
11
+ /**
12
+ * Writes a fully-generated file, always overwriting. Use only for artifacts
13
+ * under `src/generated/` that carry {@link GENERATED_BANNER}. Creates parent
14
+ * directories as needed.
15
+ */
16
+ export declare const writeGeneratedFile: (filePath: string, content: string) => void;
3
17
  export declare const IGNORE_FOLDERS: string[];
4
18
  //# sourceMappingURL=utils.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/config/utils.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,iBAAiB,QAAS,MAAM,WAK5C,CAAC;AAEF,eAAO,MAAM,qBAAqB,aAAc,MAAM,WAAW,MAAM,SAQtE,CAAC;AAEF,eAAO,MAAM,cAAc,UAK1B,CAAC"}
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"}