@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.
Files changed (191) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +427 -0
  3. package/dist/blueprint/blueprint-parser.d.ts +130 -0
  4. package/dist/blueprint/blueprint-parser.js +276 -0
  5. package/dist/blueprint/blueprint-parser.js.map +1 -0
  6. package/dist/blueprint/blueprint-runner.d.ts +51 -0
  7. package/dist/blueprint/blueprint-runner.js +238 -0
  8. package/dist/blueprint/blueprint-runner.js.map +1 -0
  9. package/dist/blueprint/blueprint-validator.d.ts +19 -0
  10. package/dist/blueprint/blueprint-validator.js +163 -0
  11. package/dist/blueprint/blueprint-validator.js.map +1 -0
  12. package/dist/blueprint/generators/policy-generator.d.ts +40 -0
  13. package/dist/blueprint/generators/policy-generator.js +157 -0
  14. package/dist/blueprint/generators/policy-generator.js.map +1 -0
  15. package/dist/blueprint/generators/prisma-schema-generator.d.ts +43 -0
  16. package/dist/blueprint/generators/prisma-schema-generator.js +197 -0
  17. package/dist/blueprint/generators/prisma-schema-generator.js.map +1 -0
  18. package/dist/blueprint/generators/resource-definition-generator.d.ts +52 -0
  19. package/dist/blueprint/generators/resource-definition-generator.js +229 -0
  20. package/dist/blueprint/generators/resource-definition-generator.js.map +1 -0
  21. package/dist/blueprint/generators/seeder-generator.d.ts +21 -0
  22. package/dist/blueprint/generators/seeder-generator.js +135 -0
  23. package/dist/blueprint/generators/seeder-generator.js.map +1 -0
  24. package/dist/blueprint/generators/test-generator.d.ts +28 -0
  25. package/dist/blueprint/generators/test-generator.js +275 -0
  26. package/dist/blueprint/generators/test-generator.js.map +1 -0
  27. package/dist/blueprint/manifest-manager.d.ts +58 -0
  28. package/dist/blueprint/manifest-manager.js +137 -0
  29. package/dist/blueprint/manifest-manager.js.map +1 -0
  30. package/dist/cli/commands/blueprint.command.d.ts +16 -0
  31. package/dist/cli/commands/blueprint.command.js +35 -0
  32. package/dist/cli/commands/blueprint.command.js.map +1 -0
  33. package/dist/cli/commands/export-postman.command.d.ts +5 -0
  34. package/dist/cli/commands/export-postman.command.js +39 -0
  35. package/dist/cli/commands/export-postman.command.js.map +1 -0
  36. package/dist/cli/commands/export-types.command.d.ts +4 -0
  37. package/dist/cli/commands/export-types.command.js +36 -0
  38. package/dist/cli/commands/export-types.command.js.map +1 -0
  39. package/dist/cli/commands/generate.command.d.ts +6 -0
  40. package/dist/cli/commands/generate.command.js +212 -0
  41. package/dist/cli/commands/generate.command.js.map +1 -0
  42. package/dist/cli/commands/install.command.d.ts +8 -0
  43. package/dist/cli/commands/install.command.js +205 -0
  44. package/dist/cli/commands/install.command.js.map +1 -0
  45. package/dist/cli/index.d.ts +17 -0
  46. package/dist/cli/index.js +123 -0
  47. package/dist/cli/index.js.map +1 -0
  48. package/dist/cli/utils/io.d.ts +12 -0
  49. package/dist/cli/utils/io.js +74 -0
  50. package/dist/cli/utils/io.js.map +1 -0
  51. package/dist/cli/utils/prompt.d.ts +12 -0
  52. package/dist/cli/utils/prompt.js +78 -0
  53. package/dist/cli/utils/prompt.js.map +1 -0
  54. package/dist/constants/tokens.d.ts +3 -0
  55. package/dist/constants/tokens.js +7 -0
  56. package/dist/constants/tokens.js.map +1 -0
  57. package/dist/controllers/auth.controller.d.ts +59 -0
  58. package/dist/controllers/auth.controller.js +121 -0
  59. package/dist/controllers/auth.controller.js.map +1 -0
  60. package/dist/controllers/global.controller.d.ts +44 -0
  61. package/dist/controllers/global.controller.js +357 -0
  62. package/dist/controllers/global.controller.js.map +1 -0
  63. package/dist/controllers/invitation.controller.d.ts +30 -0
  64. package/dist/controllers/invitation.controller.js +111 -0
  65. package/dist/controllers/invitation.controller.js.map +1 -0
  66. package/dist/controllers/nested.controller.d.ts +12 -0
  67. package/dist/controllers/nested.controller.js +69 -0
  68. package/dist/controllers/nested.controller.js.map +1 -0
  69. package/dist/decorators/index.d.ts +33 -0
  70. package/dist/decorators/index.js +79 -0
  71. package/dist/decorators/index.js.map +1 -0
  72. package/dist/errors/rhino-exception.d.ts +24 -0
  73. package/dist/errors/rhino-exception.js +47 -0
  74. package/dist/errors/rhino-exception.js.map +1 -0
  75. package/dist/exporters/postman-exporter.d.ts +21 -0
  76. package/dist/exporters/postman-exporter.js +396 -0
  77. package/dist/exporters/postman-exporter.js.map +1 -0
  78. package/dist/exporters/typescript-exporter.d.ts +23 -0
  79. package/dist/exporters/typescript-exporter.js +129 -0
  80. package/dist/exporters/typescript-exporter.js.map +1 -0
  81. package/dist/guards/jwt-auth.guard.d.ts +16 -0
  82. package/dist/guards/jwt-auth.guard.js +61 -0
  83. package/dist/guards/jwt-auth.guard.js.map +1 -0
  84. package/dist/guards/resource-policy.guard.d.ts +12 -0
  85. package/dist/guards/resource-policy.guard.js +111 -0
  86. package/dist/guards/resource-policy.guard.js.map +1 -0
  87. package/dist/index.d.ts +60 -0
  88. package/dist/index.js +171 -0
  89. package/dist/index.js.map +1 -0
  90. package/dist/interceptors/hidden-columns.interceptor.d.ts +18 -0
  91. package/dist/interceptors/hidden-columns.interceptor.js +66 -0
  92. package/dist/interceptors/hidden-columns.interceptor.js.map +1 -0
  93. package/dist/interceptors/response.interceptor.d.ts +19 -0
  94. package/dist/interceptors/response.interceptor.js +47 -0
  95. package/dist/interceptors/response.interceptor.js.map +1 -0
  96. package/dist/interfaces/rhino-config.interface.d.ts +103 -0
  97. package/dist/interfaces/rhino-config.interface.js +3 -0
  98. package/dist/interfaces/rhino-config.interface.js.map +1 -0
  99. package/dist/interfaces/rhino-request.interface.d.ts +12 -0
  100. package/dist/interfaces/rhino-request.interface.js +3 -0
  101. package/dist/interfaces/rhino-request.interface.js.map +1 -0
  102. package/dist/middleware/resolve-organization.middleware.d.ts +15 -0
  103. package/dist/middleware/resolve-organization.middleware.js +38 -0
  104. package/dist/middleware/resolve-organization.middleware.js.map +1 -0
  105. package/dist/middleware/route-group.middleware.d.ts +17 -0
  106. package/dist/middleware/route-group.middleware.js +60 -0
  107. package/dist/middleware/route-group.middleware.js.map +1 -0
  108. package/dist/middleware/tenant-route-rewrite.d.ts +76 -0
  109. package/dist/middleware/tenant-route-rewrite.js +124 -0
  110. package/dist/middleware/tenant-route-rewrite.js.map +1 -0
  111. package/dist/policies/resource-policy.d.ts +25 -0
  112. package/dist/policies/resource-policy.js +77 -0
  113. package/dist/policies/resource-policy.js.map +1 -0
  114. package/dist/prisma/prisma-soft-delete.extension.d.ts +16 -0
  115. package/dist/prisma/prisma-soft-delete.extension.js +57 -0
  116. package/dist/prisma/prisma-soft-delete.extension.js.map +1 -0
  117. package/dist/prisma/prisma-uuid.extension.d.ts +9 -0
  118. package/dist/prisma/prisma-uuid.extension.js +28 -0
  119. package/dist/prisma/prisma-uuid.extension.js.map +1 -0
  120. package/dist/prisma/prisma.service.d.ts +38 -0
  121. package/dist/prisma/prisma.service.js +100 -0
  122. package/dist/prisma/prisma.service.js.map +1 -0
  123. package/dist/rhino.config.d.ts +43 -0
  124. package/dist/rhino.config.js +130 -0
  125. package/dist/rhino.config.js.map +1 -0
  126. package/dist/rhino.module.d.ts +61 -0
  127. package/dist/rhino.module.js +268 -0
  128. package/dist/rhino.module.js.map +1 -0
  129. package/dist/services/audit.service.d.ts +35 -0
  130. package/dist/services/audit.service.js +92 -0
  131. package/dist/services/audit.service.js.map +1 -0
  132. package/dist/services/auth.service.d.ts +33 -0
  133. package/dist/services/auth.service.js +157 -0
  134. package/dist/services/auth.service.js.map +1 -0
  135. package/dist/services/invitation.service.d.ts +37 -0
  136. package/dist/services/invitation.service.js +143 -0
  137. package/dist/services/invitation.service.js.map +1 -0
  138. package/dist/services/nested.service.d.ts +34 -0
  139. package/dist/services/nested.service.js +163 -0
  140. package/dist/services/nested.service.js.map +1 -0
  141. package/dist/services/organization.service.d.ts +13 -0
  142. package/dist/services/organization.service.js +60 -0
  143. package/dist/services/organization.service.js.map +1 -0
  144. package/dist/services/query-builder.service.d.ts +31 -0
  145. package/dist/services/query-builder.service.js +151 -0
  146. package/dist/services/query-builder.service.js.map +1 -0
  147. package/dist/services/resource.service.d.ts +38 -0
  148. package/dist/services/resource.service.js +230 -0
  149. package/dist/services/resource.service.js.map +1 -0
  150. package/dist/services/route-registration.service.d.ts +40 -0
  151. package/dist/services/route-registration.service.js +68 -0
  152. package/dist/services/route-registration.service.js.map +1 -0
  153. package/dist/services/scope.service.d.ts +16 -0
  154. package/dist/services/scope.service.js +32 -0
  155. package/dist/services/scope.service.js.map +1 -0
  156. package/dist/services/serializer.service.d.ts +36 -0
  157. package/dist/services/serializer.service.js +90 -0
  158. package/dist/services/serializer.service.js.map +1 -0
  159. package/dist/services/validation.service.d.ts +50 -0
  160. package/dist/services/validation.service.js +203 -0
  161. package/dist/services/validation.service.js.map +1 -0
  162. package/dist/tsconfig.build.tsbuildinfo +1 -0
  163. package/dist/utils/fk-chain-walker.d.ts +30 -0
  164. package/dist/utils/fk-chain-walker.js +23 -0
  165. package/dist/utils/fk-chain-walker.js.map +1 -0
  166. package/dist/utils/format.d.ts +7 -0
  167. package/dist/utils/format.js +28 -0
  168. package/dist/utils/format.js.map +1 -0
  169. package/dist/utils/model-builder.d.ts +64 -0
  170. package/dist/utils/model-builder.js +67 -0
  171. package/dist/utils/model-builder.js.map +1 -0
  172. package/dist/utils/permission-matcher.d.ts +46 -0
  173. package/dist/utils/permission-matcher.js +120 -0
  174. package/dist/utils/permission-matcher.js.map +1 -0
  175. package/dist/utils/scope-discovery.d.ts +34 -0
  176. package/dist/utils/scope-discovery.js +102 -0
  177. package/dist/utils/scope-discovery.js.map +1 -0
  178. package/package.json +64 -0
  179. package/stubs/skills/rhino-audit.md +184 -0
  180. package/stubs/skills/rhino-bugfix.md +117 -0
  181. package/stubs/skills/rhino-deploy.md +5 -0
  182. package/stubs/skills/rhino-docs.md +147 -0
  183. package/stubs/skills/rhino-feature.md +146 -0
  184. package/stubs/skills/rhino-migrate.md +181 -0
  185. package/stubs/skills/rhino-model.md +180 -0
  186. package/stubs/skills/rhino-plan.md +5 -0
  187. package/stubs/skills/rhino-policy.md +183 -0
  188. package/stubs/skills/rhino-refactor.md +147 -0
  189. package/stubs/skills/rhino-review.md +113 -0
  190. package/stubs/skills/rhino-scope.md +140 -0
  191. 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":"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"}
@@ -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`).