omni-rest 0.1.0

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/dist/index.mjs ADDED
@@ -0,0 +1,1006 @@
1
+ import { Prisma } from '@prisma/client';
2
+
3
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
4
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
5
+ }) : x)(function(x) {
6
+ if (typeof require !== "undefined") return require.apply(this, arguments);
7
+ throw Error('Dynamic require of "' + x + '" is not supported');
8
+ });
9
+ function getModels(prisma) {
10
+ let raw;
11
+ if (prisma?._runtimeDataModel?.models) {
12
+ const modelsObj = prisma._runtimeDataModel.models;
13
+ raw = Object.entries(modelsObj).map(([name, model]) => ({
14
+ name,
15
+ ...model,
16
+ fields: (model.fields || []).map((f) => ({
17
+ ...f,
18
+ relationName: f.kind === "object" ? f.name : void 0
19
+ }))
20
+ }));
21
+ }
22
+ if (!raw) {
23
+ const dmmfModels = Prisma?.dmmf?.datamodel?.models || __require("@prisma/client")?.Prisma?.dmmf?.datamodel?.models;
24
+ if (dmmfModels) {
25
+ raw = dmmfModels;
26
+ }
27
+ }
28
+ if (!raw) {
29
+ throw new Error(
30
+ "[omni-rest] Could not find Prisma DMMF. Ensure Prisma client is generated and you're passing a PrismaClient instance to omni-rest."
31
+ );
32
+ }
33
+ if (!Array.isArray(raw)) {
34
+ throw new Error(
35
+ `[omni-rest] Expected models to be an array, got ${typeof raw}. Debug: prisma._runtimeDataModel.models=${!!prisma?._runtimeDataModel?.models}, raw value=${JSON.stringify(raw).slice(0, 100)}`
36
+ );
37
+ }
38
+ return raw.map((model) => {
39
+ const fields = model.fields.map((f) => ({
40
+ name: f.name,
41
+ type: f.type,
42
+ isId: f.isId,
43
+ isRequired: f.isRequired,
44
+ isList: f.isList,
45
+ isRelation: !!f.relationName
46
+ }));
47
+ const idField = model.fields.find((f) => f.isId)?.name ?? "id";
48
+ return {
49
+ name: model.name,
50
+ routeName: toRouteName(model.name),
51
+ fields,
52
+ idField
53
+ };
54
+ });
55
+ }
56
+ function toRouteName(modelName) {
57
+ return modelName.toLowerCase();
58
+ }
59
+ function buildModelMap(models, allowList) {
60
+ const filtered = allowList ? models.filter((m) => allowList.includes(m.routeName)) : models;
61
+ return Object.fromEntries(filtered.map((m) => [m.routeName, m]));
62
+ }
63
+ function getDelegate(prisma, meta) {
64
+ const key = meta.name.charAt(0).toLowerCase() + meta.name.slice(1);
65
+ const delegate = prisma[key];
66
+ if (!delegate) {
67
+ throw new Error(
68
+ `Could not find Prisma delegate for model "${meta.name}". Expected prisma.${key} to exist.`
69
+ );
70
+ }
71
+ return delegate;
72
+ }
73
+
74
+ // src/query-builder.ts
75
+ var FILTER_OPERATORS = {
76
+ _gte: "gte",
77
+ _lte: "lte",
78
+ _gt: "gt",
79
+ _lt: "lt",
80
+ _contains: "contains",
81
+ _icontains: "contains",
82
+ // case-insensitive version (mode: insensitive)
83
+ _startsWith: "startsWith",
84
+ _endsWith: "endsWith",
85
+ _in: "in",
86
+ _notIn: "notIn",
87
+ _not: "not"
88
+ };
89
+ var RESERVED_KEYS = /* @__PURE__ */ new Set([
90
+ "page",
91
+ "limit",
92
+ "sort",
93
+ "include",
94
+ "select"
95
+ ]);
96
+ function buildQuery(searchParams, defaultLimit = 20, maxLimit = 100) {
97
+ const where = {};
98
+ const orderBy = {};
99
+ let include = {};
100
+ let select = null;
101
+ const page = Math.max(1, parseInt(searchParams.get("page") ?? "1"));
102
+ const rawLimit = parseInt(searchParams.get("limit") ?? String(defaultLimit));
103
+ const take = Math.min(rawLimit, maxLimit);
104
+ const skip = (page - 1) * take;
105
+ const sortParam = searchParams.get("sort");
106
+ if (sortParam) {
107
+ for (const part of sortParam.split(",")) {
108
+ const [field, dir] = part.trim().split(":");
109
+ if (field) {
110
+ orderBy[field] = dir === "desc" ? "desc" : "asc";
111
+ }
112
+ }
113
+ }
114
+ const includeParam = searchParams.get("include");
115
+ if (includeParam) {
116
+ for (const rel of includeParam.split(",")) {
117
+ if (rel.trim()) include[rel.trim()] = true;
118
+ }
119
+ }
120
+ const selectParam = searchParams.get("select");
121
+ if (selectParam) {
122
+ select = {};
123
+ for (const field of selectParam.split(",")) {
124
+ if (field.trim()) select[field.trim()] = true;
125
+ }
126
+ }
127
+ for (const [key, value] of searchParams.entries()) {
128
+ if (RESERVED_KEYS.has(key)) continue;
129
+ let matched = false;
130
+ const sortedOps = Object.keys(FILTER_OPERATORS).sort(
131
+ (a, b) => b.length - a.length
132
+ );
133
+ for (const suffix of sortedOps) {
134
+ if (key.endsWith(suffix)) {
135
+ const field = key.slice(0, -suffix.length);
136
+ const prismaOp = FILTER_OPERATORS[suffix];
137
+ let parsedValue = value;
138
+ if (prismaOp === "in" || prismaOp === "notIn") {
139
+ parsedValue = value.split(",").map((v) => v.trim());
140
+ }
141
+ if (!isNaN(Number(parsedValue)) && typeof parsedValue === "string") {
142
+ parsedValue = Number(parsedValue);
143
+ }
144
+ const extra = suffix === "_icontains" ? { mode: "insensitive" } : {};
145
+ where[field] = { [prismaOp]: parsedValue, ...extra };
146
+ matched = true;
147
+ break;
148
+ }
149
+ }
150
+ if (!matched) {
151
+ let parsedValue = value;
152
+ if (value === "true") parsedValue = true;
153
+ else if (value === "false") parsedValue = false;
154
+ else if (!isNaN(Number(value)) && value !== "") {
155
+ parsedValue = Number(value);
156
+ }
157
+ where[key] = parsedValue;
158
+ }
159
+ }
160
+ return { where, orderBy, skip, take, include, select };
161
+ }
162
+
163
+ // src/middleware.ts
164
+ async function runGuard(guards, model, method, ctx) {
165
+ const modelGuards = guards[model];
166
+ if (!modelGuards) return null;
167
+ const fn = modelGuards[method];
168
+ if (!fn) return null;
169
+ return fn({ ...ctx, method });
170
+ }
171
+ async function runHook(hook, ctx) {
172
+ if (!hook) return;
173
+ try {
174
+ await hook(ctx);
175
+ } catch (e) {
176
+ console.error("[omni-rest] Hook error:", e);
177
+ }
178
+ }
179
+
180
+ // src/router.ts
181
+ function createRouter(prisma, options = {}) {
182
+ const {
183
+ allow,
184
+ guards = {},
185
+ beforeOperation,
186
+ afterOperation,
187
+ defaultLimit = 20,
188
+ maxLimit = 100
189
+ } = options;
190
+ const models = getModels(prisma);
191
+ const modelMap = buildModelMap(models, allow);
192
+ async function handle(method, modelName, id, body, searchParams, operation) {
193
+ const meta = modelMap[modelName.toLowerCase()];
194
+ if (!meta) {
195
+ return {
196
+ status: 404,
197
+ data: {
198
+ error: `Model "${modelName}" not found or not exposed.`,
199
+ available: Object.keys(modelMap)
200
+ }
201
+ };
202
+ }
203
+ const guardError = await runGuard(guards, meta.routeName, method, {
204
+ id,
205
+ body
206
+ });
207
+ if (guardError) {
208
+ return { status: 403, data: { error: guardError } };
209
+ }
210
+ await runHook(beforeOperation, { model: meta.name, method, id, body });
211
+ let result;
212
+ try {
213
+ result = await executeOperation(
214
+ prisma,
215
+ meta,
216
+ method,
217
+ id,
218
+ body,
219
+ searchParams,
220
+ defaultLimit,
221
+ maxLimit,
222
+ operation
223
+ );
224
+ } catch (e) {
225
+ return handlePrismaError(e);
226
+ }
227
+ await runHook(afterOperation, {
228
+ model: meta.name,
229
+ method,
230
+ id,
231
+ body,
232
+ result: result.data
233
+ });
234
+ return result;
235
+ }
236
+ return { handle, modelMap, models };
237
+ }
238
+ async function executeOperation(prisma, meta, method, id, body, searchParams, defaultLimit, maxLimit, operation) {
239
+ const delegate = getDelegate(prisma, meta);
240
+ const { where, orderBy, skip, take, include, select } = buildQuery(
241
+ searchParams,
242
+ defaultLimit,
243
+ maxLimit
244
+ );
245
+ const includeArg = Object.keys(include).length > 0 ? include : void 0;
246
+ const selectArg = select && Object.keys(select).length > 0 ? select : void 0;
247
+ const projection = selectArg ? { select: selectArg } : includeArg ? { include: includeArg } : {};
248
+ if (method === "PATCH" && operation === "bulk-update") {
249
+ if (!Array.isArray(body) || body.length === 0) {
250
+ return {
251
+ status: 400,
252
+ data: { error: "Request body must be a non-empty array of update records" }
253
+ };
254
+ }
255
+ for (const item of body) {
256
+ if (!item[meta.idField]) {
257
+ return {
258
+ status: 400,
259
+ data: { error: `Each record must have an ${meta.idField} field` }
260
+ };
261
+ }
262
+ }
263
+ const results = await Promise.all(
264
+ body.map((item) => {
265
+ const id2 = item[meta.idField];
266
+ const updateData = { ...item };
267
+ delete updateData[meta.idField];
268
+ return delegate.update({
269
+ where: { [meta.idField]: coerceId(id2) },
270
+ data: updateData,
271
+ ...projection
272
+ });
273
+ })
274
+ );
275
+ return {
276
+ status: 200,
277
+ data: {
278
+ updated: results.length,
279
+ records: results
280
+ }
281
+ };
282
+ }
283
+ if (method === "DELETE" && operation === "bulk-delete") {
284
+ if (!Array.isArray(body) || body.length === 0) {
285
+ return {
286
+ status: 400,
287
+ data: { error: "Request body must be a non-empty array of IDs" }
288
+ };
289
+ }
290
+ const ids = body.map(
291
+ (item) => typeof item === "object" ? item[meta.idField] : item
292
+ );
293
+ const result = await delegate.deleteMany({
294
+ where: {
295
+ [meta.idField]: { in: ids.map(coerceId) }
296
+ }
297
+ });
298
+ return {
299
+ status: 200,
300
+ data: {
301
+ deleted: result.count
302
+ }
303
+ };
304
+ }
305
+ if (method === "GET" && !id) {
306
+ const [data, total] = await prisma.$transaction([
307
+ delegate.findMany({ where, orderBy, skip, take, ...projection }),
308
+ delegate.count({ where })
309
+ ]);
310
+ return {
311
+ status: 200,
312
+ data: {
313
+ data,
314
+ meta: {
315
+ total,
316
+ page: Math.floor(skip / take) + 1,
317
+ limit: take,
318
+ totalPages: Math.ceil(total / take)
319
+ }
320
+ }
321
+ };
322
+ }
323
+ if (method === "GET" && id) {
324
+ const record = await delegate.findUnique({
325
+ where: { [meta.idField]: coerceId(id) },
326
+ ...projection
327
+ });
328
+ if (!record) {
329
+ return { status: 404, data: { error: `${meta.name} with id "${id}" not found.` } };
330
+ }
331
+ return { status: 200, data: record };
332
+ }
333
+ if (method === "POST" && !id) {
334
+ const record = await delegate.create({ data: body });
335
+ return { status: 201, data: record };
336
+ }
337
+ if ((method === "PUT" || method === "PATCH") && id) {
338
+ const record = await delegate.update({
339
+ where: { [meta.idField]: coerceId(id) },
340
+ data: body
341
+ });
342
+ return { status: 200, data: record };
343
+ }
344
+ if (method === "DELETE" && id) {
345
+ await delegate.delete({
346
+ where: { [meta.idField]: coerceId(id) }
347
+ });
348
+ return { status: 204, data: null };
349
+ }
350
+ return { status: 405, data: { error: `Method ${method} not allowed.` } };
351
+ }
352
+ function coerceId(id) {
353
+ const n = Number(id);
354
+ return isNaN(n) ? id : n;
355
+ }
356
+ function handlePrismaError(e) {
357
+ const code = e?.code;
358
+ if (code === "P2025") {
359
+ return { status: 404, data: { error: "Record not found." } };
360
+ }
361
+ if (code === "P2002") {
362
+ const fields = e?.meta?.target ?? "unknown fields";
363
+ return {
364
+ status: 409,
365
+ data: { error: `Unique constraint failed on: ${fields}` }
366
+ };
367
+ }
368
+ if (code === "P2003") {
369
+ return { status: 400, data: { error: "Foreign key constraint failed." } };
370
+ }
371
+ if (code === "P2014") {
372
+ return { status: 400, data: { error: "Relation violation." } };
373
+ }
374
+ return { status: 500, data: { error: e?.message ?? "Internal server error." } };
375
+ }
376
+
377
+ // src/zod-generator.ts
378
+ var PRISMA_TO_ZOD = {
379
+ String: "z.string()",
380
+ Int: "z.number().int()",
381
+ Float: "z.number()",
382
+ Decimal: "z.number()",
383
+ Boolean: "z.boolean()",
384
+ DateTime: "z.coerce.date()",
385
+ Json: "z.any()",
386
+ BigInt: "z.bigint()",
387
+ Bytes: "z.any()"
388
+ };
389
+ function fieldToZod(field) {
390
+ if (field.isRelation) return null;
391
+ let zod = PRISMA_TO_ZOD[field.type] ?? "z.any()";
392
+ if (!field.isRequired) {
393
+ zod = `${zod}.optional()`;
394
+ }
395
+ if (field.isList) {
396
+ zod = `z.array(${zod})`;
397
+ }
398
+ return zod;
399
+ }
400
+ function generateModelSchema(meta) {
401
+ const name = meta.name;
402
+ const fields = meta.fields.filter((f) => !f.isRelation && !f.isId).map((f) => {
403
+ const zodExpr = fieldToZod(f);
404
+ if (!zodExpr) return null;
405
+ return ` ${f.name}: ${zodExpr},`;
406
+ }).filter(Boolean).join("\n");
407
+ return `
408
+ // \u2500\u2500\u2500 ${name} \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
409
+
410
+ export const ${name}CreateSchema = z.object({
411
+ ${fields}
412
+ });
413
+
414
+ export const ${name}UpdateSchema = ${name}CreateSchema.partial();
415
+
416
+ export type ${name}Create = z.infer<typeof ${name}CreateSchema>;
417
+ export type ${name}Update = z.infer<typeof ${name}UpdateSchema>;
418
+ `.trim();
419
+ }
420
+ function generateZodSchemas() {
421
+ const models = getModels();
422
+ const schemas = models.map(generateModelSchema).join("\n\n");
423
+ return `/**
424
+ * Auto-generated Zod schemas from Prisma schema.
425
+ * Generated by omni-rest \u2014 do not edit manually.
426
+ * Re-run after schema changes.
427
+ */
428
+ import { z } from "zod";
429
+
430
+ ${schemas}
431
+ `;
432
+ }
433
+ function buildRuntimeSchemas() {
434
+ let z;
435
+ try {
436
+ z = __require("zod").z;
437
+ } catch {
438
+ throw new Error(
439
+ "[omni-rest] zod is required for runtime validation. Run: npm install zod"
440
+ );
441
+ }
442
+ const ZOD_FACTORIES = {
443
+ String: () => z.string(),
444
+ Int: () => z.number().int(),
445
+ Float: () => z.number(),
446
+ Decimal: () => z.number(),
447
+ Boolean: () => z.boolean(),
448
+ DateTime: () => z.coerce.date(),
449
+ Json: () => z.any(),
450
+ BigInt: () => z.bigint(),
451
+ Bytes: () => z.any()
452
+ };
453
+ const models = getModels();
454
+ const result = {};
455
+ for (const meta of models) {
456
+ const shape = {};
457
+ for (const field of meta.fields) {
458
+ if (field.isRelation || field.isId) continue;
459
+ const factory = ZOD_FACTORIES[field.type] ?? (() => z.any());
460
+ let schema = factory();
461
+ if (!field.isRequired) schema = schema.optional();
462
+ if (field.isList) schema = z.array(schema);
463
+ shape[field.name] = schema;
464
+ }
465
+ const createSchema = z.object(shape);
466
+ result[meta.routeName] = {
467
+ create: createSchema,
468
+ update: createSchema.partial()
469
+ };
470
+ }
471
+ return result;
472
+ }
473
+
474
+ // src/validate.ts
475
+ var cachedSchemas = null;
476
+ function getSchemas() {
477
+ if (!cachedSchemas) {
478
+ cachedSchemas = buildRuntimeSchemas();
479
+ }
480
+ return cachedSchemas;
481
+ }
482
+ function validateBody(modelRouteName, method, body) {
483
+ let schemas;
484
+ try {
485
+ schemas = getSchemas();
486
+ } catch {
487
+ return null;
488
+ }
489
+ const modelSchemas = schemas[modelRouteName];
490
+ if (!modelSchemas) return null;
491
+ const schema = method === "POST" ? modelSchemas.create : modelSchemas.update;
492
+ const result = schema.safeParse(body);
493
+ if (!result.success) {
494
+ const issues = result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join(", ");
495
+ return `Validation failed \u2014 ${issues}`;
496
+ }
497
+ return null;
498
+ }
499
+ function withValidation(overrides = {}) {
500
+ return new Proxy(overrides, {
501
+ get(target, modelName) {
502
+ if (modelName in target) return target[modelName];
503
+ return {
504
+ POST: ({ body }) => validateBody(modelName, "POST", body),
505
+ PUT: ({ body }) => validateBody(modelName, "PUT", body),
506
+ PATCH: ({ body }) => validateBody(modelName, "PATCH", body)
507
+ };
508
+ }
509
+ });
510
+ }
511
+
512
+ // src/openapi.ts
513
+ var PRISMA_TO_OAS = {
514
+ String: { type: "string" },
515
+ Int: { type: "integer", format: "int32" },
516
+ Float: { type: "number", format: "float" },
517
+ Decimal: { type: "number" },
518
+ Boolean: { type: "boolean" },
519
+ DateTime: { type: "string", format: "date-time" },
520
+ Json: { type: "object" },
521
+ BigInt: { type: "integer", format: "int64" }
522
+ };
523
+ function fieldToOasSchema(field) {
524
+ if (field.isRelation) return null;
525
+ const base = PRISMA_TO_OAS[field.type] ?? { type: "string" };
526
+ if (field.isList) return { type: "array", items: base };
527
+ return base;
528
+ }
529
+ function buildModelSchema(meta, forCreate = false) {
530
+ const properties = {};
531
+ const required = [];
532
+ for (const field of meta.fields) {
533
+ if (field.isRelation) continue;
534
+ if (forCreate && field.isId) continue;
535
+ const schema = fieldToOasSchema(field);
536
+ if (!schema) continue;
537
+ properties[field.name] = schema;
538
+ if (field.isRequired && !field.isId && forCreate) {
539
+ required.push(field.name);
540
+ }
541
+ }
542
+ return {
543
+ type: "object",
544
+ properties,
545
+ ...required.length > 0 ? { required } : {}
546
+ };
547
+ }
548
+ function generateOpenApiSpec(prisma, options = {}) {
549
+ const {
550
+ title = "omni-rest API",
551
+ version = "1.0.0",
552
+ basePath = "/api",
553
+ allow,
554
+ servers = [{ url: "http://localhost:3000" }]
555
+ } = options;
556
+ const models = getModels(prisma).filter(
557
+ (m) => !allow || allow.includes(m.routeName)
558
+ );
559
+ const paths = {};
560
+ const schemas = {};
561
+ for (const meta of models) {
562
+ const name = meta.name;
563
+ const route = meta.routeName;
564
+ schemas[name] = buildModelSchema(meta, false);
565
+ schemas[`${name}Create`] = buildModelSchema(meta, true);
566
+ schemas[`${name}Update`] = {
567
+ ...buildModelSchema(meta, true),
568
+ required: []
569
+ // all optional for PATCH
570
+ };
571
+ paths[`${basePath}/${route}`] = {
572
+ get: {
573
+ summary: `List ${name}s`,
574
+ tags: [name],
575
+ parameters: buildListParameters(),
576
+ responses: {
577
+ 200: {
578
+ description: `List of ${name}s`,
579
+ content: {
580
+ "application/json": {
581
+ schema: {
582
+ type: "object",
583
+ properties: {
584
+ data: { type: "array", items: { $ref: `#/components/schemas/${name}` } },
585
+ meta: { $ref: "#/components/schemas/PaginationMeta" }
586
+ }
587
+ }
588
+ }
589
+ }
590
+ }
591
+ }
592
+ },
593
+ post: {
594
+ summary: `Create ${name}`,
595
+ tags: [name],
596
+ requestBody: {
597
+ required: true,
598
+ content: {
599
+ "application/json": {
600
+ schema: { $ref: `#/components/schemas/${name}Create` }
601
+ }
602
+ }
603
+ },
604
+ responses: {
605
+ 201: {
606
+ description: `Created ${name}`,
607
+ content: {
608
+ "application/json": {
609
+ schema: { $ref: `#/components/schemas/${name}` }
610
+ }
611
+ }
612
+ },
613
+ 400: { $ref: "#/components/responses/BadRequest" },
614
+ 409: { $ref: "#/components/responses/Conflict" }
615
+ }
616
+ }
617
+ };
618
+ paths[`${basePath}/${route}/{id}`] = {
619
+ parameters: [
620
+ {
621
+ name: "id",
622
+ in: "path",
623
+ required: true,
624
+ schema: { type: "string" },
625
+ description: `${name} ID`
626
+ }
627
+ ],
628
+ get: {
629
+ summary: `Get ${name} by ID`,
630
+ tags: [name],
631
+ responses: {
632
+ 200: {
633
+ description: `${name} record`,
634
+ content: {
635
+ "application/json": {
636
+ schema: { $ref: `#/components/schemas/${name}` }
637
+ }
638
+ }
639
+ },
640
+ 404: { $ref: "#/components/responses/NotFound" }
641
+ }
642
+ },
643
+ put: {
644
+ summary: `Update ${name}`,
645
+ tags: [name],
646
+ requestBody: {
647
+ required: true,
648
+ content: {
649
+ "application/json": {
650
+ schema: { $ref: `#/components/schemas/${name}Create` }
651
+ }
652
+ }
653
+ },
654
+ responses: {
655
+ 200: {
656
+ description: `Updated ${name}`,
657
+ content: {
658
+ "application/json": {
659
+ schema: { $ref: `#/components/schemas/${name}` }
660
+ }
661
+ }
662
+ },
663
+ 404: { $ref: "#/components/responses/NotFound" }
664
+ }
665
+ },
666
+ patch: {
667
+ summary: `Partially update ${name}`,
668
+ tags: [name],
669
+ requestBody: {
670
+ required: true,
671
+ content: {
672
+ "application/json": {
673
+ schema: { $ref: `#/components/schemas/${name}Update` }
674
+ }
675
+ }
676
+ },
677
+ responses: {
678
+ 200: {
679
+ description: `Updated ${name}`,
680
+ content: {
681
+ "application/json": {
682
+ schema: { $ref: `#/components/schemas/${name}` }
683
+ }
684
+ }
685
+ },
686
+ 404: { $ref: "#/components/responses/NotFound" }
687
+ }
688
+ },
689
+ delete: {
690
+ summary: `Delete ${name}`,
691
+ tags: [name],
692
+ responses: {
693
+ 204: { description: "Deleted successfully" },
694
+ 404: { $ref: "#/components/responses/NotFound" }
695
+ }
696
+ }
697
+ };
698
+ paths[`${basePath}/${route}/bulk/update`] = {
699
+ patch: {
700
+ summary: `Bulk update ${name}s`,
701
+ tags: [name],
702
+ requestBody: {
703
+ required: true,
704
+ description: `Array of ${name} objects with id field to update`,
705
+ content: {
706
+ "application/json": {
707
+ schema: {
708
+ type: "array",
709
+ items: { $ref: `#/components/schemas/${name}Update` }
710
+ }
711
+ }
712
+ }
713
+ },
714
+ responses: {
715
+ 200: {
716
+ description: `Bulk update result`,
717
+ content: {
718
+ "application/json": {
719
+ schema: {
720
+ type: "object",
721
+ properties: {
722
+ updated: { type: "integer" },
723
+ records: {
724
+ type: "array",
725
+ items: { $ref: `#/components/schemas/${name}` }
726
+ }
727
+ }
728
+ }
729
+ }
730
+ }
731
+ },
732
+ 400: { $ref: "#/components/responses/BadRequest" }
733
+ }
734
+ }
735
+ };
736
+ paths[`${basePath}/${route}/bulk/delete`] = {
737
+ delete: {
738
+ summary: `Bulk delete ${name}s`,
739
+ tags: [name],
740
+ requestBody: {
741
+ required: true,
742
+ description: `Array of IDs to delete`,
743
+ content: {
744
+ "application/json": {
745
+ schema: {
746
+ type: "array",
747
+ items: { type: "string" }
748
+ }
749
+ }
750
+ }
751
+ },
752
+ responses: {
753
+ 200: {
754
+ description: `Bulk delete result`,
755
+ content: {
756
+ "application/json": {
757
+ schema: {
758
+ type: "object",
759
+ properties: {
760
+ deleted: { type: "integer" }
761
+ }
762
+ }
763
+ }
764
+ }
765
+ },
766
+ 400: { $ref: "#/components/responses/BadRequest" }
767
+ }
768
+ }
769
+ };
770
+ }
771
+ return {
772
+ openapi: "3.0.3",
773
+ info: { title, version },
774
+ servers,
775
+ paths,
776
+ components: {
777
+ schemas: {
778
+ ...schemas,
779
+ PaginationMeta: {
780
+ type: "object",
781
+ properties: {
782
+ total: { type: "integer" },
783
+ page: { type: "integer" },
784
+ limit: { type: "integer" },
785
+ totalPages: { type: "integer" }
786
+ }
787
+ },
788
+ Error: {
789
+ type: "object",
790
+ properties: { error: { type: "string" } }
791
+ }
792
+ },
793
+ responses: {
794
+ NotFound: {
795
+ description: "Record not found",
796
+ content: {
797
+ "application/json": {
798
+ schema: { $ref: "#/components/schemas/Error" }
799
+ }
800
+ }
801
+ },
802
+ BadRequest: {
803
+ description: "Bad request",
804
+ content: {
805
+ "application/json": {
806
+ schema: { $ref: "#/components/schemas/Error" }
807
+ }
808
+ }
809
+ },
810
+ Conflict: {
811
+ description: "Unique constraint violation",
812
+ content: {
813
+ "application/json": {
814
+ schema: { $ref: "#/components/schemas/Error" }
815
+ }
816
+ }
817
+ }
818
+ }
819
+ },
820
+ tags: models.map((m) => ({ name: m.name }))
821
+ };
822
+ }
823
+ function buildListParameters() {
824
+ return [
825
+ { name: "page", in: "query", schema: { type: "integer", default: 1 }, description: "Page number" },
826
+ { name: "limit", in: "query", schema: { type: "integer", default: 20 }, description: "Items per page" },
827
+ { name: "sort", in: "query", schema: { type: "string" }, description: "e.g. createdAt:desc" },
828
+ { name: "include", in: "query", schema: { type: "string" }, description: "Comma-separated relations" },
829
+ { name: "select", in: "query", schema: { type: "string" }, description: "Comma-separated fields" }
830
+ ];
831
+ }
832
+
833
+ // src/adapters/express.ts
834
+ function expressAdapter(prisma, options = {}) {
835
+ const { Router } = __require("express");
836
+ const router = Router();
837
+ const { handle } = createRouter(prisma, options);
838
+ router.patch("/:model/bulk/update", async (req, res) => {
839
+ try {
840
+ const { status, data } = await handle(
841
+ "PATCH",
842
+ req.params.model,
843
+ null,
844
+ req.body ?? [],
845
+ new URLSearchParams(
846
+ Object.entries(req.query).map(([k, v]) => `${k}=${v}`).join("&")
847
+ ),
848
+ "bulk-update"
849
+ );
850
+ if (status === 204) {
851
+ return res.sendStatus(204);
852
+ }
853
+ return res.status(status).json(data);
854
+ } catch (e) {
855
+ return res.status(500).json({ error: e.message });
856
+ }
857
+ });
858
+ router.delete("/:model/bulk/delete", async (req, res) => {
859
+ try {
860
+ const { status, data } = await handle(
861
+ "DELETE",
862
+ req.params.model,
863
+ null,
864
+ req.body ?? [],
865
+ new URLSearchParams(
866
+ Object.entries(req.query).map(([k, v]) => `${k}=${v}`).join("&")
867
+ ),
868
+ "bulk-delete"
869
+ );
870
+ if (status === 204) {
871
+ return res.sendStatus(204);
872
+ }
873
+ return res.status(status).json(data);
874
+ } catch (e) {
875
+ return res.status(500).json({ error: e.message });
876
+ }
877
+ });
878
+ router.route("/:model").get(handler).post(handler);
879
+ router.route("/:model/:id").get(handler).put(handler).patch(handler).delete(handler);
880
+ async function handler(req, res) {
881
+ try {
882
+ const { status, data } = await handle(
883
+ req.method,
884
+ req.params.model,
885
+ req.params.id ?? null,
886
+ req.body ?? {},
887
+ new URLSearchParams(
888
+ Object.entries(req.query).map(([k, v]) => `${k}=${v}`).join("&")
889
+ )
890
+ );
891
+ if (status === 204) {
892
+ return res.sendStatus(204);
893
+ }
894
+ return res.status(status).json(data);
895
+ } catch (e) {
896
+ return res.status(500).json({ error: e.message });
897
+ }
898
+ }
899
+ return router;
900
+ }
901
+
902
+ // src/adapters/nextjs.ts
903
+ function nextjsAdapter(prisma, options = {}) {
904
+ const { handle } = createRouter(prisma, options);
905
+ return async function handler(req, context) {
906
+ const segments = context.params.prismaRest ?? [];
907
+ const [modelName, ...pathSegments] = segments;
908
+ if (!modelName) {
909
+ return Response.json(
910
+ { error: "No model specified in path." },
911
+ { status: 400 }
912
+ );
913
+ }
914
+ const url = new URL(req.url);
915
+ let body = {};
916
+ if (req.method !== "GET" && req.method !== "DELETE") {
917
+ try {
918
+ body = await req.json();
919
+ } catch {
920
+ body = {};
921
+ }
922
+ }
923
+ let operation;
924
+ let id = null;
925
+ if (pathSegments[0] === "bulk" && pathSegments[1] === "update") {
926
+ operation = "bulk-update";
927
+ } else if (pathSegments[0] === "bulk" && pathSegments[1] === "delete") {
928
+ operation = "bulk-delete";
929
+ } else {
930
+ id = pathSegments[0] ?? null;
931
+ }
932
+ const { status, data } = await handle(
933
+ req.method,
934
+ modelName,
935
+ id,
936
+ body,
937
+ url.searchParams,
938
+ operation
939
+ );
940
+ if (status === 204) {
941
+ return new Response(null, { status: 204 });
942
+ }
943
+ return Response.json(data, { status });
944
+ };
945
+ }
946
+
947
+ // src/adapters/fastify.ts
948
+ function fastifyAdapter(fastify, prisma, options = {}) {
949
+ const { handle } = createRouter(prisma, options);
950
+ const prefix = options.prefix ?? "/api";
951
+ async function routeHandler(request, reply) {
952
+ const { model, id } = request.params;
953
+ const body = request.body ?? {};
954
+ const query = request.query ?? {};
955
+ const searchParams = new URLSearchParams(
956
+ Object.entries(query).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&")
957
+ );
958
+ const { status, data } = await handle(
959
+ request.method,
960
+ model,
961
+ id ?? null,
962
+ body,
963
+ searchParams
964
+ );
965
+ if (status === 204) {
966
+ return reply.status(204).send();
967
+ }
968
+ return reply.status(status).send(data);
969
+ }
970
+ async function bulkHandler(request, reply, operation) {
971
+ const { model } = request.params;
972
+ const body = request.body ?? [];
973
+ const query = request.query ?? {};
974
+ const searchParams = new URLSearchParams(
975
+ Object.entries(query).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&")
976
+ );
977
+ const { status, data } = await handle(
978
+ operation.includes("update") ? "PATCH" : "DELETE",
979
+ model,
980
+ null,
981
+ body,
982
+ searchParams,
983
+ operation
984
+ );
985
+ if (status === 204) {
986
+ return reply.status(204).send();
987
+ }
988
+ return reply.status(status).send(data);
989
+ }
990
+ fastify.get(`${prefix}/:model`, routeHandler);
991
+ fastify.post(`${prefix}/:model`, routeHandler);
992
+ fastify.get(`${prefix}/:model/:id`, routeHandler);
993
+ fastify.put(`${prefix}/:model/:id`, routeHandler);
994
+ fastify.patch(`${prefix}/:model/:id`, routeHandler);
995
+ fastify.delete(`${prefix}/:model/:id`, routeHandler);
996
+ fastify.patch(`${prefix}/:model/bulk/update`, async (request, reply) => {
997
+ await bulkHandler(request, reply, "bulk-update");
998
+ });
999
+ fastify.delete(`${prefix}/:model/bulk/delete`, async (request, reply) => {
1000
+ await bulkHandler(request, reply, "bulk-delete");
1001
+ });
1002
+ }
1003
+
1004
+ export { buildModelMap, buildQuery, buildRuntimeSchemas, createRouter, expressAdapter, fastifyAdapter, generateOpenApiSpec, generateZodSchemas, getDelegate, getModels, nextjsAdapter, runGuard, runHook, toRouteName, validateBody, withValidation };
1005
+ //# sourceMappingURL=index.mjs.map
1006
+ //# sourceMappingURL=index.mjs.map