@zenstackhq/server 3.5.0-beta.2 → 3.5.0-beta.4

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/api.js CHANGED
@@ -2,7 +2,7 @@ var __defProp = Object.defineProperty;
2
2
  var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
3
3
 
4
4
  // src/api/rest/index.ts
5
- import { clone, enumerate, lowerCaseFirst, paramCase } from "@zenstackhq/common-helpers";
5
+ import { clone, enumerate, lowerCaseFirst as lowerCaseFirst3, paramCase, safeJSONStringify } from "@zenstackhq/common-helpers";
6
6
  import { ORMError as ORMError2, ORMErrorReason } from "@zenstackhq/orm";
7
7
  import { Decimal as Decimal2 } from "decimal.js";
8
8
  import SuperJSON3 from "superjson";
@@ -92,6 +92,26 @@ var loggerSchema = z.union([
92
92
  ]).array(),
93
93
  z.function()
94
94
  ]);
95
+ var fieldSlicingSchema = z.looseObject({
96
+ includedFilterKinds: z.string().array().optional(),
97
+ excludedFilterKinds: z.string().array().optional()
98
+ });
99
+ var modelSlicingSchema = z.looseObject({
100
+ includedOperations: z.array(z.string()).optional(),
101
+ excludedOperations: z.array(z.string()).optional(),
102
+ fields: z.record(z.string(), fieldSlicingSchema).optional()
103
+ });
104
+ var slicingSchema = z.looseObject({
105
+ includedModels: z.array(z.string()).optional(),
106
+ excludedModels: z.array(z.string()).optional(),
107
+ models: z.record(z.string(), modelSlicingSchema).optional(),
108
+ includedProcedures: z.array(z.string()).optional(),
109
+ excludedProcedures: z.array(z.string()).optional()
110
+ });
111
+ var queryOptionsSchema = z.looseObject({
112
+ omit: z.record(z.string(), z.record(z.string(), z.boolean())).optional(),
113
+ slicing: slicingSchema.optional()
114
+ });
95
115
 
96
116
  // src/api/common/utils.ts
97
117
  import SuperJSON from "superjson";
@@ -194,6 +214,1401 @@ function getZodErrorMessage(error) {
194
214
  }
195
215
  __name(getZodErrorMessage, "getZodErrorMessage");
196
216
 
217
+ // src/api/rest/openapi.ts
218
+ import { lowerCaseFirst as lowerCaseFirst2 } from "@zenstackhq/common-helpers";
219
+
220
+ // src/api/common/spec-utils.ts
221
+ import { lowerCaseFirst } from "@zenstackhq/common-helpers";
222
+ import { ExpressionUtils } from "@zenstackhq/orm/schema";
223
+ function isModelIncluded(modelName, queryOptions) {
224
+ const slicing = queryOptions?.slicing;
225
+ if (!slicing) return true;
226
+ const excluded = slicing.excludedModels;
227
+ if (excluded?.includes(modelName)) return false;
228
+ const included = slicing.includedModels;
229
+ if (included && !included.includes(modelName)) return false;
230
+ return true;
231
+ }
232
+ __name(isModelIncluded, "isModelIncluded");
233
+ function isOperationIncluded(modelName, op, queryOptions) {
234
+ const slicing = queryOptions?.slicing;
235
+ if (!slicing?.models) return true;
236
+ const modelKey = lowerCaseFirst(modelName);
237
+ const modelSlicing = slicing.models[modelKey] ?? slicing.models.$all;
238
+ if (!modelSlicing) return true;
239
+ const excluded = modelSlicing.excludedOperations;
240
+ if (excluded?.includes(op)) return false;
241
+ const included = modelSlicing.includedOperations;
242
+ if (included && !included.includes(op)) return false;
243
+ return true;
244
+ }
245
+ __name(isOperationIncluded, "isOperationIncluded");
246
+ function isProcedureIncluded(procName, queryOptions) {
247
+ const slicing = queryOptions?.slicing;
248
+ if (!slicing) return true;
249
+ const excluded = slicing.excludedProcedures;
250
+ if (excluded?.includes(procName)) return false;
251
+ const included = slicing.includedProcedures;
252
+ if (included && !included.includes(procName)) return false;
253
+ return true;
254
+ }
255
+ __name(isProcedureIncluded, "isProcedureIncluded");
256
+ function isFieldOmitted(modelName, fieldName, queryOptions) {
257
+ const omit = queryOptions?.omit;
258
+ return omit?.[modelName]?.[fieldName] === true;
259
+ }
260
+ __name(isFieldOmitted, "isFieldOmitted");
261
+ function getIncludedModels(schema, queryOptions) {
262
+ return Object.keys(schema.models).filter((name) => isModelIncluded(name, queryOptions));
263
+ }
264
+ __name(getIncludedModels, "getIncludedModels");
265
+ function isFilterKindIncluded(modelName, fieldName, filterKind, queryOptions) {
266
+ const slicing = queryOptions?.slicing;
267
+ if (!slicing?.models) return true;
268
+ const modelKey = lowerCaseFirst(modelName);
269
+ const modelSlicing = slicing.models[modelKey] ?? slicing.models.$all;
270
+ if (!modelSlicing?.fields) return true;
271
+ const fieldSlicing = modelSlicing.fields[fieldName] ?? modelSlicing.fields.$all;
272
+ if (!fieldSlicing) return true;
273
+ const excluded = fieldSlicing.excludedFilterKinds;
274
+ if (excluded?.includes(filterKind)) return false;
275
+ const included = fieldSlicing.includedFilterKinds;
276
+ if (included && !included.includes(filterKind)) return false;
277
+ return true;
278
+ }
279
+ __name(isFilterKindIncluded, "isFilterKindIncluded");
280
+ function getMetaDescription(attributes) {
281
+ if (!attributes) return void 0;
282
+ for (const attr of attributes) {
283
+ if (attr.name !== "@meta" && attr.name !== "@@meta") continue;
284
+ const nameArg = attr.args?.find((a) => a.name === "name");
285
+ if (!nameArg || ExpressionUtils.getLiteralValue(nameArg.value) !== "description") continue;
286
+ const valueArg = attr.args?.find((a) => a.name === "value");
287
+ if (valueArg) {
288
+ return ExpressionUtils.getLiteralValue(valueArg.value);
289
+ }
290
+ }
291
+ return void 0;
292
+ }
293
+ __name(getMetaDescription, "getMetaDescription");
294
+
295
+ // src/api/rest/openapi.ts
296
+ function errorResponse(description) {
297
+ return {
298
+ description,
299
+ content: {
300
+ "application/vnd.api+json": {
301
+ schema: {
302
+ $ref: "#/components/schemas/_errorResponse"
303
+ }
304
+ }
305
+ }
306
+ };
307
+ }
308
+ __name(errorResponse, "errorResponse");
309
+ var ERROR_400 = errorResponse("Error occurred while processing the request");
310
+ var ERROR_403 = errorResponse("Forbidden: insufficient permissions to perform this operation");
311
+ var ERROR_404 = errorResponse("Resource not found");
312
+ var ERROR_422 = errorResponse("Operation is unprocessable due to validation errors");
313
+ var SCALAR_STRING_OPS = [
314
+ "$contains",
315
+ "$icontains",
316
+ "$search",
317
+ "$startsWith",
318
+ "$endsWith"
319
+ ];
320
+ var SCALAR_COMPARABLE_OPS = [
321
+ "$lt",
322
+ "$lte",
323
+ "$gt",
324
+ "$gte"
325
+ ];
326
+ var SCALAR_ARRAY_OPS = [
327
+ "$has",
328
+ "$hasEvery",
329
+ "$hasSome",
330
+ "$isEmpty"
331
+ ];
332
+ var RestApiSpecGenerator = class {
333
+ static {
334
+ __name(this, "RestApiSpecGenerator");
335
+ }
336
+ handlerOptions;
337
+ specOptions;
338
+ constructor(handlerOptions) {
339
+ this.handlerOptions = handlerOptions;
340
+ }
341
+ get schema() {
342
+ return this.handlerOptions.schema;
343
+ }
344
+ get modelNameMapping() {
345
+ const mapping = {};
346
+ if (this.handlerOptions.modelNameMapping) {
347
+ for (const [k, v] of Object.entries(this.handlerOptions.modelNameMapping)) {
348
+ mapping[lowerCaseFirst2(k)] = v;
349
+ }
350
+ }
351
+ return mapping;
352
+ }
353
+ get queryOptions() {
354
+ return this.handlerOptions?.queryOptions;
355
+ }
356
+ generateSpec(options) {
357
+ this.specOptions = options;
358
+ return {
359
+ openapi: "3.1.0",
360
+ info: {
361
+ title: options?.title ?? "ZenStack Generated API",
362
+ version: options?.version ?? "1.0.0",
363
+ ...options?.description && {
364
+ description: options.description
365
+ },
366
+ ...options?.summary && {
367
+ summary: options.summary
368
+ }
369
+ },
370
+ tags: this.generateTags(),
371
+ paths: this.generatePaths(),
372
+ components: {
373
+ schemas: this.generateSchemas(),
374
+ parameters: this.generateSharedParams()
375
+ }
376
+ };
377
+ }
378
+ generateTags() {
379
+ return getIncludedModels(this.schema, this.queryOptions).map((modelName) => ({
380
+ name: lowerCaseFirst2(modelName),
381
+ description: `${modelName} operations`
382
+ }));
383
+ }
384
+ getModelPath(modelName) {
385
+ const lower = lowerCaseFirst2(modelName);
386
+ return this.modelNameMapping[lower] ?? lower;
387
+ }
388
+ generatePaths() {
389
+ const paths = {};
390
+ for (const modelName of getIncludedModels(this.schema, this.queryOptions)) {
391
+ const modelDef = this.schema.models[modelName];
392
+ const idFields = this.getIdFields(modelDef);
393
+ if (idFields.length === 0) continue;
394
+ const modelPath = this.getModelPath(modelName);
395
+ const tag = lowerCaseFirst2(modelName);
396
+ const collectionPath = this.buildCollectionPath(modelName, modelDef, tag);
397
+ if (Object.keys(collectionPath).length > 0) {
398
+ paths[`/${modelPath}`] = collectionPath;
399
+ }
400
+ const singlePath = this.buildSinglePath(modelDef, tag);
401
+ if (Object.keys(singlePath).length > 0) {
402
+ paths[`/${modelPath}/{id}`] = singlePath;
403
+ }
404
+ for (const [fieldName, fieldDef] of Object.entries(modelDef.fields)) {
405
+ if (!fieldDef.relation) continue;
406
+ if (!isModelIncluded(fieldDef.type, this.queryOptions)) continue;
407
+ const relModelDef = this.schema.models[fieldDef.type];
408
+ if (!relModelDef) continue;
409
+ const relIdFields = this.getIdFields(relModelDef);
410
+ if (relIdFields.length === 0) continue;
411
+ paths[`/${modelPath}/{id}/${fieldName}`] = this.buildFetchRelatedPath(modelName, fieldName, fieldDef, tag);
412
+ paths[`/${modelPath}/{id}/relationships/${fieldName}`] = this.buildRelationshipPath(modelDef, fieldName, fieldDef, tag);
413
+ }
414
+ }
415
+ if (this.schema.procedures) {
416
+ for (const [procName, procDef] of Object.entries(this.schema.procedures)) {
417
+ if (!isProcedureIncluded(procName, this.queryOptions)) continue;
418
+ const isMutation = !!procDef.mutation;
419
+ if (isMutation) {
420
+ paths[`/${PROCEDURE_ROUTE_PREFIXES}/${procName}`] = {
421
+ post: this.buildProcedureOperation(procName, "post")
422
+ };
423
+ } else {
424
+ paths[`/${PROCEDURE_ROUTE_PREFIXES}/${procName}`] = {
425
+ get: this.buildProcedureOperation(procName, "get")
426
+ };
427
+ }
428
+ }
429
+ }
430
+ return paths;
431
+ }
432
+ buildCollectionPath(modelName, modelDef, tag) {
433
+ const filterParams = this.buildFilterParams(modelName, modelDef);
434
+ const listOp = {
435
+ tags: [
436
+ tag
437
+ ],
438
+ summary: `List ${modelName} resources`,
439
+ operationId: `list${modelName}`,
440
+ parameters: [
441
+ {
442
+ $ref: "#/components/parameters/include"
443
+ },
444
+ {
445
+ $ref: "#/components/parameters/sort"
446
+ },
447
+ {
448
+ $ref: "#/components/parameters/pageOffset"
449
+ },
450
+ {
451
+ $ref: "#/components/parameters/pageLimit"
452
+ },
453
+ ...filterParams
454
+ ],
455
+ responses: {
456
+ "200": {
457
+ description: `List of ${modelName} resources`,
458
+ content: {
459
+ "application/vnd.api+json": {
460
+ schema: {
461
+ $ref: `#/components/schemas/${modelName}ListResponse`
462
+ }
463
+ }
464
+ }
465
+ },
466
+ "400": ERROR_400
467
+ }
468
+ };
469
+ const createOp = {
470
+ tags: [
471
+ tag
472
+ ],
473
+ summary: `Create a ${modelName} resource`,
474
+ operationId: `create${modelName}`,
475
+ requestBody: {
476
+ required: true,
477
+ content: {
478
+ "application/vnd.api+json": {
479
+ schema: {
480
+ $ref: `#/components/schemas/${modelName}CreateRequest`
481
+ }
482
+ }
483
+ }
484
+ },
485
+ responses: {
486
+ "201": {
487
+ description: `Created ${modelName} resource`,
488
+ content: {
489
+ "application/vnd.api+json": {
490
+ schema: {
491
+ $ref: `#/components/schemas/${modelName}Response`
492
+ }
493
+ }
494
+ }
495
+ },
496
+ "400": ERROR_400,
497
+ ...this.mayDenyAccess(modelDef, "create") && {
498
+ "403": ERROR_403
499
+ },
500
+ "422": ERROR_422
501
+ }
502
+ };
503
+ const result = {};
504
+ if (isOperationIncluded(modelName, "findMany", this.queryOptions)) {
505
+ result["get"] = listOp;
506
+ }
507
+ if (isOperationIncluded(modelName, "create", this.queryOptions)) {
508
+ result["post"] = createOp;
509
+ }
510
+ return result;
511
+ }
512
+ buildSinglePath(modelDef, tag) {
513
+ const modelName = modelDef.name;
514
+ const idParam = {
515
+ $ref: "#/components/parameters/id"
516
+ };
517
+ const result = {};
518
+ if (isOperationIncluded(modelName, "findUnique", this.queryOptions)) {
519
+ result["get"] = {
520
+ tags: [
521
+ tag
522
+ ],
523
+ summary: `Get a ${modelName} resource by ID`,
524
+ operationId: `get${modelName}`,
525
+ parameters: [
526
+ idParam,
527
+ {
528
+ $ref: "#/components/parameters/include"
529
+ }
530
+ ],
531
+ responses: {
532
+ "200": {
533
+ description: `${modelName} resource`,
534
+ content: {
535
+ "application/vnd.api+json": {
536
+ schema: {
537
+ $ref: `#/components/schemas/${modelName}Response`
538
+ }
539
+ }
540
+ }
541
+ },
542
+ "404": ERROR_404
543
+ }
544
+ };
545
+ }
546
+ if (isOperationIncluded(modelName, "update", this.queryOptions)) {
547
+ result["patch"] = {
548
+ tags: [
549
+ tag
550
+ ],
551
+ summary: `Update a ${modelName} resource`,
552
+ operationId: `update${modelName}`,
553
+ parameters: [
554
+ idParam
555
+ ],
556
+ requestBody: {
557
+ required: true,
558
+ content: {
559
+ "application/vnd.api+json": {
560
+ schema: {
561
+ $ref: `#/components/schemas/${modelName}UpdateRequest`
562
+ }
563
+ }
564
+ }
565
+ },
566
+ responses: {
567
+ "200": {
568
+ description: `Updated ${modelName} resource`,
569
+ content: {
570
+ "application/vnd.api+json": {
571
+ schema: {
572
+ $ref: `#/components/schemas/${modelName}Response`
573
+ }
574
+ }
575
+ }
576
+ },
577
+ "400": ERROR_400,
578
+ ...this.mayDenyAccess(modelDef, "update") && {
579
+ "403": ERROR_403
580
+ },
581
+ "404": ERROR_404,
582
+ "422": ERROR_422
583
+ }
584
+ };
585
+ }
586
+ if (isOperationIncluded(modelName, "delete", this.queryOptions)) {
587
+ result["delete"] = {
588
+ tags: [
589
+ tag
590
+ ],
591
+ summary: `Delete a ${modelName} resource`,
592
+ operationId: `delete${modelName}`,
593
+ parameters: [
594
+ idParam
595
+ ],
596
+ responses: {
597
+ "200": {
598
+ description: "Deleted successfully"
599
+ },
600
+ ...this.mayDenyAccess(modelDef, "delete") && {
601
+ "403": ERROR_403
602
+ },
603
+ "404": ERROR_404
604
+ }
605
+ };
606
+ }
607
+ return result;
608
+ }
609
+ buildFetchRelatedPath(modelName, fieldName, fieldDef, tag) {
610
+ const isCollection = !!fieldDef.array;
611
+ const params = [
612
+ {
613
+ $ref: "#/components/parameters/id"
614
+ },
615
+ {
616
+ $ref: "#/components/parameters/include"
617
+ }
618
+ ];
619
+ if (isCollection && this.schema.models[fieldDef.type]) {
620
+ const relModelDef = this.schema.models[fieldDef.type];
621
+ params.push({
622
+ $ref: "#/components/parameters/sort"
623
+ }, {
624
+ $ref: "#/components/parameters/pageOffset"
625
+ }, {
626
+ $ref: "#/components/parameters/pageLimit"
627
+ }, ...this.buildFilterParams(fieldDef.type, relModelDef));
628
+ }
629
+ return {
630
+ get: {
631
+ tags: [
632
+ tag
633
+ ],
634
+ summary: `Fetch related ${fieldDef.type} for ${modelName}`,
635
+ operationId: `get${modelName}_${fieldName}`,
636
+ parameters: params,
637
+ responses: {
638
+ "200": {
639
+ description: `Related ${fieldDef.type} resource(s)`,
640
+ content: {
641
+ "application/vnd.api+json": {
642
+ schema: isCollection ? {
643
+ $ref: `#/components/schemas/${fieldDef.type}ListResponse`
644
+ } : {
645
+ $ref: `#/components/schemas/${fieldDef.type}Response`
646
+ }
647
+ }
648
+ }
649
+ },
650
+ "404": ERROR_404
651
+ }
652
+ }
653
+ };
654
+ }
655
+ buildRelationshipPath(modelDef, fieldName, fieldDef, tag) {
656
+ const modelName = modelDef.name;
657
+ const isCollection = !!fieldDef.array;
658
+ const idParam = {
659
+ $ref: "#/components/parameters/id"
660
+ };
661
+ const relSchemaRef = isCollection ? {
662
+ $ref: "#/components/schemas/_toManyRelationshipWithLinks"
663
+ } : {
664
+ $ref: "#/components/schemas/_toOneRelationshipWithLinks"
665
+ };
666
+ const relRequestRef = isCollection ? {
667
+ $ref: "#/components/schemas/_toManyRelationshipRequest"
668
+ } : {
669
+ $ref: "#/components/schemas/_toOneRelationshipRequest"
670
+ };
671
+ const mayDeny = this.mayDenyAccess(modelDef, "update");
672
+ const pathItem = {
673
+ get: {
674
+ tags: [
675
+ tag
676
+ ],
677
+ summary: `Fetch ${fieldName} relationship`,
678
+ operationId: `get${modelName}_relationships_${fieldName}`,
679
+ parameters: [
680
+ idParam
681
+ ],
682
+ responses: {
683
+ "200": {
684
+ description: `${fieldName} relationship`,
685
+ content: {
686
+ "application/vnd.api+json": {
687
+ schema: relSchemaRef
688
+ }
689
+ }
690
+ },
691
+ "404": ERROR_404
692
+ }
693
+ },
694
+ put: {
695
+ tags: [
696
+ tag
697
+ ],
698
+ summary: `Replace ${fieldName} relationship`,
699
+ operationId: `put${modelName}_relationships_${fieldName}`,
700
+ parameters: [
701
+ idParam
702
+ ],
703
+ requestBody: {
704
+ required: true,
705
+ content: {
706
+ "application/vnd.api+json": {
707
+ schema: relRequestRef
708
+ }
709
+ }
710
+ },
711
+ responses: {
712
+ "200": {
713
+ description: "Relationship updated"
714
+ },
715
+ "400": ERROR_400,
716
+ ...mayDeny && {
717
+ "403": ERROR_403
718
+ }
719
+ }
720
+ },
721
+ patch: {
722
+ tags: [
723
+ tag
724
+ ],
725
+ summary: `Update ${fieldName} relationship`,
726
+ operationId: `patch${modelName}_relationships_${fieldName}`,
727
+ parameters: [
728
+ idParam
729
+ ],
730
+ requestBody: {
731
+ required: true,
732
+ content: {
733
+ "application/vnd.api+json": {
734
+ schema: relRequestRef
735
+ }
736
+ }
737
+ },
738
+ responses: {
739
+ "200": {
740
+ description: "Relationship updated"
741
+ },
742
+ "400": ERROR_400,
743
+ ...mayDeny && {
744
+ "403": ERROR_403
745
+ }
746
+ }
747
+ }
748
+ };
749
+ if (isCollection) {
750
+ pathItem["post"] = {
751
+ tags: [
752
+ tag
753
+ ],
754
+ summary: `Add to ${fieldName} collection relationship`,
755
+ operationId: `post${modelName}_relationships_${fieldName}`,
756
+ parameters: [
757
+ idParam
758
+ ],
759
+ requestBody: {
760
+ required: true,
761
+ content: {
762
+ "application/vnd.api+json": {
763
+ schema: {
764
+ $ref: "#/components/schemas/_toManyRelationshipRequest"
765
+ }
766
+ }
767
+ }
768
+ },
769
+ responses: {
770
+ "200": {
771
+ description: "Added to relationship collection"
772
+ },
773
+ "400": ERROR_400,
774
+ ...mayDeny && {
775
+ "403": ERROR_403
776
+ }
777
+ }
778
+ };
779
+ }
780
+ return pathItem;
781
+ }
782
+ buildProcedureOperation(procName, method) {
783
+ const op = {
784
+ tags: [
785
+ "$procs"
786
+ ],
787
+ summary: `Execute procedure ${procName}`,
788
+ operationId: `proc_${procName}`,
789
+ responses: {
790
+ "200": {
791
+ description: `Result of ${procName}`
792
+ },
793
+ "400": ERROR_400
794
+ }
795
+ };
796
+ if (method === "get") {
797
+ op["parameters"] = [
798
+ {
799
+ name: "q",
800
+ in: "query",
801
+ description: "Procedure arguments as JSON",
802
+ schema: {
803
+ type: "string"
804
+ }
805
+ }
806
+ ];
807
+ } else {
808
+ op["requestBody"] = {
809
+ content: {
810
+ "application/json": {
811
+ schema: {
812
+ type: "object"
813
+ }
814
+ }
815
+ }
816
+ };
817
+ }
818
+ return op;
819
+ }
820
+ buildFilterParams(modelName, modelDef) {
821
+ const params = [];
822
+ const idFieldNames = new Set(modelDef.idFields);
823
+ if (isFilterKindIncluded(modelName, "id", "Equality", this.queryOptions)) {
824
+ params.push({
825
+ name: "filter[id]",
826
+ in: "query",
827
+ schema: {
828
+ type: "string"
829
+ },
830
+ description: `Filter by ${modelName} ID`
831
+ });
832
+ }
833
+ for (const [fieldName, fieldDef] of Object.entries(modelDef.fields)) {
834
+ if (fieldDef.relation) continue;
835
+ if (idFieldNames.has(fieldName)) continue;
836
+ const type = fieldDef.type;
837
+ if (isFilterKindIncluded(modelName, fieldName, "Equality", this.queryOptions)) {
838
+ params.push({
839
+ name: `filter[${fieldName}]`,
840
+ in: "query",
841
+ schema: {
842
+ type: "string"
843
+ },
844
+ description: `Filter by ${fieldName}`
845
+ });
846
+ }
847
+ if (type === "String" && isFilterKindIncluded(modelName, fieldName, "Like", this.queryOptions)) {
848
+ for (const op of SCALAR_STRING_OPS) {
849
+ params.push({
850
+ name: `filter[${fieldName}][${op}]`,
851
+ in: "query",
852
+ schema: {
853
+ type: "string"
854
+ }
855
+ });
856
+ }
857
+ } else if ((type === "Int" || type === "Float" || type === "BigInt" || type === "Decimal" || type === "DateTime") && isFilterKindIncluded(modelName, fieldName, "Range", this.queryOptions)) {
858
+ for (const op of SCALAR_COMPARABLE_OPS) {
859
+ params.push({
860
+ name: `filter[${fieldName}][${op}]`,
861
+ in: "query",
862
+ schema: {
863
+ type: "string"
864
+ }
865
+ });
866
+ }
867
+ }
868
+ if (fieldDef.array && isFilterKindIncluded(modelName, fieldName, "List", this.queryOptions)) {
869
+ for (const op of SCALAR_ARRAY_OPS) {
870
+ params.push({
871
+ name: `filter[${fieldName}][${op}]`,
872
+ in: "query",
873
+ schema: {
874
+ type: "string"
875
+ }
876
+ });
877
+ }
878
+ }
879
+ }
880
+ return params;
881
+ }
882
+ generateSchemas() {
883
+ const schemas = {};
884
+ Object.assign(schemas, this.buildSharedSchemas());
885
+ if (this.schema.enums) {
886
+ for (const [_enumName, enumDef] of Object.entries(this.schema.enums)) {
887
+ schemas[_enumName] = this.buildEnumSchema(enumDef);
888
+ }
889
+ }
890
+ if (this.schema.typeDefs) {
891
+ for (const [typeName, typeDef] of Object.entries(this.schema.typeDefs)) {
892
+ schemas[typeName] = this.buildTypeDefSchema(typeDef);
893
+ }
894
+ }
895
+ for (const modelName of getIncludedModels(this.schema, this.queryOptions)) {
896
+ const modelDef = this.schema.models[modelName];
897
+ const idFields = this.getIdFields(modelDef);
898
+ if (idFields.length === 0) continue;
899
+ schemas[modelName] = this.buildModelReadSchema(modelName, modelDef);
900
+ schemas[`${modelName}CreateRequest`] = this.buildCreateRequestSchema(modelName, modelDef);
901
+ schemas[`${modelName}UpdateRequest`] = this.buildUpdateRequestSchema(modelDef);
902
+ schemas[`${modelName}Response`] = this.buildModelResponseSchema(modelName);
903
+ schemas[`${modelName}ListResponse`] = this.buildModelListResponseSchema(modelName);
904
+ }
905
+ return schemas;
906
+ }
907
+ buildSharedSchemas() {
908
+ const nullableString = {
909
+ oneOf: [
910
+ {
911
+ type: "string"
912
+ },
913
+ {
914
+ type: "null"
915
+ }
916
+ ]
917
+ };
918
+ return {
919
+ _jsonapi: {
920
+ type: "object",
921
+ properties: {
922
+ version: {
923
+ type: "string"
924
+ },
925
+ meta: {
926
+ type: "object"
927
+ }
928
+ }
929
+ },
930
+ _meta: {
931
+ type: "object",
932
+ additionalProperties: true
933
+ },
934
+ _links: {
935
+ type: "object",
936
+ properties: {
937
+ self: {
938
+ type: "string"
939
+ },
940
+ related: {
941
+ type: "string"
942
+ }
943
+ }
944
+ },
945
+ _pagination: {
946
+ type: "object",
947
+ properties: {
948
+ first: nullableString,
949
+ last: nullableString,
950
+ prev: nullableString,
951
+ next: nullableString
952
+ }
953
+ },
954
+ _errors: {
955
+ type: "array",
956
+ items: {
957
+ type: "object",
958
+ properties: {
959
+ status: {
960
+ type: "integer"
961
+ },
962
+ code: {
963
+ type: "string"
964
+ },
965
+ title: {
966
+ type: "string"
967
+ },
968
+ detail: {
969
+ type: "string"
970
+ }
971
+ },
972
+ required: [
973
+ "status",
974
+ "title"
975
+ ]
976
+ }
977
+ },
978
+ _errorResponse: {
979
+ type: "object",
980
+ properties: {
981
+ errors: {
982
+ $ref: "#/components/schemas/_errors"
983
+ }
984
+ },
985
+ required: [
986
+ "errors"
987
+ ]
988
+ },
989
+ _resourceIdentifier: {
990
+ type: "object",
991
+ properties: {
992
+ type: {
993
+ type: "string"
994
+ },
995
+ id: {
996
+ type: "string"
997
+ }
998
+ },
999
+ required: [
1000
+ "type",
1001
+ "id"
1002
+ ]
1003
+ },
1004
+ _resource: {
1005
+ type: "object",
1006
+ properties: {
1007
+ type: {
1008
+ type: "string"
1009
+ },
1010
+ id: {
1011
+ type: "string"
1012
+ },
1013
+ attributes: {
1014
+ type: "object"
1015
+ },
1016
+ relationships: {
1017
+ type: "object"
1018
+ },
1019
+ links: {
1020
+ $ref: "#/components/schemas/_links"
1021
+ },
1022
+ meta: {
1023
+ $ref: "#/components/schemas/_meta"
1024
+ }
1025
+ },
1026
+ required: [
1027
+ "type",
1028
+ "id"
1029
+ ]
1030
+ },
1031
+ _relationLinks: {
1032
+ type: "object",
1033
+ properties: {
1034
+ self: {
1035
+ type: "string"
1036
+ },
1037
+ related: {
1038
+ type: "string"
1039
+ }
1040
+ }
1041
+ },
1042
+ _pagedRelationLinks: {
1043
+ type: "object",
1044
+ allOf: [
1045
+ {
1046
+ $ref: "#/components/schemas/_relationLinks"
1047
+ },
1048
+ {
1049
+ $ref: "#/components/schemas/_pagination"
1050
+ }
1051
+ ]
1052
+ },
1053
+ _toOneRelationship: {
1054
+ type: "object",
1055
+ properties: {
1056
+ data: {
1057
+ oneOf: [
1058
+ {
1059
+ $ref: "#/components/schemas/_resourceIdentifier"
1060
+ },
1061
+ {
1062
+ type: "null"
1063
+ }
1064
+ ]
1065
+ }
1066
+ }
1067
+ },
1068
+ _toManyRelationship: {
1069
+ type: "object",
1070
+ properties: {
1071
+ data: {
1072
+ type: "array",
1073
+ items: {
1074
+ $ref: "#/components/schemas/_resourceIdentifier"
1075
+ }
1076
+ }
1077
+ }
1078
+ },
1079
+ _toOneRelationshipWithLinks: {
1080
+ type: "object",
1081
+ allOf: [
1082
+ {
1083
+ $ref: "#/components/schemas/_toOneRelationship"
1084
+ },
1085
+ {
1086
+ properties: {
1087
+ links: {
1088
+ $ref: "#/components/schemas/_relationLinks"
1089
+ }
1090
+ }
1091
+ }
1092
+ ]
1093
+ },
1094
+ _toManyRelationshipWithLinks: {
1095
+ type: "object",
1096
+ allOf: [
1097
+ {
1098
+ $ref: "#/components/schemas/_toManyRelationship"
1099
+ },
1100
+ {
1101
+ properties: {
1102
+ links: {
1103
+ $ref: "#/components/schemas/_pagedRelationLinks"
1104
+ }
1105
+ }
1106
+ }
1107
+ ]
1108
+ },
1109
+ _toManyRelationshipRequest: {
1110
+ type: "object",
1111
+ properties: {
1112
+ data: {
1113
+ type: "array",
1114
+ items: {
1115
+ $ref: "#/components/schemas/_resourceIdentifier"
1116
+ }
1117
+ }
1118
+ },
1119
+ required: [
1120
+ "data"
1121
+ ]
1122
+ },
1123
+ _toOneRelationshipRequest: {
1124
+ type: "object",
1125
+ properties: {
1126
+ data: {
1127
+ oneOf: [
1128
+ {
1129
+ $ref: "#/components/schemas/_resourceIdentifier"
1130
+ },
1131
+ {
1132
+ type: "null"
1133
+ }
1134
+ ]
1135
+ }
1136
+ },
1137
+ required: [
1138
+ "data"
1139
+ ]
1140
+ },
1141
+ _toManyRelationshipResponse: {
1142
+ type: "object",
1143
+ properties: {
1144
+ data: {
1145
+ type: "array",
1146
+ items: {
1147
+ $ref: "#/components/schemas/_resourceIdentifier"
1148
+ }
1149
+ },
1150
+ links: {
1151
+ $ref: "#/components/schemas/_pagedRelationLinks"
1152
+ },
1153
+ meta: {
1154
+ $ref: "#/components/schemas/_meta"
1155
+ }
1156
+ }
1157
+ },
1158
+ _toOneRelationshipResponse: {
1159
+ type: "object",
1160
+ properties: {
1161
+ data: {
1162
+ oneOf: [
1163
+ {
1164
+ $ref: "#/components/schemas/_resourceIdentifier"
1165
+ },
1166
+ {
1167
+ type: "null"
1168
+ }
1169
+ ]
1170
+ },
1171
+ links: {
1172
+ $ref: "#/components/schemas/_relationLinks"
1173
+ },
1174
+ meta: {
1175
+ $ref: "#/components/schemas/_meta"
1176
+ }
1177
+ }
1178
+ }
1179
+ };
1180
+ }
1181
+ buildEnumSchema(enumDef) {
1182
+ return {
1183
+ type: "string",
1184
+ enum: Object.values(enumDef.values)
1185
+ };
1186
+ }
1187
+ buildTypeDefSchema(typeDef) {
1188
+ const properties = {};
1189
+ const required = [];
1190
+ for (const [fieldName, fieldDef] of Object.entries(typeDef.fields)) {
1191
+ properties[fieldName] = this.fieldToSchema(fieldDef);
1192
+ if (!fieldDef.optional && !fieldDef.array) {
1193
+ required.push(fieldName);
1194
+ }
1195
+ }
1196
+ const result = {
1197
+ type: "object",
1198
+ properties
1199
+ };
1200
+ if (required.length > 0) {
1201
+ result.required = required;
1202
+ }
1203
+ return result;
1204
+ }
1205
+ buildModelReadSchema(modelName, modelDef) {
1206
+ const attrProperties = {};
1207
+ const attrRequired = [];
1208
+ const relProperties = {};
1209
+ for (const [fieldName, fieldDef] of Object.entries(modelDef.fields)) {
1210
+ if (fieldDef.omit) continue;
1211
+ if (isFieldOmitted(modelName, fieldName, this.queryOptions)) continue;
1212
+ if (fieldDef.relation && !isModelIncluded(fieldDef.type, this.queryOptions)) continue;
1213
+ if (fieldDef.relation) {
1214
+ const relRef = fieldDef.array ? {
1215
+ $ref: "#/components/schemas/_toManyRelationshipWithLinks"
1216
+ } : {
1217
+ $ref: "#/components/schemas/_toOneRelationshipWithLinks"
1218
+ };
1219
+ relProperties[fieldName] = fieldDef.optional ? {
1220
+ oneOf: [
1221
+ {
1222
+ type: "null"
1223
+ },
1224
+ relRef
1225
+ ]
1226
+ } : relRef;
1227
+ } else {
1228
+ const schema = this.fieldToSchema(fieldDef);
1229
+ const fieldDescription = getMetaDescription(fieldDef.attributes);
1230
+ if (fieldDescription && !("$ref" in schema)) {
1231
+ schema.description = fieldDescription;
1232
+ }
1233
+ attrProperties[fieldName] = schema;
1234
+ if (!fieldDef.optional && !fieldDef.array) {
1235
+ attrRequired.push(fieldName);
1236
+ }
1237
+ }
1238
+ }
1239
+ const properties = {};
1240
+ if (Object.keys(attrProperties).length > 0) {
1241
+ const attrSchema = {
1242
+ type: "object",
1243
+ properties: attrProperties
1244
+ };
1245
+ if (attrRequired.length > 0) attrSchema.required = attrRequired;
1246
+ properties["attributes"] = attrSchema;
1247
+ }
1248
+ if (Object.keys(relProperties).length > 0) {
1249
+ properties["relationships"] = {
1250
+ type: "object",
1251
+ properties: relProperties
1252
+ };
1253
+ }
1254
+ const result = {
1255
+ type: "object",
1256
+ properties
1257
+ };
1258
+ const description = getMetaDescription(modelDef.attributes);
1259
+ if (description) {
1260
+ result.description = description;
1261
+ }
1262
+ return result;
1263
+ }
1264
+ buildCreateRequestSchema(_modelName, modelDef) {
1265
+ const idFieldNames = new Set(modelDef.idFields);
1266
+ const attributes = {};
1267
+ const attrRequired = [];
1268
+ const relationships = {};
1269
+ for (const [fieldName, fieldDef] of Object.entries(modelDef.fields)) {
1270
+ if (fieldDef.updatedAt) continue;
1271
+ if (fieldDef.foreignKeyFor) continue;
1272
+ if (idFieldNames.has(fieldName) && fieldDef.default !== void 0) continue;
1273
+ if (fieldDef.relation && !isModelIncluded(fieldDef.type, this.queryOptions)) continue;
1274
+ if (fieldDef.relation) {
1275
+ relationships[fieldName] = fieldDef.array ? {
1276
+ type: "object",
1277
+ properties: {
1278
+ data: {
1279
+ type: "array",
1280
+ items: {
1281
+ $ref: "#/components/schemas/_resourceIdentifier"
1282
+ }
1283
+ }
1284
+ }
1285
+ } : {
1286
+ type: "object",
1287
+ properties: {
1288
+ data: {
1289
+ $ref: "#/components/schemas/_resourceIdentifier"
1290
+ }
1291
+ }
1292
+ };
1293
+ } else {
1294
+ attributes[fieldName] = this.fieldToSchema(fieldDef);
1295
+ if (!fieldDef.optional && fieldDef.default === void 0 && !fieldDef.array) {
1296
+ attrRequired.push(fieldName);
1297
+ }
1298
+ }
1299
+ }
1300
+ const dataProperties = {
1301
+ type: {
1302
+ type: "string"
1303
+ }
1304
+ };
1305
+ if (Object.keys(attributes).length > 0) {
1306
+ const attrSchema = {
1307
+ type: "object",
1308
+ properties: attributes
1309
+ };
1310
+ if (attrRequired.length > 0) attrSchema.required = attrRequired;
1311
+ dataProperties["attributes"] = attrSchema;
1312
+ }
1313
+ if (Object.keys(relationships).length > 0) {
1314
+ dataProperties["relationships"] = {
1315
+ type: "object",
1316
+ properties: relationships
1317
+ };
1318
+ }
1319
+ return {
1320
+ type: "object",
1321
+ properties: {
1322
+ data: {
1323
+ type: "object",
1324
+ properties: dataProperties,
1325
+ required: [
1326
+ "type"
1327
+ ]
1328
+ }
1329
+ },
1330
+ required: [
1331
+ "data"
1332
+ ]
1333
+ };
1334
+ }
1335
+ buildUpdateRequestSchema(modelDef) {
1336
+ const attributes = {};
1337
+ const relationships = {};
1338
+ for (const [fieldName, fieldDef] of Object.entries(modelDef.fields)) {
1339
+ if (fieldDef.updatedAt) continue;
1340
+ if (fieldDef.foreignKeyFor) continue;
1341
+ if (fieldDef.relation && !isModelIncluded(fieldDef.type, this.queryOptions)) continue;
1342
+ if (fieldDef.relation) {
1343
+ relationships[fieldName] = fieldDef.array ? {
1344
+ type: "object",
1345
+ properties: {
1346
+ data: {
1347
+ type: "array",
1348
+ items: {
1349
+ $ref: "#/components/schemas/_resourceIdentifier"
1350
+ }
1351
+ }
1352
+ }
1353
+ } : {
1354
+ type: "object",
1355
+ properties: {
1356
+ data: {
1357
+ $ref: "#/components/schemas/_resourceIdentifier"
1358
+ }
1359
+ }
1360
+ };
1361
+ } else {
1362
+ attributes[fieldName] = this.fieldToSchema(fieldDef);
1363
+ }
1364
+ }
1365
+ const dataProperties = {
1366
+ type: {
1367
+ type: "string"
1368
+ },
1369
+ id: {
1370
+ type: "string"
1371
+ }
1372
+ };
1373
+ if (Object.keys(attributes).length > 0) {
1374
+ dataProperties["attributes"] = {
1375
+ type: "object",
1376
+ properties: attributes
1377
+ };
1378
+ }
1379
+ if (Object.keys(relationships).length > 0) {
1380
+ dataProperties["relationships"] = {
1381
+ type: "object",
1382
+ properties: relationships
1383
+ };
1384
+ }
1385
+ return {
1386
+ type: "object",
1387
+ properties: {
1388
+ data: {
1389
+ type: "object",
1390
+ properties: dataProperties,
1391
+ required: [
1392
+ "type",
1393
+ "id"
1394
+ ]
1395
+ }
1396
+ },
1397
+ required: [
1398
+ "data"
1399
+ ]
1400
+ };
1401
+ }
1402
+ buildModelResponseSchema(modelName) {
1403
+ return {
1404
+ type: "object",
1405
+ properties: {
1406
+ jsonapi: {
1407
+ $ref: "#/components/schemas/_jsonapi"
1408
+ },
1409
+ data: {
1410
+ allOf: [
1411
+ {
1412
+ $ref: `#/components/schemas/${modelName}`
1413
+ },
1414
+ {
1415
+ $ref: "#/components/schemas/_resource"
1416
+ }
1417
+ ]
1418
+ },
1419
+ meta: {
1420
+ $ref: "#/components/schemas/_meta"
1421
+ }
1422
+ }
1423
+ };
1424
+ }
1425
+ buildModelListResponseSchema(modelName) {
1426
+ return {
1427
+ type: "object",
1428
+ properties: {
1429
+ jsonapi: {
1430
+ $ref: "#/components/schemas/_jsonapi"
1431
+ },
1432
+ data: {
1433
+ type: "array",
1434
+ items: {
1435
+ allOf: [
1436
+ {
1437
+ $ref: `#/components/schemas/${modelName}`
1438
+ },
1439
+ {
1440
+ $ref: "#/components/schemas/_resource"
1441
+ }
1442
+ ]
1443
+ }
1444
+ },
1445
+ links: {
1446
+ allOf: [
1447
+ {
1448
+ $ref: "#/components/schemas/_pagination"
1449
+ },
1450
+ {
1451
+ $ref: "#/components/schemas/_links"
1452
+ }
1453
+ ]
1454
+ },
1455
+ meta: {
1456
+ $ref: "#/components/schemas/_meta"
1457
+ }
1458
+ }
1459
+ };
1460
+ }
1461
+ generateSharedParams() {
1462
+ return {
1463
+ id: {
1464
+ name: "id",
1465
+ in: "path",
1466
+ required: true,
1467
+ schema: {
1468
+ type: "string"
1469
+ },
1470
+ description: "Resource ID"
1471
+ },
1472
+ include: {
1473
+ name: "include",
1474
+ in: "query",
1475
+ schema: {
1476
+ type: "string"
1477
+ },
1478
+ description: "Comma-separated list of relationships to include"
1479
+ },
1480
+ sort: {
1481
+ name: "sort",
1482
+ in: "query",
1483
+ schema: {
1484
+ type: "string"
1485
+ },
1486
+ description: "Comma-separated list of fields to sort by. Prefix with - for descending"
1487
+ },
1488
+ pageOffset: {
1489
+ name: "page[offset]",
1490
+ in: "query",
1491
+ schema: {
1492
+ type: "integer",
1493
+ minimum: 0
1494
+ },
1495
+ description: "Page offset"
1496
+ },
1497
+ pageLimit: {
1498
+ name: "page[limit]",
1499
+ in: "query",
1500
+ schema: {
1501
+ type: "integer",
1502
+ minimum: 1
1503
+ },
1504
+ description: "Page limit"
1505
+ }
1506
+ };
1507
+ }
1508
+ fieldToSchema(fieldDef) {
1509
+ const baseSchema = this.typeToSchema(fieldDef.type);
1510
+ if (fieldDef.array) {
1511
+ return {
1512
+ type: "array",
1513
+ items: baseSchema
1514
+ };
1515
+ }
1516
+ if (fieldDef.optional) {
1517
+ return {
1518
+ oneOf: [
1519
+ baseSchema,
1520
+ {
1521
+ type: "null"
1522
+ }
1523
+ ]
1524
+ };
1525
+ }
1526
+ return baseSchema;
1527
+ }
1528
+ typeToSchema(type) {
1529
+ switch (type) {
1530
+ case "String":
1531
+ return {
1532
+ type: "string"
1533
+ };
1534
+ case "Int":
1535
+ case "BigInt":
1536
+ return {
1537
+ type: "integer"
1538
+ };
1539
+ case "Float":
1540
+ return {
1541
+ type: "number"
1542
+ };
1543
+ case "Decimal":
1544
+ return {
1545
+ oneOf: [
1546
+ {
1547
+ type: "number"
1548
+ },
1549
+ {
1550
+ type: "string"
1551
+ }
1552
+ ]
1553
+ };
1554
+ case "Boolean":
1555
+ return {
1556
+ type: "boolean"
1557
+ };
1558
+ case "DateTime":
1559
+ return {
1560
+ type: "string",
1561
+ format: "date-time"
1562
+ };
1563
+ case "Bytes":
1564
+ return {
1565
+ type: "string",
1566
+ format: "byte"
1567
+ };
1568
+ case "Json":
1569
+ case "Unsupported":
1570
+ return {};
1571
+ default:
1572
+ return {
1573
+ $ref: `#/components/schemas/${type}`
1574
+ };
1575
+ }
1576
+ }
1577
+ getIdFields(modelDef) {
1578
+ return modelDef.idFields.map((name) => modelDef.fields[name]).filter((f) => f !== void 0);
1579
+ }
1580
+ /**
1581
+ * Checks if an operation on a model may be denied by access policies.
1582
+ * Returns true when `respectAccessPolicies` is enabled and the model's
1583
+ * policies for the given operation are NOT a constant allow (i.e., not
1584
+ * simply `@@allow('...', true)` with no `@@deny` rules).
1585
+ */
1586
+ mayDenyAccess(modelDef, operation) {
1587
+ if (!this.specOptions?.respectAccessPolicies) return false;
1588
+ const policyAttrs = (modelDef.attributes ?? []).filter((attr) => attr.name === "@@allow" || attr.name === "@@deny");
1589
+ if (policyAttrs.length === 0) return true;
1590
+ const getArgByName = /* @__PURE__ */ __name((args, name) => args?.find((a) => a.name === name)?.value, "getArgByName");
1591
+ const matchesOperation = /* @__PURE__ */ __name((args) => {
1592
+ const val = getArgByName(args, "operation");
1593
+ if (!val || val.kind !== "literal" || typeof val.value !== "string") return false;
1594
+ const ops = val.value.split(",").map((s) => s.trim());
1595
+ return ops.includes(operation) || ops.includes("all");
1596
+ }, "matchesOperation");
1597
+ const hasEffectiveDeny = policyAttrs.some((attr) => {
1598
+ if (attr.name !== "@@deny" || !matchesOperation(attr.args)) return false;
1599
+ const condition = getArgByName(attr.args, "condition");
1600
+ return !(condition?.kind === "literal" && condition.value === false);
1601
+ });
1602
+ if (hasEffectiveDeny) return true;
1603
+ const relevantAllow = policyAttrs.filter((attr) => attr.name === "@@allow" && matchesOperation(attr.args));
1604
+ const hasConstantAllow = relevantAllow.some((attr) => {
1605
+ const condition = getArgByName(attr.args, "condition");
1606
+ return condition?.kind === "literal" && condition.value === true;
1607
+ });
1608
+ return !hasConstantAllow;
1609
+ }
1610
+ };
1611
+
197
1612
  // src/api/rest/index.ts
198
1613
  var InvalidValueError = class InvalidValueError2 extends Error {
199
1614
  static {
@@ -376,7 +1791,7 @@ var RestApiHandler = class {
376
1791
  const segmentCharset = options.urlSegmentCharset ?? "a-zA-Z0-9-_~ %";
377
1792
  this.modelNameMapping = options.modelNameMapping ?? {};
378
1793
  this.modelNameMapping = Object.fromEntries(Object.entries(this.modelNameMapping).map(([k, v]) => [
379
- lowerCaseFirst(k),
1794
+ lowerCaseFirst3(k),
380
1795
  v
381
1796
  ]));
382
1797
  this.reverseModelNameMapping = Object.fromEntries(Object.entries(this.modelNameMapping).map(([k, v]) => [
@@ -385,7 +1800,7 @@ var RestApiHandler = class {
385
1800
  ]));
386
1801
  this.externalIdMapping = options.externalIdMapping ?? {};
387
1802
  this.externalIdMapping = Object.fromEntries(Object.entries(this.externalIdMapping).map(([k, v]) => [
388
- lowerCaseFirst(k),
1803
+ lowerCaseFirst3(k),
389
1804
  v
390
1805
  ]));
391
1806
  this.urlPatternMap = this.buildUrlPatternMap(segmentCharset);
@@ -404,7 +1819,8 @@ var RestApiHandler = class {
404
1819
  idDivider: z2.string().min(1).optional(),
405
1820
  urlSegmentCharset: z2.string().min(1).optional(),
406
1821
  modelNameMapping: z2.record(z2.string(), z2.string()).optional(),
407
- externalIdMapping: z2.record(z2.string(), z2.string()).optional()
1822
+ externalIdMapping: z2.record(z2.string(), z2.string()).optional(),
1823
+ queryOptions: queryOptionsSchema.optional()
408
1824
  });
409
1825
  const parseResult = schema.safeParse(options);
410
1826
  if (!parseResult.success) {
@@ -562,8 +1978,9 @@ var RestApiHandler = class {
562
1978
  }
563
1979
  }
564
1980
  handleGenericError(err) {
565
- return this.makeError("unknownError", err instanceof Error ? `${err.message}
566
- ${err.stack}` : "Unknown error");
1981
+ const resp = this.makeError("unknownError", err instanceof Error ? `${err.message}` : "Unknown error");
1982
+ log(this.options.log, "debug", () => `sending error response: ${safeJSONStringify(resp)}${err instanceof Error ? "\n" + err.stack : ""}`);
1983
+ return resp;
567
1984
  }
568
1985
  async processProcedureRequest({ client, method, proc, query, requestBody }) {
569
1986
  if (!proc) {
@@ -621,13 +2038,13 @@ ${err.stack}` : "Unknown error");
621
2038
  }
622
2039
  makeProcBadInputErrorResponse(message) {
623
2040
  const resp = this.makeError("invalidPayload", message, 400);
624
- log(this.log, "debug", () => `sending error response: ${JSON.stringify(resp)}`);
2041
+ log(this.log, "debug", () => `sending error response: ${safeJSONStringify(resp)}`);
625
2042
  return resp;
626
2043
  }
627
2044
  makeProcGenericErrorResponse(err) {
628
2045
  const message = err instanceof Error ? err.message : "unknown error";
629
2046
  const resp = this.makeError("unknownError", message, 500);
630
- log(this.log, "debug", () => `sending error response: ${JSON.stringify(resp)}`);
2047
+ log(this.log, "debug", () => `sending error response: ${safeJSONStringify(resp)}${err instanceof Error ? "\n" + err.stack : ""}`);
631
2048
  return resp;
632
2049
  }
633
2050
  async processSingleRead(client, type, resourceId, query) {
@@ -700,7 +2117,7 @@ ${err.stack}` : "Unknown error");
700
2117
  select = relationSelect;
701
2118
  }
702
2119
  if (!select) {
703
- const { select: partialFields, error } = this.buildPartialSelect(lowerCaseFirst(relationInfo.type), query);
2120
+ const { select: partialFields, error } = this.buildPartialSelect(lowerCaseFirst3(relationInfo.type), query);
704
2121
  if (error) return error;
705
2122
  select = partialFields ? {
706
2123
  [relationship]: {
@@ -1244,7 +2661,7 @@ ${err.stack}` : "Unknown error");
1244
2661
  }
1245
2662
  getIdFields(model) {
1246
2663
  const modelDef = this.requireModel(model);
1247
- const modelLower = lowerCaseFirst(model);
2664
+ const modelLower = lowerCaseFirst3(model);
1248
2665
  if (!(modelLower in this.externalIdMapping)) {
1249
2666
  return Object.values(modelDef.fields).filter((f) => modelDef.idFields.includes(f.name));
1250
2667
  }
@@ -1278,7 +2695,7 @@ ${err.stack}` : "Unknown error");
1278
2695
  log(this.options.log, "warn", `Not including model ${model} in the API because it has no ID field`);
1279
2696
  continue;
1280
2697
  }
1281
- const modelInfo = this.typeMap[lowerCaseFirst(model)] = {
2698
+ const modelInfo = this.typeMap[lowerCaseFirst3(model)] = {
1282
2699
  name: model,
1283
2700
  idFields,
1284
2701
  relationships: {},
@@ -1303,7 +2720,7 @@ ${err.stack}` : "Unknown error");
1303
2720
  }
1304
2721
  }
1305
2722
  getModelInfo(model) {
1306
- return this.typeMap[lowerCaseFirst(model)];
2723
+ return this.typeMap[lowerCaseFirst3(model)];
1307
2724
  }
1308
2725
  makeLinkUrl(path) {
1309
2726
  return `${this.options.endpoint}${path}`;
@@ -1312,7 +2729,7 @@ ${err.stack}` : "Unknown error");
1312
2729
  const linkers = {};
1313
2730
  for (const model of Object.keys(this.schema.models)) {
1314
2731
  const ids = this.getIdFields(model);
1315
- const modelLower = lowerCaseFirst(model);
2732
+ const modelLower = lowerCaseFirst3(model);
1316
2733
  const mappedModel = this.mapModelName(modelLower);
1317
2734
  if (ids.length < 1) {
1318
2735
  continue;
@@ -1341,7 +2758,7 @@ ${err.stack}` : "Unknown error");
1341
2758
  this.serializers.set(modelLower, serializer);
1342
2759
  }
1343
2760
  for (const model of Object.keys(this.schema.models)) {
1344
- const modelLower = lowerCaseFirst(model);
2761
+ const modelLower = lowerCaseFirst3(model);
1345
2762
  const serializer = this.serializers.get(modelLower);
1346
2763
  if (!serializer) {
1347
2764
  continue;
@@ -1352,7 +2769,7 @@ ${err.stack}` : "Unknown error");
1352
2769
  if (!fieldDef.relation) {
1353
2770
  continue;
1354
2771
  }
1355
- const fieldSerializer = this.serializers.get(lowerCaseFirst(fieldDef.type));
2772
+ const fieldSerializer = this.serializers.get(lowerCaseFirst3(fieldDef.type));
1356
2773
  if (!fieldSerializer) {
1357
2774
  continue;
1358
2775
  }
@@ -1386,7 +2803,7 @@ ${err.stack}` : "Unknown error");
1386
2803
  }
1387
2804
  }
1388
2805
  async serializeItems(model, items, options) {
1389
- model = lowerCaseFirst(model);
2806
+ model = lowerCaseFirst3(model);
1390
2807
  const serializer = this.serializers.get(model);
1391
2808
  if (!serializer) {
1392
2809
  throw new Error(`serializer not found for model ${model}`);
@@ -1828,7 +3245,7 @@ ${err.stack}` : "Unknown error");
1828
3245
  error: this.makeUnsupportedModelError(relationInfo.type)
1829
3246
  };
1830
3247
  }
1831
- const { select, error } = this.buildPartialSelect(lowerCaseFirst(relationInfo.type), query);
3248
+ const { select, error } = this.buildPartialSelect(lowerCaseFirst3(relationInfo.type), query);
1832
3249
  if (error) return {
1833
3250
  select: void 0,
1834
3251
  error
@@ -2028,10 +3445,15 @@ ${err.stack}` : "Unknown error");
2028
3445
  makeUnsupportedRelationshipError(model, relationship, status) {
2029
3446
  return this.makeError("unsupportedRelationship", `Relationship ${model}.${relationship} doesn't exist`, status);
2030
3447
  }
3448
+ //#endregion
3449
+ async generateSpec(options) {
3450
+ const generator = new RestApiSpecGenerator(this.options);
3451
+ return generator.generateSpec(options);
3452
+ }
2031
3453
  };
2032
3454
 
2033
3455
  // src/api/rpc/index.ts
2034
- import { lowerCaseFirst as lowerCaseFirst2, safeJSONStringify } from "@zenstackhq/common-helpers";
3456
+ import { lowerCaseFirst as lowerCaseFirst4, safeJSONStringify as safeJSONStringify2 } from "@zenstackhq/common-helpers";
2035
3457
  import { CoreCrudOperations, ORMError as ORMError3, ORMErrorReason as ORMErrorReason2 } from "@zenstackhq/orm";
2036
3458
  import SuperJSON4 from "superjson";
2037
3459
  import { match as match3 } from "ts-pattern";
@@ -2052,7 +3474,8 @@ var RPCApiHandler = class {
2052
3474
  validateOptions(options) {
2053
3475
  const schema = z3.strictObject({
2054
3476
  schema: z3.object(),
2055
- log: loggerSchema.optional()
3477
+ log: loggerSchema.optional(),
3478
+ queryOptions: queryOptionsSchema.optional()
2056
3479
  });
2057
3480
  const parseResult = schema.safeParse(options);
2058
3481
  if (!parseResult.success) {
@@ -2089,7 +3512,7 @@ var RPCApiHandler = class {
2089
3512
  requestBody
2090
3513
  });
2091
3514
  }
2092
- model = lowerCaseFirst2(model);
3515
+ model = lowerCaseFirst4(model);
2093
3516
  method = method.toUpperCase();
2094
3517
  let args;
2095
3518
  let resCode = 200;
@@ -2156,7 +3579,7 @@ var RPCApiHandler = class {
2156
3579
  if (!this.isValidModel(client, model)) {
2157
3580
  return this.makeBadInputErrorResponse(`unknown model name: ${model}`);
2158
3581
  }
2159
- log(this.options.log, "debug", () => `handling "${model}.${op}" request with args: ${safeJSONStringify(processedArgs)}`);
3582
+ log(this.options.log, "debug", () => `handling "${model}.${op}" request with args: ${safeJSONStringify2(processedArgs)}`);
2160
3583
  const clientResult = await client[model][op](processedArgs);
2161
3584
  let responseBody = {
2162
3585
  data: clientResult
@@ -2176,7 +3599,7 @@ var RPCApiHandler = class {
2176
3599
  status: resCode,
2177
3600
  body: responseBody
2178
3601
  };
2179
- log(this.options.log, "debug", () => `sending response for "${model}.${op}" request: ${safeJSONStringify(response)}`);
3602
+ log(this.options.log, "debug", () => `sending response for "${model}.${op}" request: ${safeJSONStringify2(response)}`);
2180
3603
  return response;
2181
3604
  } catch (err) {
2182
3605
  log(this.options.log, "error", `error occurred when handling "${model}.${op}" request`, err);
@@ -2213,7 +3636,7 @@ var RPCApiHandler = class {
2213
3636
  if (!VALID_OPS.has(itemOp)) {
2214
3637
  return this.makeBadInputErrorResponse(`operation at index ${i} has invalid op: ${itemOp}`);
2215
3638
  }
2216
- if (!this.isValidModel(client, lowerCaseFirst2(itemModel))) {
3639
+ if (!this.isValidModel(client, lowerCaseFirst4(itemModel))) {
2217
3640
  return this.makeBadInputErrorResponse(`operation at index ${i} has unknown model: ${itemModel}`);
2218
3641
  }
2219
3642
  if (itemArgs !== void 0 && itemArgs !== null && (typeof itemArgs !== "object" || Array.isArray(itemArgs))) {
@@ -2224,7 +3647,7 @@ var RPCApiHandler = class {
2224
3647
  return this.makeBadInputErrorResponse(`operation at index ${i}: ${argsError}`);
2225
3648
  }
2226
3649
  processedOps.push({
2227
- model: lowerCaseFirst2(itemModel),
3650
+ model: lowerCaseFirst4(itemModel),
2228
3651
  op: itemOp,
2229
3652
  args: processedArgs
2230
3653
  });
@@ -2248,7 +3671,7 @@ var RPCApiHandler = class {
2248
3671
  status: 200,
2249
3672
  body: responseBody
2250
3673
  };
2251
- log(this.options.log, "debug", () => `sending response for "$transaction" request: ${safeJSONStringify(response)}`);
3674
+ log(this.options.log, "debug", () => `sending response for "$transaction" request: ${safeJSONStringify2(response)}`);
2252
3675
  return response;
2253
3676
  } catch (err) {
2254
3677
  log(this.options.log, "error", `error occurred when handling "$transaction" request`, err);
@@ -2310,7 +3733,7 @@ var RPCApiHandler = class {
2310
3733
  status: 200,
2311
3734
  body: responseBody
2312
3735
  };
2313
- log(this.options.log, "debug", () => `sending response for "$procs.${proc}" request: ${safeJSONStringify(response)}`);
3736
+ log(this.options.log, "debug", () => `sending response for "$procs.${proc}" request: ${safeJSONStringify2(response)}`);
2314
3737
  return response;
2315
3738
  } catch (err) {
2316
3739
  log(this.options.log, "error", `error occurred when handling "$procs.${proc}" request`, err);
@@ -2321,7 +3744,7 @@ var RPCApiHandler = class {
2321
3744
  }
2322
3745
  }
2323
3746
  isValidModel(client, model) {
2324
- return Object.keys(client.$schema.models).some((m) => lowerCaseFirst2(m) === lowerCaseFirst2(model));
3747
+ return Object.keys(client.$schema.models).some((m) => lowerCaseFirst4(m) === lowerCaseFirst4(model));
2325
3748
  }
2326
3749
  makeBadInputErrorResponse(message) {
2327
3750
  const resp = {
@@ -2332,7 +3755,7 @@ var RPCApiHandler = class {
2332
3755
  }
2333
3756
  }
2334
3757
  };
2335
- log(this.options.log, "debug", () => `sending error response: ${safeJSONStringify(resp)}`);
3758
+ log(this.options.log, "debug", () => `sending error response: ${safeJSONStringify2(resp)}`);
2336
3759
  return resp;
2337
3760
  }
2338
3761
  makeGenericErrorResponse(err) {
@@ -2344,7 +3767,7 @@ var RPCApiHandler = class {
2344
3767
  }
2345
3768
  }
2346
3769
  };
2347
- log(this.options.log, "debug", () => `sending error response: ${safeJSONStringify(resp)}${err instanceof Error ? "\n" + err.stack : ""}`);
3770
+ log(this.options.log, "debug", () => `sending error response: ${safeJSONStringify2(resp)}${err instanceof Error ? "\n" + err.stack : ""}`);
2348
3771
  return resp;
2349
3772
  }
2350
3773
  makeORMErrorResponse(err) {
@@ -2376,7 +3799,7 @@ var RPCApiHandler = class {
2376
3799
  error
2377
3800
  }
2378
3801
  };
2379
- log(this.options.log, "debug", () => `sending error response: ${safeJSONStringify(resp)}`);
3802
+ log(this.options.log, "debug", () => `sending error response: ${safeJSONStringify2(resp)}`);
2380
3803
  return resp;
2381
3804
  }
2382
3805
  async processRequestPayload(args) {