@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,180 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rhino-model
|
|
3
|
+
description: Create a new Rhino model definition — Prisma schema, registration, Zod validation, and policy skeleton.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
You are creating a new model in an Rhino NestJS application. Follow these steps in order.
|
|
7
|
+
|
|
8
|
+
## Step 1: Read Context
|
|
9
|
+
|
|
10
|
+
Before creating anything, read:
|
|
11
|
+
- `prisma/schema.prisma` — existing models and conventions.
|
|
12
|
+
- `src/app.module.ts` — how models are currently registered.
|
|
13
|
+
- `CLAUDE.md` — project conventions.
|
|
14
|
+
|
|
15
|
+
Ask the user for:
|
|
16
|
+
- Model name (PascalCase singular, e.g., `BlogPost`)
|
|
17
|
+
- Fields and their types
|
|
18
|
+
- Relationships to existing models
|
|
19
|
+
- Whether it needs multi-tenancy (`belongsToOrganization`)
|
|
20
|
+
- Whether it needs soft deletes, audit trail, or UUID primary key
|
|
21
|
+
- Which roles should have access and what they can do
|
|
22
|
+
|
|
23
|
+
## Step 2: Prisma Schema
|
|
24
|
+
|
|
25
|
+
Add the model to `prisma/schema.prisma`. Follow existing casing conventions (camelCase fields):
|
|
26
|
+
|
|
27
|
+
```prisma
|
|
28
|
+
model BlogPost {
|
|
29
|
+
id Int @id @default(autoincrement())
|
|
30
|
+
title String
|
|
31
|
+
content String?
|
|
32
|
+
status String @default("draft")
|
|
33
|
+
organizationId Int?
|
|
34
|
+
userId Int
|
|
35
|
+
deletedAt DateTime?
|
|
36
|
+
createdAt DateTime @default(now())
|
|
37
|
+
updatedAt DateTime @updatedAt
|
|
38
|
+
|
|
39
|
+
organization Organization? @relation(fields: [organizationId], references: [id])
|
|
40
|
+
user User @relation(fields: [userId], references: [id])
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Run migration:
|
|
45
|
+
```bash
|
|
46
|
+
npx prisma migrate dev --name create_blog_posts
|
|
47
|
+
npx prisma generate
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Step 3: Zod Validation Schema
|
|
51
|
+
|
|
52
|
+
Create `src/schemas/blog-post.schema.ts`:
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
import { z } from 'zod';
|
|
56
|
+
|
|
57
|
+
export const BlogPostSchema = z.object({
|
|
58
|
+
title: z.string().min(1).max(255),
|
|
59
|
+
content: z.string().optional(),
|
|
60
|
+
status: z.enum(['draft', 'published', 'archived']).optional(),
|
|
61
|
+
userId: z.number().int().positive(),
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Update allows all fields optional (partial)
|
|
65
|
+
export const BlogPostUpdateSchema = BlogPostSchema.partial();
|
|
66
|
+
|
|
67
|
+
// Role-keyed schemas (optional — use when roles need different required fields)
|
|
68
|
+
export const BlogPostStoreByRole = {
|
|
69
|
+
admin: BlogPostSchema,
|
|
70
|
+
'*': BlogPostSchema.omit({ status: true }),
|
|
71
|
+
};
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Step 4: Policy
|
|
75
|
+
|
|
76
|
+
Create `src/policies/blog-post.policy.ts`:
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
import { ResourcePolicy } from '@rhino-dev/rhino-nestjs';
|
|
80
|
+
|
|
81
|
+
export class BlogPostPolicy extends ResourcePolicy {
|
|
82
|
+
// Control which fields each role can write
|
|
83
|
+
permittedAttributesForCreate(user: any): string[] {
|
|
84
|
+
if (this.hasRole(user, 'admin')) return ['*'];
|
|
85
|
+
if (this.hasRole(user, 'editor')) return ['title', 'content'];
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
permittedAttributesForUpdate(user: any): string[] {
|
|
90
|
+
return this.permittedAttributesForCreate(user);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Control which fields each role can read in responses
|
|
94
|
+
permittedAttributesForShow(user: any): string[] {
|
|
95
|
+
return ['*'];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
hiddenAttributesForShow(user: any): string[] {
|
|
99
|
+
if (this.hasRole(user, 'admin')) return [];
|
|
100
|
+
return ['internalNotes'];
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Step 5: Register in AppModule
|
|
106
|
+
|
|
107
|
+
Add to `RhinoModule.forRoot({ models: { ... } })`:
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
'blog-posts': {
|
|
111
|
+
model: 'blogPost', // matches Prisma client key
|
|
112
|
+
policy: BlogPostPolicy,
|
|
113
|
+
validation: BlogPostSchema,
|
|
114
|
+
validationUpdate: BlogPostUpdateSchema,
|
|
115
|
+
|
|
116
|
+
// Query capabilities
|
|
117
|
+
allowedFilters: ['status', 'userId'],
|
|
118
|
+
allowedSorts: ['createdAt', 'title', 'updatedAt'],
|
|
119
|
+
defaultSort: '-createdAt',
|
|
120
|
+
allowedIncludes: ['user'],
|
|
121
|
+
allowedSearch: ['title', 'content'],
|
|
122
|
+
|
|
123
|
+
// Features
|
|
124
|
+
softDeletes: true,
|
|
125
|
+
belongsToOrganization: true,
|
|
126
|
+
hasAuditTrail: true,
|
|
127
|
+
paginationEnabled: true,
|
|
128
|
+
perPage: 25,
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Step 6: Write Tests
|
|
133
|
+
|
|
134
|
+
Create `src/services/blog-post.spec.ts` (or `test/e2e/blog-post.e2e-spec.ts`):
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
describe('BlogPost endpoints', () => {
|
|
138
|
+
it('returns 201 when admin creates a blog post', async () => { /* ... */ });
|
|
139
|
+
it('returns 403 when viewer tries to create a blog post', async () => { /* ... */ });
|
|
140
|
+
it('returns 422 when title is missing', async () => { /* ... */ });
|
|
141
|
+
it('scopes blog posts to the current organization', async () => { /* ... */ });
|
|
142
|
+
it('soft deletes the blog post on DELETE', async () => { /* ... */ });
|
|
143
|
+
});
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Run:
|
|
147
|
+
```bash
|
|
148
|
+
npm test
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Step 7: Verify Route Table
|
|
152
|
+
|
|
153
|
+
Check that the model is generating the expected routes:
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
import { describeRoutes } from '@rhino-dev/rhino-nestjs';
|
|
157
|
+
// In a test or startup log:
|
|
158
|
+
console.log(describeRoutes(configService));
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Expected routes for `blog-posts`:
|
|
162
|
+
```
|
|
163
|
+
GET /api/:org/blog-posts
|
|
164
|
+
POST /api/:org/blog-posts
|
|
165
|
+
GET /api/:org/blog-posts/:id
|
|
166
|
+
PUT /api/:org/blog-posts/:id
|
|
167
|
+
DELETE /api/:org/blog-posts/:id
|
|
168
|
+
GET /api/:org/blog-posts/trashed
|
|
169
|
+
POST /api/:org/blog-posts/:id/restore
|
|
170
|
+
DELETE /api/:org/blog-posts/:id/force-delete
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## Checklist
|
|
174
|
+
|
|
175
|
+
- [ ] Prisma schema added and migrated
|
|
176
|
+
- [ ] Zod schema created (store + update variants)
|
|
177
|
+
- [ ] Policy created with `permittedAttributesForCreate/Update/Show`
|
|
178
|
+
- [ ] Model registered in `RhinoModule.forRoot()`
|
|
179
|
+
- [ ] Tests written and passing
|
|
180
|
+
- [ ] `CLAUDE.md` features table updated if this adds a new capability
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# /rhino-plan — Plan Before Coding
|
|
2
|
+
|
|
3
|
+
Guide for planning a new Rhino NestJS feature before writing code: identify affected models,
|
|
4
|
+
policies, routes, and Prisma schema changes; list edge cases and multi-tenant implications;
|
|
5
|
+
draft the blueprint YAML; outline required tests; then get approval before implementing.
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rhino-policy
|
|
3
|
+
description: Create or update an Rhino ResourcePolicy for a NestJS model — action permissions, attribute permissions, and role-based logic.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
You are creating or updating an authorization policy for an Rhino NestJS model.
|
|
7
|
+
|
|
8
|
+
## Step 1: Understand the Permission Model
|
|
9
|
+
|
|
10
|
+
Rhino policies check `{slug}.{action}` permissions. The base `ResourcePolicy` class handles this automatically. You only need to subclass it to:
|
|
11
|
+
|
|
12
|
+
1. Restrict which fields users can **write** (`permittedAttributesForCreate` / `permittedAttributesForUpdate`)
|
|
13
|
+
2. Restrict which fields users can **read** (`permittedAttributesForShow` / `hiddenAttributesForShow`)
|
|
14
|
+
3. Add custom authorization logic beyond permission checks (e.g., "only the author can edit")
|
|
15
|
+
|
|
16
|
+
Read the following before writing any code:
|
|
17
|
+
- `src/policies/resource-policy.ts` — the base class
|
|
18
|
+
- `src/utils/permission-matcher.ts` — how `userHasPermission` and `resolveUserRoleSlug` work
|
|
19
|
+
- `prisma/schema.prisma` — understand the model's fields
|
|
20
|
+
- Existing policy files in `src/policies/`
|
|
21
|
+
|
|
22
|
+
## Step 2: Identify the Permission Matrix
|
|
23
|
+
|
|
24
|
+
Ask the user (or read the blueprint YAML) to define:
|
|
25
|
+
|
|
26
|
+
| Role | index | show | store | update | destroy | Hidden fields | Create fields | Update fields |
|
|
27
|
+
|------|-------|------|-------|--------|---------|---------------|---------------|---------------|
|
|
28
|
+
| admin | ✓ | ✓ | ✓ | ✓ | ✓ | none | `*` | `*` |
|
|
29
|
+
| editor | ✓ | ✓ | ✓ | ✓ | — | none | `[title, content]` | `[title, content]` |
|
|
30
|
+
| viewer | ✓ | ✓ | — | — | — | `[cost]` | — | — |
|
|
31
|
+
|
|
32
|
+
## Step 3: Create the Policy File
|
|
33
|
+
|
|
34
|
+
Create `src/policies/[model].policy.ts`:
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
import { ResourcePolicy } from '@rhino-dev/rhino-nestjs';
|
|
38
|
+
|
|
39
|
+
export class PostPolicy extends ResourcePolicy {
|
|
40
|
+
// ── Action Authorization ──────────────────────────────────────
|
|
41
|
+
// Only needed if you want custom logic BEYOND the {slug}.{action} check.
|
|
42
|
+
// The base class already handles standard permission checks.
|
|
43
|
+
|
|
44
|
+
// Example: restrict update to the record's owner (plus admin)
|
|
45
|
+
update(user: any, model: any, organization?: any): boolean {
|
|
46
|
+
if (!super.update(user, model, organization)) return false;
|
|
47
|
+
if (this.hasRole(user, 'admin', organization)) return true;
|
|
48
|
+
return model?.userId === user?.id;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ── Field-Level Write Permissions ─────────────────────────────
|
|
52
|
+
permittedAttributesForCreate(user: any): string[] {
|
|
53
|
+
if (this.hasRole(user, 'admin')) return ['*'];
|
|
54
|
+
if (this.hasRole(user, 'editor')) return ['title', 'content', 'categoryId'];
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
permittedAttributesForUpdate(user: any): string[] {
|
|
59
|
+
if (this.hasRole(user, 'admin')) return ['*'];
|
|
60
|
+
if (this.hasRole(user, 'editor')) return ['title', 'content'];
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ── Field-Level Read Permissions ──────────────────────────────
|
|
65
|
+
// Return ['*'] to allow all fields (default).
|
|
66
|
+
// Return a specific list to whitelist.
|
|
67
|
+
permittedAttributesForShow(user: any): string[] {
|
|
68
|
+
return ['*'];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Return fields to ALWAYS hide for non-admin users.
|
|
72
|
+
hiddenAttributesForShow(user: any): string[] {
|
|
73
|
+
if (this.hasRole(user, 'admin')) return [];
|
|
74
|
+
return ['costPrice', 'internalNotes'];
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Step 4: Register the Policy
|
|
80
|
+
|
|
81
|
+
In `app.module.ts`, add `policy: PostPolicy` to the model registration:
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
posts: {
|
|
85
|
+
model: 'post',
|
|
86
|
+
policy: PostPolicy, // ← Add this
|
|
87
|
+
// ... rest of config
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Step 5: Seed Permissions
|
|
92
|
+
|
|
93
|
+
Permissions are stored as JSON on `users.permissions` (non-tenant) or `user_roles.permissions` (tenant).
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
// Non-tenant: assign directly on user
|
|
97
|
+
await prisma.user.update({
|
|
98
|
+
where: { id: adminUser.id },
|
|
99
|
+
data: { permissions: ['*'] },
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Tenant: assign via user_roles
|
|
103
|
+
await prisma.userRole.create({
|
|
104
|
+
data: {
|
|
105
|
+
userId: editorUser.id,
|
|
106
|
+
organizationId: org.id,
|
|
107
|
+
roleSlug: 'editor',
|
|
108
|
+
permissions: ['posts.index', 'posts.show', 'posts.store', 'posts.update'],
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Step 6: Write Policy Tests
|
|
114
|
+
|
|
115
|
+
Create a unit test file `src/policies/post.policy.spec.ts`:
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
describe('PostPolicy', () => {
|
|
119
|
+
let policy: PostPolicy;
|
|
120
|
+
|
|
121
|
+
beforeEach(() => {
|
|
122
|
+
policy = new PostPolicy();
|
|
123
|
+
policy.resourceSlug = 'posts';
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe('permittedAttributesForCreate', () => {
|
|
127
|
+
it('returns * for admin', () => {
|
|
128
|
+
const user = makeUserWithRole('admin', org.id);
|
|
129
|
+
expect(policy.permittedAttributesForCreate(user)).toEqual(['*']);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('returns restricted fields for editor', () => {
|
|
133
|
+
const user = makeUserWithRole('editor', org.id);
|
|
134
|
+
expect(policy.permittedAttributesForCreate(user)).toEqual(
|
|
135
|
+
expect.arrayContaining(['title', 'content'])
|
|
136
|
+
);
|
|
137
|
+
expect(policy.permittedAttributesForCreate(user)).not.toContain('status');
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('returns empty array for viewer', () => {
|
|
141
|
+
const user = makeUserWithRole('viewer', org.id);
|
|
142
|
+
expect(policy.permittedAttributesForCreate(user)).toEqual([]);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe('hiddenAttributesForShow', () => {
|
|
147
|
+
it('hides costPrice for non-admin', () => {
|
|
148
|
+
const user = makeUserWithRole('viewer', org.id);
|
|
149
|
+
expect(policy.hiddenAttributesForShow(user)).toContain('costPrice');
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('reveals all fields for admin', () => {
|
|
153
|
+
const user = makeUserWithRole('admin', org.id);
|
|
154
|
+
expect(policy.hiddenAttributesForShow(user)).toEqual([]);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Run:
|
|
161
|
+
```bash
|
|
162
|
+
npm test -- --testPathPattern=post.policy
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Common Mistakes
|
|
166
|
+
|
|
167
|
+
- Do not forget to call `super.update(...)` etc. in overridden action methods — skipping it bypasses the permission check entirely.
|
|
168
|
+
- `hasRole(user, 'admin')` checks the role in the current organization context stored on the user object. If organization context is not set, it returns false.
|
|
169
|
+
- Return `['*']` (not `[]`) from `permittedAttributesForShow` when all fields should be visible — `[]` means nothing is visible.
|
|
170
|
+
- The `resourceSlug` on the policy must match the key in `RhinoModule.forRoot({ models: { [slug]: ... } })`.
|
|
171
|
+
|
|
172
|
+
## Permission Reference
|
|
173
|
+
|
|
174
|
+
| Action | Permission checked | Policy method |
|
|
175
|
+
|--------|-------------------|---------------|
|
|
176
|
+
| GET (list) | `{slug}.index` | `viewAny` |
|
|
177
|
+
| GET (single) | `{slug}.show` | `view` |
|
|
178
|
+
| POST | `{slug}.store` | `create` |
|
|
179
|
+
| PUT/PATCH | `{slug}.update` | `update` |
|
|
180
|
+
| DELETE | `{slug}.destroy` | `delete` |
|
|
181
|
+
| GET trashed | `{slug}.trashed` | `viewTrashed` |
|
|
182
|
+
| POST restore | `{slug}.restore` | `restore` |
|
|
183
|
+
| DELETE force | `{slug}.forceDelete` | `forceDelete` |
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rhino-refactor
|
|
3
|
+
description: Safely refactor Rhino NestJS code — establish baseline, apply incremental changes, verify tests pass at each step.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
You are refactoring code in an Rhino NestJS project. The goal is to improve structure without changing observable behavior.
|
|
7
|
+
|
|
8
|
+
## Step 1: Establish Baseline
|
|
9
|
+
|
|
10
|
+
Before touching any code:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm test
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Note the exact test count and which tests pass. This is your baseline. Every test that currently passes must still pass after the refactor.
|
|
17
|
+
|
|
18
|
+
Read the code to be refactored and identify:
|
|
19
|
+
- What is the current structure?
|
|
20
|
+
- What specific problem does the refactor solve? (readability, performance, duplication, etc.)
|
|
21
|
+
- What is the target structure?
|
|
22
|
+
|
|
23
|
+
## Step 2: Identify the Scope
|
|
24
|
+
|
|
25
|
+
Clarify with the user what is in scope:
|
|
26
|
+
- Which files are affected?
|
|
27
|
+
- Are public interfaces (API config shape, service method signatures) changing?
|
|
28
|
+
- Does this touch any of these critical paths?
|
|
29
|
+
- `resource.service.ts` — any change here affects all CRUD operations
|
|
30
|
+
- `resource-policy.guard.ts` — any change here affects authorization
|
|
31
|
+
- `validation.service.ts` — any change here affects input validation
|
|
32
|
+
- `query-builder.service.ts` — any change here affects all queries
|
|
33
|
+
|
|
34
|
+
If public interfaces are changing, plan a migration path before starting.
|
|
35
|
+
|
|
36
|
+
## Step 3: Refactor Incrementally
|
|
37
|
+
|
|
38
|
+
Work in small steps. After each step, run the tests:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npm test
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Common refactor patterns in Rhino NestJS:
|
|
45
|
+
|
|
46
|
+
### Extract a service method
|
|
47
|
+
|
|
48
|
+
If controller code is doing logic that belongs in a service:
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
// Before: logic in controller
|
|
52
|
+
@Post(':modelSlug')
|
|
53
|
+
async store(...) {
|
|
54
|
+
const orgFilter = ...; // logic here
|
|
55
|
+
const record = await ...;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// After: logic in ResourceService
|
|
59
|
+
async create(modelSlug: string, data: any, ctx: ResourceContext) {
|
|
60
|
+
// moved logic
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Consolidate repeated Zod schemas
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
// Before: duplicated schema fragments
|
|
68
|
+
const StoreSchema = z.object({ title: z.string(), status: z.enum([...]) });
|
|
69
|
+
const UpdateSchema = z.object({ title: z.string().optional(), status: z.enum([...]).optional() });
|
|
70
|
+
|
|
71
|
+
// After: derived from base
|
|
72
|
+
const BaseSchema = z.object({ title: z.string(), status: z.enum(['draft', 'published']) });
|
|
73
|
+
const StoreSchema = BaseSchema;
|
|
74
|
+
const UpdateSchema = BaseSchema.partial();
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Extract repeated policy logic
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
// Before: repeated hasRole checks
|
|
81
|
+
permittedAttributesForCreate(user: any) {
|
|
82
|
+
if (this.hasRole(user, 'admin') || this.hasRole(user, 'owner')) return ['*'];
|
|
83
|
+
// ...
|
|
84
|
+
}
|
|
85
|
+
permittedAttributesForUpdate(user: any) {
|
|
86
|
+
if (this.hasRole(user, 'admin') || this.hasRole(user, 'owner')) return ['*'];
|
|
87
|
+
// ...
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// After: extracted helper
|
|
91
|
+
private isPrivileged(user: any) {
|
|
92
|
+
return this.hasRole(user, 'admin') || this.hasRole(user, 'owner');
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Consolidate model registration config
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
// Before: scattered constants
|
|
100
|
+
const POST_FILTERS = ['status', 'userId'];
|
|
101
|
+
const POST_SORTS = ['createdAt', 'title'];
|
|
102
|
+
|
|
103
|
+
// After: co-located config object
|
|
104
|
+
export const PostModelConfig: ModelRegistration = {
|
|
105
|
+
model: 'post',
|
|
106
|
+
allowedFilters: ['status', 'userId'],
|
|
107
|
+
allowedSorts: ['createdAt', 'title'],
|
|
108
|
+
// ...
|
|
109
|
+
};
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Step 4: Update Types
|
|
113
|
+
|
|
114
|
+
If method signatures changed, update:
|
|
115
|
+
- All call sites
|
|
116
|
+
- TypeScript interfaces in `src/interfaces/`
|
|
117
|
+
- Type exports in `src/index.ts`
|
|
118
|
+
|
|
119
|
+
Run:
|
|
120
|
+
```bash
|
|
121
|
+
npx tsc --noEmit
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Fix all type errors before running tests.
|
|
125
|
+
|
|
126
|
+
## Step 5: Verify
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
npm test # Full suite — must match baseline exactly
|
|
130
|
+
npx tsc --noEmit # No type errors
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
If any test fails that was passing before, fix the code (not the test). Only modify tests if the test was genuinely wrong.
|
|
134
|
+
|
|
135
|
+
## Step 6: Document Changes
|
|
136
|
+
|
|
137
|
+
If the refactor changed any public API:
|
|
138
|
+
- Update `CLAUDE.md`
|
|
139
|
+
- Update `README.md` if the config shape changed
|
|
140
|
+
- Update the relevant Docusaurus doc page
|
|
141
|
+
|
|
142
|
+
## Rules
|
|
143
|
+
|
|
144
|
+
- Never comment out a passing test to make a refactor work.
|
|
145
|
+
- Never change behavior while refactoring — separate refactor commits from behavior changes.
|
|
146
|
+
- If you discover a bug during refactoring, note it but do not fix it in the same change. Fix it separately with a test first.
|
|
147
|
+
- Keep commits small. One logical change per commit.
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rhino-review
|
|
3
|
+
description: Review Rhino NestJS code for correctness, security, and alignment with conventions. Produces a structured findings report.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
You are reviewing code in an Rhino NestJS project. Produce a structured report with findings grouped by severity.
|
|
7
|
+
|
|
8
|
+
## Step 1: Gather Context
|
|
9
|
+
|
|
10
|
+
Read the following before reviewing:
|
|
11
|
+
- `CLAUDE.md` — development rules and conventions
|
|
12
|
+
- `src/app.module.ts` — model registrations
|
|
13
|
+
- `prisma/schema.prisma` — data model
|
|
14
|
+
- The specific file(s) the user asked you to review
|
|
15
|
+
|
|
16
|
+
## Step 2: Run the Tests First
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm test
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Note any failing tests. Do not proceed with fixes until you have the test baseline.
|
|
23
|
+
|
|
24
|
+
## Step 3: Review Checklist
|
|
25
|
+
|
|
26
|
+
Work through each category and note findings. Severity: **Critical** (security / data loss), **Major** (bug / missing behavior), **Minor** (convention / style).
|
|
27
|
+
|
|
28
|
+
### Security
|
|
29
|
+
|
|
30
|
+
- [ ] No `organizationId` accepted from request body — `ValidationService` must strip it.
|
|
31
|
+
- [ ] Permissions checked before data access — `ResourcePolicyGuard` applied.
|
|
32
|
+
- [ ] Cross-tenant isolation — `belongsToOrganization: true` set on models that need it.
|
|
33
|
+
- [ ] No raw SQL queries that could be injected.
|
|
34
|
+
- [ ] JWT secret comes from env (`process.env.JWT_SECRET`), not hardcoded.
|
|
35
|
+
- [ ] Sensitive fields (`password`, `rememberToken`) in `auditExclude` and in `additionalHiddenColumns`.
|
|
36
|
+
|
|
37
|
+
### Model Registration
|
|
38
|
+
|
|
39
|
+
- [ ] Every field that should be filterable is in `allowedFilters`.
|
|
40
|
+
- [ ] Every field that should be sortable is in `allowedSorts`.
|
|
41
|
+
- [ ] `defaultSort` is set (avoid undefined ordering).
|
|
42
|
+
- [ ] `allowedIncludes` lists only relationships that exist on the Prisma model.
|
|
43
|
+
- [ ] `allowedSearch` fields are string columns (not IDs or JSON).
|
|
44
|
+
- [ ] `softDeletes: true` is set if the Prisma model has a `deletedAt` field.
|
|
45
|
+
- [ ] `hasAuditTrail: true` if audit logging is required.
|
|
46
|
+
- [ ] `paginationEnabled` explicitly set if the model returns large datasets.
|
|
47
|
+
|
|
48
|
+
### Policy
|
|
49
|
+
|
|
50
|
+
- [ ] `permittedAttributesForCreate` and `permittedAttributesForUpdate` implemented.
|
|
51
|
+
- [ ] `permittedAttributesForShow` and `hiddenAttributesForShow` implemented.
|
|
52
|
+
- [ ] Every role in the permission matrix is covered (no missing `else` branches).
|
|
53
|
+
- [ ] `hasRole(user, role)` is used correctly — it checks role within org context.
|
|
54
|
+
- [ ] Overridden action methods (`update`, `delete`, etc.) call `super.method()`.
|
|
55
|
+
- [ ] `resourceSlug` is set correctly (must match the model key in `RhinoModule.forRoot`).
|
|
56
|
+
|
|
57
|
+
### Zod Validation
|
|
58
|
+
|
|
59
|
+
- [ ] `validationStore` schema marks required fields as required.
|
|
60
|
+
- [ ] `validationUpdate` schema makes all fields optional (`.partial()` or equivalent).
|
|
61
|
+
- [ ] Enum values in the Zod schema match the valid values in Prisma schema / docs.
|
|
62
|
+
- [ ] FK fields validated as positive integers (not strings).
|
|
63
|
+
- [ ] No `organizationId` / `organization_id` in the Zod schema.
|
|
64
|
+
- [ ] Role-keyed schemas (`Record<string, ZodSchema>`) have a `'*'` fallback.
|
|
65
|
+
|
|
66
|
+
### Tests
|
|
67
|
+
|
|
68
|
+
- [ ] Happy path (200/201) tests exist for each CRUD action.
|
|
69
|
+
- [ ] 403 tests for each role that should be denied.
|
|
70
|
+
- [ ] 422 test for missing required field on store.
|
|
71
|
+
- [ ] 404 test for non-existent ID.
|
|
72
|
+
- [ ] Multi-tenant isolation test (org A data not visible to org B).
|
|
73
|
+
- [ ] Soft delete tests (trashed, restore, force-delete) if `softDeletes: true`.
|
|
74
|
+
- [ ] Audit log created for creates, updates, and deletes if `hasAuditTrail: true`.
|
|
75
|
+
|
|
76
|
+
### Code Style
|
|
77
|
+
|
|
78
|
+
- [ ] No `class-validator` decorators — Zod only.
|
|
79
|
+
- [ ] No per-model controllers — all CRUD through `GlobalController`.
|
|
80
|
+
- [ ] Business logic in services, not controllers.
|
|
81
|
+
- [ ] No direct `prisma.client.[model]` calls in controllers — use `ResourceService`.
|
|
82
|
+
- [ ] `@Injectable()` decorator on all services.
|
|
83
|
+
- [ ] All method parameters and return types explicitly typed.
|
|
84
|
+
|
|
85
|
+
## Step 4: Produce Report
|
|
86
|
+
|
|
87
|
+
Format your findings as:
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
## Code Review Report — [filename or feature]
|
|
91
|
+
|
|
92
|
+
### Critical
|
|
93
|
+
1. [Finding]: [file:line] — [explanation and fix]
|
|
94
|
+
|
|
95
|
+
### Major
|
|
96
|
+
1. [Finding]: [file:line] — [explanation and fix]
|
|
97
|
+
|
|
98
|
+
### Minor
|
|
99
|
+
1. [Finding]: [file:line] — [explanation and fix]
|
|
100
|
+
|
|
101
|
+
### Passed Checks
|
|
102
|
+
- Security: organizationId stripped ✓
|
|
103
|
+
- Tests: multi-tenant isolation ✓
|
|
104
|
+
- ...
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Step 5: Apply Fixes (if requested)
|
|
108
|
+
|
|
109
|
+
If the user asks you to fix the findings:
|
|
110
|
+
1. Fix Critical issues first.
|
|
111
|
+
2. Write regression tests for any bugs found.
|
|
112
|
+
3. Run `npm test` after each fix to confirm nothing breaks.
|
|
113
|
+
4. Summarize what was changed.
|