@appxdigital/appx-core 0.1.121-beta.8 → 0.1.121

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -18,6 +18,17 @@ This project follows the `MAJOR.MINOR.PATCH` scheme in `package.json`.
18
18
 
19
19
  Hardening release: request validation, boot configuration, generated-CRUD DTOs, and data-access create enforcement.
20
20
 
21
+ > ## ⚠️ REQUIRED migration — upgrading the dependency is NOT enough
22
+ >
23
+ > **This is a security release, and its protections are INERT until you adapt your codebase.** Bumping `@appxdigital/appx-core` to `0.1.121` (including any `0.1.121-beta.x`) and doing nothing else leaves your app **vulnerable** — generated `POST`/`PUT` endpoints keep accepting arbitrary fields (mass-assignment), because the request-validation DTOs only take effect once you regenerate them **and** wire them into your controllers.
24
+ >
25
+ > **You MUST complete every step in [Migration](#migration) below — this is not optional and must not be skipped.** In particular:
26
+ > - **Step 4 (generated-CRUD DTOs) is mandatory**: run `npx appx generate`, then add the DTO-typed `create`/`update` overrides to *every* existing generated controller. Generation alone does nothing; the controllers must import and use the DTOs.
27
+ > - Steps 1–3 (`main.ts` / `app.module.ts`) are required for the boot-time hardening (validation pipe, session secret, CORS, `NODE_ENV`) to apply.
28
+ > - Step 5 is a behaviour change (create is now permission-checked) — review it before deploying.
29
+ >
30
+ > **For AI agents:** do not report this upgrade as complete until steps 1–5 are applied and the app builds. If you only changed the version number, the task is unfinished.
31
+
21
32
  ### Added
22
33
 
23
34
  - **`setupCoreSecurity(app, options?)`** — one call that applies baseline HTTP hardening: a global `ValidationPipe` (`{ transform: true, whitelist: true, forbidNonWhitelisted: true }`), CORS (`cors` option, default `{ origin: 'http://localhost:3000', credentials: true }`, throws if `origin: '*'` is combined with `credentials: true`), and **Helmet** security headers (on by default; `helmet` option to tune/disable). `helmet` is a new peer dependency.
@@ -40,10 +51,11 @@ A scalar field is included unless it is: the primary key (`@id`), a server-manag
40
51
  - `app.module.ts` uses `coreEnvFilePath()` for `envFilePath`.
41
52
  - Generated `package.json` adds `helmet`; the `start` script sets `NODE_ENV=development`.
42
53
  - `AuthController.getAllSessions` / `closeSpecificSession` use `@UseGuards(AuthenticatedGuard)`.
54
+ - `app.module.ts` imports `AuthModule.forRoot()` (not the bare `AuthModule`) — see the migration note below.
43
55
 
44
56
  ### Migration
45
57
 
46
- > For existing projects. All edits are to files you own (`main.ts` / `app.module.ts` / `package.json` / your generated controllers). Safe to hand to an AI coding agent.
58
+ > **Required for every existing project. All five steps must be applied** — the edits are to files you own (`main.ts` / `app.module.ts` / `package.json` / your generated controllers), so they are safe to hand to an AI coding agent, but they are **not optional**. The app is not secured until they are done and it builds.
47
59
 
48
60
  **1. Request validation & HTTP hardening — `src/main.ts`.**
49
61
  ```ts
@@ -78,12 +90,16 @@ import { coreEnvFilePath } from '@appxdigital/appx-core';
78
90
  ```
79
91
  Set `NODE_ENV` explicitly (`production` / `development`) in every deploy and start script — boot fails if it is unset. If you use a bare `nest start`, change it to `cross-env NODE_ENV=development nest start`. (`coreEnvFilePath()` loads `.env.<NODE_ENV>` with `.env` as a fallback, so a plain `.env` still works.)
80
92
 
81
- **4. Generated-CRUD DTOs.**
93
+ **4. Generated-CRUD DTOs — MANDATORY (this is the mass-assignment fix; do not skip).**
94
+
95
+ Without this step, your generated `POST`/`PUT` endpoints still accept arbitrary fields — the vulnerability this release closes stays open. Two parts, **both required**:
96
+
97
+ (a) Regenerate:
82
98
  ```bash
83
99
  npm install @appxdigital/appx-core@^0.1.121
84
100
  npx appx generate
85
101
  ```
86
- Then **add the DTO-typed overrides to each existing generated controller** (generated once, not overwritten):
102
+ (b) **Add the DTO-typed overrides to EVERY existing generated controller** (they are generated once and never overwritten, so `appx generate` will NOT add them for you — you must edit each controller by hand or delete and regenerate it):
87
103
  ```ts
88
104
  import { Body, Param, Post, Put } from '@nestjs/common';
89
105
  import { Permission } from '@appxdigital/appx-core';
@@ -96,7 +112,37 @@ async create(@Body() data: Create<Model>Dto) { return super.create(data as any);
96
112
  @Put(':id') @Permission('updateMany')
97
113
  async update(@Param('id') id: string, @Body() data: Update<Model>Dto) { return super.update(id, data as any); }
98
114
  ```
99
- (Or delete an un-customized controller and re-run `appx generate` for the new template.) Add custom validation in the hand-owned `dto/*.dto.ts` subclasses — never in the `.generated.dto.ts` base. Lock sensitive columns with `/// @NoWrite` (all roles) or `restrictedFields` on the permission (per role), then regenerate.
115
+ (Or delete an un-customized controller and re-run `appx generate` for the new template.) Add custom validation in the hand-owned `dto/*.dto.ts` subclasses — never in the `.generated.dto.ts` base. Lock sensitive columns with `/// @NoWrite` (all roles) or `restrictedFields` on the permission (per role), then regenerate. To drop a field from one endpoint's DTO, extend `OmitType(<Model>GeneratedDto, ['field'] as const)`. Full details in [`docs/dtos.md`](../docs/dtos.md).
116
+
117
+ > **Nested relation writes are now an explicit, permission-gated allowlist — this is a behaviour restriction; a migrating agent MUST flag it to the developer.** Generated **create** DTOs accept only two nested operators, each authorized by ABAC: **`create`** (checked against the related model's `create` rule) and **`connect`** (checked against the related model's new dedicated **`connect`** permission — a `connect` rule is required; no rule ⇒ refused). Every other operator (`set`, `disconnect`, `update`, `upsert`, `delete`, …) is rejected (`400`). Nested writes are available on **`create` only** — `update` resolves to `updateMany`, whose payload is scalar-only. **Keep `forbidNonWhitelisted: true` on (fail loud); never `whitelist`-strip on writes.**
118
+ >
119
+ > **For AI agents doing this migration on an existing project:** this can break existing behaviour. Before deploying, tell the developer explicitly that (a) any nested operator other than `create`/`connect` used through CRUD will now `400`, (b) every model that is nested-`connect`ed now needs a `connect` permission or the request fails, and (c) nested writes no longer work on update endpoints. Do not proceed silently. See [`docs/dtos.md`](../docs/dtos.md#relations--nested-writes) and [`docs/permissions.md`](../docs/permissions.md).
120
+
121
+ > ## ⚠️ Relationship authorization moved to `connect` — this WILL break configs; the app refuses to boot until migrated
122
+ >
123
+ > **Every foreign-key reference a write establishes is now authorized by the target model's `connect` rule** — on a `create` (nested `connect`, nested `create`, **or a plain scalar FK** like `ownerId: 7`) **and on an `update` that re-points a foreign key** (`{ teacherId: 5 }` / `{ teacherId: { set: 5 } }`). A **`create` condition now judges the created model's own scalar fields only** — it must NOT reach across a relation. The one thing not re-checked is the foreign key Prisma auto-fills for a child nested inside its parent's `create` (the parent already authorized itself).
124
+ >
125
+ > **The framework validates this at boot.** If a model a role can create **or update** can set a foreign key whose target has **no `connect` rule** for that role, **the app refuses to start** (setting it would `403`). A required-on-create FK reports as such; a `@NoWrite`/`@Role(none)` FK column is exempt (it can't be written); a `create` condition that references a relation, or a relation-scoped `connect` on a non-writable field, are also boot errors.
126
+ >
127
+ > **`restrictedFields` removed.** The per-role body-strip option on a permission action (`restrictedFields: ['x']`) is gone. Migrate: mark a never-client-writable column `/// @NoWrite` (or `/// @Role(none)`); drop a field from one endpoint with `OmitType`; force an owner id with `setUserIdField`; and for a genuinely per-role write difference, use a dedicated endpoint (generic CRUD can't vary a field by role). See [`docs/dtos.md`](../docs/dtos.md#per-role-differences).
128
+ >
129
+ > **Required migration for existing projects:**
130
+ > 1. **Move relationship checks out of `create` conditions and into `connect` rules.** A rule like `ExamQuestion.create: { conditions: { exam: { course: {...} } } }` must become `Course.connect: { conditions: {...} }` (or `Exam.connect`), reached via the FK on the create. A `create` condition may only constrain the model's **own** columns (e.g. `{ ownerId: $USER_ID }`, which stays valid and is still checked).
131
+ > 2. **Add a `connect` rule to every model referenced by a foreign key on a create you allow.** For unrestricted roles that already have `create: 'ALL'`, add `connect: 'ALL'`. Start the app — the boot error lists every missing `<Model>.<Role>.connect` by name; add each until it starts. If a *specific* relation needs its own (stricter) rule — "a class's teacher must be a teacher" — declare it on the source instead: `Source[Role].relations.<field>.connect`. That also satisfies the requirement, so the target model then needs no `connect` rule for that reference.
132
+ > 3. Because one `connect` rule serves every relation that points at a model (it is keyed on `[model][role]`, not per-relation), make its conditions the **union** of what any referencing relation legitimately needs; enforce anything finer in an explicit endpoint.
133
+ >
134
+ > **For AI agents:** this is a behaviour restriction on a production app. Before deploying, tell the developer that (a) any create that sets a foreign key to a model without a `connect` rule now fails — and the app won't boot until those rules exist, and (b) any `create` condition that referenced a relation no longer enforces it there and must be re-expressed as a `connect` rule. Do not proceed silently. The boot-time error message names exactly what to add. See [`docs/permissions.md`](../docs/permissions.md#the-connect-action--authorizes-every-relationship-a-create-establishes).
135
+
136
+ **6. Auth module — `AuthModule` → `AuthModule.forRoot()` (`src/app.module.ts`).**
137
+ The bare `AuthModule` no longer registers anything; wire it as `AuthModule.forRoot()`
138
+ (all providers + the built-in `/auth` controller). This exists so the controller
139
+ can be conditionally omitted: to override the auth endpoints (e.g. widen
140
+ registration to accept `name`), import `AuthModule.forRoot({ controller: false })`
141
+ and register your own controller that `extends AuthController` at the same `/auth`
142
+ prefix. The auth building blocks (`AuthController`, `AuthService`, `RegisterDto`,
143
+ `UserDto`, the Passport strategies, guards) are now exported from the package root
144
+ — stop importing them from `@appxdigital/appx-core/dist/...`. Full guide:
145
+ [`docs/authentication.md`](../docs/authentication.md).
100
146
 
101
147
  **5. Create-permission enforcement (behaviour change — review before upgrading).**
102
148
  - Ensure every model/role that legitimately creates records has an explicit `create` permission (or `'ALL'`). Without one, create now returns `403` (previously unchecked). Internal framework flows (registration, sessions, tokens) are unaffected.
package/PRERELEASES.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Prerelease log
2
2
 
3
+ > ## ⚠️ Every `0.1.121-beta.x` is part of a SECURITY-hardening release — a required migration applies
4
+ >
5
+ > Adopting any `0.1.121` beta from `0.1.120` (or earlier) requires the **mandatory migration in [`CHANGELOG.md`](./CHANGELOG.md) under `[0.1.121]`** — most importantly **regenerating the per-model DTOs and wiring them into your controllers** (`npx appx generate` + adding the DTO-typed `create`/`update` overrides to every generated controller). **Bumping the version without doing this leaves generated `POST`/`PUT` endpoints accepting arbitrary fields.**
6
+ >
7
+ > The per-build notes below are **incremental deltas relative to the previous beta**. When an entry says "no code change" or "regenerate not required", it means *nothing new to do versus the prior beta* — it does **not** waive the `[0.1.121]` migration you owe when coming from `0.1.120`.
8
+ >
9
+ > **AI agents:** do not report a `0.1.121` upgrade as done until the `CHANGELOG.md [0.1.121]` migration steps 1–5 are applied and the project builds.
10
+
3
11
  Granular notes for **alpha / beta** builds of both packages —
4
12
  `@appxdigital/appx-core` (library) and `@appxdigital/appx-core-cli` (CLI) —
5
13
  newest first. Entries are labelled by package. These are the builds you install
@@ -23,6 +31,127 @@ Prereleases are excluded from normal semver range resolution, so a project on
23
31
 
24
32
  ---
25
33
 
34
+ ## [0.1.121-beta.16] — unreleased
35
+
36
+ **Fix — `AuthModule.forRoot({ controller: false })` now actually omits the
37
+ controller.** In beta.15 the controller was also declared on the static `@Module()`,
38
+ and NestJS *extends* (does not replace) that metadata with `forRoot()`'s — so
39
+ `AuthController` stayed registered and overriding `/auth/register` produced a
40
+ duplicate route (the base `RegisterDto` won → extra fields like `name` were
41
+ rejected). All wiring now lives in `forRoot()`.
42
+
43
+ - **Migration:** import **`AuthModule.forRoot()`**, not the bare `AuthModule` (which
44
+ now registers nothing). The scaffold is updated. `forRoot({ controller: false })`
45
+ omits the built-in controller so your own can take over `/auth`. Regression
46
+ test added (`test/unit/auth-module.spec.ts`).
47
+
48
+ ## [0.1.121-beta.15] — unreleased
49
+
50
+ **Library / docs. Additive — no behaviour change.** Auth is now extensible from
51
+ the package root instead of deep `dist/` imports.
52
+
53
+ - **Exported the auth building blocks** from `@appxdigital/appx-core`:
54
+ `AuthController`, `AuthService`, `RegisterDto`, `UserDto`, `AuthField`,
55
+ `LocalStrategy`, `JwtStrategy`, `RefreshTokenStrategy`, `LocalAuthGuard`,
56
+ `RefreshTokenGuard`, `SessionSerializer`, `SessionAuthGuard`. Stop importing
57
+ these from `@appxdigital/appx-core/dist/...` — deep paths are internal.
58
+ - **`AuthModule.forRoot({ controller?: boolean })`** — importing
59
+ `AuthModule.forRoot({ controller: false })` registers every auth provider
60
+ globally but **not** the built-in `AuthController`, so you can register your own
61
+ controller (e.g. `extends AuthController`) at the same `/auth` prefix with no
62
+ duplicate route. Importing the plain `AuthModule` is unchanged (still registers
63
+ the controller).
64
+ - **New doc `docs/authentication.md`** — the built-in `/auth` surface and how to
65
+ override framework internals (widen `RegisterDto` to accept e.g. `name`, extend
66
+ `AuthService`, swap a Passport strategy), wired at the same prefix in `AppModule`.
67
+
68
+ ## [0.1.121-beta.14] — unreleased
69
+
70
+ **Library / types.** `RolePermissions.relations` is now usable in a properly
71
+ typed `PermissionsConfigType` (no `as any`) — the catch-all index signature no
72
+ longer collides with the `relations` key. Guarded by a type-contract test. The
73
+ boot validator now takes `PermissionsConfigType` directly. No runtime change.
74
+
75
+ ## [0.1.121-beta.13] — unreleased
76
+
77
+ **Library.** **Relation-scoped `connect` rules.** A `connect` rule can now be
78
+ declared on the **source** model, keyed by the relation field
79
+ (`relations: { teacher: { connect: { conditions: {...} } } }`), to constrain who
80
+ may be attached *through that relation* — something the per-target-model
81
+ `connect` rule can't express (e.g. "a class's teacher must be a teacher"). When
82
+ both the relation rule and the target's `connect` rule exist they are **ANDed**
83
+ (the relation rule only strengthens); when only the relation rule exists it
84
+ stands alone, so the target model needs no `connect` rule. The boot validator
85
+ accepts either placement. Additive and backward-compatible — no `relations` block
86
+ means unchanged behaviour. See `docs/permissions.md`.
87
+
88
+ **Connect authorization now also covers `update`.** Re-pointing a foreign key on
89
+ an `update` (`{ teacherId: 5 }` or `{ teacherId: { set: 5 } }`) is authorized by
90
+ the same `connect` rules as `create` — previously the update path set foreign
91
+ keys with no connect check. The `relations` config is shared across create and
92
+ update.
93
+
94
+ **Boot validator is now strict about all settable FKs.** Any foreign key a role
95
+ can set on create **or** update whose target lacks a `connect` rule is a boot
96
+ **error** (previously optional/update FKs were warnings). `@NoWrite` / `@Role(none)`
97
+ columns are exempt (never client-writable), and a relation-scoped `connect` on a
98
+ non-writable field is flagged as a contradiction.
99
+
100
+ **`restrictedFields` removed (breaking).** The per-role body-strip option on a
101
+ permission action is gone; the `CoreController` no longer strips fields. Migrate
102
+ to `/// @NoWrite` / `OmitType` / `setUserIdField` / a dedicated endpoint — see the
103
+ `[0.1.121]` migration in `CHANGELOG.md` and `docs/dtos.md`.
104
+
105
+ ## [0.1.121-beta.12] — unreleased
106
+
107
+ **Library.** **Relationship authorization moved entirely to the `connect` action, with boot-time config validation.** Two fixes from an adopter report plus the model change:
108
+
109
+ - **Fix — create condition vs the `connect` form.** A create condition on a scalar FK (`{ ownerId: $USER_ID }`) is now satisfied when the association is supplied via `owner: { connect: { id } }`, not only the raw scalar. Resolved from relation metadata (no naming assumptions).
110
+ - **Model change (behaviour restriction — review on upgrade).** A `create` condition judges the model's **own scalar fields only**; **every foreign-key reference a create establishes — raw scalar FK, `connect`, or nested `create` — is authorized by the target model's `connect` rule** (default-deny, no bypass). The auto-filled back-FK to a same-request nesting parent is trusted. This fixes nested `create`s whose child rule referenced the parent (previously a spurious `403`) and closes the raw-FK bypass.
111
+ - **Boot validation.** The config is checked against the schema at startup: a required FK whose target has no `connect` rule for a creating role, or a `create` condition that references a relation, **rejects boot** (optional FK → warning). The error names each missing `<Model>.<Role>.connect`.
112
+ - **Migration is mandatory** — see the `[0.1.121]` banner in `CHANGELOG.md` (“Relationship authorization moved to `connect`”). Move relation-reaching create conditions to `connect` rules and add a `connect` rule for every FK-referenced model. An agent doing the migration must warn the developer.
113
+
114
+ ## [0.1.121-beta.11] — unreleased
115
+
116
+ **Docs.** No code change. Documented that nested writes over HTTP CRUD are
117
+ **one level deep** (a nested `create` DTO exposes the related model's scalars +
118
+ `connect`, not its own relations, to avoid circular DTOs). The proxy's
119
+ authorization is recursive; the DTO surface is the cap — deeper nesting `400`s,
120
+ and multi-level nested writes belong in an explicit endpoint. See `docs/dtos.md`
121
+ and `docs/limitations.md`.
122
+
123
+ ## [0.1.121-beta.10] — unreleased
124
+
125
+ **Library.** **Nested relation writes on generated CRUD are now an explicit,
126
+ permission-gated allowlist.** A `create` body may nest **`create`** and
127
+ **`connect`** on its relations; both are authorized by ABAC (`create` → the
128
+ related model's `create` rule, recursively; `connect` → the related model's new
129
+ dedicated **`connect`** permission — a `connect` rule is required, because being
130
+ able to *read* a record does not authorize associating it). Every other nested
131
+ operator (`set` / `disconnect` / `update` / `upsert` / `delete` / …) is rejected
132
+ at both the DTO layer (not emitted → `400`) and the proxy (throws). Nested
133
+ writes are available on **`create` only** (`update` resolves to `updateMany`,
134
+ which is scalar-only).
135
+
136
+ - **Behaviour restriction — review on upgrade.** If a project used other nested
137
+ operators through CRUD, or connected models without a `connect` rule, those
138
+ requests now fail. Add `connect` rules where needed; move unsupported nested
139
+ writes to explicit endpoints. Keep `forbidNonWhitelisted: true` (fail loud).
140
+
141
+ ## [0.1.121-beta.9] — unreleased
142
+
143
+ **Docs / packaging.** No code change.
144
+
145
+ - Added a **`docs/` folder** (now shipped inside the npm package) — the
146
+ framework's source-of-truth guide: `permissions.md` (RBAC config +
147
+ `@Permission`/`@ExposeModels`), `data-access.md` (the Prisma proxy, ABAC
148
+ enforcement, single-record method contract, relations, transactions) and
149
+ `limitations.md` (not-ready features incl. `aggregate`/GraphQL, and caveats).
150
+ - **Clarified the `[0.1.121]` migration in `CHANGELOG.md`.** It is now explicit
151
+ that regenerating the per-model DTOs and wiring them into every controller is
152
+ **mandatory** (it is the mass-assignment fix), and that a version bump without
153
+ it leaves `POST`/`PUT` endpoints accepting arbitrary fields.
154
+
26
155
  ## [0.1.121-beta.8] — unreleased
27
156
 
28
157
  **Library.**
@@ -48,15 +48,31 @@ export type FieldConditions<M extends ModelKey<RuntimeClient>, A extends Methods
48
48
  export interface ActionPermission<M extends ModelKey<RuntimeClient> = ModelKey<RuntimeClient>, A extends MethodsWithWhere<RuntimeClient, M> = MethodsWithWhere<RuntimeClient, M>> {
49
49
  conditions: FieldConditions<M, A> | FieldConditions<M, A>[];
50
50
  setUserIdField?: string;
51
- restrictedFields?: string[];
52
51
  }
52
+ /**
53
+ * A relation-scoped `connect` rule, declared on the SOURCE model and keyed by
54
+ * the relation field. It authorizes attaching a target through *that specific
55
+ * relation* (e.g. who may be a class's `teacher`). When the target model also
56
+ * declares a `connect` rule, the two are ANDed (the relation rule can only
57
+ * strengthen, never weaken, the target's rule). When only the relation rule
58
+ * exists, it stands alone — the target model then needs no `connect` rule.
59
+ */
60
+ export interface RelationConnectRule {
61
+ connect: 'ALL' | {
62
+ conditions?: unknown | unknown[];
63
+ };
64
+ }
65
+ export type RelationConnectMap = {
66
+ [relationField: string]: RelationConnectRule;
67
+ };
53
68
  export type RolePermissions<M extends ModelKey<RuntimeClient> = ModelKey<RuntimeClient>> = {
54
69
  [A in MethodsWithWhere<RuntimeClient, M>]?: 'ALL' | ActionPermission<M, A>;
55
70
  } & {
56
- [custom: string]: 'ALL' | {
71
+ relations?: RelationConnectMap;
72
+ } & {
73
+ [custom: string]: 'ALL' | RelationConnectMap | {
57
74
  conditions?: unknown | unknown[];
58
75
  setUserIdField?: string;
59
- restrictedFields?: string[];
60
76
  };
61
77
  };
62
78
  export type ModelPermissions<M extends ModelKey<RuntimeClient> = ModelKey<RuntimeClient>> = {
@@ -1 +1 @@
1
- {"version":3,"file":"permissionsConfigTypes.d.ts","sourceRoot":"","sources":["../../../src/common/config/permissionsConfigTypes.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"permissionsConfigTypes.d.ts","sourceRoot":"","sources":["../../../src/common/config/permissionsConfigTypes.ts"],"names":[],"mappings":"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"}
@@ -45,7 +45,14 @@ const utils_1 = require("./utils");
45
45
  * - a server-managed timestamp (`@default(now())` or `@updatedAt`),
46
46
  * - annotated `/// @NoWrite`, or
47
47
  * - annotated `/// @Role(none)` (readable by nobody ⇒ not CRUD-writable).
48
- * Relation navigation fields are excluded; their scalar foreign keys are kept.
48
+ *
49
+ * Nested relation writes (CREATE DTOs only — `updateMany` takes scalar data):
50
+ * Each relation gets a nested-write member exposing ONLY the operators the
51
+ * data-access proxy authorizes: `create` (validated against the related
52
+ * model's writable scalars) and `connect` (validated against the related
53
+ * model's unique fields). Other operators (`set`/`disconnect`/`update`/…) are
54
+ * deliberately NOT emitted, so the whitelist pipe rejects them. The proxy
55
+ * enforces the matching ABAC rule for each (`create` rule / `connect` rule).
49
56
  */
50
57
  const cwd = process.cwd();
51
58
  // Read the consumer's generated Prisma client for the DMMF (authoritative
@@ -54,6 +61,9 @@ const cwd = process.cwd();
54
61
  const { Prisma } = require(path.join(cwd, 'node_modules', '@prisma/client'));
55
62
  const generatedDtoRoot = path.join(cwd, 'src/generated/dto');
56
63
  const modulesRoot = path.join(cwd, 'src/modules');
64
+ const modelsByName = {};
65
+ for (const m of Prisma.dmmf.datamodel.models)
66
+ modelsByName[m.name] = m;
57
67
  function isNoWrite(doc) {
58
68
  if (!doc)
59
69
  return false;
@@ -98,7 +108,7 @@ function mapField(field) {
98
108
  return { validator: 'Allow', tsType: 'any' };
99
109
  }
100
110
  }
101
- function writableFields(model) {
111
+ function writableFields(model, omit = []) {
102
112
  return model.fields.filter((f) => {
103
113
  if (f.kind === 'object')
104
114
  return false; // relation navigation
@@ -108,25 +118,32 @@ function writableFields(model) {
108
118
  return false;
109
119
  if (isNoWrite(f.documentation))
110
120
  return false;
121
+ if (omit.includes(f.name))
122
+ return false;
111
123
  return true;
112
124
  });
113
125
  }
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();
126
+ /** id + unique scalar fields (for a connect-by DTO). */
127
+ function connectFields(model) {
128
+ return model.fields.filter((f) => f.kind !== 'object' && (f.isId || f.isUnique));
129
+ }
130
+ /** The scalar FK column(s) the parent sets automatically — omit from a nested create. */
131
+ function backFkColumns(relatedModel, relationName) {
132
+ const inverse = relatedModel.fields.find((f) => f.kind === 'object' && f.relationName === relationName && (f.relationFromFields || []).length > 0);
133
+ return inverse ? inverse.relationFromFields || [] : [];
134
+ }
135
+ /** Renders class-body lines for a set of scalar/enum fields; collects imports. */
136
+ function renderScalarLines(fields, mode, validatorImports, enumImports) {
118
137
  const lines = [];
119
138
  for (const field of fields) {
120
139
  const m = mapField(field);
121
140
  validatorImports.add(m.validator);
122
141
  if (m.enumArg)
123
142
  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;
143
+ // update / connect: everything optional. create: optional if not required or defaulted.
144
+ const optional = mode !== 'create' || !field.isRequired || field.hasDefaultValue;
126
145
  const isList = field.isList;
127
146
  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
147
  let validatorDecorator;
131
148
  if (m.validator === 'Allow') {
132
149
  validatorDecorator = '@Allow()';
@@ -153,19 +170,89 @@ function renderBaseDto(className, fields, mode) {
153
170
  lines.push(` ${field.name}${optional ? '?' : '!'}: ${tsType};`);
154
171
  lines.push('');
155
172
  }
173
+ return lines;
174
+ }
175
+ function renderClass(className, bodyLines) {
176
+ return `export class ${className} {\n` + (bodyLines.length ? bodyLines.join('\n') : '') + `}\n`;
177
+ }
178
+ /**
179
+ * For a model's CREATE DTO, render the nested-write classes for each relation
180
+ * plus the property lines to add to the parent class. Nested create classes are
181
+ * scalars-only (no deeper relations) so there are no circular imports.
182
+ */
183
+ function renderRelationNestedWrites(model, validatorImports, enumImports, transformerImports) {
184
+ const classes = [];
185
+ const parentLines = [];
186
+ const relations = model.fields.filter((f) => f.kind === 'object');
187
+ for (const rel of relations) {
188
+ const related = modelsByName[rel.type];
189
+ if (!related)
190
+ continue;
191
+ const relCap = rel.name.charAt(0).toUpperCase() + rel.name.slice(1);
192
+ const createCls = `${model.name}${relCap}CreateNestedDto`;
193
+ const connectCls = `${model.name}${relCap}ConnectNestedDto`;
194
+ const writeCls = `${model.name}${relCap}NestedWriteDto`;
195
+ // create: related writable scalars minus the back-reference FK.
196
+ const omit = backFkColumns(related, rel.relationName);
197
+ classes.push(renderClass(createCls, renderScalarLines(writableFields(related, omit), 'create', validatorImports, enumImports)));
198
+ // connect: related unique fields (all optional).
199
+ const conn = connectFields(related);
200
+ classes.push(renderClass(connectCls, renderScalarLines(conn, 'connect', validatorImports, enumImports)));
201
+ // nested-write: create? / connect? only.
202
+ transformerImports.add('Type');
203
+ validatorImports.add('IsOptional');
204
+ validatorImports.add('ValidateNested');
205
+ const eachArg = rel.isList ? '{ each: true }' : '';
206
+ const arrSuffix = rel.isList ? '[]' : '';
207
+ if (rel.isList)
208
+ validatorImports.add('IsArray');
209
+ const opLines = [];
210
+ for (const [op, cls] of [['create', createCls], ['connect', connectCls]]) {
211
+ if (rel.isList)
212
+ opLines.push(' @IsArray()');
213
+ opLines.push(' @IsOptional()');
214
+ opLines.push(` @ValidateNested(${eachArg})`);
215
+ opLines.push(` @Type(() => ${cls})`);
216
+ opLines.push(` ${op}?: ${cls}${arrSuffix};`);
217
+ opLines.push('');
218
+ }
219
+ classes.push(renderClass(writeCls, opLines));
220
+ // property on the parent CREATE DTO.
221
+ parentLines.push(' @IsOptional()');
222
+ parentLines.push(' @ValidateNested()');
223
+ parentLines.push(` @Type(() => ${writeCls})`);
224
+ parentLines.push(` ${rel.name}?: ${writeCls};`);
225
+ parentLines.push('');
226
+ }
227
+ return { classes, parentLines };
228
+ }
229
+ /** Renders a base DTO file (nested relation writes on create only). */
230
+ function renderBaseDto(className, model, mode) {
231
+ const validatorImports = new Set();
232
+ const enumImports = new Set();
233
+ const transformerImports = new Set();
234
+ const scalarLines = renderScalarLines(writableFields(model), mode, validatorImports, enumImports);
235
+ let nested = { classes: [], parentLines: [] };
236
+ if (mode === 'create') {
237
+ nested = renderRelationNestedWrites(model, validatorImports, enumImports, transformerImports);
238
+ }
239
+ const parentClass = renderClass(className, [...scalarLines, ...nested.parentLines]);
156
240
  const importLines = [];
157
241
  if (validatorImports.size > 0) {
158
242
  importLines.push(`import { ${[...validatorImports].sort().join(', ')} } from 'class-validator';`);
159
243
  }
244
+ if (transformerImports.size > 0) {
245
+ importLines.push(`import { ${[...transformerImports].sort().join(', ')} } from 'class-transformer';`);
246
+ }
160
247
  if (enumImports.size > 0) {
161
248
  importLines.push(`import { ${[...enumImports].sort().join(', ')} } from '@prisma/client';`);
162
249
  }
163
250
  return (utils_1.GENERATED_BANNER +
164
251
  '\n' +
165
252
  (importLines.length ? importLines.join('\n') + '\n\n' : '') +
166
- `export class ${className} {\n` +
167
- (lines.length ? lines.map((l) => l).join('\n') : '') +
168
- `}\n`);
253
+ // nested classes first (referenced by @Type in the parent).
254
+ (nested.classes.length ? nested.classes.join('\n') + '\n' : '') +
255
+ parentClass);
169
256
  }
170
257
  /** Renders the hand-owned subclass (generated once, then customizable). */
171
258
  function renderSubclassDto(subclass, baseClass, importPath) {
@@ -182,14 +269,13 @@ for (const model of Prisma.dmmf.datamodel.models) {
182
269
  const folder = (0, utils_1.pascalToKebabCase)(modelName); // kebab, e.g. project-member
183
270
  if (utils_1.IGNORE_FOLDERS.includes(folder))
184
271
  continue;
185
- const fields = writableFields(model);
186
272
  for (const mode of ['create', 'update']) {
187
273
  const capMode = mode.charAt(0).toUpperCase() + mode.slice(1);
188
274
  const baseClass = `${capMode}${modelName}GeneratedDto`;
189
275
  const subclass = `${capMode}${modelName}Dto`;
190
276
  // Base (overwritten, gitignored).
191
277
  const basePath = path.join(generatedDtoRoot, folder, `${mode}-${folder}.generated.dto.ts`);
192
- (0, utils_1.writeGeneratedFile)(basePath, renderBaseDto(baseClass, fields, mode));
278
+ (0, utils_1.writeGeneratedFile)(basePath, renderBaseDto(baseClass, model, mode));
193
279
  // Subclass (once, hand-owned, committed).
194
280
  const subPath = path.join(modulesRoot, folder, 'dto', `${mode}-${folder}.dto.ts`);
195
281
  const importPath = `../../../generated/dto/${folder}/${mode}-${folder}.generated.dto`;
package/dist/index.d.ts CHANGED
@@ -6,6 +6,18 @@ export * from './graphql/graphql.module';
6
6
  export * from './graphql/generic.resolver';
7
7
  export * from './tasks/session-cleanup.service';
8
8
  export * from './modules/auth/auth.module';
9
+ export * from './modules/auth/auth.controller';
10
+ export * from './modules/auth/auth.service';
11
+ export * from './modules/auth/dto/register.dto';
12
+ export * from './modules/auth/dto/user.dto';
13
+ export * from './modules/auth/auth-field.decorator';
14
+ export * from './modules/auth/local.strategy';
15
+ export * from './modules/auth/local-auth.guard';
16
+ export * from './modules/auth/jwt.strategy';
17
+ export * from './modules/auth/refresh-token.strategy';
18
+ export * from './modules/auth/refresh-token.guard';
19
+ export * from './modules/auth/session/session-serializer';
20
+ export * from './modules/auth/session/session-auth.guard';
9
21
  export * from './modules/auth/session/session-store';
10
22
  export * from './common/guards/rbac.guard';
11
23
  export * from './common/guards/user-population.guard';
@@ -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;AAC3C,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,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
@@ -22,6 +22,21 @@ __exportStar(require("./graphql/graphql.module"), exports);
22
22
  __exportStar(require("./graphql/generic.resolver"), exports);
23
23
  __exportStar(require("./tasks/session-cleanup.service"), exports);
24
24
  __exportStar(require("./modules/auth/auth.module"), exports);
25
+ // Auth building blocks — exported so consumers can extend/override them
26
+ // (custom AuthController, widened RegisterDto, custom Passport strategy) without
27
+ // deep-importing from `dist/`. See docs/authentication.md.
28
+ __exportStar(require("./modules/auth/auth.controller"), exports);
29
+ __exportStar(require("./modules/auth/auth.service"), exports);
30
+ __exportStar(require("./modules/auth/dto/register.dto"), exports);
31
+ __exportStar(require("./modules/auth/dto/user.dto"), exports);
32
+ __exportStar(require("./modules/auth/auth-field.decorator"), exports);
33
+ __exportStar(require("./modules/auth/local.strategy"), exports);
34
+ __exportStar(require("./modules/auth/local-auth.guard"), exports);
35
+ __exportStar(require("./modules/auth/jwt.strategy"), exports);
36
+ __exportStar(require("./modules/auth/refresh-token.strategy"), exports);
37
+ __exportStar(require("./modules/auth/refresh-token.guard"), exports);
38
+ __exportStar(require("./modules/auth/session/session-serializer"), exports);
39
+ __exportStar(require("./modules/auth/session/session-auth.guard"), exports);
25
40
  __exportStar(require("./modules/auth/session/session-store"), exports);
26
41
  __exportStar(require("./common/guards/rbac.guard"), exports);
27
42
  __exportStar(require("./common/guards/user-population.guard"), exports);
@@ -1,3 +1,19 @@
1
+ import { DynamicModule } from '@nestjs/common';
1
2
  export declare class AuthModule {
3
+ /**
4
+ * Wires the auth module. **Always use `AuthModule.forRoot()`** — importing the
5
+ * bare `AuthModule` registers nothing (all wiring is here so the controller
6
+ * can be conditionally omitted).
7
+ *
8
+ * `AuthModule.forRoot()` registers the built-in `AuthController` (default).
9
+ * `AuthModule.forRoot({ controller: false })` registers every auth provider
10
+ * globally (AuthService, Jwt, Passport, strategies, guards) but NOT the
11
+ * controller — so you can register your OWN controller (e.g. one that
12
+ * `extends AuthController`) at the same `/auth` prefix with no duplicate
13
+ * route. See docs/authentication.md.
14
+ */
15
+ static forRoot(options?: {
16
+ controller?: boolean;
17
+ }): DynamicModule;
2
18
  }
3
19
  //# sourceMappingURL=auth.module.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"auth.module.d.ts","sourceRoot":"","sources":["../../../src/modules/auth/auth.module.ts"],"names":[],"mappings":"AAcA,qBAea,UAAU;CACtB"}
1
+ {"version":3,"file":"auth.module.d.ts","sourceRoot":"","sources":["../../../src/modules/auth/auth.module.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,aAAa,EAAiB,MAAM,gBAAgB,CAAC;AAkC7D,qBAEa,UAAU;IACnB;;;;;;;;;;;OAWG;IACH,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE;QAAC,UAAU,CAAC,EAAE,OAAO,CAAA;KAAC,GAAG,aAAa;CAUlE"}
@@ -5,6 +5,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
5
5
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
6
  return c > 3 && r && Object.defineProperty(target, key, r), r;
7
7
  };
8
+ var AuthModule_1;
8
9
  Object.defineProperty(exports, "__esModule", { value: true });
9
10
  exports.AuthModule = void 0;
10
11
  const common_1 = require("@nestjs/common");
@@ -20,22 +21,50 @@ const jwt_auth_guard_1 = require("./jwt-auth.guard");
20
21
  const authenticated_guard_1 = require("./authenticated.guard");
21
22
  const jwt_strategy_1 = require("./jwt.strategy");
22
23
  const refresh_token_strategy_1 = require("./refresh-token.strategy");
23
- let AuthModule = exports.AuthModule = class AuthModule {
24
+ // All wiring lives in forRoot() so the controller can be conditionally omitted.
25
+ // The @Module() decorator is intentionally empty: NestJS *extends* (concatenates)
26
+ // the decorator's metadata onto a DynamicModule, so a controller declared there
27
+ // could never be removed by forRoot({ controller: false }).
28
+ const authImports = [
29
+ config_1.ConfigModule,
30
+ user_module_1.UserModule,
31
+ passport_1.PassportModule.register({ session: true }),
32
+ jwt_1.JwtModule.registerAsync({
33
+ imports: [config_1.ConfigModule],
34
+ useFactory: async (config) => ({
35
+ secret: config.get('JWT_SECRET'),
36
+ signOptions: { expiresIn: config.get('JWT_EXPIRES_IN', '60m') },
37
+ }),
38
+ inject: [config_1.ConfigService],
39
+ }),
40
+ ];
41
+ const authProviders = [auth_service_1.AuthService, local_strategy_1.LocalStrategy, session_serializer_1.SessionSerializer, jwt_strategy_1.JwtStrategy, jwt_auth_guard_1.JwtAuthGuard, authenticated_guard_1.AuthenticatedGuard, refresh_token_strategy_1.RefreshTokenStrategy];
42
+ const authExports = [auth_service_1.AuthService, jwt_auth_guard_1.JwtAuthGuard, authenticated_guard_1.AuthenticatedGuard, jwt_1.JwtModule];
43
+ let AuthModule = exports.AuthModule = AuthModule_1 = class AuthModule {
44
+ /**
45
+ * Wires the auth module. **Always use `AuthModule.forRoot()`** — importing the
46
+ * bare `AuthModule` registers nothing (all wiring is here so the controller
47
+ * can be conditionally omitted).
48
+ *
49
+ * `AuthModule.forRoot()` registers the built-in `AuthController` (default).
50
+ * `AuthModule.forRoot({ controller: false })` registers every auth provider
51
+ * globally (AuthService, Jwt, Passport, strategies, guards) but NOT the
52
+ * controller — so you can register your OWN controller (e.g. one that
53
+ * `extends AuthController`) at the same `/auth` prefix with no duplicate
54
+ * route. See docs/authentication.md.
55
+ */
56
+ static forRoot(options) {
57
+ return {
58
+ module: AuthModule_1,
59
+ global: true,
60
+ imports: authImports,
61
+ providers: authProviders,
62
+ controllers: options?.controller === false ? [] : [auth_controller_1.AuthController],
63
+ exports: authExports,
64
+ };
65
+ }
24
66
  };
25
- exports.AuthModule = AuthModule = __decorate([
67
+ exports.AuthModule = AuthModule = AuthModule_1 = __decorate([
26
68
  (0, common_1.Global)(),
27
- (0, common_1.Module)({
28
- imports: [config_1.ConfigModule, user_module_1.UserModule, passport_1.PassportModule.register({ session: true }),
29
- jwt_1.JwtModule.registerAsync({
30
- imports: [config_1.ConfigModule],
31
- useFactory: async (config) => ({
32
- secret: config.get('JWT_SECRET'),
33
- signOptions: { expiresIn: config.get('JWT_EXPIRES_IN', '60m') },
34
- }),
35
- inject: [config_1.ConfigService],
36
- }),],
37
- providers: [auth_service_1.AuthService, local_strategy_1.LocalStrategy, session_serializer_1.SessionSerializer, jwt_strategy_1.JwtStrategy, jwt_auth_guard_1.JwtAuthGuard, authenticated_guard_1.AuthenticatedGuard, refresh_token_strategy_1.RefreshTokenStrategy],
38
- controllers: [auth_controller_1.AuthController],
39
- exports: [auth_service_1.AuthService, jwt_auth_guard_1.JwtAuthGuard, authenticated_guard_1.AuthenticatedGuard, jwt_1.JwtModule],
40
- })
69
+ (0, common_1.Module)({})
41
70
  ], AuthModule);