@lenne.tech/nest-server 11.27.6 → 11.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/rules/architecture.md +66 -0
- package/.claude/rules/better-auth.md +88 -2
- package/.claude/rules/package-management.md +23 -14
- package/.claude/rules/role-system.md +90 -0
- package/.claude/rules/versioning.md +17 -2
- package/CLAUDE.md +8 -2
- package/FRAMEWORK-API.md +27 -1
- package/dist/core/common/decorators/restricted.decorator.d.ts +4 -4
- package/dist/core/common/decorators/restricted.decorator.js +29 -26
- package/dist/core/common/decorators/restricted.decorator.js.map +1 -1
- package/dist/core/common/exceptions/access-denied.exception.d.ts +4 -0
- package/dist/core/common/exceptions/access-denied.exception.js +12 -0
- package/dist/core/common/exceptions/access-denied.exception.js.map +1 -0
- package/dist/core/common/helpers/clone.helper.d.ts +6 -0
- package/dist/core/common/helpers/clone.helper.js +67 -0
- package/dist/core/common/helpers/clone.helper.js.map +1 -0
- package/dist/core/common/helpers/db.helper.d.ts +2 -14
- package/dist/core/common/helpers/db.helper.js +13 -106
- package/dist/core/common/helpers/db.helper.js.map +1 -1
- package/dist/core/common/helpers/id.helper.d.ts +15 -0
- package/dist/core/common/helpers/id.helper.js +104 -0
- package/dist/core/common/helpers/id.helper.js.map +1 -0
- package/dist/core/common/helpers/input.helper.d.ts +1 -6
- package/dist/core/common/helpers/input.helper.js +15 -72
- package/dist/core/common/helpers/input.helper.js.map +1 -1
- package/dist/core/common/helpers/service.helper.js +4 -3
- package/dist/core/common/helpers/service.helper.js.map +1 -1
- package/dist/core/common/inputs/combined-filter.input.d.ts +1 -12
- package/dist/core/common/inputs/combined-filter.input.js +2 -51
- package/dist/core/common/inputs/combined-filter.input.js.map +1 -1
- package/dist/core/common/inputs/filter.input.d.ts +10 -1
- package/dist/core/common/inputs/filter.input.js +40 -5
- package/dist/core/common/inputs/filter.input.js.map +1 -1
- package/dist/core/common/services/config.service.js +5 -5
- package/dist/core/common/services/config.service.js.map +1 -1
- package/dist/core/modules/ai/core-ai.constants.d.ts +22 -0
- package/dist/core/modules/ai/core-ai.constants.js +26 -0
- package/dist/core/modules/ai/core-ai.constants.js.map +1 -0
- package/dist/core/modules/ai/index.d.ts +1 -0
- package/dist/core/modules/ai/index.js +1 -0
- package/dist/core/modules/ai/index.js.map +1 -1
- package/dist/core/modules/ai/interfaces/ai-interaction-record.interface.d.ts +17 -0
- package/dist/core/modules/ai/interfaces/ai-interaction-record.interface.js +3 -0
- package/dist/core/modules/ai/interfaces/ai-interaction-record.interface.js.map +1 -0
- package/dist/core/modules/ai/services/core-ai-budget.service.d.ts +1 -2
- package/dist/core/modules/ai/services/core-ai-budget.service.js +7 -5
- package/dist/core/modules/ai/services/core-ai-budget.service.js.map +1 -1
- package/dist/core/modules/ai/services/core-ai-connection-preference.service.d.ts +1 -2
- package/dist/core/modules/ai/services/core-ai-connection-preference.service.js +7 -5
- package/dist/core/modules/ai/services/core-ai-connection-preference.service.js.map +1 -1
- package/dist/core/modules/ai/services/core-ai-connection.service.d.ts +1 -2
- package/dist/core/modules/ai/services/core-ai-connection.service.js +7 -5
- package/dist/core/modules/ai/services/core-ai-connection.service.js.map +1 -1
- package/dist/core/modules/ai/services/core-ai-conversation.service.d.ts +1 -2
- package/dist/core/modules/ai/services/core-ai-conversation.service.js +7 -5
- package/dist/core/modules/ai/services/core-ai-conversation.service.js.map +1 -1
- package/dist/core/modules/ai/services/core-ai-interaction.service.d.ts +2 -3
- package/dist/core/modules/ai/services/core-ai-interaction.service.js +7 -5
- package/dist/core/modules/ai/services/core-ai-interaction.service.js.map +1 -1
- package/dist/core/modules/ai/services/core-ai-mode.service.d.ts +1 -2
- package/dist/core/modules/ai/services/core-ai-mode.service.js +7 -5
- package/dist/core/modules/ai/services/core-ai-mode.service.js.map +1 -1
- package/dist/core/modules/ai/services/core-ai-prompt-hint.service.d.ts +1 -3
- package/dist/core/modules/ai/services/core-ai-prompt-hint.service.js +7 -5
- package/dist/core/modules/ai/services/core-ai-prompt-hint.service.js.map +1 -1
- package/dist/core/modules/ai/services/core-ai-prompt.service.d.ts +1 -2
- package/dist/core/modules/ai/services/core-ai-prompt.service.js +7 -5
- package/dist/core/modules/ai/services/core-ai-prompt.service.js.map +1 -1
- package/dist/core/modules/ai/services/core-ai-slot.service.d.ts +1 -2
- package/dist/core/modules/ai/services/core-ai-slot.service.js +7 -5
- package/dist/core/modules/ai/services/core-ai-slot.service.js.map +1 -1
- package/dist/core/modules/ai/services/core-ai-tool-grant.service.d.ts +1 -2
- package/dist/core/modules/ai/services/core-ai-tool-grant.service.js +7 -5
- package/dist/core/modules/ai/services/core-ai-tool-grant.service.js.map +1 -1
- package/dist/core/modules/ai/services/core-ai-tool-policy.service.d.ts +1 -2
- package/dist/core/modules/ai/services/core-ai-tool-policy.service.js +7 -5
- package/dist/core/modules/ai/services/core-ai-tool-policy.service.js.map +1 -1
- package/dist/core/modules/ai/services/core-ai.service.d.ts +2 -17
- package/dist/core/modules/ai/services/core-ai.service.js.map +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 +3 -3
- package/dist/core/modules/better-auth/better-auth-roles.guard.js.map +1 -1
- package/dist/core/modules/better-auth/better-auth.config.js +6 -0
- package/dist/core/modules/better-auth/better-auth.config.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth.constants.d.ts +3 -0
- package/dist/core/modules/better-auth/core-better-auth.constants.js +7 -0
- package/dist/core/modules/better-auth/core-better-auth.constants.js.map +1 -0
- package/dist/core/modules/better-auth/core-better-auth.controller.js +1 -3
- package/dist/core/modules/better-auth/core-better-auth.controller.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth.module.d.ts +1 -2
- package/dist/core/modules/better-auth/core-better-auth.module.js +23 -20
- package/dist/core/modules/better-auth/core-better-auth.module.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth.registry.d.ts +4 -0
- package/dist/core/modules/better-auth/core-better-auth.registry.js +16 -0
- package/dist/core/modules/better-auth/core-better-auth.registry.js.map +1 -0
- package/dist/core/modules/better-auth/core-better-auth.service.d.ts +1 -2
- package/dist/core/modules/better-auth/core-better-auth.service.js +7 -6
- package/dist/core/modules/better-auth/core-better-auth.service.js.map +1 -1
- package/dist/core/modules/better-auth/index.d.ts +1 -0
- package/dist/core/modules/better-auth/index.js +1 -0
- package/dist/core/modules/better-auth/index.js.map +1 -1
- package/dist/core/modules/tenant/core-tenant-member.model.js +3 -3
- package/dist/core/modules/tenant/core-tenant-member.model.js.map +1 -1
- package/dist/core/modules/tenant/core-tenant.guard.js +7 -6
- package/dist/core/modules/tenant/core-tenant.guard.js.map +1 -1
- package/dist/core/modules/tus/tus.constants.d.ts +1 -0
- package/dist/core/modules/tus/tus.constants.js +5 -0
- package/dist/core/modules/tus/tus.constants.js.map +1 -0
- package/dist/core/modules/tus/tus.module.d.ts +1 -1
- package/dist/core/modules/tus/tus.module.js +8 -6
- package/dist/core/modules/tus/tus.module.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/server/modules/user/user.service.js +1 -1
- package/dist/server/modules/user/user.service.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/docs/REQUEST-LIFECYCLE.md +39 -0
- package/migration-guides/11.27.6-to-11.27.7.md +374 -0
- package/migration-guides/11.27.7-to-11.28.0.md +350 -0
- package/package.json +6 -104
- package/src/core/common/decorators/restricted.decorator.ts +67 -20
- package/src/core/common/exceptions/access-denied.exception.ts +49 -0
- package/src/core/common/helpers/clone.helper.ts +110 -0
- package/src/core/common/helpers/db.helper.ts +14 -161
- package/src/core/common/helpers/id.helper.ts +198 -0
- package/src/core/common/helpers/input.helper.ts +21 -88
- package/src/core/common/helpers/service.helper.ts +6 -4
- package/src/core/common/inputs/combined-filter.input.ts +10 -57
- package/src/core/common/inputs/filter.input.ts +92 -1
- package/src/core/common/services/config.service.ts +4 -1
- package/src/core/modules/ai/core-ai.constants.ts +92 -0
- package/src/core/modules/ai/index.ts +1 -0
- package/src/core/modules/ai/interfaces/ai-interaction-record.interface.ts +34 -0
- package/src/core/modules/ai/services/core-ai-budget.service.ts +8 -4
- package/src/core/modules/ai/services/core-ai-connection-preference.service.ts +8 -4
- package/src/core/modules/ai/services/core-ai-connection.service.ts +5 -6
- package/src/core/modules/ai/services/core-ai-conversation.service.ts +5 -6
- package/src/core/modules/ai/services/core-ai-interaction.service.ts +6 -7
- package/src/core/modules/ai/services/core-ai-mode.service.ts +8 -2
- package/src/core/modules/ai/services/core-ai-prompt-hint.service.ts +7 -4
- package/src/core/modules/ai/services/core-ai-prompt.service.ts +8 -2
- package/src/core/modules/ai/services/core-ai-slot.service.ts +7 -4
- package/src/core/modules/ai/services/core-ai-tool-grant.service.ts +8 -4
- package/src/core/modules/ai/services/core-ai-tool-policy.service.ts +8 -4
- package/src/core/modules/ai/services/core-ai.service.ts +8 -11
- package/src/core/modules/auth/guards/roles.guard.ts +5 -4
- package/src/core/modules/better-auth/better-auth-roles.guard.ts +15 -7
- package/src/core/modules/better-auth/better-auth.config.ts +16 -0
- package/src/core/modules/better-auth/core-better-auth.constants.ts +73 -0
- package/src/core/modules/better-auth/core-better-auth.controller.ts +5 -6
- package/src/core/modules/better-auth/core-better-auth.module.ts +34 -8
- package/src/core/modules/better-auth/core-better-auth.registry.ts +53 -0
- package/src/core/modules/better-auth/core-better-auth.service.ts +19 -12
- package/src/core/modules/better-auth/index.ts +9 -0
- package/src/core/modules/tenant/core-tenant-member.model.ts +8 -3
- package/src/core/modules/tenant/core-tenant.guard.ts +26 -11
- package/src/core/modules/tus/tus.constants.ts +25 -0
- package/src/core/modules/tus/tus.module.ts +7 -2
- package/src/index.ts +1 -0
- package/src/server/modules/user/user.service.ts +3 -2
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { ForbiddenException, UnauthorizedException } from '@nestjs/common';
|
|
2
|
+
|
|
3
|
+
import { ErrorCode } from '../../modules/error-code/error-codes';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Creates the access error that matches the requester's auth state (RFC 9110, mirrors RolesGuard):
|
|
7
|
+
* **403 Forbidden** for authenticated requesters (a permission problem) and **401 Unauthorized**
|
|
8
|
+
* only when the requester is not authenticated.
|
|
9
|
+
*
|
|
10
|
+
* Frontends commonly treat 401 as "session expired" and auto-logout (the `@lenne.tech/nuxt-extensions`
|
|
11
|
+
* auth interceptor patches `$fetch`/`fetch` globally and does exactly this), so a mere permission
|
|
12
|
+
* error must never surface as 401 — it would kick a logged-in user out of the whole app.
|
|
13
|
+
*
|
|
14
|
+
* This is a **factory, not a class**, on purpose: it returns the *native* Nest exceptions, so
|
|
15
|
+
* `instanceof ForbiddenException` / `instanceof UnauthorizedException` and `@Catch(...)` filters in
|
|
16
|
+
* consuming projects keep working, and the REST wire body (which `HttpExceptionLogFilter` builds via
|
|
17
|
+
* `{ ...exception }`, including `name`) stays identical to what it was before. A custom
|
|
18
|
+
* `HttpException` subclass would satisfy neither.
|
|
19
|
+
*
|
|
20
|
+
* The default messages are the same translatable `ErrorCode`s the role guards throw (`#LTNS_xxxx:`
|
|
21
|
+
* marker → resolvable by the frontend error-translation layer). Pass an explicit `message` only
|
|
22
|
+
* where a raw string is genuinely required; prefer logging request-specific detail (class names,
|
|
23
|
+
* field names) over returning it to the client.
|
|
24
|
+
*
|
|
25
|
+
* @param user The **requesting** user (never the target object). The decision key is `user.id`: an
|
|
26
|
+
* id that is present — even a falsy one like `0` or `''` — counts as authenticated, while
|
|
27
|
+
* `undefined`, `null` or no user at all does not. This mirrors how `check()` defines "logged in"
|
|
28
|
+
* (`S_USER` requires `user?.id`).
|
|
29
|
+
* @param message Overrides the default `ErrorCode`. Omit it to stay consistent with `RolesGuard`.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* // 403 for an authenticated user who lacks a right, 401 for an anonymous requester:
|
|
33
|
+
* throw accessDeniedException(currentUser);
|
|
34
|
+
*
|
|
35
|
+
* @see src/core/modules/auth/guards/roles.guard.ts — the pre-existing 401/403 pattern this mirrors
|
|
36
|
+
* @see migration-guides/11.27.7-to-11.28.0.md
|
|
37
|
+
*/
|
|
38
|
+
export function accessDeniedException(
|
|
39
|
+
user: { id?: unknown } | null | undefined,
|
|
40
|
+
message?: string,
|
|
41
|
+
): ForbiddenException | UnauthorizedException {
|
|
42
|
+
// A present id means authenticated. `!!user?.id` would misjudge falsy-but-real ids (0, '') as
|
|
43
|
+
// anonymous and hand an authenticated user the very 401 this mechanism exists to avoid.
|
|
44
|
+
const authenticated = user?.id !== undefined && user?.id !== null;
|
|
45
|
+
|
|
46
|
+
return authenticated
|
|
47
|
+
? new ForbiddenException(message ?? ErrorCode.ACCESS_DENIED)
|
|
48
|
+
: new UnauthorizedException(message ?? ErrorCode.UNAUTHORIZED);
|
|
49
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloning / freezing helpers.
|
|
3
|
+
*
|
|
4
|
+
* These live in their own file — instead of `input.helper.ts` — so that `config.service` can reach
|
|
5
|
+
* them without importing `input.helper`, which imports `restricted.decorator` behind it.
|
|
6
|
+
*
|
|
7
|
+
* That edge closed a runtime cycle:
|
|
8
|
+
*
|
|
9
|
+
* restricted.decorator → tenant/core-tenant.helpers → config.service → input.helper
|
|
10
|
+
* → restricted.decorator
|
|
11
|
+
*
|
|
12
|
+
* so `input.helper` evaluated while `restricted.decorator` was still initializing. Nothing crashed,
|
|
13
|
+
* but only because every cross-cycle dereference sat inside a function body. A single top-level line
|
|
14
|
+
* in `input.helper` — a module-level alias of `checkRestricted`, an `@Restricted`-decorated class,
|
|
15
|
+
* `design:type` metadata — would have thrown under SWC → CommonJS:
|
|
16
|
+
*
|
|
17
|
+
* ReferenceError: Cannot access 'checkRestricted' before initialization
|
|
18
|
+
*
|
|
19
|
+
* …in the file that drives field-level access control, on a compiler this repo's default build never
|
|
20
|
+
* runs, with a green test suite. Moving these two pure functions out removes the edge, so the cycle
|
|
21
|
+
* no longer exists rather than merely being disarmed.
|
|
22
|
+
*
|
|
23
|
+
* This file imports only Node built-ins, lodash and rfdc — no framework code — so it can never be
|
|
24
|
+
* part of a cycle itself. Keep it that way.
|
|
25
|
+
*
|
|
26
|
+
* `input.helper` re-exports both functions, so every existing import path keeps working.
|
|
27
|
+
* See .claude/rules/architecture.md → "DI Token Placement (SWC-Safe)".
|
|
28
|
+
*/
|
|
29
|
+
import * as inspector from 'inspector';
|
|
30
|
+
import _ = require('lodash');
|
|
31
|
+
import rfdc = require('rfdc');
|
|
32
|
+
import * as util from 'util';
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Get clone of object
|
|
36
|
+
*
|
|
37
|
+
* @param object Object to clone
|
|
38
|
+
* @param options Options for cloning
|
|
39
|
+
* @param options.checkResult Whether to check the result of the cloning process
|
|
40
|
+
* @param options.circles Keeping track of circular references will slow down performance with an additional 25% overhead.
|
|
41
|
+
* Even if an object doesn't have any circular references, the tracking overhead is the cost.
|
|
42
|
+
* By default if an object with a circular reference is passed to rfdc, it will throw
|
|
43
|
+
* (similar to how JSON.stringify would throw). Use the circles option to detect and preserve
|
|
44
|
+
* circular references in the object. If performance is important, try removing the circular
|
|
45
|
+
* reference from the object (set to undefined) and then add it back manually after cloning
|
|
46
|
+
* instead of using this option.
|
|
47
|
+
* @param options.proto Copy prototype properties as well as own properties into the new object.
|
|
48
|
+
* It's marginally faster to allow enumerable properties on the prototype to be copied into the
|
|
49
|
+
* cloned object (not onto it's prototype, directly onto the object).
|
|
50
|
+
*/
|
|
51
|
+
export function clone(object: any, options?: { checkResult?: boolean; circles?: boolean; proto?: boolean }) {
|
|
52
|
+
const config = {
|
|
53
|
+
checkResult: true,
|
|
54
|
+
circles: true,
|
|
55
|
+
debug: inspector.url() !== undefined,
|
|
56
|
+
proto: false,
|
|
57
|
+
...options,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const cloned = rfdc(config)(object);
|
|
62
|
+
if (config.checkResult && !util.isDeepStrictEqual(object, cloned)) {
|
|
63
|
+
throw new Error('Cloned object differs from original object');
|
|
64
|
+
}
|
|
65
|
+
return cloned;
|
|
66
|
+
} catch (e) {
|
|
67
|
+
if (!config.circles) {
|
|
68
|
+
if (config.debug) {
|
|
69
|
+
console.debug(e, config, object, 'automatic try to use rfdc with circles');
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
const clonedWithCircles = rfdc({
|
|
73
|
+
...config,
|
|
74
|
+
circles: true,
|
|
75
|
+
})(object);
|
|
76
|
+
if (config.checkResult && !util.isDeepStrictEqual(object, clonedWithCircles)) {
|
|
77
|
+
throw new Error('Cloned object differs from original object', { cause: e });
|
|
78
|
+
}
|
|
79
|
+
return clonedWithCircles;
|
|
80
|
+
} catch (innerError) {
|
|
81
|
+
if (config.debug) {
|
|
82
|
+
console.debug(innerError, 'rfcd with circles did not work => automatic use of _.clone!');
|
|
83
|
+
}
|
|
84
|
+
return _.cloneDeep(object);
|
|
85
|
+
}
|
|
86
|
+
} else {
|
|
87
|
+
if (config.debug) {
|
|
88
|
+
console.debug(e, config, object, 'automatic try to use _.clone instead rfdc');
|
|
89
|
+
}
|
|
90
|
+
return _.cloneDeep(object);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Get deep frozen object
|
|
97
|
+
*/
|
|
98
|
+
export function deepFreeze(object: any, visited: WeakSet<object> = new WeakSet()) {
|
|
99
|
+
if (!object || typeof object !== 'object') {
|
|
100
|
+
return object;
|
|
101
|
+
}
|
|
102
|
+
if (visited.has(object)) {
|
|
103
|
+
return object;
|
|
104
|
+
}
|
|
105
|
+
visited.add(object);
|
|
106
|
+
for (const [key, value] of Object.entries(object)) {
|
|
107
|
+
object[key] = deepFreeze(value, visited);
|
|
108
|
+
}
|
|
109
|
+
return Object.freeze(object);
|
|
110
|
+
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { FieldNode, GraphQLResolveInfo, SelectionNode } from 'graphql';
|
|
2
|
-
import _ = require('lodash');
|
|
3
2
|
import { Document, Model, PopulateOptions, Query, Types } from 'mongoose';
|
|
4
3
|
|
|
5
4
|
import { ResolveSelector } from '../interfaces/resolve-selector.interface';
|
|
@@ -7,8 +6,19 @@ import { CoreModel } from '../models/core-model.model';
|
|
|
7
6
|
import { FieldSelection } from '../types/field-selection.type';
|
|
8
7
|
import { IdsType } from '../types/ids.type';
|
|
9
8
|
import { StringOrObjectId } from '../types/string-or-object-id.type';
|
|
9
|
+
import { equalIds, getObjectIds, getStringIds } from './id.helper';
|
|
10
10
|
import { removePropertiesDeep } from './input.helper';
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* The ID helpers moved to `./id.helper` and are re-exported here so every existing import path keeps
|
|
14
|
+
* working. They were extracted because `restricted.decorator` needs `equalIds` / `getIncludedIds`,
|
|
15
|
+
* and pulling them from HERE put that decorator on a runtime import cycle
|
|
16
|
+
* (restricted.decorator → db.helper → input.helper → restricted.decorator) that is one top-level
|
|
17
|
+
* line away from an SWC temporal-dead-zone crash. `id.helper` imports no framework code, so it can
|
|
18
|
+
* never be part of a cycle. Do NOT move them back.
|
|
19
|
+
*/
|
|
20
|
+
export { equalIds, getIncludedIds, getObjectIds, getStringIds } from './id.helper';
|
|
21
|
+
|
|
12
22
|
// =====================================================================================================================
|
|
13
23
|
// Export functions
|
|
14
24
|
// =====================================================================================================================
|
|
@@ -71,7 +81,9 @@ export function addIds(
|
|
|
71
81
|
// Convert array
|
|
72
82
|
if (['object', 'string'].includes(convert as string)) {
|
|
73
83
|
for (let i = 0; i < result.length; i++) {
|
|
74
|
-
|
|
84
|
+
// getStringIds() delegates to the (module-private) getStringId() for a non-array argument, so
|
|
85
|
+
// this is the same conversion the removed private helper performed — result[i] is a single ID.
|
|
86
|
+
result[i] = convert === 'string' ? getStringIds(result[i]) : getObjectIds(result[i]);
|
|
75
87
|
}
|
|
76
88
|
}
|
|
77
89
|
|
|
@@ -97,20 +109,6 @@ export function checkStringIds(ids: string | string[]): boolean {
|
|
|
97
109
|
return false;
|
|
98
110
|
}
|
|
99
111
|
|
|
100
|
-
/**
|
|
101
|
-
* Checks if all IDs are equal
|
|
102
|
-
*/
|
|
103
|
-
export function equalIds(...ids: IdsType[]): boolean {
|
|
104
|
-
if (!ids) {
|
|
105
|
-
return false;
|
|
106
|
-
}
|
|
107
|
-
const compare = getStringIds(ids[0]);
|
|
108
|
-
if (!compare) {
|
|
109
|
-
return false;
|
|
110
|
-
}
|
|
111
|
-
return ids.every((id) => getStringIds(id) === compare);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
112
|
/**
|
|
115
113
|
* Get (and remove) elements with specific IDs from array
|
|
116
114
|
*/
|
|
@@ -160,48 +158,6 @@ export function getElementsViaIds<T = any>(
|
|
|
160
158
|
// Return elements
|
|
161
159
|
return elements;
|
|
162
160
|
}
|
|
163
|
-
/**
|
|
164
|
-
* Get included ids
|
|
165
|
-
* @param includes IdsType, which should be checked if it contains the ID
|
|
166
|
-
* @param ids IdsType, which should be included
|
|
167
|
-
* @param convert If set the result array will be converted to pure type String array or ObjectId array
|
|
168
|
-
* @return IdsType with IDs which are included, undefined if includes or ids are missing or null if none is included
|
|
169
|
-
*/
|
|
170
|
-
export function getIncludedIds(includes: IdsType, ids: IdsType, convert?: 'string'): string[];
|
|
171
|
-
export function getIncludedIds(includes: IdsType, ids: IdsType, convert?: 'object'): Types.ObjectId[];
|
|
172
|
-
|
|
173
|
-
export function getIncludedIds<T = IdsType>(
|
|
174
|
-
includes: IdsType,
|
|
175
|
-
ids: IdsType | T,
|
|
176
|
-
convert?: 'object' | 'string',
|
|
177
|
-
): null | T[] | undefined {
|
|
178
|
-
if (!includes || !ids) {
|
|
179
|
-
return undefined;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
if (!Array.isArray(includes)) {
|
|
183
|
-
includes = [includes];
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
if (!Array.isArray(ids)) {
|
|
187
|
-
ids = [ids];
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
let result = [];
|
|
191
|
-
const includesStrings = getStringIds(includes);
|
|
192
|
-
for (const id of ids) {
|
|
193
|
-
if (includesStrings.includes(getStringIds(id))) {
|
|
194
|
-
result.push(id);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
if (convert) {
|
|
199
|
-
result = convert === 'string' ? getStringIds(result) : getObjectIds(result);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
return result.length ? result : null;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
161
|
/**
|
|
206
162
|
* Get indexes of IDs in an array
|
|
207
163
|
*/
|
|
@@ -252,18 +208,6 @@ export function getNextFieldNodes(nodes: readonly SelectionNode[]): FieldNode[]
|
|
|
252
208
|
return result;
|
|
253
209
|
}
|
|
254
210
|
|
|
255
|
-
/**
|
|
256
|
-
* Convert string(s) to ObjectId(s)
|
|
257
|
-
*/
|
|
258
|
-
export function getObjectIds(ids: any[]): Types.ObjectId[];
|
|
259
|
-
export function getObjectIds(ids: any): Types.ObjectId;
|
|
260
|
-
export function getObjectIds<T extends any | any[]>(ids: T): Types.ObjectId | Types.ObjectId[] {
|
|
261
|
-
if (Array.isArray(ids)) {
|
|
262
|
-
return ids.map((id) => new Types.ObjectId(getStringId(id)));
|
|
263
|
-
}
|
|
264
|
-
return new Types.ObjectId(getStringId(ids));
|
|
265
|
-
}
|
|
266
|
-
|
|
267
211
|
/**
|
|
268
212
|
* Get populate options from GraphQL resolve info
|
|
269
213
|
*/
|
|
@@ -356,55 +300,6 @@ export function getPopulatOptionsFromSelections(selectionNodes: readonly Selecti
|
|
|
356
300
|
return populateOptions;
|
|
357
301
|
}
|
|
358
302
|
|
|
359
|
-
/**
|
|
360
|
-
* Get IDs from string of ObjectId array in a flat string array
|
|
361
|
-
*/
|
|
362
|
-
export function getStringIds(elements: any[], options?: { deep?: boolean; unique?: boolean }): string[];
|
|
363
|
-
|
|
364
|
-
export function getStringIds(elements: any, options?: { deep?: boolean; unique?: boolean }): string;
|
|
365
|
-
|
|
366
|
-
export function getStringIds<T extends any | any[]>(
|
|
367
|
-
elements: T,
|
|
368
|
-
options?: { deep?: boolean; unique?: boolean },
|
|
369
|
-
): string | string[] {
|
|
370
|
-
// Process options
|
|
371
|
-
const { deep, unique } = {
|
|
372
|
-
deep: false,
|
|
373
|
-
unique: false,
|
|
374
|
-
...options,
|
|
375
|
-
};
|
|
376
|
-
|
|
377
|
-
// Check elements
|
|
378
|
-
if (!elements) {
|
|
379
|
-
return elements as any;
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
// Init ids
|
|
383
|
-
let ids = [];
|
|
384
|
-
|
|
385
|
-
// Process non array
|
|
386
|
-
if (!Array.isArray(elements)) {
|
|
387
|
-
return getStringId(elements);
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
// Process array
|
|
391
|
-
for (const element of elements) {
|
|
392
|
-
if (Array.isArray(element)) {
|
|
393
|
-
if (deep) {
|
|
394
|
-
ids = ids.concat(getStringIds(element, { deep }));
|
|
395
|
-
}
|
|
396
|
-
} else {
|
|
397
|
-
const id = getStringId(element);
|
|
398
|
-
if (id) {
|
|
399
|
-
ids.push(id);
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
// Return (unique) ID array
|
|
405
|
-
return unique ? _.uniq(ids) : ids;
|
|
406
|
-
}
|
|
407
|
-
|
|
408
303
|
/**
|
|
409
304
|
* Convert all ObjectIds to strings
|
|
410
305
|
*/
|
|
@@ -664,45 +559,3 @@ export async function setPopulates<T = Document | Query<any, any>>(
|
|
|
664
559
|
// =====================================================================================================================
|
|
665
560
|
// Not exported helper functions
|
|
666
561
|
// =====================================================================================================================
|
|
667
|
-
/**
|
|
668
|
-
* Get ID of element as string
|
|
669
|
-
*/
|
|
670
|
-
function getStringId(element: any): string {
|
|
671
|
-
// Check element
|
|
672
|
-
if (!element) {
|
|
673
|
-
return element;
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
// Buffer handling
|
|
677
|
-
if (element instanceof Buffer) {
|
|
678
|
-
return element.toString();
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
// String handling
|
|
682
|
-
if (typeof element === 'string') {
|
|
683
|
-
return element;
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
// Object handling
|
|
687
|
-
if (typeof element === 'object') {
|
|
688
|
-
if (element instanceof Types.ObjectId) {
|
|
689
|
-
return element.toHexString();
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
if (element.id) {
|
|
693
|
-
if (element.id instanceof Buffer && element.toHexString) {
|
|
694
|
-
return element.toHexString();
|
|
695
|
-
}
|
|
696
|
-
return getStringId(element.id);
|
|
697
|
-
} else if (element._id) {
|
|
698
|
-
return getStringId(element._id);
|
|
699
|
-
}
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
// Other types
|
|
703
|
-
if (typeof element.toString === 'function') {
|
|
704
|
-
return element.toString();
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
return undefined;
|
|
708
|
-
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ID helpers.
|
|
3
|
+
*
|
|
4
|
+
* These live in their own file — instead of `db.helper.ts` — so that files which need nothing but
|
|
5
|
+
* ID comparison can reach them without importing `db.helper`, which drags in GraphQL, Mongoose
|
|
6
|
+
* models and `input.helper` behind it.
|
|
7
|
+
*
|
|
8
|
+
* That was not an aesthetic choice. `restricted.decorator.ts` needs exactly two of these functions
|
|
9
|
+
* (`equalIds`, `getIncludedIds`), and importing them from `db.helper` put it on a real runtime
|
|
10
|
+
* cycle:
|
|
11
|
+
*
|
|
12
|
+
* restricted.decorator → db.helper → input.helper → restricted.decorator
|
|
13
|
+
*
|
|
14
|
+
* `input.helper` therefore evaluated while `restricted.decorator` was still initializing. Nothing
|
|
15
|
+
* crashed, but only because every cross-cycle dereference happened to sit inside a function body:
|
|
16
|
+
* a single top-level line in `input.helper` (a module-level alias, an `@Restricted`-decorated class,
|
|
17
|
+
* `design:type` metadata) would have thrown under SWC → CommonJS:
|
|
18
|
+
*
|
|
19
|
+
* ReferenceError: Cannot access 'checkRestricted' before initialization
|
|
20
|
+
*
|
|
21
|
+
* …in the file that drives field-level access control, on a compiler this repo's default build never
|
|
22
|
+
* runs. Moving the ID cluster into this leaf removes the `restricted.decorator → db.helper` edge
|
|
23
|
+
* outright, so the cycle no longer exists rather than merely being disarmed.
|
|
24
|
+
*
|
|
25
|
+
* This file imports only Mongoose's `Types`, lodash, and a type — no framework code — so it can
|
|
26
|
+
* never be part of a cycle itself. Keep it that way.
|
|
27
|
+
*
|
|
28
|
+
* `db.helper` re-exports all four public functions, so every existing import path keeps working.
|
|
29
|
+
* See .claude/rules/architecture.md → "DI Token Placement (SWC-Safe)".
|
|
30
|
+
*/
|
|
31
|
+
import _ = require('lodash');
|
|
32
|
+
import { Types } from 'mongoose';
|
|
33
|
+
|
|
34
|
+
import { IdsType } from '../types/ids.type';
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Checks if all IDs are equal
|
|
38
|
+
*/
|
|
39
|
+
export function equalIds(...ids: IdsType[]): boolean {
|
|
40
|
+
if (!ids) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
const compare = getStringIds(ids[0]);
|
|
44
|
+
if (!compare) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
return ids.every((id) => getStringIds(id) === compare);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Get included ids
|
|
52
|
+
* @param includes IdsType, which should be checked if it contains the ID
|
|
53
|
+
* @param ids IdsType, which should be included
|
|
54
|
+
* @param convert If set the result array will be converted to pure type String array or ObjectId array
|
|
55
|
+
* @return IdsType with IDs which are included, undefined if includes or ids are missing or null if none is included
|
|
56
|
+
*/
|
|
57
|
+
export function getIncludedIds(includes: IdsType, ids: IdsType, convert?: 'string'): string[];
|
|
58
|
+
export function getIncludedIds(includes: IdsType, ids: IdsType, convert?: 'object'): Types.ObjectId[];
|
|
59
|
+
|
|
60
|
+
export function getIncludedIds<T = IdsType>(
|
|
61
|
+
includes: IdsType,
|
|
62
|
+
ids: IdsType | T,
|
|
63
|
+
convert?: 'object' | 'string',
|
|
64
|
+
): null | T[] | undefined {
|
|
65
|
+
if (!includes || !ids) {
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (!Array.isArray(includes)) {
|
|
70
|
+
includes = [includes];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!Array.isArray(ids)) {
|
|
74
|
+
ids = [ids];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
let result = [];
|
|
78
|
+
const includesStrings = getStringIds(includes);
|
|
79
|
+
for (const id of ids) {
|
|
80
|
+
if (includesStrings.includes(getStringIds(id))) {
|
|
81
|
+
result.push(id);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (convert) {
|
|
86
|
+
result = convert === 'string' ? getStringIds(result) : getObjectIds(result);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return result.length ? result : null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Convert string(s) to ObjectId(s)
|
|
94
|
+
*/
|
|
95
|
+
export function getObjectIds(ids: any[]): Types.ObjectId[];
|
|
96
|
+
export function getObjectIds(ids: any): Types.ObjectId;
|
|
97
|
+
export function getObjectIds<T extends any | any[]>(ids: T): Types.ObjectId | Types.ObjectId[] {
|
|
98
|
+
if (Array.isArray(ids)) {
|
|
99
|
+
return ids.map((id) => new Types.ObjectId(getStringId(id)));
|
|
100
|
+
}
|
|
101
|
+
return new Types.ObjectId(getStringId(ids));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Get IDs from string of ObjectId array in a flat string array
|
|
106
|
+
*/
|
|
107
|
+
export function getStringIds(elements: any[], options?: { deep?: boolean; unique?: boolean }): string[];
|
|
108
|
+
|
|
109
|
+
export function getStringIds(elements: any, options?: { deep?: boolean; unique?: boolean }): string;
|
|
110
|
+
|
|
111
|
+
export function getStringIds<T extends any | any[]>(
|
|
112
|
+
elements: T,
|
|
113
|
+
options?: { deep?: boolean; unique?: boolean },
|
|
114
|
+
): string | string[] {
|
|
115
|
+
// Process options
|
|
116
|
+
const { deep, unique } = {
|
|
117
|
+
deep: false,
|
|
118
|
+
unique: false,
|
|
119
|
+
...options,
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// Check elements
|
|
123
|
+
if (!elements) {
|
|
124
|
+
return elements as any;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Init ids
|
|
128
|
+
let ids = [];
|
|
129
|
+
|
|
130
|
+
// Process non array
|
|
131
|
+
if (!Array.isArray(elements)) {
|
|
132
|
+
return getStringId(elements);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Process array
|
|
136
|
+
for (const element of elements) {
|
|
137
|
+
if (Array.isArray(element)) {
|
|
138
|
+
if (deep) {
|
|
139
|
+
ids = ids.concat(getStringIds(element, { deep }));
|
|
140
|
+
}
|
|
141
|
+
} else {
|
|
142
|
+
const id = getStringId(element);
|
|
143
|
+
if (id) {
|
|
144
|
+
ids.push(id);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Return (unique) ID array
|
|
150
|
+
return unique ? _.uniq(ids) : ids;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// =====================================================================================================================
|
|
154
|
+
// Not exported helper functions
|
|
155
|
+
// =====================================================================================================================
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Get ID of element as string
|
|
159
|
+
*/
|
|
160
|
+
function getStringId(element: any): string {
|
|
161
|
+
// Check element
|
|
162
|
+
if (!element) {
|
|
163
|
+
return element;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Buffer handling
|
|
167
|
+
if (element instanceof Buffer) {
|
|
168
|
+
return element.toString();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// String handling
|
|
172
|
+
if (typeof element === 'string') {
|
|
173
|
+
return element;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Object handling
|
|
177
|
+
if (typeof element === 'object') {
|
|
178
|
+
if (element instanceof Types.ObjectId) {
|
|
179
|
+
return element.toHexString();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (element.id) {
|
|
183
|
+
if (element.id instanceof Buffer && element.toHexString) {
|
|
184
|
+
return element.toHexString();
|
|
185
|
+
}
|
|
186
|
+
return getStringId(element.id);
|
|
187
|
+
} else if (element._id) {
|
|
188
|
+
return getStringId(element._id);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Other types
|
|
193
|
+
if (typeof element.toString === 'function') {
|
|
194
|
+
return element.toString();
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return undefined;
|
|
198
|
+
}
|