@lenne.tech/nest-server 11.34.1 → 11.35.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/rules/architecture.md +3 -1
- package/.claude/rules/configurable-features.md +3 -3
- package/.claude/rules/role-system.md +281 -0
- package/CLAUDE.md +11 -1
- package/FRAMEWORK-API.md +7 -3
- package/dist/core/common/decorators/nested-type.registry.d.ts +2 -0
- package/dist/core/common/decorators/nested-type.registry.js +17 -0
- package/dist/core/common/decorators/nested-type.registry.js.map +1 -0
- package/dist/core/common/decorators/restricted.decorator.d.ts +2 -1
- package/dist/core/common/decorators/restricted.decorator.js +52 -16
- package/dist/core/common/decorators/restricted.decorator.js.map +1 -1
- package/dist/core/common/decorators/restrictions-checked.marker.d.ts +4 -0
- package/dist/core/common/decorators/restrictions-checked.marker.js +30 -0
- package/dist/core/common/decorators/restrictions-checked.marker.js.map +1 -0
- package/dist/core/common/decorators/unified-field.decorator.d.ts +1 -1
- package/dist/core/common/decorators/unified-field.decorator.js +6 -3
- package/dist/core/common/decorators/unified-field.decorator.js.map +1 -1
- package/dist/core/common/enums/role.enum.d.ts +8 -0
- package/dist/core/common/enums/role.enum.js +28 -1
- package/dist/core/common/enums/role.enum.js.map +1 -1
- package/dist/core/common/helpers/execution-context-request.helper.d.ts +7 -0
- package/dist/core/common/helpers/execution-context-request.helper.js +31 -0
- package/dist/core/common/helpers/execution-context-request.helper.js.map +1 -0
- package/dist/core/common/helpers/file.helper.js +3 -2
- package/dist/core/common/helpers/file.helper.js.map +1 -1
- package/dist/core/common/helpers/graphql-ws-context.helper.d.ts +15 -0
- package/dist/core/common/helpers/graphql-ws-context.helper.js +96 -0
- package/dist/core/common/helpers/graphql-ws-context.helper.js.map +1 -0
- package/dist/core/common/helpers/gridfs.helper.js +1 -1
- package/dist/core/common/helpers/gridfs.helper.js.map +1 -1
- package/dist/core/common/interceptors/check-security.interceptor.js +2 -1
- package/dist/core/common/interceptors/check-security.interceptor.js.map +1 -1
- package/dist/core/common/interceptors/response-model.interceptor.js +3 -2
- package/dist/core/common/interceptors/response-model.interceptor.js.map +1 -1
- package/dist/core/common/interfaces/server-options.interface.d.ts +5 -0
- package/dist/core/common/plugins/mongoose-system-role.plugin.d.ts +1 -0
- package/dist/core/common/plugins/mongoose-system-role.plugin.js +106 -0
- package/dist/core/common/plugins/mongoose-system-role.plugin.js.map +1 -0
- package/dist/core/common/plugins/mongoose-tenant.plugin.js +134 -9
- package/dist/core/common/plugins/mongoose-tenant.plugin.js.map +1 -1
- package/dist/core/common/services/core-s3.service.js +4 -1
- package/dist/core/common/services/core-s3.service.js.map +1 -1
- package/dist/core/common/services/core-tenant-context.registry.d.ts +10 -0
- package/dist/core/common/services/core-tenant-context.registry.js +12 -0
- package/dist/core/common/services/core-tenant-context.registry.js.map +1 -0
- package/dist/core/modules/auth/guards/roles.guard.d.ts +1 -1
- package/dist/core/modules/auth/guards/roles.guard.js +2 -2
- package/dist/core/modules/auth/guards/roles.guard.js.map +1 -1
- package/dist/core/modules/better-auth/better-auth-roles.guard.js +2 -10
- package/dist/core/modules/better-auth/better-auth-roles.guard.js.map +1 -1
- package/dist/core/modules/file/core-file-access-audit.initializer.d.ts +13 -0
- package/dist/core/modules/file/core-file-access-audit.initializer.js +109 -0
- package/dist/core/modules/file/core-file-access-audit.initializer.js.map +1 -0
- package/dist/core/modules/file/core-file.service.d.ts +3 -1
- package/dist/core/modules/file/core-file.service.js +62 -7
- package/dist/core/modules/file/core-file.service.js.map +1 -1
- package/dist/core/modules/file/file-access.helper.d.ts +18 -0
- package/dist/core/modules/file/file-access.helper.js +46 -0
- package/dist/core/modules/file/file-access.helper.js.map +1 -0
- package/dist/core/modules/file/file-metadata.helper.js +1 -1
- package/dist/core/modules/file/file-metadata.helper.js.map +1 -1
- package/dist/core/modules/file/file-roles.config.d.ts +31 -0
- package/dist/core/modules/file/file-roles.config.js +127 -0
- package/dist/core/modules/file/file-roles.config.js.map +1 -0
- package/dist/core/modules/file/file-roles.helper.d.ts +2 -2
- package/dist/core/modules/file/file-roles.helper.js +16 -34
- package/dist/core/modules/file/file-roles.helper.js.map +1 -1
- package/dist/core/modules/tenant/core-role-scope.registry.d.ts +28 -0
- package/dist/core/modules/tenant/core-role-scope.registry.js +93 -0
- package/dist/core/modules/tenant/core-role-scope.registry.js.map +1 -0
- package/dist/core/modules/tenant/core-tenant.guard.d.ts +5 -0
- package/dist/core/modules/tenant/core-tenant.guard.js +37 -11
- package/dist/core/modules/tenant/core-tenant.guard.js.map +1 -1
- package/dist/core/modules/tenant/core-tenant.helpers.d.ts +9 -1
- package/dist/core/modules/tenant/core-tenant.helpers.js +65 -4
- package/dist/core/modules/tenant/core-tenant.helpers.js.map +1 -1
- package/dist/core/modules/tenant/core-tenant.module.js +5 -0
- package/dist/core/modules/tenant/core-tenant.module.js.map +1 -1
- package/dist/core/modules/tenant/core-tenant.service.d.ts +1 -0
- package/dist/core/modules/tenant/core-tenant.service.js +25 -0
- package/dist/core/modules/tenant/core-tenant.service.js.map +1 -1
- package/dist/core/modules/tus/core-tus.service.d.ts +2 -0
- package/dist/core/modules/tus/core-tus.service.js +34 -4
- package/dist/core/modules/tus/core-tus.service.js.map +1 -1
- package/dist/core/modules/tus/index.d.ts +1 -0
- package/dist/core/modules/tus/index.js +1 -0
- package/dist/core/modules/tus/index.js.map +1 -1
- package/dist/core/modules/tus/tus.constants.d.ts +1 -0
- package/dist/core/modules/tus/tus.constants.js +2 -1
- package/dist/core/modules/tus/tus.constants.js.map +1 -1
- package/dist/core/modules/user/core-user.service.js +5 -0
- package/dist/core/modules/user/core-user.service.js.map +1 -1
- package/dist/core/modules/user/inputs/core-user.input.js +8 -0
- package/dist/core/modules/user/inputs/core-user.input.js.map +1 -1
- package/dist/core.module.js +13 -1
- package/dist/core.module.js.map +1 -1
- package/dist/index.d.ts +7 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/server/modules/file/file.service.js +4 -1
- package/dist/server/modules/file/file.service.js.map +1 -1
- package/dist/test/test.helper.d.ts +1 -0
- package/dist/test/test.helper.js +26 -6
- package/dist/test/test.helper.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/docs/REQUEST-LIFECYCLE.md +60 -1
- package/migration-guides/11.34.x-to-11.35.x.md +846 -0
- package/migration-guides/11.35.0-to-11.35.1.md +152 -0
- package/package.json +1 -1
- package/src/core/common/decorators/nested-type.registry.ts +42 -0
- package/src/core/common/decorators/restricted.decorator.ts +149 -21
- package/src/core/common/decorators/restrictions-checked.marker.ts +87 -0
- package/src/core/common/decorators/unified-field.decorator.ts +4 -3
- package/src/core/common/enums/role.enum.ts +116 -0
- package/src/core/common/helpers/execution-context-request.helper.ts +102 -0
- package/src/core/common/helpers/file.helper.ts +11 -5
- package/src/core/common/helpers/graphql-ws-context.helper.ts +192 -0
- package/src/core/common/helpers/gridfs.helper.ts +18 -2
- package/src/core/common/interceptors/check-security.interceptor.ts +4 -1
- package/src/core/common/interceptors/response-model.interceptor.ts +3 -2
- package/src/core/common/interfaces/server-options.interface.ts +139 -7
- package/src/core/common/plugins/mongoose-system-role.plugin.ts +193 -0
- package/src/core/common/plugins/mongoose-tenant.plugin.ts +244 -12
- package/src/core/common/services/core-s3.service.ts +9 -1
- package/src/core/common/services/core-tenant-context.registry.ts +50 -0
- package/src/core/modules/auth/guards/roles.guard.ts +7 -5
- package/src/core/modules/better-auth/better-auth-roles.guard.ts +7 -13
- package/src/core/modules/file/INTEGRATION-CHECKLIST.md +95 -1
- package/src/core/modules/file/README.md +71 -0
- package/src/core/modules/file/core-file-access-audit.initializer.ts +164 -0
- package/src/core/modules/file/core-file.service.ts +166 -9
- package/src/core/modules/file/file-access.helper.ts +149 -0
- package/src/core/modules/file/file-metadata.helper.ts +13 -5
- package/src/core/modules/file/file-roles.config.ts +357 -0
- package/src/core/modules/file/file-roles.helper.ts +24 -60
- package/src/core/modules/tenant/README.md +20 -1
- package/src/core/modules/tenant/core-role-scope.registry.ts +195 -0
- package/src/core/modules/tenant/core-tenant.guard.ts +92 -12
- package/src/core/modules/tenant/core-tenant.helpers.ts +139 -7
- package/src/core/modules/tenant/core-tenant.module.ts +12 -0
- package/src/core/modules/tenant/core-tenant.service.ts +56 -0
- package/src/core/modules/tus/README.md +46 -7
- package/src/core/modules/tus/core-tus.service.ts +94 -5
- package/src/core/modules/tus/index.ts +1 -0
- package/src/core/modules/tus/tus.constants.ts +17 -1
- package/src/core/modules/user/core-user.service.ts +12 -0
- package/src/core/modules/user/inputs/core-user.input.ts +28 -2
- package/src/core.module.ts +72 -3
- package/src/index.ts +7 -0
- package/src/server/modules/file/file.service.ts +45 -2
- package/src/test/test.helper.ts +54 -11
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# Migration Guide: 11.35.0 → 11.35.1
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
| Category | Details |
|
|
6
|
+
|----------|---------|
|
|
7
|
+
| **Breaking Changes** | None in signatures. One **client-visible** change: a refused upload now answers **400** instead of **500**, with Nest's standard error envelope instead of an internal-error one (§1) |
|
|
8
|
+
| **Bugfixes** | `multerFileFilter` rejections finally map to a 4xx (§1). Seven documented helper names are reachable from the package root for the first time (§2). The file-access boot warning can no longer be fooled by a subclassed controller (§3) |
|
|
9
|
+
| **Migration Effort** | Nothing to configure. Read §1 if a frontend, a test, or an alerting rule branches on the status or body of a rejected upload. **Read §3 if you subclass `CoreFileController` / `CoreFileResolver` and re-declare a member** — you may see a new warning at startup, and it is probably telling you something true |
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Quick Migration
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pnpm update @lenne.tech/nest-server@11.35.1
|
|
17
|
+
pnpm run build
|
|
18
|
+
pnpm test
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
No configuration change is required, and no source change is required in a typical project.
|
|
22
|
+
|
|
23
|
+
**Vendor-mode projects:** §1 and §2 are single-file and independent —
|
|
24
|
+
`src/core/common/helpers/file.helper.ts` and `src/index.ts`. §3 is **not**: it adds
|
|
25
|
+
`src/core/modules/file/core-file-access-audit.initializer.ts` and changes
|
|
26
|
+
`file-roles.config.ts`, `file-roles.helper.ts` and `core.module.ts` together. Taking the initializer
|
|
27
|
+
without the `core.module.ts` provider registration gives you a file that is never constructed — a
|
|
28
|
+
security check that silently does not run, which is worse than not having it. Sync those four
|
|
29
|
+
together.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## 1. A refused upload answers 400, not 500
|
|
34
|
+
|
|
35
|
+
11.32.4 §8 changed `multerFileFilter`'s rejection from a bare string to `new Error(...)`, intending a
|
|
36
|
+
clean 400. It never produced one.
|
|
37
|
+
|
|
38
|
+
`transformException` in `@nestjs/platform-express` returns any **non-`HttpException`** unchanged. Its
|
|
39
|
+
switch matches only multer's own message constants (`LIMIT_FILE_SIZE`, `LIMIT_UNEXPECTED_FILE`, …),
|
|
40
|
+
and a message this helper writes matches none of them — so the rejection fell off the end of the
|
|
41
|
+
switch, reached Nest's exception layer as a plain error, and the client was told the **server** had
|
|
42
|
+
failed for a file the **client** sent wrong.
|
|
43
|
+
|
|
44
|
+
Both rejection paths are affected and both are fixed: the allow-list refusal, and the
|
|
45
|
+
scriptable-type refusal (`text/html`, `image/svg+xml`) added by 11.32.4 §6.
|
|
46
|
+
|
|
47
|
+
| | Before | After |
|
|
48
|
+
|---|---|---|
|
|
49
|
+
| Status | `500 Internal Server Error` | `400 Bad Request` |
|
|
50
|
+
| Body | internal-error envelope | Nest's standard 400 shape |
|
|
51
|
+
| Message text | unchanged | unchanged — same text, still built from the accept list |
|
|
52
|
+
|
|
53
|
+
**What to check:**
|
|
54
|
+
|
|
55
|
+
- **Frontends** that branch on the status of an upload response. A rejected file was previously
|
|
56
|
+
indistinguishable from a storage outage; it now identifies itself, and the message is worth showing
|
|
57
|
+
to the user.
|
|
58
|
+
- **Tests** pinned to the broken output. A test asserting `500` on a refused upload fails and should
|
|
59
|
+
be updated to `400`.
|
|
60
|
+
- **Alerting.** Refused uploads no longer raise 5xx. If a dashboard counted them, its numbers drop —
|
|
61
|
+
that is the fix, not a regression. Conversely, an alert that was tuned to tolerate that noise can
|
|
62
|
+
be tightened again.
|
|
63
|
+
|
|
64
|
+
A consumer catching the rejection in code is unaffected: `BadRequestException extends HttpException
|
|
65
|
+
extends Error`, so an `instanceof Error` check keeps working.
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## 2. Seven documented helpers are now exported from the package root
|
|
70
|
+
|
|
71
|
+
The 11.34.x → 11.35.x guide's *New Exports* block lists names that `src/index.ts` never exported, so
|
|
72
|
+
importing them from `@lenne.tech/nest-server` failed to compile:
|
|
73
|
+
|
|
74
|
+
| Name | Module |
|
|
75
|
+
|---|---|
|
|
76
|
+
| `resolveGuardRequest` | `common/helpers/execution-context-request.helper` |
|
|
77
|
+
| `buildRequestContextAwareExecute` | `common/helpers/graphql-ws-context.helper` |
|
|
78
|
+
| `buildRequestContextAwareSubscribe` | same |
|
|
79
|
+
| `createRequestContextAwareExecute` | same |
|
|
80
|
+
| `createRequestContextAwareSubscribe` | same |
|
|
81
|
+
| `getTenantContextResolver` | `common/services/core-tenant-context.registry` |
|
|
82
|
+
| `setTenantContextResolver` | same |
|
|
83
|
+
|
|
84
|
+
Purely additive — three `export * from` lines. Nothing was renamed or removed, and no existing import
|
|
85
|
+
changes meaning.
|
|
86
|
+
|
|
87
|
+
**Who this affected:** npm-mode consumers only, and only those who followed the guide. In-tree and
|
|
88
|
+
vendor-mode consumers import over relative paths and never touched the barrel, which is also why
|
|
89
|
+
nothing in this repo noticed: no framework module, test or e2e spec imports from `src/index.ts`, so a
|
|
90
|
+
helper could be written, wired, documented and shipped with the full suite green.
|
|
91
|
+
`tests/unit/public-api-barrel.spec.ts` now reads the expected names out of the migration guides
|
|
92
|
+
themselves and walks the re-export graph, so a documented-but-unexported name fails at test time
|
|
93
|
+
rather than at a consumer's `import`.
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## 3. The file-access warning now sees a subclassed controller
|
|
98
|
+
|
|
99
|
+
The boot warning that reports "the file gate is open and nothing declares a per-file policy" read
|
|
100
|
+
**configuration** — `file.downloadRoles` and friends. That is the correct source for a member you
|
|
101
|
+
inherit, and the wrong one for a member you re-declare.
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
@Controller('files')
|
|
105
|
+
export class FileController extends CoreFileController {
|
|
106
|
+
@Get('id/:id')
|
|
107
|
+
@Roles(RoleEnum.S_EVERYONE) // anonymous downloads
|
|
108
|
+
override async getFileById(...) { return super.getFileById(...); }
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
`applyFileRoles()` writes the configured roles onto `CoreFileController.prototype.getFileById`, but
|
|
113
|
+
decorator metadata lives on the **function object** — your override is a different function, it keeps
|
|
114
|
+
its own `@Roles()`, and that is the function Nest registers. The configuration never reaches the
|
|
115
|
+
route. The old warning asked `resolveRoles('downloadRoles', config)`, got `[ADMIN]`, and stayed quiet
|
|
116
|
+
while the route served everyone. Two consumer projects shipped exactly that.
|
|
117
|
+
|
|
118
|
+
`CoreFileAccessAuditInitializer` now reads the roles off the class you actually registered, at
|
|
119
|
+
`onApplicationBootstrap` (the earliest point at which the route table exists), and names the member:
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
A registered file endpoint is open beyond platform admins through roles declared in your own class,
|
|
123
|
+
not through configuration (FileController.getFileById → ["s_everyone"]), and no per-file policy is
|
|
124
|
+
declared: file.access is unset and CoreFileService.checkRights() is not overridden. …
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
**Nothing changes about who may reach what.** Your `@Roles()` still wins; the audit reports and does
|
|
128
|
+
not correct. That is deliberate: rewriting an override's metadata would silently relax a route a
|
|
129
|
+
project pinned on purpose — which is why the TUS module's approach, where configuration wins over the
|
|
130
|
+
override, was not adopted here.
|
|
131
|
+
|
|
132
|
+
**What to do if the warning appears:**
|
|
133
|
+
|
|
134
|
+
| If the open access is | Then |
|
|
135
|
+
|---|---|
|
|
136
|
+
| intended, with a per-file rule | declare it: `file.access: 'owner' \| 'tenant' \| …`, or override `CoreFileService.checkRights()`. Either silences the warning, for both halves of the check |
|
|
137
|
+
| intended, genuinely public files | nothing is broken — but the warning is the reminder that ids are enumerable and the routes are not rate-limited |
|
|
138
|
+
| **not** intended | inherit the member instead of re-declaring it, so `file.downloadRoles` governs it again |
|
|
139
|
+
|
|
140
|
+
**No new warning if you do not subclass**, and none if you subclass without re-declaring a governed
|
|
141
|
+
member — the audit only reports roles the configuration cannot account for, so it never duplicates
|
|
142
|
+
the existing configuration-side warning.
|
|
143
|
+
|
|
144
|
+
Governed members: `getFileById`, `getFile` (controller); `getFileInfo`, `uploadFile`, `uploadFiles`,
|
|
145
|
+
`deleteFile` (resolver).
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## Module Documentation
|
|
150
|
+
|
|
151
|
+
- File module, upload filters and access rules: [`src/core/modules/file/README.md`](../src/core/modules/file/README.md)
|
|
152
|
+
- Request lifecycle and exception handling: [`docs/REQUEST-LIFECYCLE.md`](../docs/REQUEST-LIFECYCLE.md)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lenne.tech/nest-server",
|
|
3
|
-
"version": "11.
|
|
3
|
+
"version": "11.35.1",
|
|
4
4
|
"description": "Modern, fast, powerful Node.js web framework in TypeScript based on Nest with a GraphQL API and a connection to MongoDB (or other databases).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"node",
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which CLASS a model property declares — the only record of that fact at runtime.
|
|
3
|
+
*
|
|
4
|
+
* DELIBERATELY IMPORT-FREE. Two files need this map and they sit on opposite ends of an import
|
|
5
|
+
* edge: `unified-field.decorator` writes it (and already imports `restricted.decorator` for
|
|
6
|
+
* `@Restricted`), while `restricted.decorator` reads it. Keeping the map here means the reader adds
|
|
7
|
+
* no import back into the writer — `restricted.decorator` drives field-level access control and its
|
|
8
|
+
* position on zero import cycles is a defended property, not an accident.
|
|
9
|
+
* See `.claude/rules/architecture.md` → "DI Token Placement (SWC-Safe)".
|
|
10
|
+
*
|
|
11
|
+
* WHY IT EXISTS AT ALL: TypeScript types are erased, and `design:type` degrades to `Array` for a
|
|
12
|
+
* typed array and is only emitted for decorated properties. So a nested value read out of MongoDB —
|
|
13
|
+
* an embedded subdocument, or an array of them — arrives as a PLAIN object whose constructor is
|
|
14
|
+
* `Object`. Nothing on the value itself can say what it was declared as, which means nothing can
|
|
15
|
+
* find the `@Restricted` / validation metadata that belongs to it. This map is what closes that gap.
|
|
16
|
+
*
|
|
17
|
+
* Filled by `@UnifiedField()` for every non-primitive property. A property declared without
|
|
18
|
+
* `@UnifiedField` is absent, and consumers must treat "absent" as "unknown", never as "no
|
|
19
|
+
* restrictions" — see `checkRestricted()` for how that is handled.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/** Key: `${className}.${propertyName}` — value: the declared (element) type. */
|
|
23
|
+
export const nestedTypeRegistry = new Map<string, any>();
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The class a property of `ownerClass` declares, or `undefined` when nothing recorded it.
|
|
27
|
+
*
|
|
28
|
+
* Walks the PROTOTYPE CHAIN of the class, because `@UnifiedField()` registers under the class it was
|
|
29
|
+
* applied to — a property inherited from a base model is recorded under the BASE class name, and a
|
|
30
|
+
* lookup that only tried the leaf would miss it.
|
|
31
|
+
*/
|
|
32
|
+
export function resolveNestedType(ownerClass: unknown, propertyKey: string): any {
|
|
33
|
+
let current: any = ownerClass;
|
|
34
|
+
while (current && current !== Object && typeof current === 'function') {
|
|
35
|
+
const found = nestedTypeRegistry.get(`${current.name}.${propertyKey}`);
|
|
36
|
+
if (found) {
|
|
37
|
+
return found;
|
|
38
|
+
}
|
|
39
|
+
current = Object.getPrototypeOf(current);
|
|
40
|
+
}
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
@@ -22,17 +22,37 @@
|
|
|
22
22
|
* See .claude/rules/architecture.md → "DI Token Placement (SWC-Safe)".
|
|
23
23
|
*/
|
|
24
24
|
import 'reflect-metadata';
|
|
25
|
+
import { Logger } from '@nestjs/common';
|
|
25
26
|
import _ = require('lodash');
|
|
26
27
|
|
|
27
28
|
import { ProcessType } from '../enums/process-type.enum';
|
|
28
|
-
|
|
29
|
+
// isSystemRole comes from the role.enum LEAF (import-free), not from core-tenant.helpers, so this
|
|
30
|
+
// adds no new import edge to a file whose TDZ-immunity is a deliberate safety property.
|
|
31
|
+
import { isSystemRole, RoleEnum } from '../enums/role.enum';
|
|
29
32
|
import { accessDeniedException } from '../exceptions/access-denied.exception';
|
|
33
|
+
// From the import-free LEAF, never from unified-field.decorator: that file imports THIS one for
|
|
34
|
+
// `@Restricted`, so reading the map from there would put this file back on an import cycle.
|
|
35
|
+
import { resolveNestedType } from './nested-type.registry';
|
|
36
|
+
// From an import-free LEAF: the marker is read here and in both interceptors, and this file's
|
|
37
|
+
// position on zero import cycles is a defended property.
|
|
38
|
+
import {
|
|
39
|
+
hasRestrictionsCheckedMarker,
|
|
40
|
+
hasUnrecognizedRestrictionsFlag,
|
|
41
|
+
RESTRICTIONS_CHECKED_KEY,
|
|
42
|
+
} from './restrictions-checked.marker';
|
|
30
43
|
// Import from the id.helper LEAF, never from db.helper: db.helper imports input.helper, which
|
|
31
44
|
// imports this file back — that cycle is what the extraction removed. See id.helper's docblock.
|
|
32
45
|
import { equalIds, getIncludedIds } from '../helpers/id.helper';
|
|
33
46
|
import { RequestContext } from '../services/request-context.service';
|
|
34
47
|
import { RequireAtLeastOne } from '../types/required-at-least-one.type';
|
|
35
|
-
import { checkRoleAccess } from '../../modules/tenant/core-tenant.helpers';
|
|
48
|
+
import { checkRoleAccess, resolveGlobalAndTenantRoles } from '../../modules/tenant/core-tenant.helpers';
|
|
49
|
+
|
|
50
|
+
export {
|
|
51
|
+
hasRestrictionsCheckedMarker,
|
|
52
|
+
hasUnrecognizedRestrictionsFlag,
|
|
53
|
+
markRestrictionsChecked,
|
|
54
|
+
RESTRICTIONS_CHECKED_KEY,
|
|
55
|
+
} from './restrictions-checked.marker';
|
|
36
56
|
|
|
37
57
|
/**
|
|
38
58
|
* Restricted meta key
|
|
@@ -121,6 +141,66 @@ export function getRestricted(object: unknown, propertyKey?: string): Restricted
|
|
|
121
141
|
return metadata;
|
|
122
142
|
}
|
|
123
143
|
|
|
144
|
+
/**
|
|
145
|
+
* Resolve the NON-system portion of a field's required roles against the requester.
|
|
146
|
+
*
|
|
147
|
+
* Two things must be filtered before the roles reach `checkRoleAccess`, because that function
|
|
148
|
+
* matches non-hierarchy roles by exact string against a SINGLE available role — the tenant
|
|
149
|
+
* membership role, when a tenant context is active:
|
|
150
|
+
*
|
|
151
|
+
* 1. **System roles.** `S_SELF`, `S_CREATOR`, `S_VERIFIED`, `S_USER` and `S_EVERYONE` are decided
|
|
152
|
+
* by the dedicated checks in `checkRestricted` (ownership, verification, session). Passing them
|
|
153
|
+
* on means a membership role literally named `'s_self'` satisfies `@Restricted(S_SELF)` on
|
|
154
|
+
* ARBITRARY records — no ownership is ever compared. `CoreTenantGuard` already filters these at
|
|
155
|
+
* its own call site; this is the same filter for the field-level path.
|
|
156
|
+
*
|
|
157
|
+
* 2. **Global-only roles.** `RoleEnum.ADMIN` is platform authority and must be answered from
|
|
158
|
+
* `user.roles`, never from a customer-assigned membership role.
|
|
159
|
+
*
|
|
160
|
+
* Declared as a hoisted `function` so it stays temporal-dead-zone immune — this file sits on no
|
|
161
|
+
* import cycle today and the property is defended deliberately (see `.claude/rules/architecture.md`).
|
|
162
|
+
*/
|
|
163
|
+
function checkFieldRoleAccess(roles: string[], user: any): boolean {
|
|
164
|
+
const { global: globalRoles, tenant: tenantRoles } = resolveGlobalAndTenantRoles(
|
|
165
|
+
roles.filter((role) => !isSystemRole(role)),
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
if (globalRoles.some((role) => user?.roles?.includes(role))) {
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// The length guard is NOT redundant: checkRoleAccess returns TRUE for an empty required-roles
|
|
173
|
+
// list, so calling it with no tenant roles would unlock a field that only required a global one.
|
|
174
|
+
return tenantRoles.length > 0 && checkRoleAccess(tenantRoles, user?.roles, RequestContext.get()?.tenantRole);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Emitted at most once per process — a per-object warning on a hot path would be noise, not signal. */
|
|
178
|
+
let unrecognizedFlagWarned = false;
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Warn once when an object carries `_objectAlreadyCheckedForRestrictions` without the framework marker.
|
|
182
|
+
*
|
|
183
|
+
* Declared as a hoisted `function` like everything else in this file, so it stays temporal-dead-zone
|
|
184
|
+
* immune. Uses the Nest `Logger` rather than `console.warn` for two reasons: it is the convention every
|
|
185
|
+
* other framework warning follows, and `console` output from a hot path is forwarded over vitest's
|
|
186
|
+
* worker RPC — a late one produces `Closing rpc while "onUserConsoleLog" was pending` in whichever
|
|
187
|
+
* spec happens to be tearing down. `@nestjs/common` is an external package and already in this file's
|
|
188
|
+
* transitive graph via `accessDeniedException`, so it adds no project import edge.
|
|
189
|
+
*/
|
|
190
|
+
function warnUnrecognizedRestrictionsFlag(data: unknown): void {
|
|
191
|
+
if (unrecognizedFlagWarned || !hasUnrecognizedRestrictionsFlag(data)) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
unrecognizedFlagWarned = true;
|
|
195
|
+
new Logger('Restricted').warn(
|
|
196
|
+
`[Restricted] An object carries "${RESTRICTIONS_CHECKED_KEY}" without the framework marker, so it ` +
|
|
197
|
+
'is being CHECKED rather than skipped. Since 11.35.0 the opt-out is only honoured for objects ' +
|
|
198
|
+
'marked via markRestrictionsChecked() — a plain truthy property could arrive from the database ' +
|
|
199
|
+
'or a payload and would switch off field-level access control. If you set it deliberately, call ' +
|
|
200
|
+
'markRestrictionsChecked(obj); if this came from stored data, remove the field.',
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
124
204
|
/**
|
|
125
205
|
* Check data for restricted properties (properties with `Restricted` decorator)
|
|
126
206
|
* For special Roles and member of group checking the dbObject must be set in options
|
|
@@ -150,6 +230,7 @@ export function checkRestricted(
|
|
|
150
230
|
throwError?: boolean;
|
|
151
231
|
} = {},
|
|
152
232
|
processedObjects: WeakSet<object> = new WeakSet(),
|
|
233
|
+
declaredType?: any,
|
|
153
234
|
) {
|
|
154
235
|
// Act like Roles handling: checkObjectItself = false & mergeRoles = true
|
|
155
236
|
// For Input: throwError = true
|
|
@@ -168,16 +249,52 @@ export function checkRestricted(
|
|
|
168
249
|
};
|
|
169
250
|
|
|
170
251
|
// Primitives
|
|
171
|
-
|
|
252
|
+
// The "already checked" opt-out is recognised by the framework's MARKER, never by a truthy property
|
|
253
|
+
// of that name: nothing in src/ ever wrote the property, so a truthy one could only have come from
|
|
254
|
+
// outside — and a document carrying it disabled every field-level check for that object, silently,
|
|
255
|
+
// on the output path. See restrictions-checked.marker.ts.
|
|
256
|
+
if (!data || typeof data !== 'object' || (config.noteCheckedObjects && hasRestrictionsCheckedMarker(data))) {
|
|
172
257
|
return data;
|
|
173
258
|
}
|
|
174
259
|
|
|
260
|
+
// Say it out loud when the KEY is present without the marker. Two very different callers land here
|
|
261
|
+
// and they want opposite answers — a project that used to set the flag by hand has lost its opt-out,
|
|
262
|
+
// a document that carries it from a raw write has lost a bypass it should never have had. A warning
|
|
263
|
+
// states the situation without guessing which; honouring the value is what the marker exists to stop.
|
|
264
|
+
warnUnrecognizedRestrictionsFlag(data);
|
|
265
|
+
|
|
175
266
|
// Prevent infinite recursion
|
|
176
267
|
if (processedObjects.has(data)) {
|
|
177
268
|
return data;
|
|
178
269
|
}
|
|
179
270
|
processedObjects.add(data);
|
|
180
271
|
|
|
272
|
+
// WHERE the restriction metadata for `data` lives.
|
|
273
|
+
//
|
|
274
|
+
// For an instance of a model class this is simply its constructor, and reading it off the object
|
|
275
|
+
// works — which is why the top level has always been checked correctly. One level down it stops
|
|
276
|
+
// working: `CoreModel.map()` is a shallow `Object.assign`, `prepareOutput()` maps only the target
|
|
277
|
+
// model, and `ResponseModelInterceptor` maps only the top-level item. So an embedded subdocument
|
|
278
|
+
// read out of MongoDB arrives as a PLAIN object whose constructor is `Object` — and `Object`
|
|
279
|
+
// carries no `@Restricted` metadata, so every nested restriction silently evaluated to "no
|
|
280
|
+
// restrictions at all" and the field was returned in full. `declaredType` closes that: the type
|
|
281
|
+
// the PARENT declared for this property, looked up in the nested-type registry.
|
|
282
|
+
//
|
|
283
|
+
// An unregistered nested type still resolves to nothing, exactly as before — the framework cannot
|
|
284
|
+
// invent a declaration. That is deliberate rather than fail-closed: nested values also legitimately
|
|
285
|
+
// hold free-form JSON, `Map`s and scalars, and refusing those would break far more than it
|
|
286
|
+
// protects. Declare nested types with `@UnifiedField({ type: () => X })` to have them enforced.
|
|
287
|
+
const metadataOwner: any =
|
|
288
|
+
data.constructor && data.constructor !== Object
|
|
289
|
+
? data.constructor
|
|
290
|
+
: typeof declaredType === 'function'
|
|
291
|
+
? declaredType
|
|
292
|
+
: undefined;
|
|
293
|
+
// What `Reflect.getMetadata` must walk for PROPERTY lookups: the instance when there is one, the
|
|
294
|
+
// declared class's prototype otherwise (metadata for property decorators is defined there, and
|
|
295
|
+
// Reflect traverses the prototype chain either way).
|
|
296
|
+
const metadataTarget: any = !metadataOwner || data.constructor === metadataOwner ? data : metadataOwner.prototype;
|
|
297
|
+
|
|
181
298
|
// Array
|
|
182
299
|
if (Array.isArray(data)) {
|
|
183
300
|
if (data.length === 0) {
|
|
@@ -189,15 +306,18 @@ export function checkRestricted(
|
|
|
189
306
|
// Per-item checks are only needed when S_CREATOR or S_SELF restrictions exist
|
|
190
307
|
// (because createdBy/id differ per item).
|
|
191
308
|
const sample = data[0];
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
sample.constructor
|
|
197
|
-
|
|
198
|
-
|
|
309
|
+
// The element type: the sample's own class, or — for an array of embedded subdocuments, which
|
|
310
|
+
// arrive as plain objects — the type the parent property declared. `declaredType` is the ELEMENT
|
|
311
|
+
// type here: `@UnifiedField({ isArray: true, type: () => X })` registers `X`, not `X[]`.
|
|
312
|
+
const sampleOwner: any =
|
|
313
|
+
sample && typeof sample === 'object' && !Array.isArray(sample) && sample.constructor !== Object
|
|
314
|
+
? sample.constructor
|
|
315
|
+
: typeof declaredType === 'function'
|
|
316
|
+
? declaredType
|
|
317
|
+
: undefined;
|
|
318
|
+
if (sample && typeof sample === 'object' && !Array.isArray(sample) && sampleOwner) {
|
|
199
319
|
// Check class-level restrictions once
|
|
200
|
-
const classRestrictions = getRestricted(
|
|
320
|
+
const classRestrictions = getRestricted(sampleOwner) || [];
|
|
201
321
|
if (classRestrictions.length) {
|
|
202
322
|
const hasCreatorOrSelf = classRestrictions.some(
|
|
203
323
|
(r) =>
|
|
@@ -223,7 +343,7 @@ export function checkRestricted(
|
|
|
223
343
|
// also validates the class-level restriction as a standalone gate. With the
|
|
224
344
|
// default checkObjectItself=false, class restrictions are merged into each
|
|
225
345
|
// property's restrictions (properties get stripped if the class restriction denies).
|
|
226
|
-
const sampleResult = checkRestricted(sample, user, config, processedObjects);
|
|
346
|
+
const sampleResult = checkRestricted(sample, user, config, processedObjects, declaredType);
|
|
227
347
|
if (sampleResult === undefined || sampleResult === null) {
|
|
228
348
|
// Class-level restriction blocks access → entire array is blocked
|
|
229
349
|
if (config.throwError) {
|
|
@@ -236,7 +356,7 @@ export function checkRestricted(
|
|
|
236
356
|
// are O(1) lookups, but we still need to recurse into nested properties per item.
|
|
237
357
|
const result = [sampleResult];
|
|
238
358
|
for (let i = 1; i < data.length; i++) {
|
|
239
|
-
result.push(checkRestricted(data[i], user, config, processedObjects));
|
|
359
|
+
result.push(checkRestricted(data[i], user, config, processedObjects, declaredType));
|
|
240
360
|
}
|
|
241
361
|
if (!config.throwError && config.removeUndefinedFromResultArray) {
|
|
242
362
|
return result.filter((item) => item !== undefined);
|
|
@@ -247,7 +367,7 @@ export function checkRestricted(
|
|
|
247
367
|
}
|
|
248
368
|
|
|
249
369
|
// Fallback: plain objects, mixed types, or S_CREATOR/S_SELF checks needed
|
|
250
|
-
let result = data.map((item) => checkRestricted(item, user, config, processedObjects));
|
|
370
|
+
let result = data.map((item) => checkRestricted(item, user, config, processedObjects, declaredType));
|
|
251
371
|
if (!config.throwError && config.removeUndefinedFromResultArray) {
|
|
252
372
|
result = result.filter((item) => item !== undefined);
|
|
253
373
|
}
|
|
@@ -256,7 +376,7 @@ export function checkRestricted(
|
|
|
256
376
|
|
|
257
377
|
// Check function
|
|
258
378
|
const validateRestricted = (restricted) => {
|
|
259
|
-
if (config.noteCheckedObjects && data
|
|
379
|
+
if (config.noteCheckedObjects && hasRestrictionsCheckedMarker(data)) {
|
|
260
380
|
return true;
|
|
261
381
|
}
|
|
262
382
|
|
|
@@ -320,7 +440,7 @@ export function checkRestricted(
|
|
|
320
440
|
((owner && 'createdBy' in owner && equalIds(owner.createdBy, user)) ||
|
|
321
441
|
(config.allowCreatorOfParent && owner && !('createdBy' in owner) && config.isCreatorOfParent))) ||
|
|
322
442
|
(roles.includes(RoleEnum.S_VERIFIED) && (user?.verified || user?.verifiedAt || user?.emailVerified)) ||
|
|
323
|
-
(user?.id &&
|
|
443
|
+
(user?.id && checkFieldRoleAccess(roles, user))
|
|
324
444
|
) {
|
|
325
445
|
valid = true;
|
|
326
446
|
}
|
|
@@ -375,7 +495,7 @@ export function checkRestricted(
|
|
|
375
495
|
};
|
|
376
496
|
|
|
377
497
|
// Check data object
|
|
378
|
-
const objectRestrictions = getRestricted(
|
|
498
|
+
const objectRestrictions = getRestricted(metadataOwner) || [];
|
|
379
499
|
if (config.checkObjectItself) {
|
|
380
500
|
const objectIsValid = validateRestricted(objectRestrictions);
|
|
381
501
|
if (!objectIsValid) {
|
|
@@ -403,8 +523,9 @@ export function checkRestricted(
|
|
|
403
523
|
continue;
|
|
404
524
|
}
|
|
405
525
|
|
|
406
|
-
// Check restricted
|
|
407
|
-
|
|
526
|
+
// Check restricted. `metadataTarget` is `data` for a real instance and the declared class's
|
|
527
|
+
// prototype for a plain nested object — see its definition above.
|
|
528
|
+
const restricted = getRestricted(metadataTarget, propertyKey) || [];
|
|
408
529
|
const concatenatedRestrictions =
|
|
409
530
|
config.mergeRoles && objectRestrictions.length ? _.uniq(objectRestrictions.concat(restricted)) : restricted;
|
|
410
531
|
const valid = validateRestricted(concatenatedRestrictions);
|
|
@@ -420,8 +541,15 @@ export function checkRestricted(
|
|
|
420
541
|
equalIds(parent, user) ||
|
|
421
542
|
(parent && 'createdBy' in parent ? equalIds(parent.createdBy, user) : config.isCreatorOfParent);
|
|
422
543
|
|
|
423
|
-
// Check deep
|
|
424
|
-
|
|
544
|
+
// Check deep — carrying the type this property DECLARES, so a nested plain object (an
|
|
545
|
+
// embedded subdocument, or an array of them) can still be matched against its own metadata.
|
|
546
|
+
data[propertyKey] = checkRestricted(
|
|
547
|
+
data[propertyKey],
|
|
548
|
+
user,
|
|
549
|
+
config,
|
|
550
|
+
processedObjects,
|
|
551
|
+
metadataOwner ? resolveNestedType(metadataOwner, propertyKey) : undefined,
|
|
552
|
+
);
|
|
425
553
|
} else {
|
|
426
554
|
if (config.debug) {
|
|
427
555
|
console.debug(
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "This object has already been checked" — as a MARKER the framework sets, not a property data carries.
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS FILE EXISTS: three places short-circuit on
|
|
5
|
+
* `_objectAlreadyCheckedForRestrictions` — `checkRestricted()` returns the object untouched,
|
|
6
|
+
* `CheckSecurityInterceptor` skips `securityCheck()`, and `ResponseModelInterceptor` skips the
|
|
7
|
+
* Plain→Model conversion. The opt-out is legitimate: without it one response is checked three times.
|
|
8
|
+
*
|
|
9
|
+
* What was NOT legitimate is how it was recognised. Every read was a plain truthy test on a
|
|
10
|
+
* well-known property name, and nothing in `src/` ever wrote it — so the only way the flag could ever
|
|
11
|
+
* be set was from outside the framework. Which means a document carrying
|
|
12
|
+
* `_objectAlreadyCheckedForRestrictions: true` — from a raw write, a `strict: false` schema, a
|
|
13
|
+
* restored export — disabled EVERY field-level access check for that object, silently, on the output
|
|
14
|
+
* path where a skipped check produces no error at all. That is a data-shaped authorization switch,
|
|
15
|
+
* and data must never be able to flip one.
|
|
16
|
+
*
|
|
17
|
+
* The fix keeps the feature and removes the forgeability: the marker's VALUE is a module-private
|
|
18
|
+
* symbol. JSON, BSON and any client payload can produce the key, and none of them can produce that
|
|
19
|
+
* value. The property is also non-enumerable, so it never reaches a response body and cannot re-enter
|
|
20
|
+
* through a round trip as exactly the data shape above.
|
|
21
|
+
*
|
|
22
|
+
* DELIBERATELY IMPORT-FREE, so `restricted.decorator` (whose position on zero import cycles is a
|
|
23
|
+
* defended property) and both interceptors can read it without a new import edge between them.
|
|
24
|
+
* See `.claude/rules/architecture.md` → "DI Token Placement (SWC-Safe)".
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The property name, unchanged since it is part of the observable API — a project may look for it.
|
|
29
|
+
* Only the way it is RECOGNISED changed.
|
|
30
|
+
*/
|
|
31
|
+
export const RESTRICTIONS_CHECKED_KEY = '_objectAlreadyCheckedForRestrictions';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The only value that counts as "checked". Module-private on purpose: it is not exported, so nothing
|
|
35
|
+
* outside this file — and nothing that arrives as data — can reproduce it.
|
|
36
|
+
*/
|
|
37
|
+
const MARKER = Symbol('restrictionsChecked');
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Mark an object as already checked, so the framework's own layers do not check it again.
|
|
41
|
+
*
|
|
42
|
+
* Non-enumerable and non-writable: it must not appear in `Object.keys()`, in `JSON.stringify()` or in
|
|
43
|
+
* a Mongoose write. `configurable: true` so the same object can be re-marked without throwing.
|
|
44
|
+
*
|
|
45
|
+
* Returns the object, so it can be used inline.
|
|
46
|
+
*/
|
|
47
|
+
export function markRestrictionsChecked<T>(data: T): T {
|
|
48
|
+
if (!data || typeof data !== 'object') {
|
|
49
|
+
return data;
|
|
50
|
+
}
|
|
51
|
+
Object.defineProperty(data, RESTRICTIONS_CHECKED_KEY, {
|
|
52
|
+
configurable: true,
|
|
53
|
+
enumerable: false,
|
|
54
|
+
value: MARKER,
|
|
55
|
+
writable: false,
|
|
56
|
+
});
|
|
57
|
+
return data;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Has the framework marked this object as checked?
|
|
62
|
+
*
|
|
63
|
+
* The identity comparison is the whole point — `!!data[KEY]` would be satisfied by any truthy value,
|
|
64
|
+
* including one that came out of the database.
|
|
65
|
+
*/
|
|
66
|
+
export function hasRestrictionsCheckedMarker(data: unknown): boolean {
|
|
67
|
+
return !!data && typeof data === 'object' && (data as Record<string, unknown>)[RESTRICTIONS_CHECKED_KEY] === MARKER;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Does this object carry the KEY without the marker — i.e. an unforgeable-looking value that is not
|
|
72
|
+
* actually the marker?
|
|
73
|
+
*
|
|
74
|
+
* Used to warn once rather than to decide anything. Two callers can produce this state, and they want
|
|
75
|
+
* opposite answers: a project that used to set the flag by hand (`obj[KEY] = true`) has silently lost
|
|
76
|
+
* its opt-out and should hear about it, while a document that carries the field from a raw write has
|
|
77
|
+
* silently LOST a bypass it should never have had. A warning names the situation without guessing
|
|
78
|
+
* which one it is; treating it as "checked" is what this file exists to stop.
|
|
79
|
+
*/
|
|
80
|
+
export function hasUnrecognizedRestrictionsFlag(data: unknown): boolean {
|
|
81
|
+
return (
|
|
82
|
+
!!data &&
|
|
83
|
+
typeof data === 'object' &&
|
|
84
|
+
RESTRICTIONS_CHECKED_KEY in (data as object) &&
|
|
85
|
+
(data as Record<string, unknown>)[RESTRICTIONS_CHECKED_KEY] !== MARKER
|
|
86
|
+
);
|
|
87
|
+
}
|
|
@@ -22,11 +22,12 @@ import {
|
|
|
22
22
|
import { GraphQLScalarType, isEnumType } from 'graphql';
|
|
23
23
|
|
|
24
24
|
import { RoleEnum } from '../enums/role.enum';
|
|
25
|
+
// Re-exported below so no import path broke. The map moved into an import-free leaf because
|
|
26
|
+
// `restricted.decorator` — which this file imports — now READS it; see that leaf's docblock.
|
|
27
|
+
import { nestedTypeRegistry } from './nested-type.registry';
|
|
25
28
|
import { Restricted, RestrictedType } from './restricted.decorator';
|
|
26
29
|
|
|
27
|
-
|
|
28
|
-
// Key: `${className}.${propertyName}`, Value: nested type constructor
|
|
29
|
-
export const nestedTypeRegistry = new Map<string, any>();
|
|
30
|
+
export { nestedTypeRegistry, resolveNestedType } from './nested-type.registry';
|
|
30
31
|
|
|
31
32
|
/**
|
|
32
33
|
* Registry to map enum objects to their names.
|