@rhino-dev/rhino-nestjs 0.2.5
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/LICENSE +21 -0
- package/README.md +427 -0
- package/dist/blueprint/blueprint-parser.d.ts +130 -0
- package/dist/blueprint/blueprint-parser.js +276 -0
- package/dist/blueprint/blueprint-parser.js.map +1 -0
- package/dist/blueprint/blueprint-runner.d.ts +51 -0
- package/dist/blueprint/blueprint-runner.js +238 -0
- package/dist/blueprint/blueprint-runner.js.map +1 -0
- package/dist/blueprint/blueprint-validator.d.ts +19 -0
- package/dist/blueprint/blueprint-validator.js +163 -0
- package/dist/blueprint/blueprint-validator.js.map +1 -0
- package/dist/blueprint/generators/policy-generator.d.ts +40 -0
- package/dist/blueprint/generators/policy-generator.js +157 -0
- package/dist/blueprint/generators/policy-generator.js.map +1 -0
- package/dist/blueprint/generators/prisma-schema-generator.d.ts +43 -0
- package/dist/blueprint/generators/prisma-schema-generator.js +197 -0
- package/dist/blueprint/generators/prisma-schema-generator.js.map +1 -0
- package/dist/blueprint/generators/resource-definition-generator.d.ts +52 -0
- package/dist/blueprint/generators/resource-definition-generator.js +229 -0
- package/dist/blueprint/generators/resource-definition-generator.js.map +1 -0
- package/dist/blueprint/generators/seeder-generator.d.ts +21 -0
- package/dist/blueprint/generators/seeder-generator.js +135 -0
- package/dist/blueprint/generators/seeder-generator.js.map +1 -0
- package/dist/blueprint/generators/test-generator.d.ts +28 -0
- package/dist/blueprint/generators/test-generator.js +275 -0
- package/dist/blueprint/generators/test-generator.js.map +1 -0
- package/dist/blueprint/manifest-manager.d.ts +58 -0
- package/dist/blueprint/manifest-manager.js +137 -0
- package/dist/blueprint/manifest-manager.js.map +1 -0
- package/dist/cli/commands/blueprint.command.d.ts +16 -0
- package/dist/cli/commands/blueprint.command.js +35 -0
- package/dist/cli/commands/blueprint.command.js.map +1 -0
- package/dist/cli/commands/export-postman.command.d.ts +5 -0
- package/dist/cli/commands/export-postman.command.js +39 -0
- package/dist/cli/commands/export-postman.command.js.map +1 -0
- package/dist/cli/commands/export-types.command.d.ts +4 -0
- package/dist/cli/commands/export-types.command.js +36 -0
- package/dist/cli/commands/export-types.command.js.map +1 -0
- package/dist/cli/commands/generate.command.d.ts +6 -0
- package/dist/cli/commands/generate.command.js +212 -0
- package/dist/cli/commands/generate.command.js.map +1 -0
- package/dist/cli/commands/install.command.d.ts +8 -0
- package/dist/cli/commands/install.command.js +205 -0
- package/dist/cli/commands/install.command.js.map +1 -0
- package/dist/cli/index.d.ts +17 -0
- package/dist/cli/index.js +123 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/utils/io.d.ts +12 -0
- package/dist/cli/utils/io.js +74 -0
- package/dist/cli/utils/io.js.map +1 -0
- package/dist/cli/utils/prompt.d.ts +12 -0
- package/dist/cli/utils/prompt.js +78 -0
- package/dist/cli/utils/prompt.js.map +1 -0
- package/dist/constants/tokens.d.ts +3 -0
- package/dist/constants/tokens.js +7 -0
- package/dist/constants/tokens.js.map +1 -0
- package/dist/controllers/auth.controller.d.ts +59 -0
- package/dist/controllers/auth.controller.js +121 -0
- package/dist/controllers/auth.controller.js.map +1 -0
- package/dist/controllers/global.controller.d.ts +44 -0
- package/dist/controllers/global.controller.js +357 -0
- package/dist/controllers/global.controller.js.map +1 -0
- package/dist/controllers/invitation.controller.d.ts +30 -0
- package/dist/controllers/invitation.controller.js +111 -0
- package/dist/controllers/invitation.controller.js.map +1 -0
- package/dist/controllers/nested.controller.d.ts +12 -0
- package/dist/controllers/nested.controller.js +69 -0
- package/dist/controllers/nested.controller.js.map +1 -0
- package/dist/decorators/index.d.ts +33 -0
- package/dist/decorators/index.js +79 -0
- package/dist/decorators/index.js.map +1 -0
- package/dist/errors/rhino-exception.d.ts +24 -0
- package/dist/errors/rhino-exception.js +47 -0
- package/dist/errors/rhino-exception.js.map +1 -0
- package/dist/exporters/postman-exporter.d.ts +21 -0
- package/dist/exporters/postman-exporter.js +396 -0
- package/dist/exporters/postman-exporter.js.map +1 -0
- package/dist/exporters/typescript-exporter.d.ts +23 -0
- package/dist/exporters/typescript-exporter.js +129 -0
- package/dist/exporters/typescript-exporter.js.map +1 -0
- package/dist/guards/jwt-auth.guard.d.ts +16 -0
- package/dist/guards/jwt-auth.guard.js +61 -0
- package/dist/guards/jwt-auth.guard.js.map +1 -0
- package/dist/guards/resource-policy.guard.d.ts +12 -0
- package/dist/guards/resource-policy.guard.js +111 -0
- package/dist/guards/resource-policy.guard.js.map +1 -0
- package/dist/index.d.ts +60 -0
- package/dist/index.js +171 -0
- package/dist/index.js.map +1 -0
- package/dist/interceptors/hidden-columns.interceptor.d.ts +18 -0
- package/dist/interceptors/hidden-columns.interceptor.js +66 -0
- package/dist/interceptors/hidden-columns.interceptor.js.map +1 -0
- package/dist/interceptors/response.interceptor.d.ts +19 -0
- package/dist/interceptors/response.interceptor.js +47 -0
- package/dist/interceptors/response.interceptor.js.map +1 -0
- package/dist/interfaces/rhino-config.interface.d.ts +103 -0
- package/dist/interfaces/rhino-config.interface.js +3 -0
- package/dist/interfaces/rhino-config.interface.js.map +1 -0
- package/dist/interfaces/rhino-request.interface.d.ts +12 -0
- package/dist/interfaces/rhino-request.interface.js +3 -0
- package/dist/interfaces/rhino-request.interface.js.map +1 -0
- package/dist/middleware/resolve-organization.middleware.d.ts +15 -0
- package/dist/middleware/resolve-organization.middleware.js +38 -0
- package/dist/middleware/resolve-organization.middleware.js.map +1 -0
- package/dist/middleware/route-group.middleware.d.ts +17 -0
- package/dist/middleware/route-group.middleware.js +60 -0
- package/dist/middleware/route-group.middleware.js.map +1 -0
- package/dist/middleware/tenant-route-rewrite.d.ts +76 -0
- package/dist/middleware/tenant-route-rewrite.js +124 -0
- package/dist/middleware/tenant-route-rewrite.js.map +1 -0
- package/dist/policies/resource-policy.d.ts +25 -0
- package/dist/policies/resource-policy.js +77 -0
- package/dist/policies/resource-policy.js.map +1 -0
- package/dist/prisma/prisma-soft-delete.extension.d.ts +16 -0
- package/dist/prisma/prisma-soft-delete.extension.js +57 -0
- package/dist/prisma/prisma-soft-delete.extension.js.map +1 -0
- package/dist/prisma/prisma-uuid.extension.d.ts +9 -0
- package/dist/prisma/prisma-uuid.extension.js +28 -0
- package/dist/prisma/prisma-uuid.extension.js.map +1 -0
- package/dist/prisma/prisma.service.d.ts +38 -0
- package/dist/prisma/prisma.service.js +100 -0
- package/dist/prisma/prisma.service.js.map +1 -0
- package/dist/rhino.config.d.ts +43 -0
- package/dist/rhino.config.js +130 -0
- package/dist/rhino.config.js.map +1 -0
- package/dist/rhino.module.d.ts +61 -0
- package/dist/rhino.module.js +268 -0
- package/dist/rhino.module.js.map +1 -0
- package/dist/services/audit.service.d.ts +35 -0
- package/dist/services/audit.service.js +92 -0
- package/dist/services/audit.service.js.map +1 -0
- package/dist/services/auth.service.d.ts +33 -0
- package/dist/services/auth.service.js +157 -0
- package/dist/services/auth.service.js.map +1 -0
- package/dist/services/invitation.service.d.ts +37 -0
- package/dist/services/invitation.service.js +143 -0
- package/dist/services/invitation.service.js.map +1 -0
- package/dist/services/nested.service.d.ts +34 -0
- package/dist/services/nested.service.js +163 -0
- package/dist/services/nested.service.js.map +1 -0
- package/dist/services/organization.service.d.ts +13 -0
- package/dist/services/organization.service.js +60 -0
- package/dist/services/organization.service.js.map +1 -0
- package/dist/services/query-builder.service.d.ts +31 -0
- package/dist/services/query-builder.service.js +151 -0
- package/dist/services/query-builder.service.js.map +1 -0
- package/dist/services/resource.service.d.ts +38 -0
- package/dist/services/resource.service.js +230 -0
- package/dist/services/resource.service.js.map +1 -0
- package/dist/services/route-registration.service.d.ts +40 -0
- package/dist/services/route-registration.service.js +68 -0
- package/dist/services/route-registration.service.js.map +1 -0
- package/dist/services/scope.service.d.ts +16 -0
- package/dist/services/scope.service.js +32 -0
- package/dist/services/scope.service.js.map +1 -0
- package/dist/services/serializer.service.d.ts +36 -0
- package/dist/services/serializer.service.js +90 -0
- package/dist/services/serializer.service.js.map +1 -0
- package/dist/services/validation.service.d.ts +50 -0
- package/dist/services/validation.service.js +203 -0
- package/dist/services/validation.service.js.map +1 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -0
- package/dist/utils/fk-chain-walker.d.ts +30 -0
- package/dist/utils/fk-chain-walker.js +23 -0
- package/dist/utils/fk-chain-walker.js.map +1 -0
- package/dist/utils/format.d.ts +7 -0
- package/dist/utils/format.js +28 -0
- package/dist/utils/format.js.map +1 -0
- package/dist/utils/model-builder.d.ts +64 -0
- package/dist/utils/model-builder.js +67 -0
- package/dist/utils/model-builder.js.map +1 -0
- package/dist/utils/permission-matcher.d.ts +46 -0
- package/dist/utils/permission-matcher.js +120 -0
- package/dist/utils/permission-matcher.js.map +1 -0
- package/dist/utils/scope-discovery.d.ts +34 -0
- package/dist/utils/scope-discovery.js +102 -0
- package/dist/utils/scope-discovery.js.map +1 -0
- package/package.json +64 -0
- package/stubs/skills/rhino-audit.md +184 -0
- package/stubs/skills/rhino-bugfix.md +117 -0
- package/stubs/skills/rhino-deploy.md +5 -0
- package/stubs/skills/rhino-docs.md +147 -0
- package/stubs/skills/rhino-feature.md +146 -0
- package/stubs/skills/rhino-migrate.md +181 -0
- package/stubs/skills/rhino-model.md +180 -0
- package/stubs/skills/rhino-plan.md +5 -0
- package/stubs/skills/rhino-policy.md +183 -0
- package/stubs/skills/rhino-refactor.md +147 -0
- package/stubs/skills/rhino-review.md +113 -0
- package/stubs/skills/rhino-scope.md +140 -0
- package/stubs/skills/rhino-test.md +233 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.defineModel = defineModel;
|
|
4
|
+
function defineModel(def) {
|
|
5
|
+
const out = {
|
|
6
|
+
model: def.model,
|
|
7
|
+
policy: def.policy,
|
|
8
|
+
};
|
|
9
|
+
// features
|
|
10
|
+
if (def.features?.multiTenant)
|
|
11
|
+
out.belongsToOrganization = true;
|
|
12
|
+
if (def.features?.softDeletes)
|
|
13
|
+
out.softDeletes = true;
|
|
14
|
+
if (def.features?.auditTrail)
|
|
15
|
+
out.hasAuditTrail = true;
|
|
16
|
+
if (def.features?.uuid)
|
|
17
|
+
out.hasUuid = true;
|
|
18
|
+
// query
|
|
19
|
+
if (def.query) {
|
|
20
|
+
if (def.query.filters)
|
|
21
|
+
out.allowedFilters = def.query.filters;
|
|
22
|
+
if (def.query.sorts)
|
|
23
|
+
out.allowedSorts = def.query.sorts;
|
|
24
|
+
if (def.query.defaultSort)
|
|
25
|
+
out.defaultSort = def.query.defaultSort;
|
|
26
|
+
if (def.query.search)
|
|
27
|
+
out.allowedSearch = def.query.search;
|
|
28
|
+
if (def.query.includes)
|
|
29
|
+
out.allowedIncludes = def.query.includes;
|
|
30
|
+
if (def.query.fields)
|
|
31
|
+
out.allowedFields = def.query.fields;
|
|
32
|
+
if (def.query.pagination != null)
|
|
33
|
+
out.paginationEnabled = def.query.pagination;
|
|
34
|
+
if (def.query.perPage != null)
|
|
35
|
+
out.perPage = def.query.perPage;
|
|
36
|
+
}
|
|
37
|
+
// validation
|
|
38
|
+
if (def.validation?.base)
|
|
39
|
+
out.validation = def.validation.base;
|
|
40
|
+
if (def.validation?.store)
|
|
41
|
+
out.validationStore = def.validation.store;
|
|
42
|
+
if (def.validation?.update)
|
|
43
|
+
out.validationUpdate = def.validation.update;
|
|
44
|
+
// response
|
|
45
|
+
if (def.response?.hidden)
|
|
46
|
+
out.additionalHiddenColumns = def.response.hidden;
|
|
47
|
+
if (def.response?.computed)
|
|
48
|
+
out.computedAttributes = def.response.computed;
|
|
49
|
+
// restrict
|
|
50
|
+
if (def.restrict?.except)
|
|
51
|
+
out.exceptActions = def.restrict.except;
|
|
52
|
+
// pass-through
|
|
53
|
+
if (def.middleware)
|
|
54
|
+
out.middleware = def.middleware;
|
|
55
|
+
if (def.actionMiddleware)
|
|
56
|
+
out.actionMiddleware = def.actionMiddleware;
|
|
57
|
+
if (def.scopes)
|
|
58
|
+
out.scopes = def.scopes;
|
|
59
|
+
if (def.fkConstraints)
|
|
60
|
+
out.fkConstraints = def.fkConstraints;
|
|
61
|
+
if (def.owner)
|
|
62
|
+
out.owner = def.owner;
|
|
63
|
+
if (def.auditExclude)
|
|
64
|
+
out.auditExclude = def.auditExclude;
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=model-builder.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"model-builder.js","sourceRoot":"","sources":["../../src/utils/model-builder.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coerce a raw permissions value into a string array.
|
|
3
|
+
*
|
|
4
|
+
* BP-008 — databases without a native array type (SQLite, older MySQL) must
|
|
5
|
+
* store permissions as JSON strings or comma-separated lists. Laravel's
|
|
6
|
+
* Eloquent `$casts = ['permissions' => 'array']` decodes JSON on read;
|
|
7
|
+
* Prisma has no equivalent. This helper accepts all common shapes so the
|
|
8
|
+
* matcher works without a consumer-side hydration hack.
|
|
9
|
+
*
|
|
10
|
+
* Accepted inputs (in order of preference):
|
|
11
|
+
* - string[] → returned as-is
|
|
12
|
+
* - JSON array string → parsed
|
|
13
|
+
* - comma-separated string → split & trimmed
|
|
14
|
+
* - null / undefined / other → []
|
|
15
|
+
*/
|
|
16
|
+
export declare function coercePermissions(raw: unknown): string[];
|
|
17
|
+
/**
|
|
18
|
+
* Match a `{slug}.{action}` permission against a set of granted permissions.
|
|
19
|
+
*
|
|
20
|
+
* Supports wildcards identically to the Laravel version:
|
|
21
|
+
* - `*` → grants everything
|
|
22
|
+
* - `{slug}.*` → grants all actions on a specific slug
|
|
23
|
+
* - `{slug}.{act}` → exact match
|
|
24
|
+
*
|
|
25
|
+
* The `granted` argument accepts anything `coercePermissions` accepts: a
|
|
26
|
+
* real array, a JSON-string array, or a comma-separated string.
|
|
27
|
+
*/
|
|
28
|
+
export declare function matchesPermission(permission: string, granted: string[] | string | null | undefined): boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Resolve the role slug for a user in a specific organization.
|
|
31
|
+
* Expects a user object shaped like the Laravel user with userRoles relation.
|
|
32
|
+
*/
|
|
33
|
+
export declare function resolveUserRoleSlug(user: any, organizationId: number | string | null | undefined): string | null;
|
|
34
|
+
/**
|
|
35
|
+
* Resolve permissions granted to a user in an organization context.
|
|
36
|
+
* Tenant context → aggregates permissions from all user_roles entries
|
|
37
|
+
* matching the organization.
|
|
38
|
+
* No org context → returns the user's top-level permissions array.
|
|
39
|
+
*/
|
|
40
|
+
export declare function resolveUserPermissions(user: any, organizationId?: number | string | null): string[];
|
|
41
|
+
/**
|
|
42
|
+
* Top-level permission check mirroring the Laravel `hasPermission` method.
|
|
43
|
+
*/
|
|
44
|
+
export declare function userHasPermission(user: any, permission: string, organization?: {
|
|
45
|
+
id: number | string;
|
|
46
|
+
} | null): boolean;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.coercePermissions = coercePermissions;
|
|
4
|
+
exports.matchesPermission = matchesPermission;
|
|
5
|
+
exports.resolveUserRoleSlug = resolveUserRoleSlug;
|
|
6
|
+
exports.resolveUserPermissions = resolveUserPermissions;
|
|
7
|
+
exports.userHasPermission = userHasPermission;
|
|
8
|
+
/**
|
|
9
|
+
* Coerce a raw permissions value into a string array.
|
|
10
|
+
*
|
|
11
|
+
* BP-008 — databases without a native array type (SQLite, older MySQL) must
|
|
12
|
+
* store permissions as JSON strings or comma-separated lists. Laravel's
|
|
13
|
+
* Eloquent `$casts = ['permissions' => 'array']` decodes JSON on read;
|
|
14
|
+
* Prisma has no equivalent. This helper accepts all common shapes so the
|
|
15
|
+
* matcher works without a consumer-side hydration hack.
|
|
16
|
+
*
|
|
17
|
+
* Accepted inputs (in order of preference):
|
|
18
|
+
* - string[] → returned as-is
|
|
19
|
+
* - JSON array string → parsed
|
|
20
|
+
* - comma-separated string → split & trimmed
|
|
21
|
+
* - null / undefined / other → []
|
|
22
|
+
*/
|
|
23
|
+
function coercePermissions(raw) {
|
|
24
|
+
if (Array.isArray(raw))
|
|
25
|
+
return raw;
|
|
26
|
+
if (typeof raw !== 'string')
|
|
27
|
+
return [];
|
|
28
|
+
const trimmed = raw.trim();
|
|
29
|
+
if (trimmed === '')
|
|
30
|
+
return [];
|
|
31
|
+
if (trimmed.startsWith('[')) {
|
|
32
|
+
try {
|
|
33
|
+
const parsed = JSON.parse(trimmed);
|
|
34
|
+
if (Array.isArray(parsed))
|
|
35
|
+
return parsed.map(String);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
/* fall through to comma split */
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return trimmed
|
|
42
|
+
.split(',')
|
|
43
|
+
.map((s) => s.trim())
|
|
44
|
+
.filter((s) => s.length > 0);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Match a `{slug}.{action}` permission against a set of granted permissions.
|
|
48
|
+
*
|
|
49
|
+
* Supports wildcards identically to the Laravel version:
|
|
50
|
+
* - `*` → grants everything
|
|
51
|
+
* - `{slug}.*` → grants all actions on a specific slug
|
|
52
|
+
* - `{slug}.{act}` → exact match
|
|
53
|
+
*
|
|
54
|
+
* The `granted` argument accepts anything `coercePermissions` accepts: a
|
|
55
|
+
* real array, a JSON-string array, or a comma-separated string.
|
|
56
|
+
*/
|
|
57
|
+
function matchesPermission(permission, granted) {
|
|
58
|
+
const list = coercePermissions(granted);
|
|
59
|
+
if (list.length === 0)
|
|
60
|
+
return false;
|
|
61
|
+
const slug = permission.split('.')[0] ?? '';
|
|
62
|
+
const slugWildcard = `${slug}.*`;
|
|
63
|
+
for (const p of list) {
|
|
64
|
+
if (p === permission || p === '*' || p === slugWildcard)
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Resolve the role slug for a user in a specific organization.
|
|
71
|
+
* Expects a user object shaped like the Laravel user with userRoles relation.
|
|
72
|
+
*/
|
|
73
|
+
function resolveUserRoleSlug(user, organizationId) {
|
|
74
|
+
if (!user || organizationId == null)
|
|
75
|
+
return null;
|
|
76
|
+
const userRoles = user.userRoles ?? user.user_roles ?? [];
|
|
77
|
+
for (const ur of userRoles) {
|
|
78
|
+
const orgId = ur.organizationId ?? ur.organization_id;
|
|
79
|
+
if (orgId === organizationId) {
|
|
80
|
+
return ur.role?.slug ?? ur.roleSlug ?? ur.role_slug ?? null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Resolve permissions granted to a user in an organization context.
|
|
87
|
+
* Tenant context → aggregates permissions from all user_roles entries
|
|
88
|
+
* matching the organization.
|
|
89
|
+
* No org context → returns the user's top-level permissions array.
|
|
90
|
+
*/
|
|
91
|
+
function resolveUserPermissions(user, organizationId) {
|
|
92
|
+
if (!user)
|
|
93
|
+
return [];
|
|
94
|
+
if (organizationId != null) {
|
|
95
|
+
const userRoles = user.userRoles ?? user.user_roles ?? [];
|
|
96
|
+
const all = [];
|
|
97
|
+
for (const ur of userRoles) {
|
|
98
|
+
const orgId = ur.organizationId ?? ur.organization_id;
|
|
99
|
+
if (orgId === organizationId) {
|
|
100
|
+
// BP-008: coerce into an array first — value may arrive as a raw
|
|
101
|
+
// JSON string from SQLite/MySQL or similar DBs without native arrays.
|
|
102
|
+
for (const p of coercePermissions(ur.permissions))
|
|
103
|
+
all.push(p);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return all;
|
|
107
|
+
}
|
|
108
|
+
return coercePermissions(user.permissions);
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Top-level permission check mirroring the Laravel `hasPermission` method.
|
|
112
|
+
*/
|
|
113
|
+
function userHasPermission(user, permission, organization) {
|
|
114
|
+
if (!user)
|
|
115
|
+
return false;
|
|
116
|
+
const orgId = organization ? organization.id : null;
|
|
117
|
+
const permissions = resolveUserPermissions(user, orgId);
|
|
118
|
+
return matchesPermission(permission, permissions);
|
|
119
|
+
}
|
|
120
|
+
//# sourceMappingURL=permission-matcher.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"permission-matcher.js","sourceRoot":"","sources":["../../src/utils/permission-matcher.ts"],"names":[],"mappings":";;AAeA,8CAiBC;AAaD,8CAYC;AAMD,kDAUC;AAQD,wDAgBC;AAKD,8CASC;AA/GD;;;;;;;;;;;;;;GAcG;AACH,SAAgB,iBAAiB,CAAC,GAAY;IAC5C,IAAI,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC;QAAE,OAAO,GAAe,CAAC;IAC/C,IAAI,OAAO,GAAG,KAAK,QAAQ;QAAE,OAAO,EAAE,CAAC;IACvC,MAAM,OAAO,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;IAC3B,IAAI,OAAO,KAAK,EAAE;QAAE,OAAO,EAAE,CAAC;IAC9B,IAAI,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QAC5B,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;YACnC,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC;gBAAE,OAAO,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QACvD,CAAC;QAAC,MAAM,CAAC;YACP,iCAAiC;QACnC,CAAC;IACH,CAAC;IACD,OAAO,OAAO;SACX,KAAK,CAAC,GAAG,CAAC;SACV,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;SACpB,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;AACjC,CAAC;AAED;;;;;;;;;;GAUG;AACH,SAAgB,iBAAiB,CAC/B,UAAkB,EAClB,OAA6C;IAE7C,MAAM,IAAI,GAAG,iBAAiB,CAAC,OAAO,CAAC,CAAC;IACxC,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IACpC,MAAM,IAAI,GAAG,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IAC5C,MAAM,YAAY,GAAG,GAAG,IAAI,IAAI,CAAC;IACjC,KAAK,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;QACrB,IAAI,CAAC,KAAK,UAAU,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,YAAY;YAAE,OAAO,IAAI,CAAC;IACvE,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;GAGG;AACH,SAAgB,mBAAmB,CAAC,IAAS,EAAE,cAAkD;IAC/F,IAAI,CAAC,IAAI,IAAI,cAAc,IAAI,IAAI;QAAE,OAAO,IAAI,CAAC;IACjD,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC,UAAU,IAAI,EAAE,CAAC;IAC1D,KAAK,MAAM,EAAE,IAAI,SAAS,EAAE,CAAC;QAC3B,MAAM,KAAK,GAAG,EAAE,CAAC,cAAc,IAAI,EAAE,CAAC,eAAe,CAAC;QACtD,IAAI,KAAK,KAAK,cAAc,EAAE,CAAC;YAC7B,OAAO,EAAE,CAAC,IAAI,EAAE,IAAI,IAAI,EAAE,CAAC,QAAQ,IAAI,EAAE,CAAC,SAAS,IAAI,IAAI,CAAC;QAC9D,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;GAKG;AACH,SAAgB,sBAAsB,CAAC,IAAS,EAAE,cAAuC;IACvF,IAAI,CAAC,IAAI;QAAE,OAAO,EAAE,CAAC;IACrB,IAAI,cAAc,IAAI,IAAI,EAAE,CAAC;QAC3B,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC,UAAU,IAAI,EAAE,CAAC;QAC1D,MAAM,GAAG,GAAa,EAAE,CAAC;QACzB,KAAK,MAAM,EAAE,IAAI,SAAS,EAAE,CAAC;YAC3B,MAAM,KAAK,GAAG,EAAE,CAAC,cAAc,IAAI,EAAE,CAAC,eAAe,CAAC;YACtD,IAAI,KAAK,KAAK,cAAc,EAAE,CAAC;gBAC7B,iEAAiE;gBACjE,sEAAsE;gBACtE,KAAK,MAAM,CAAC,IAAI,iBAAiB,CAAC,EAAE,CAAC,WAAW,CAAC;oBAAE,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YACjE,CAAC;QACH,CAAC;QACD,OAAO,GAAG,CAAC;IACb,CAAC;IACD,OAAO,iBAAiB,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;AAC7C,CAAC;AAED;;GAEG;AACH,SAAgB,iBAAiB,CAC/B,IAAS,EACT,UAAkB,EAClB,YAA6C;IAE7C,IAAI,CAAC,IAAI;QAAE,OAAO,KAAK,CAAC;IACxB,MAAM,KAAK,GAAG,YAAY,CAAC,CAAC,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;IACpD,MAAM,WAAW,GAAG,sBAAsB,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IACxD,OAAO,iBAAiB,CAAC,UAAU,EAAE,WAAW,CAAC,CAAC;AACpD,CAAC"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { RhinoConfig } from '../interfaces/rhino-config.interface';
|
|
2
|
+
export interface ScopeDiscoveryOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Absolute or cwd-relative directory to scan. Defaults to `./src/scopes`.
|
|
5
|
+
*/
|
|
6
|
+
scopesDir?: string;
|
|
7
|
+
/**
|
|
8
|
+
* File extensions to probe, in order. Defaults to `['.ts', '.js']`.
|
|
9
|
+
*/
|
|
10
|
+
extensions?: string[];
|
|
11
|
+
/**
|
|
12
|
+
* When true, silently skip models whose scope file cannot be loaded.
|
|
13
|
+
* When false, throw the underlying require error. Default: true.
|
|
14
|
+
*/
|
|
15
|
+
silent?: boolean;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Filesystem-based scope auto-discovery, mirroring Laravel's naming-convention
|
|
19
|
+
* discovery (`App\Models\Scopes\{Model}Scope`).
|
|
20
|
+
*
|
|
21
|
+
* For each model registered in `config.models`, the helper looks for a file
|
|
22
|
+
* named `{ModelName}Scope.{ts,js}` in `scopesDir`. If found and the file
|
|
23
|
+
* default-exports a class (or exports a class under `{ModelName}Scope`), that
|
|
24
|
+
* class is appended to the model's `scopes` array (deduped).
|
|
25
|
+
*
|
|
26
|
+
* Usage:
|
|
27
|
+
*
|
|
28
|
+
* const config = autoDiscoverScopes({ models: {...} }, { scopesDir: 'src/scopes' });
|
|
29
|
+
* RhinoModule.forRoot(config);
|
|
30
|
+
*
|
|
31
|
+
* The function is synchronous and returns a new config object — it does not
|
|
32
|
+
* mutate the input.
|
|
33
|
+
*/
|
|
34
|
+
export declare function autoDiscoverScopes(config: RhinoConfig, options?: ScopeDiscoveryOptions): RhinoConfig;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.autoDiscoverScopes = autoDiscoverScopes;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
/**
|
|
40
|
+
* Filesystem-based scope auto-discovery, mirroring Laravel's naming-convention
|
|
41
|
+
* discovery (`App\Models\Scopes\{Model}Scope`).
|
|
42
|
+
*
|
|
43
|
+
* For each model registered in `config.models`, the helper looks for a file
|
|
44
|
+
* named `{ModelName}Scope.{ts,js}` in `scopesDir`. If found and the file
|
|
45
|
+
* default-exports a class (or exports a class under `{ModelName}Scope`), that
|
|
46
|
+
* class is appended to the model's `scopes` array (deduped).
|
|
47
|
+
*
|
|
48
|
+
* Usage:
|
|
49
|
+
*
|
|
50
|
+
* const config = autoDiscoverScopes({ models: {...} }, { scopesDir: 'src/scopes' });
|
|
51
|
+
* RhinoModule.forRoot(config);
|
|
52
|
+
*
|
|
53
|
+
* The function is synchronous and returns a new config object — it does not
|
|
54
|
+
* mutate the input.
|
|
55
|
+
*/
|
|
56
|
+
function autoDiscoverScopes(config, options = {}) {
|
|
57
|
+
const scopesDir = path.resolve(options.scopesDir ?? path.join(process.cwd(), 'src', 'scopes'));
|
|
58
|
+
const extensions = options.extensions ?? ['.ts', '.js'];
|
|
59
|
+
const silent = options.silent ?? true;
|
|
60
|
+
if (!fs.existsSync(scopesDir))
|
|
61
|
+
return config;
|
|
62
|
+
const newModels = {};
|
|
63
|
+
for (const [slug, reg] of Object.entries(config.models ?? {})) {
|
|
64
|
+
const modelClassName = pascal(reg.model);
|
|
65
|
+
const scopeClass = loadScopeFile(scopesDir, modelClassName, extensions, silent);
|
|
66
|
+
if (scopeClass) {
|
|
67
|
+
const existing = reg.scopes ?? [];
|
|
68
|
+
const deduped = existing.includes(scopeClass) ? existing : [...existing, scopeClass];
|
|
69
|
+
newModels[slug] = { ...reg, scopes: deduped };
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
newModels[slug] = reg;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return { ...config, models: newModels };
|
|
76
|
+
}
|
|
77
|
+
function pascal(name) {
|
|
78
|
+
return name.charAt(0).toUpperCase() + name.slice(1);
|
|
79
|
+
}
|
|
80
|
+
function loadScopeFile(dir, modelName, extensions, silent) {
|
|
81
|
+
const basename = `${modelName}Scope`;
|
|
82
|
+
for (const ext of extensions) {
|
|
83
|
+
const candidate = path.join(dir, `${basename}${ext}`);
|
|
84
|
+
if (!fs.existsSync(candidate))
|
|
85
|
+
continue;
|
|
86
|
+
try {
|
|
87
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
88
|
+
const mod = require(candidate);
|
|
89
|
+
// Support: `module.exports = Class`, `export default Class`, `export class XScope`
|
|
90
|
+
if (mod && typeof mod === 'function')
|
|
91
|
+
return mod;
|
|
92
|
+
return mod?.default ?? mod?.[basename] ?? null;
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
if (!silent)
|
|
96
|
+
throw err;
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=scope-discovery.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scope-discovery.js","sourceRoot":"","sources":["../../src/utils/scope-discovery.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqCA,gDAwBC;AA7DD,uCAAyB;AACzB,2CAA6B;AAmB7B;;;;;;;;;;;;;;;;GAgBG;AACH,SAAgB,kBAAkB,CAChC,MAAmB,EACnB,UAAiC,EAAE;IAEnC,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,SAAS,IAAI,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,QAAQ,CAAC,CAAC,CAAC;IAC/F,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,IAAI,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;IACxD,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,IAAI,CAAC;IAEtC,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC;QAAE,OAAO,MAAM,CAAC;IAE7C,MAAM,SAAS,GAAsC,EAAE,CAAC;IACxD,KAAK,MAAM,CAAC,IAAI,EAAE,GAAG,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,IAAI,EAAE,CAAC,EAAE,CAAC;QAC9D,MAAM,cAAc,GAAG,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QACzC,MAAM,UAAU,GAAG,aAAa,CAAC,SAAS,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,CAAC,CAAC;QAChF,IAAI,UAAU,EAAE,CAAC;YACf,MAAM,QAAQ,GAAG,GAAG,CAAC,MAAM,IAAI,EAAE,CAAC;YAClC,MAAM,OAAO,GAAG,QAAQ,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,GAAG,QAAQ,EAAE,UAAU,CAAC,CAAC;YACrF,SAAS,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC;QAChD,CAAC;aAAM,CAAC;YACN,SAAS,CAAC,IAAI,CAAC,GAAG,GAAG,CAAC;QACxB,CAAC;IACH,CAAC;IAED,OAAO,EAAE,GAAG,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;AAC1C,CAAC;AAED,SAAS,MAAM,CAAC,IAAY;IAC1B,OAAO,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AACtD,CAAC;AAED,SAAS,aAAa,CACpB,GAAW,EACX,SAAiB,EACjB,UAAoB,EACpB,MAAe;IAEf,MAAM,QAAQ,GAAG,GAAG,SAAS,OAAO,CAAC;IACrC,KAAK,MAAM,GAAG,IAAI,UAAU,EAAE,CAAC;QAC7B,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,QAAQ,GAAG,GAAG,EAAE,CAAC,CAAC;QACtD,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC;YAAE,SAAS;QACxC,IAAI,CAAC;YACH,8DAA8D;YAC9D,MAAM,GAAG,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC;YAC/B,mFAAmF;YACnF,IAAI,GAAG,IAAI,OAAO,GAAG,KAAK,UAAU;gBAAE,OAAO,GAAG,CAAC;YACjD,OAAO,GAAG,EAAE,OAAO,IAAI,GAAG,EAAE,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC;QACjD,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,CAAC,MAAM;gBAAE,MAAM,GAAG,CAAC;YACvB,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rhino-dev/rhino-nestjs",
|
|
3
|
+
"version": "0.2.5",
|
|
4
|
+
"description": "Rhino for NestJS — auto-generated REST APIs from model definitions.",
|
|
5
|
+
"author": "Rhino",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"publishConfig": {
|
|
8
|
+
"access": "public"
|
|
9
|
+
},
|
|
10
|
+
"main": "dist/index.js",
|
|
11
|
+
"types": "dist/index.d.ts",
|
|
12
|
+
"files": [
|
|
13
|
+
"dist",
|
|
14
|
+
"stubs",
|
|
15
|
+
"README.md"
|
|
16
|
+
],
|
|
17
|
+
"bin": {
|
|
18
|
+
"rhino": "./dist/cli/index.js"
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsc -p tsconfig.build.json",
|
|
22
|
+
"test": "jest",
|
|
23
|
+
"test:watch": "jest --watch",
|
|
24
|
+
"test:cov": "jest --coverage"
|
|
25
|
+
},
|
|
26
|
+
"peerDependencies": {
|
|
27
|
+
"@nestjs/common": "^10.0.0 || ^11.0.0",
|
|
28
|
+
"@nestjs/core": "^10.0.0 || ^11.0.0",
|
|
29
|
+
"@prisma/client": "^5.0.0 || ^6.0.0",
|
|
30
|
+
"reflect-metadata": "^0.1.13 || ^0.2.0",
|
|
31
|
+
"rxjs": "^7.0.0"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@nestjs/passport": "^10.0.3",
|
|
35
|
+
"bcryptjs": "^2.4.3",
|
|
36
|
+
"js-yaml": "^4.1.0",
|
|
37
|
+
"jsonwebtoken": "^9.0.2",
|
|
38
|
+
"passport": "^0.7.0",
|
|
39
|
+
"passport-jwt": "^4.0.1",
|
|
40
|
+
"zod": "^3.23.0"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@nestjs/common": "^10.4.0",
|
|
44
|
+
"@nestjs/core": "^10.4.0",
|
|
45
|
+
"@nestjs/platform-express": "^10.4.0",
|
|
46
|
+
"@nestjs/testing": "^10.4.0",
|
|
47
|
+
"@prisma/client": "^5.22.0",
|
|
48
|
+
"@types/bcryptjs": "^2.4.6",
|
|
49
|
+
"@types/express": "^4.17.21",
|
|
50
|
+
"@types/jest": "^29.5.12",
|
|
51
|
+
"@types/js-yaml": "^4.0.9",
|
|
52
|
+
"@types/jsonwebtoken": "^9.0.6",
|
|
53
|
+
"@types/node": "^20.11.0",
|
|
54
|
+
"@types/passport-jwt": "^4.0.1",
|
|
55
|
+
"@types/supertest": "^6.0.2",
|
|
56
|
+
"jest": "^29.7.0",
|
|
57
|
+
"prisma": "^5.22.0",
|
|
58
|
+
"reflect-metadata": "^0.2.2",
|
|
59
|
+
"rxjs": "^7.8.1",
|
|
60
|
+
"supertest": "^7.0.0",
|
|
61
|
+
"ts-jest": "^29.1.2",
|
|
62
|
+
"typescript": "^5.4.0"
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rhino-audit
|
|
3
|
+
description: Enable the audit trail on an Rhino NestJS model — Prisma schema, model registration, exclusions, and tests.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
You are enabling the audit trail for one or more models in an Rhino NestJS application.
|
|
7
|
+
|
|
8
|
+
## Step 1: Read Context
|
|
9
|
+
|
|
10
|
+
Read:
|
|
11
|
+
- `prisma/schema.prisma` — check if `AuditLog` model already exists.
|
|
12
|
+
- `src/app.module.ts` — current model registrations.
|
|
13
|
+
- `src/services/audit.service.ts` — understand what is logged and how.
|
|
14
|
+
- `CLAUDE.md` — development rules.
|
|
15
|
+
|
|
16
|
+
## Step 2: Add the AuditLog Prisma Model
|
|
17
|
+
|
|
18
|
+
If `AuditLog` does not already exist in `prisma/schema.prisma`, add it:
|
|
19
|
+
|
|
20
|
+
```prisma
|
|
21
|
+
model AuditLog {
|
|
22
|
+
id Int @id @default(autoincrement())
|
|
23
|
+
auditableType String
|
|
24
|
+
auditableId String
|
|
25
|
+
action String // created | updated | deleted | forceDeleted | restored
|
|
26
|
+
oldValues Json?
|
|
27
|
+
newValues Json?
|
|
28
|
+
userId Int?
|
|
29
|
+
organizationId Int?
|
|
30
|
+
ipAddress String?
|
|
31
|
+
userAgent String?
|
|
32
|
+
createdAt DateTime @default(now())
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Run migration:
|
|
37
|
+
```bash
|
|
38
|
+
npx prisma migrate dev --name add_audit_logs
|
|
39
|
+
npx prisma generate
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Step 3: Enable Audit Trail on the Model
|
|
43
|
+
|
|
44
|
+
In `app.module.ts`, set `hasAuditTrail: true` and configure `auditExclude` for any sensitive fields:
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
users: {
|
|
48
|
+
model: 'user',
|
|
49
|
+
hasAuditTrail: true,
|
|
50
|
+
auditExclude: ['password', 'rememberToken', 'twoFactorSecret'],
|
|
51
|
+
// ... rest of config
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`AuditService` automatically excludes `password` and `rememberToken` by default. The `auditExclude` array adds to this list.
|
|
56
|
+
|
|
57
|
+
## Step 4: Verify What Gets Logged
|
|
58
|
+
|
|
59
|
+
The `GlobalController` calls `AuditService.log()` after each mutation. The following events are logged automatically:
|
|
60
|
+
|
|
61
|
+
| Event | Action recorded | Old values | New values |
|
|
62
|
+
|-------|----------------|------------|------------|
|
|
63
|
+
| `POST /posts` (create) | `created` | `null` | All new fields |
|
|
64
|
+
| `PUT /posts/:id` (update) | `updated` | Changed fields before | Changed fields after |
|
|
65
|
+
| `DELETE /posts/:id` (soft delete) | `deleted` | All fields | `null` |
|
|
66
|
+
| `POST /posts/:id/restore` | `restored` | `null` | All current fields |
|
|
67
|
+
| `DELETE /posts/:id/force-delete` | `forceDeleted` | All fields | `null` |
|
|
68
|
+
|
|
69
|
+
On update, only the fields that actually changed are recorded (via `AuditService.diff()`).
|
|
70
|
+
|
|
71
|
+
## Step 5: Query the Audit Trail via API
|
|
72
|
+
|
|
73
|
+
If `hasAuditTrail: true` is set, the audit endpoint is available automatically:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
GET /api/:org/posts/:id/audit
|
|
77
|
+
GET /api/:org/posts/:id/audit?page=1&per_page=20
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Response:
|
|
81
|
+
```json
|
|
82
|
+
[
|
|
83
|
+
{
|
|
84
|
+
"id": 1,
|
|
85
|
+
"action": "created",
|
|
86
|
+
"userId": 5,
|
|
87
|
+
"auditableType": "post",
|
|
88
|
+
"auditableId": "42",
|
|
89
|
+
"oldValues": null,
|
|
90
|
+
"newValues": { "title": "Hello", "status": "draft" },
|
|
91
|
+
"ipAddress": "192.168.1.1",
|
|
92
|
+
"createdAt": "2025-01-15T10:30:00Z"
|
|
93
|
+
}
|
|
94
|
+
]
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Step 6: Write Tests
|
|
98
|
+
|
|
99
|
+
Create or update the spec file for this model:
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
describe('Audit trail for posts', () => {
|
|
103
|
+
it('creates an audit log entry when a post is created', async () => {
|
|
104
|
+
await request(app.getHttpServer())
|
|
105
|
+
.post('/api/test-org/posts')
|
|
106
|
+
.set('Authorization', `Bearer ${adminToken}`)
|
|
107
|
+
.send({ title: 'Hello', content: 'World' })
|
|
108
|
+
.expect(201);
|
|
109
|
+
|
|
110
|
+
const log = await prisma.client.auditLog.findFirst({
|
|
111
|
+
where: { auditableType: 'post', action: 'created' },
|
|
112
|
+
orderBy: { createdAt: 'desc' },
|
|
113
|
+
});
|
|
114
|
+
expect(log).not.toBeNull();
|
|
115
|
+
expect(log.newValues).toMatchObject({ title: 'Hello' });
|
|
116
|
+
expect(log.oldValues).toBeNull();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('logs only changed fields on update', async () => {
|
|
120
|
+
const post = await prisma.client.post.create({ data: { title: 'Original', status: 'draft' } });
|
|
121
|
+
|
|
122
|
+
await request(app.getHttpServer())
|
|
123
|
+
.put(`/api/test-org/posts/${post.id}`)
|
|
124
|
+
.set('Authorization', `Bearer ${adminToken}`)
|
|
125
|
+
.send({ title: 'Updated' })
|
|
126
|
+
.expect(200);
|
|
127
|
+
|
|
128
|
+
const log = await prisma.client.auditLog.findFirst({
|
|
129
|
+
where: { auditableType: 'post', action: 'updated', auditableId: String(post.id) },
|
|
130
|
+
orderBy: { createdAt: 'desc' },
|
|
131
|
+
});
|
|
132
|
+
expect(log.oldValues).toMatchObject({ title: 'Original' });
|
|
133
|
+
expect(log.newValues).toMatchObject({ title: 'Updated' });
|
|
134
|
+
// status was not changed — should not appear
|
|
135
|
+
expect(log.oldValues).not.toHaveProperty('status');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('does not log excluded fields', async () => {
|
|
139
|
+
const user = await prisma.client.user.create({
|
|
140
|
+
data: { email: 'test@test.com', password: 'hashed' },
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
await request(app.getHttpServer())
|
|
144
|
+
.put(`/api/admin/users/${user.id}`)
|
|
145
|
+
.set('Authorization', `Bearer ${adminToken}`)
|
|
146
|
+
.send({ password: 'newpassword' })
|
|
147
|
+
.expect(200);
|
|
148
|
+
|
|
149
|
+
const log = await prisma.client.auditLog.findFirst({
|
|
150
|
+
where: { auditableType: 'user', auditableId: String(user.id), action: 'updated' },
|
|
151
|
+
orderBy: { createdAt: 'desc' },
|
|
152
|
+
});
|
|
153
|
+
// password must NEVER appear in audit logs
|
|
154
|
+
expect(log?.newValues).not.toHaveProperty('password');
|
|
155
|
+
expect(log?.oldValues).not.toHaveProperty('password');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('records organizationId in the audit log for tenant routes', async () => {
|
|
159
|
+
await request(app.getHttpServer())
|
|
160
|
+
.post('/api/test-org/posts')
|
|
161
|
+
.set('Authorization', `Bearer ${adminToken}`)
|
|
162
|
+
.send({ title: 'Test' })
|
|
163
|
+
.expect(201);
|
|
164
|
+
|
|
165
|
+
const log = await prisma.client.auditLog.findFirst({
|
|
166
|
+
where: { action: 'created' },
|
|
167
|
+
orderBy: { createdAt: 'desc' },
|
|
168
|
+
});
|
|
169
|
+
expect(log.organizationId).toBe(testOrg.id);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Run:
|
|
175
|
+
```bash
|
|
176
|
+
npm test
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Common Mistakes
|
|
180
|
+
|
|
181
|
+
- **AuditLog model missing**: `AuditService.log()` silently swallows errors if the `auditLog` Prisma model does not exist. Add the model and re-run migrations.
|
|
182
|
+
- **auditExclude not set**: Always exclude `password`, `rememberToken`, and any token/secret fields. The default exclusions only cover `password` and `rememberToken`.
|
|
183
|
+
- **Direct Prisma calls bypass audit**: Only mutations going through `GlobalController` → `AuditService` are logged. If you call `prisma.client.post.update()` directly in a custom service, those changes are NOT audited. Use `ResourceService.update()` instead.
|
|
184
|
+
- **Nested operations**: Mutations via `POST /nested` do NOT currently produce audit logs (known gap — see `MISSING_FEATURES.md`).
|