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.
@@ -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.
@@ -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 {