omni-rest 0.5.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AI/codex/omni-rest/SKILL.md +373 -0
- package/dist/adapters/express.js +1 -1
- package/dist/adapters/express.js.map +1 -1
- package/dist/adapters/express.mjs +1 -1
- package/dist/adapters/express.mjs.map +1 -1
- package/dist/adapters/fastify.js +1 -1
- package/dist/adapters/fastify.js.map +1 -1
- package/dist/adapters/fastify.mjs +1 -1
- package/dist/adapters/fastify.mjs.map +1 -1
- package/dist/adapters/hapi.js +1 -1
- package/dist/adapters/hapi.js.map +1 -1
- package/dist/adapters/hapi.mjs +1 -1
- package/dist/adapters/hapi.mjs.map +1 -1
- package/dist/adapters/hono.js +1 -1
- package/dist/adapters/hono.js.map +1 -1
- package/dist/adapters/hono.mjs +1 -1
- package/dist/adapters/hono.mjs.map +1 -1
- package/dist/adapters/koa.js +1 -1
- package/dist/adapters/koa.js.map +1 -1
- package/dist/adapters/koa.mjs +1 -1
- package/dist/adapters/koa.mjs.map +1 -1
- package/dist/adapters/nestjs.js +1 -1
- package/dist/adapters/nestjs.js.map +1 -1
- package/dist/adapters/nestjs.mjs +1 -1
- package/dist/adapters/nestjs.mjs.map +1 -1
- package/dist/adapters/nextjs.js +1 -1
- package/dist/adapters/nextjs.js.map +1 -1
- package/dist/adapters/nextjs.mjs +1 -1
- package/dist/adapters/nextjs.mjs.map +1 -1
- package/dist/cli.js +234 -101
- package/dist/cli.js.map +1 -1
- package/dist/cli.mjs +234 -101
- package/dist/cli.mjs.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
|
@@ -14,6 +14,7 @@ For cross-project API work, also read [AI/PORTABLE_API_PLAYBOOK.md](../../../AI/
|
|
|
14
14
|
- Editing adapters, generators, CLI behavior, or tests.
|
|
15
15
|
- Explaining the repo structure to another agent.
|
|
16
16
|
- Preparing portable instructions for other tools.
|
|
17
|
+
- Integrating omni-rest into a real-world project (NestJS, Express, Next.js, etc.).
|
|
17
18
|
|
|
18
19
|
## Working Order
|
|
19
20
|
|
|
@@ -35,3 +36,375 @@ For cross-project API work, also read [AI/PORTABLE_API_PLAYBOOK.md](../../../AI/
|
|
|
35
36
|
- Prefer code over README text.
|
|
36
37
|
- Preserve existing exports and generated output shapes.
|
|
37
38
|
- Do not rename package entrypoints without a specific request.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Real-World Integration Guide
|
|
43
|
+
|
|
44
|
+
This section documents how omni-rest is consumed in a production project.
|
|
45
|
+
The reference implementation is `examples/rms/` — a NestJS Restaurant Management System
|
|
46
|
+
with Prisma v7, multi-file schema, and a custom client output path.
|
|
47
|
+
|
|
48
|
+
### Prisma v7 Compatibility
|
|
49
|
+
|
|
50
|
+
Prisma v7 made two breaking changes that affect omni-rest:
|
|
51
|
+
|
|
52
|
+
1. **`Prisma.dmmf` removed** — `@prisma/client` no longer exports the DMMF object.
|
|
53
|
+
omni-rest's `src/introspect.ts` now guards this access and only uses it for Prisma ≤4.
|
|
54
|
+
The primary path (`prisma._runtimeDataModel.models`) works for Prisma v5+.
|
|
55
|
+
|
|
56
|
+
2. **Custom output generates TypeScript source files** — When `output` is set in the
|
|
57
|
+
generator block, Prisma v7 generates `.ts` files (not a compiled `index.js`).
|
|
58
|
+
The CLI's `extractRuntimeDataModelFromFile()` now handles:
|
|
59
|
+
- `internal/class.ts` — the v7 TypeScript config object with embedded model map
|
|
60
|
+
- `config.runtimeDataModel = JSON.parse("...")` — v7 compiled JS
|
|
61
|
+
- Inline `runtimeDataModel = { models: {...} }` — Prisma v4/v5 format
|
|
62
|
+
- `inlineSchema` SDL parsing — last-resort fallback for v7 TS output
|
|
63
|
+
|
|
64
|
+
3. **Multi-file schema via `prisma.config.ts`** — Prisma v7 supports a `prisma.config.ts`
|
|
65
|
+
file that points to a schema directory. The CLI now reads this file first and searches
|
|
66
|
+
all `.prisma` files in the directory for the `output` generator setting.
|
|
67
|
+
|
|
68
|
+
### Project Structure Pattern
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
my-app/
|
|
72
|
+
├── prisma/
|
|
73
|
+
│ ├── schema/ # Multi-file schema (Prisma v7)
|
|
74
|
+
│ │ ├── base.prisma # generator + datasource
|
|
75
|
+
│ │ ├── 01_auth.prisma
|
|
76
|
+
│ │ └── 02_domain.prisma
|
|
77
|
+
│ └── generated/
|
|
78
|
+
│ └── prisma/ # Custom output path
|
|
79
|
+
│ ├── client.ts # Main import
|
|
80
|
+
│ └── internal/
|
|
81
|
+
│ └── class.ts # Contains runtimeDataModel
|
|
82
|
+
├── prisma.config.ts # Prisma v7 config
|
|
83
|
+
├── src/
|
|
84
|
+
│ ├── prisma/
|
|
85
|
+
│ │ ├── prisma.service.ts # Wraps PrismaClient for NestJS DI
|
|
86
|
+
│ │ └── prisma.module.ts # @Global() module
|
|
87
|
+
│ └── omni-rest/
|
|
88
|
+
│ ├── omni-rest.controller.ts # nestjsController() factory + options
|
|
89
|
+
│ └── omni-rest.module.ts # NestJS module wiring
|
|
90
|
+
└── src/main.ts # Bootstrap with CORS
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### PrismaService Pattern (Prisma v7 + NestJS)
|
|
94
|
+
|
|
95
|
+
Prisma v7 with a custom output path generates TypeScript source files.
|
|
96
|
+
Import directly from the output path — do NOT import from `@prisma/client`:
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
// src/prisma/prisma.service.ts
|
|
100
|
+
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
|
|
101
|
+
import { PrismaClient } from '../../prisma/generated/prisma/client.js';
|
|
102
|
+
|
|
103
|
+
@Injectable()
|
|
104
|
+
export class PrismaService implements OnModuleInit, OnModuleDestroy {
|
|
105
|
+
readonly client: any;
|
|
106
|
+
|
|
107
|
+
constructor() {
|
|
108
|
+
this.client = new (PrismaClient as any)();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async onModuleInit() { await this.client.$connect(); }
|
|
112
|
+
async onModuleDestroy() { await this.client.$disconnect(); }
|
|
113
|
+
|
|
114
|
+
[key: string]: any;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Proxy property access so prismaService.user.findMany() works
|
|
118
|
+
const handler: ProxyHandler<PrismaService> = {
|
|
119
|
+
get(target, prop) {
|
|
120
|
+
if (prop in target) return (target as any)[prop];
|
|
121
|
+
const client = target.client;
|
|
122
|
+
if (client && prop in client) {
|
|
123
|
+
const val = (client as any)[prop];
|
|
124
|
+
return typeof val === 'function' ? val.bind(client) : val;
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Make it `@Global()` so it's available everywhere without re-importing:
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
// src/prisma/prisma.module.ts
|
|
134
|
+
@Global()
|
|
135
|
+
@Module({ providers: [PrismaService], exports: [PrismaService] })
|
|
136
|
+
export class PrismaModule {}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### omni-rest Options: Production Checklist
|
|
140
|
+
|
|
141
|
+
When configuring `PrismaRestOptions` for a real project, work through this checklist:
|
|
142
|
+
|
|
143
|
+
#### 1. Allow List — Explicit Model Exposure
|
|
144
|
+
|
|
145
|
+
Never expose all models by default. Always set `allow` to the exact list of models
|
|
146
|
+
that external clients should access. Exclude:
|
|
147
|
+
- Auth/session/OTP models (managed by the auth service)
|
|
148
|
+
- Internal audit logs (write-once, managed by middleware)
|
|
149
|
+
- Edge sync queues (internal to the sync engine)
|
|
150
|
+
- Any model with raw secrets or tokens
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
allow: [
|
|
154
|
+
'brand', 'branch', 'menucategory', 'menuitem',
|
|
155
|
+
'order', 'orderlineitem', 'payment', 'customer',
|
|
156
|
+
// ... only what clients actually need
|
|
157
|
+
]
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
#### 2. Field Guards — Protect Sensitive Fields
|
|
161
|
+
|
|
162
|
+
Use `fieldGuards` to control field visibility per model:
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
fieldGuards: {
|
|
166
|
+
// Hide fields that must never leave the server
|
|
167
|
+
user: {
|
|
168
|
+
hidden: ['passwordHash'], // Never in any response
|
|
169
|
+
readOnly: ['id', 'createdAt'], // Stripped from write bodies
|
|
170
|
+
},
|
|
171
|
+
payment: {
|
|
172
|
+
hidden: ['gatewayResponse'], // Raw gateway payload — internal only
|
|
173
|
+
readOnly: ['processedAt', 'refundedAt'],
|
|
174
|
+
},
|
|
175
|
+
// Computed fields that clients must not overwrite
|
|
176
|
+
order: {
|
|
177
|
+
readOnly: ['subtotal', 'taxAmount', 'totalAmount', 'orderNumber'],
|
|
178
|
+
},
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Three guard types:
|
|
183
|
+
- `hidden` — field never appears in any GET response AND is stripped from writes
|
|
184
|
+
- `readOnly` — field is stripped from POST/PUT/PATCH bodies (clients can't set it)
|
|
185
|
+
- `writeOnly` — field is accepted in writes but never returned in GET responses
|
|
186
|
+
|
|
187
|
+
#### 3. Guards — Method-Level Access Control
|
|
188
|
+
|
|
189
|
+
Use `guards` to block specific HTTP methods on models:
|
|
190
|
+
|
|
191
|
+
```ts
|
|
192
|
+
// Read-only guard: block all writes
|
|
193
|
+
const readOnlyGuard: GuardFn = ({ method }) =>
|
|
194
|
+
['POST', 'PUT', 'PATCH', 'DELETE'].includes(method)
|
|
195
|
+
? 'This resource is read-only.'
|
|
196
|
+
: null;
|
|
197
|
+
|
|
198
|
+
// Blocked guard: block all access
|
|
199
|
+
const blockedGuard: GuardFn = () =>
|
|
200
|
+
'Access to this resource is not permitted via the REST API.';
|
|
201
|
+
|
|
202
|
+
guards: {
|
|
203
|
+
dailyanalyticssnapshot: {
|
|
204
|
+
POST: readOnlyGuard, PUT: readOnlyGuard,
|
|
205
|
+
PATCH: readOnlyGuard, DELETE: readOnlyGuard,
|
|
206
|
+
},
|
|
207
|
+
loyaltytransaction: {
|
|
208
|
+
// Immutable ledger — no updates or deletes
|
|
209
|
+
PUT: readOnlyGuard, PATCH: readOnlyGuard, DELETE: readOnlyGuard,
|
|
210
|
+
},
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
For auth-aware guards (JWT-based), inject the request context:
|
|
215
|
+
|
|
216
|
+
```ts
|
|
217
|
+
guards: {
|
|
218
|
+
order: {
|
|
219
|
+
DELETE: async ({ id, body }) => {
|
|
220
|
+
// In a real app, extract user from request context
|
|
221
|
+
// and check if they have the MANAGER role
|
|
222
|
+
return null; // or return error string to block
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
#### 4. Soft Delete
|
|
229
|
+
|
|
230
|
+
Models with `isActive: Boolean` or `deletedAt: DateTime` get soft-delete automatically
|
|
231
|
+
when `softDelete: true` is set. DELETE sets `isActive = false` or `deletedAt = new Date()`
|
|
232
|
+
instead of destroying the record. GET list queries automatically filter out soft-deleted records.
|
|
233
|
+
|
|
234
|
+
```ts
|
|
235
|
+
softDelete: true,
|
|
236
|
+
// Optional: override the auto-detected field name
|
|
237
|
+
// softDeleteField: 'archivedAt',
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Models in the RMS project that benefit from soft delete:
|
|
241
|
+
- `Branch` (isActive)
|
|
242
|
+
- `RestaurantTable` (isActive)
|
|
243
|
+
- `MenuItem` (isAvailable — use explicit softDeleteField)
|
|
244
|
+
- `Customer` (isActive)
|
|
245
|
+
- `TaxRule` (isActive)
|
|
246
|
+
- `ModifierOption` (isAvailable)
|
|
247
|
+
|
|
248
|
+
#### 5. Complexity Limits
|
|
249
|
+
|
|
250
|
+
Protect against abusive queries that join many relations or request huge result sets:
|
|
251
|
+
|
|
252
|
+
```ts
|
|
253
|
+
complexity: {
|
|
254
|
+
maxScore: 50,
|
|
255
|
+
rules: {
|
|
256
|
+
perInclude: 10, // Each ?include=relation costs 10 points
|
|
257
|
+
perFilter: 2, // Each filter key costs 2 points
|
|
258
|
+
perSort: 1, // Each sort field costs 1 point
|
|
259
|
+
perLimit100: 5, // Each 100 records requested costs 5 points
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
#### 6. Pagination
|
|
265
|
+
|
|
266
|
+
```ts
|
|
267
|
+
defaultLimit: 25, // Default page size
|
|
268
|
+
maxLimit: 200, // Hard cap — clients can't request more
|
|
269
|
+
paginationMode: 'offset', // or 'cursor' for large datasets
|
|
270
|
+
envelope: true, // Wrap in { data: [...], meta: { total, page, limit, totalPages } }
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
Use cursor pagination for high-volume models (orders, stock movements, audit logs)
|
|
274
|
+
where offset pagination becomes slow at large offsets.
|
|
275
|
+
|
|
276
|
+
#### 7. Aggregation Endpoints
|
|
277
|
+
|
|
278
|
+
When `features.aggregation: true`, omni-rest exposes:
|
|
279
|
+
- `GET /api/order/aggregate?_count=*&_sum=totalAmount` — aggregate stats
|
|
280
|
+
- `GET /api/order/groupBy?by=status&_count=*` — group by field
|
|
281
|
+
|
|
282
|
+
These are powerful for analytics dashboards. Disable them on sensitive models
|
|
283
|
+
by setting `features: { aggregation: false }` or using a guard.
|
|
284
|
+
|
|
285
|
+
#### 8. SSE Real-Time Subscriptions
|
|
286
|
+
|
|
287
|
+
Every exposed model gets a `GET /api/:model/subscribe` SSE endpoint automatically.
|
|
288
|
+
The POS tablet can subscribe to order updates without polling:
|
|
289
|
+
|
|
290
|
+
```ts
|
|
291
|
+
const es = new EventSource('/api/order/subscribe');
|
|
292
|
+
es.onmessage = (e) => {
|
|
293
|
+
const { event, model, record } = JSON.parse(e.data);
|
|
294
|
+
if (event === 'create') addOrderToKDS(record);
|
|
295
|
+
if (event === 'update') updateOrderStatus(record);
|
|
296
|
+
};
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
Tune the polling interval to balance freshness vs. DB load:
|
|
300
|
+
|
|
301
|
+
```ts
|
|
302
|
+
subscription: {
|
|
303
|
+
pollInterval: 500, // 500ms for POS (low latency)
|
|
304
|
+
heartbeatInterval: 30_000, // 30s keepalive
|
|
305
|
+
},
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
### NestJS Wiring
|
|
309
|
+
|
|
310
|
+
```ts
|
|
311
|
+
// src/omni-rest/omni-rest.controller.ts
|
|
312
|
+
import { nestjsController } from 'omni-rest';
|
|
313
|
+
import { PrismaService } from '../prisma/prisma.service';
|
|
314
|
+
|
|
315
|
+
let _prisma: PrismaService | null = null;
|
|
316
|
+
function getPrisma() {
|
|
317
|
+
if (!_prisma) {
|
|
318
|
+
_prisma = new PrismaService();
|
|
319
|
+
void _prisma.onModuleInit();
|
|
320
|
+
}
|
|
321
|
+
return _prisma;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
export const OmniRestDynamicModule = nestjsController(
|
|
325
|
+
getPrisma(),
|
|
326
|
+
omniRestOptions,
|
|
327
|
+
'api', // URL prefix → /api/:model
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
// src/omni-rest/omni-rest.module.ts
|
|
331
|
+
@Module({
|
|
332
|
+
imports: [PrismaModule],
|
|
333
|
+
controllers: [OmniRestDynamicModule],
|
|
334
|
+
})
|
|
335
|
+
export class OmniRestModule {}
|
|
336
|
+
|
|
337
|
+
// src/app.module.ts
|
|
338
|
+
@Module({
|
|
339
|
+
imports: [PrismaModule, OmniRestModule],
|
|
340
|
+
// ...
|
|
341
|
+
})
|
|
342
|
+
export class AppModule {}
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
### CLI Usage with Prisma v7
|
|
346
|
+
|
|
347
|
+
The CLI reads `prisma.config.ts` automatically to find the schema and output path:
|
|
348
|
+
|
|
349
|
+
```bash
|
|
350
|
+
# From the project root (where prisma.config.ts lives)
|
|
351
|
+
npx omni-rest generate # Zod schemas + OpenAPI spec
|
|
352
|
+
npx omni-rest generate:zod # Zod schemas only → src/schemas.generated.ts
|
|
353
|
+
npx omni-rest generate:openapi # OpenAPI spec → openapi.json
|
|
354
|
+
npx omni-rest generate:config # omni-rest.config.json for the frontend client
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
For Prisma v7 with multi-file schema + custom output, the CLI:
|
|
358
|
+
1. Reads `prisma.config.ts` to find `schema: "prisma/schema"` (directory)
|
|
359
|
+
2. Scans all `.prisma` files in that directory for `output = "..."` in the generator block
|
|
360
|
+
3. Reads `prisma/generated/prisma/internal/class.ts` and extracts the model map
|
|
361
|
+
4. Generates schemas without requiring a DB connection
|
|
362
|
+
|
|
363
|
+
### API Usage Examples
|
|
364
|
+
|
|
365
|
+
```bash
|
|
366
|
+
# List all active branches with pagination
|
|
367
|
+
GET /api/branch?page=1&limit=25
|
|
368
|
+
|
|
369
|
+
# Filter orders by status and branch
|
|
370
|
+
GET /api/order?status=PENDING&branchId=clxyz123&orderBy=createdAt:desc
|
|
371
|
+
|
|
372
|
+
# Include related line items
|
|
373
|
+
GET /api/order/clxyz123?include=lineItems
|
|
374
|
+
|
|
375
|
+
# Aggregate: total revenue by branch today
|
|
376
|
+
GET /api/dailyanalyticssnapshot/aggregate?_sum=totalRevenue&branchId=clxyz123
|
|
377
|
+
|
|
378
|
+
# Group orders by status
|
|
379
|
+
GET /api/order/groupBy?by=status&_count=*
|
|
380
|
+
|
|
381
|
+
# Soft delete a menu item (sets isAvailable=false)
|
|
382
|
+
DELETE /api/menuitem/clxyz456
|
|
383
|
+
|
|
384
|
+
# Bulk create menu items
|
|
385
|
+
POST /api/menuitem/bulk
|
|
386
|
+
[{ "name": "Zinger Burger", "basePrice": 450, ... }, ...]
|
|
387
|
+
|
|
388
|
+
# Real-time order updates (SSE)
|
|
389
|
+
GET /api/order/subscribe
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
### Security Considerations
|
|
393
|
+
|
|
394
|
+
1. **Never expose auth models** — `User`, `Session`, `OtpCode`, `UserAuthProvider`,
|
|
395
|
+
`RoleAssignment`, `PermissionOverride` should never be in the `allow` list.
|
|
396
|
+
These are managed by the dedicated auth service with proper password hashing,
|
|
397
|
+
token rotation, and rate limiting.
|
|
398
|
+
|
|
399
|
+
2. **Guard write operations on financial models** — `Payment`, `Shift`, `AppliedDiscount`
|
|
400
|
+
should require manager-level auth before POST/PUT/PATCH/DELETE.
|
|
401
|
+
|
|
402
|
+
3. **Hide all token/hash fields** — `refreshToken`, `codeHash`, `passwordHash`,
|
|
403
|
+
`accessToken`, `gatewayResponse` must be in `hidden` field guards.
|
|
404
|
+
|
|
405
|
+
4. **Rate limit the API** — Use the `rateLimit` option with Redis or an in-memory
|
|
406
|
+
counter to prevent abuse of bulk endpoints and aggregation queries.
|
|
407
|
+
|
|
408
|
+
5. **Validate writes with Zod** — Run `npx omni-rest generate:zod` to generate
|
|
409
|
+
Zod schemas, then use `withValidation()` middleware to validate request bodies
|
|
410
|
+
before they reach Prisma.
|
package/dist/adapters/express.js
CHANGED
|
@@ -29,7 +29,7 @@ function getModels(prisma) {
|
|
|
29
29
|
try {
|
|
30
30
|
const prismaModule = __require("@prisma/client");
|
|
31
31
|
const dmmfModels = prismaModule?.Prisma?.dmmf?.datamodel?.models;
|
|
32
|
-
if (dmmfModels) {
|
|
32
|
+
if (Array.isArray(dmmfModels) && dmmfModels.length > 0) {
|
|
33
33
|
raw = dmmfModels;
|
|
34
34
|
}
|
|
35
35
|
} catch {
|