@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.cjs CHANGED
@@ -37,7 +37,7 @@ __export(api_exports, {
37
37
  module.exports = __toCommonJS(api_exports);
38
38
 
39
39
  // src/api/rest/index.ts
40
- var import_common_helpers = require("@zenstackhq/common-helpers");
40
+ var import_common_helpers3 = require("@zenstackhq/common-helpers");
41
41
  var import_orm2 = require("@zenstackhq/orm");
42
42
  var import_decimal2 = require("decimal.js");
43
43
  var import_superjson3 = __toESM(require("superjson"), 1);
@@ -127,6 +127,26 @@ var loggerSchema = import_zod.default.union([
127
127
  ]).array(),
128
128
  import_zod.default.function()
129
129
  ]);
130
+ var fieldSlicingSchema = import_zod.default.looseObject({
131
+ includedFilterKinds: import_zod.default.string().array().optional(),
132
+ excludedFilterKinds: import_zod.default.string().array().optional()
133
+ });
134
+ var modelSlicingSchema = import_zod.default.looseObject({
135
+ includedOperations: import_zod.default.array(import_zod.default.string()).optional(),
136
+ excludedOperations: import_zod.default.array(import_zod.default.string()).optional(),
137
+ fields: import_zod.default.record(import_zod.default.string(), fieldSlicingSchema).optional()
138
+ });
139
+ var slicingSchema = import_zod.default.looseObject({
140
+ includedModels: import_zod.default.array(import_zod.default.string()).optional(),
141
+ excludedModels: import_zod.default.array(import_zod.default.string()).optional(),
142
+ models: import_zod.default.record(import_zod.default.string(), modelSlicingSchema).optional(),
143
+ includedProcedures: import_zod.default.array(import_zod.default.string()).optional(),
144
+ excludedProcedures: import_zod.default.array(import_zod.default.string()).optional()
145
+ });
146
+ var queryOptionsSchema = import_zod.default.looseObject({
147
+ omit: import_zod.default.record(import_zod.default.string(), import_zod.default.record(import_zod.default.string(), import_zod.default.boolean())).optional(),
148
+ slicing: slicingSchema.optional()
149
+ });
130
150
 
131
151
  // src/api/common/utils.ts
132
152
  var import_superjson = __toESM(require("superjson"), 1);
@@ -229,6 +249,1401 @@ function getZodErrorMessage(error) {
229
249
  }
230
250
  __name(getZodErrorMessage, "getZodErrorMessage");
231
251
 
252
+ // src/api/rest/openapi.ts
253
+ var import_common_helpers2 = require("@zenstackhq/common-helpers");
254
+
255
+ // src/api/common/spec-utils.ts
256
+ var import_common_helpers = require("@zenstackhq/common-helpers");
257
+ var import_schema = require("@zenstackhq/orm/schema");
258
+ function isModelIncluded(modelName, queryOptions) {
259
+ const slicing = queryOptions?.slicing;
260
+ if (!slicing) return true;
261
+ const excluded = slicing.excludedModels;
262
+ if (excluded?.includes(modelName)) return false;
263
+ const included = slicing.includedModels;
264
+ if (included && !included.includes(modelName)) return false;
265
+ return true;
266
+ }
267
+ __name(isModelIncluded, "isModelIncluded");
268
+ function isOperationIncluded(modelName, op, queryOptions) {
269
+ const slicing = queryOptions?.slicing;
270
+ if (!slicing?.models) return true;
271
+ const modelKey = (0, import_common_helpers.lowerCaseFirst)(modelName);
272
+ const modelSlicing = slicing.models[modelKey] ?? slicing.models.$all;
273
+ if (!modelSlicing) return true;
274
+ const excluded = modelSlicing.excludedOperations;
275
+ if (excluded?.includes(op)) return false;
276
+ const included = modelSlicing.includedOperations;
277
+ if (included && !included.includes(op)) return false;
278
+ return true;
279
+ }
280
+ __name(isOperationIncluded, "isOperationIncluded");
281
+ function isProcedureIncluded(procName, queryOptions) {
282
+ const slicing = queryOptions?.slicing;
283
+ if (!slicing) return true;
284
+ const excluded = slicing.excludedProcedures;
285
+ if (excluded?.includes(procName)) return false;
286
+ const included = slicing.includedProcedures;
287
+ if (included && !included.includes(procName)) return false;
288
+ return true;
289
+ }
290
+ __name(isProcedureIncluded, "isProcedureIncluded");
291
+ function isFieldOmitted(modelName, fieldName, queryOptions) {
292
+ const omit = queryOptions?.omit;
293
+ return omit?.[modelName]?.[fieldName] === true;
294
+ }
295
+ __name(isFieldOmitted, "isFieldOmitted");
296
+ function getIncludedModels(schema, queryOptions) {
297
+ return Object.keys(schema.models).filter((name) => isModelIncluded(name, queryOptions));
298
+ }
299
+ __name(getIncludedModels, "getIncludedModels");
300
+ function isFilterKindIncluded(modelName, fieldName, filterKind, queryOptions) {
301
+ const slicing = queryOptions?.slicing;
302
+ if (!slicing?.models) return true;
303
+ const modelKey = (0, import_common_helpers.lowerCaseFirst)(modelName);
304
+ const modelSlicing = slicing.models[modelKey] ?? slicing.models.$all;
305
+ if (!modelSlicing?.fields) return true;
306
+ const fieldSlicing = modelSlicing.fields[fieldName] ?? modelSlicing.fields.$all;
307
+ if (!fieldSlicing) return true;
308
+ const excluded = fieldSlicing.excludedFilterKinds;
309
+ if (excluded?.includes(filterKind)) return false;
310
+ const included = fieldSlicing.includedFilterKinds;
311
+ if (included && !included.includes(filterKind)) return false;
312
+ return true;
313
+ }
314
+ __name(isFilterKindIncluded, "isFilterKindIncluded");
315
+ function getMetaDescription(attributes) {
316
+ if (!attributes) return void 0;
317
+ for (const attr of attributes) {
318
+ if (attr.name !== "@meta" && attr.name !== "@@meta") continue;
319
+ const nameArg = attr.args?.find((a) => a.name === "name");
320
+ if (!nameArg || import_schema.ExpressionUtils.getLiteralValue(nameArg.value) !== "description") continue;
321
+ const valueArg = attr.args?.find((a) => a.name === "value");
322
+ if (valueArg) {
323
+ return import_schema.ExpressionUtils.getLiteralValue(valueArg.value);
324
+ }
325
+ }
326
+ return void 0;
327
+ }
328
+ __name(getMetaDescription, "getMetaDescription");
329
+
330
+ // src/api/rest/openapi.ts
331
+ function errorResponse(description) {
332
+ return {
333
+ description,
334
+ content: {
335
+ "application/vnd.api+json": {
336
+ schema: {
337
+ $ref: "#/components/schemas/_errorResponse"
338
+ }
339
+ }
340
+ }
341
+ };
342
+ }
343
+ __name(errorResponse, "errorResponse");
344
+ var ERROR_400 = errorResponse("Error occurred while processing the request");
345
+ var ERROR_403 = errorResponse("Forbidden: insufficient permissions to perform this operation");
346
+ var ERROR_404 = errorResponse("Resource not found");
347
+ var ERROR_422 = errorResponse("Operation is unprocessable due to validation errors");
348
+ var SCALAR_STRING_OPS = [
349
+ "$contains",
350
+ "$icontains",
351
+ "$search",
352
+ "$startsWith",
353
+ "$endsWith"
354
+ ];
355
+ var SCALAR_COMPARABLE_OPS = [
356
+ "$lt",
357
+ "$lte",
358
+ "$gt",
359
+ "$gte"
360
+ ];
361
+ var SCALAR_ARRAY_OPS = [
362
+ "$has",
363
+ "$hasEvery",
364
+ "$hasSome",
365
+ "$isEmpty"
366
+ ];
367
+ var RestApiSpecGenerator = class {
368
+ static {
369
+ __name(this, "RestApiSpecGenerator");
370
+ }
371
+ handlerOptions;
372
+ specOptions;
373
+ constructor(handlerOptions) {
374
+ this.handlerOptions = handlerOptions;
375
+ }
376
+ get schema() {
377
+ return this.handlerOptions.schema;
378
+ }
379
+ get modelNameMapping() {
380
+ const mapping = {};
381
+ if (this.handlerOptions.modelNameMapping) {
382
+ for (const [k, v] of Object.entries(this.handlerOptions.modelNameMapping)) {
383
+ mapping[(0, import_common_helpers2.lowerCaseFirst)(k)] = v;
384
+ }
385
+ }
386
+ return mapping;
387
+ }
388
+ get queryOptions() {
389
+ return this.handlerOptions?.queryOptions;
390
+ }
391
+ generateSpec(options) {
392
+ this.specOptions = options;
393
+ return {
394
+ openapi: "3.1.0",
395
+ info: {
396
+ title: options?.title ?? "ZenStack Generated API",
397
+ version: options?.version ?? "1.0.0",
398
+ ...options?.description && {
399
+ description: options.description
400
+ },
401
+ ...options?.summary && {
402
+ summary: options.summary
403
+ }
404
+ },
405
+ tags: this.generateTags(),
406
+ paths: this.generatePaths(),
407
+ components: {
408
+ schemas: this.generateSchemas(),
409
+ parameters: this.generateSharedParams()
410
+ }
411
+ };
412
+ }
413
+ generateTags() {
414
+ return getIncludedModels(this.schema, this.queryOptions).map((modelName) => ({
415
+ name: (0, import_common_helpers2.lowerCaseFirst)(modelName),
416
+ description: `${modelName} operations`
417
+ }));
418
+ }
419
+ getModelPath(modelName) {
420
+ const lower = (0, import_common_helpers2.lowerCaseFirst)(modelName);
421
+ return this.modelNameMapping[lower] ?? lower;
422
+ }
423
+ generatePaths() {
424
+ const paths = {};
425
+ for (const modelName of getIncludedModels(this.schema, this.queryOptions)) {
426
+ const modelDef = this.schema.models[modelName];
427
+ const idFields = this.getIdFields(modelDef);
428
+ if (idFields.length === 0) continue;
429
+ const modelPath = this.getModelPath(modelName);
430
+ const tag = (0, import_common_helpers2.lowerCaseFirst)(modelName);
431
+ const collectionPath = this.buildCollectionPath(modelName, modelDef, tag);
432
+ if (Object.keys(collectionPath).length > 0) {
433
+ paths[`/${modelPath}`] = collectionPath;
434
+ }
435
+ const singlePath = this.buildSinglePath(modelDef, tag);
436
+ if (Object.keys(singlePath).length > 0) {
437
+ paths[`/${modelPath}/{id}`] = singlePath;
438
+ }
439
+ for (const [fieldName, fieldDef] of Object.entries(modelDef.fields)) {
440
+ if (!fieldDef.relation) continue;
441
+ if (!isModelIncluded(fieldDef.type, this.queryOptions)) continue;
442
+ const relModelDef = this.schema.models[fieldDef.type];
443
+ if (!relModelDef) continue;
444
+ const relIdFields = this.getIdFields(relModelDef);
445
+ if (relIdFields.length === 0) continue;
446
+ paths[`/${modelPath}/{id}/${fieldName}`] = this.buildFetchRelatedPath(modelName, fieldName, fieldDef, tag);
447
+ paths[`/${modelPath}/{id}/relationships/${fieldName}`] = this.buildRelationshipPath(modelDef, fieldName, fieldDef, tag);
448
+ }
449
+ }
450
+ if (this.schema.procedures) {
451
+ for (const [procName, procDef] of Object.entries(this.schema.procedures)) {
452
+ if (!isProcedureIncluded(procName, this.queryOptions)) continue;
453
+ const isMutation = !!procDef.mutation;
454
+ if (isMutation) {
455
+ paths[`/${PROCEDURE_ROUTE_PREFIXES}/${procName}`] = {
456
+ post: this.buildProcedureOperation(procName, "post")
457
+ };
458
+ } else {
459
+ paths[`/${PROCEDURE_ROUTE_PREFIXES}/${procName}`] = {
460
+ get: this.buildProcedureOperation(procName, "get")
461
+ };
462
+ }
463
+ }
464
+ }
465
+ return paths;
466
+ }
467
+ buildCollectionPath(modelName, modelDef, tag) {
468
+ const filterParams = this.buildFilterParams(modelName, modelDef);
469
+ const listOp = {
470
+ tags: [
471
+ tag
472
+ ],
473
+ summary: `List ${modelName} resources`,
474
+ operationId: `list${modelName}`,
475
+ parameters: [
476
+ {
477
+ $ref: "#/components/parameters/include"
478
+ },
479
+ {
480
+ $ref: "#/components/parameters/sort"
481
+ },
482
+ {
483
+ $ref: "#/components/parameters/pageOffset"
484
+ },
485
+ {
486
+ $ref: "#/components/parameters/pageLimit"
487
+ },
488
+ ...filterParams
489
+ ],
490
+ responses: {
491
+ "200": {
492
+ description: `List of ${modelName} resources`,
493
+ content: {
494
+ "application/vnd.api+json": {
495
+ schema: {
496
+ $ref: `#/components/schemas/${modelName}ListResponse`
497
+ }
498
+ }
499
+ }
500
+ },
501
+ "400": ERROR_400
502
+ }
503
+ };
504
+ const createOp = {
505
+ tags: [
506
+ tag
507
+ ],
508
+ summary: `Create a ${modelName} resource`,
509
+ operationId: `create${modelName}`,
510
+ requestBody: {
511
+ required: true,
512
+ content: {
513
+ "application/vnd.api+json": {
514
+ schema: {
515
+ $ref: `#/components/schemas/${modelName}CreateRequest`
516
+ }
517
+ }
518
+ }
519
+ },
520
+ responses: {
521
+ "201": {
522
+ description: `Created ${modelName} resource`,
523
+ content: {
524
+ "application/vnd.api+json": {
525
+ schema: {
526
+ $ref: `#/components/schemas/${modelName}Response`
527
+ }
528
+ }
529
+ }
530
+ },
531
+ "400": ERROR_400,
532
+ ...this.mayDenyAccess(modelDef, "create") && {
533
+ "403": ERROR_403
534
+ },
535
+ "422": ERROR_422
536
+ }
537
+ };
538
+ const result = {};
539
+ if (isOperationIncluded(modelName, "findMany", this.queryOptions)) {
540
+ result["get"] = listOp;
541
+ }
542
+ if (isOperationIncluded(modelName, "create", this.queryOptions)) {
543
+ result["post"] = createOp;
544
+ }
545
+ return result;
546
+ }
547
+ buildSinglePath(modelDef, tag) {
548
+ const modelName = modelDef.name;
549
+ const idParam = {
550
+ $ref: "#/components/parameters/id"
551
+ };
552
+ const result = {};
553
+ if (isOperationIncluded(modelName, "findUnique", this.queryOptions)) {
554
+ result["get"] = {
555
+ tags: [
556
+ tag
557
+ ],
558
+ summary: `Get a ${modelName} resource by ID`,
559
+ operationId: `get${modelName}`,
560
+ parameters: [
561
+ idParam,
562
+ {
563
+ $ref: "#/components/parameters/include"
564
+ }
565
+ ],
566
+ responses: {
567
+ "200": {
568
+ description: `${modelName} resource`,
569
+ content: {
570
+ "application/vnd.api+json": {
571
+ schema: {
572
+ $ref: `#/components/schemas/${modelName}Response`
573
+ }
574
+ }
575
+ }
576
+ },
577
+ "404": ERROR_404
578
+ }
579
+ };
580
+ }
581
+ if (isOperationIncluded(modelName, "update", this.queryOptions)) {
582
+ result["patch"] = {
583
+ tags: [
584
+ tag
585
+ ],
586
+ summary: `Update a ${modelName} resource`,
587
+ operationId: `update${modelName}`,
588
+ parameters: [
589
+ idParam
590
+ ],
591
+ requestBody: {
592
+ required: true,
593
+ content: {
594
+ "application/vnd.api+json": {
595
+ schema: {
596
+ $ref: `#/components/schemas/${modelName}UpdateRequest`
597
+ }
598
+ }
599
+ }
600
+ },
601
+ responses: {
602
+ "200": {
603
+ description: `Updated ${modelName} resource`,
604
+ content: {
605
+ "application/vnd.api+json": {
606
+ schema: {
607
+ $ref: `#/components/schemas/${modelName}Response`
608
+ }
609
+ }
610
+ }
611
+ },
612
+ "400": ERROR_400,
613
+ ...this.mayDenyAccess(modelDef, "update") && {
614
+ "403": ERROR_403
615
+ },
616
+ "404": ERROR_404,
617
+ "422": ERROR_422
618
+ }
619
+ };
620
+ }
621
+ if (isOperationIncluded(modelName, "delete", this.queryOptions)) {
622
+ result["delete"] = {
623
+ tags: [
624
+ tag
625
+ ],
626
+ summary: `Delete a ${modelName} resource`,
627
+ operationId: `delete${modelName}`,
628
+ parameters: [
629
+ idParam
630
+ ],
631
+ responses: {
632
+ "200": {
633
+ description: "Deleted successfully"
634
+ },
635
+ ...this.mayDenyAccess(modelDef, "delete") && {
636
+ "403": ERROR_403
637
+ },
638
+ "404": ERROR_404
639
+ }
640
+ };
641
+ }
642
+ return result;
643
+ }
644
+ buildFetchRelatedPath(modelName, fieldName, fieldDef, tag) {
645
+ const isCollection = !!fieldDef.array;
646
+ const params = [
647
+ {
648
+ $ref: "#/components/parameters/id"
649
+ },
650
+ {
651
+ $ref: "#/components/parameters/include"
652
+ }
653
+ ];
654
+ if (isCollection && this.schema.models[fieldDef.type]) {
655
+ const relModelDef = this.schema.models[fieldDef.type];
656
+ params.push({
657
+ $ref: "#/components/parameters/sort"
658
+ }, {
659
+ $ref: "#/components/parameters/pageOffset"
660
+ }, {
661
+ $ref: "#/components/parameters/pageLimit"
662
+ }, ...this.buildFilterParams(fieldDef.type, relModelDef));
663
+ }
664
+ return {
665
+ get: {
666
+ tags: [
667
+ tag
668
+ ],
669
+ summary: `Fetch related ${fieldDef.type} for ${modelName}`,
670
+ operationId: `get${modelName}_${fieldName}`,
671
+ parameters: params,
672
+ responses: {
673
+ "200": {
674
+ description: `Related ${fieldDef.type} resource(s)`,
675
+ content: {
676
+ "application/vnd.api+json": {
677
+ schema: isCollection ? {
678
+ $ref: `#/components/schemas/${fieldDef.type}ListResponse`
679
+ } : {
680
+ $ref: `#/components/schemas/${fieldDef.type}Response`
681
+ }
682
+ }
683
+ }
684
+ },
685
+ "404": ERROR_404
686
+ }
687
+ }
688
+ };
689
+ }
690
+ buildRelationshipPath(modelDef, fieldName, fieldDef, tag) {
691
+ const modelName = modelDef.name;
692
+ const isCollection = !!fieldDef.array;
693
+ const idParam = {
694
+ $ref: "#/components/parameters/id"
695
+ };
696
+ const relSchemaRef = isCollection ? {
697
+ $ref: "#/components/schemas/_toManyRelationshipWithLinks"
698
+ } : {
699
+ $ref: "#/components/schemas/_toOneRelationshipWithLinks"
700
+ };
701
+ const relRequestRef = isCollection ? {
702
+ $ref: "#/components/schemas/_toManyRelationshipRequest"
703
+ } : {
704
+ $ref: "#/components/schemas/_toOneRelationshipRequest"
705
+ };
706
+ const mayDeny = this.mayDenyAccess(modelDef, "update");
707
+ const pathItem = {
708
+ get: {
709
+ tags: [
710
+ tag
711
+ ],
712
+ summary: `Fetch ${fieldName} relationship`,
713
+ operationId: `get${modelName}_relationships_${fieldName}`,
714
+ parameters: [
715
+ idParam
716
+ ],
717
+ responses: {
718
+ "200": {
719
+ description: `${fieldName} relationship`,
720
+ content: {
721
+ "application/vnd.api+json": {
722
+ schema: relSchemaRef
723
+ }
724
+ }
725
+ },
726
+ "404": ERROR_404
727
+ }
728
+ },
729
+ put: {
730
+ tags: [
731
+ tag
732
+ ],
733
+ summary: `Replace ${fieldName} relationship`,
734
+ operationId: `put${modelName}_relationships_${fieldName}`,
735
+ parameters: [
736
+ idParam
737
+ ],
738
+ requestBody: {
739
+ required: true,
740
+ content: {
741
+ "application/vnd.api+json": {
742
+ schema: relRequestRef
743
+ }
744
+ }
745
+ },
746
+ responses: {
747
+ "200": {
748
+ description: "Relationship updated"
749
+ },
750
+ "400": ERROR_400,
751
+ ...mayDeny && {
752
+ "403": ERROR_403
753
+ }
754
+ }
755
+ },
756
+ patch: {
757
+ tags: [
758
+ tag
759
+ ],
760
+ summary: `Update ${fieldName} relationship`,
761
+ operationId: `patch${modelName}_relationships_${fieldName}`,
762
+ parameters: [
763
+ idParam
764
+ ],
765
+ requestBody: {
766
+ required: true,
767
+ content: {
768
+ "application/vnd.api+json": {
769
+ schema: relRequestRef
770
+ }
771
+ }
772
+ },
773
+ responses: {
774
+ "200": {
775
+ description: "Relationship updated"
776
+ },
777
+ "400": ERROR_400,
778
+ ...mayDeny && {
779
+ "403": ERROR_403
780
+ }
781
+ }
782
+ }
783
+ };
784
+ if (isCollection) {
785
+ pathItem["post"] = {
786
+ tags: [
787
+ tag
788
+ ],
789
+ summary: `Add to ${fieldName} collection relationship`,
790
+ operationId: `post${modelName}_relationships_${fieldName}`,
791
+ parameters: [
792
+ idParam
793
+ ],
794
+ requestBody: {
795
+ required: true,
796
+ content: {
797
+ "application/vnd.api+json": {
798
+ schema: {
799
+ $ref: "#/components/schemas/_toManyRelationshipRequest"
800
+ }
801
+ }
802
+ }
803
+ },
804
+ responses: {
805
+ "200": {
806
+ description: "Added to relationship collection"
807
+ },
808
+ "400": ERROR_400,
809
+ ...mayDeny && {
810
+ "403": ERROR_403
811
+ }
812
+ }
813
+ };
814
+ }
815
+ return pathItem;
816
+ }
817
+ buildProcedureOperation(procName, method) {
818
+ const op = {
819
+ tags: [
820
+ "$procs"
821
+ ],
822
+ summary: `Execute procedure ${procName}`,
823
+ operationId: `proc_${procName}`,
824
+ responses: {
825
+ "200": {
826
+ description: `Result of ${procName}`
827
+ },
828
+ "400": ERROR_400
829
+ }
830
+ };
831
+ if (method === "get") {
832
+ op["parameters"] = [
833
+ {
834
+ name: "q",
835
+ in: "query",
836
+ description: "Procedure arguments as JSON",
837
+ schema: {
838
+ type: "string"
839
+ }
840
+ }
841
+ ];
842
+ } else {
843
+ op["requestBody"] = {
844
+ content: {
845
+ "application/json": {
846
+ schema: {
847
+ type: "object"
848
+ }
849
+ }
850
+ }
851
+ };
852
+ }
853
+ return op;
854
+ }
855
+ buildFilterParams(modelName, modelDef) {
856
+ const params = [];
857
+ const idFieldNames = new Set(modelDef.idFields);
858
+ if (isFilterKindIncluded(modelName, "id", "Equality", this.queryOptions)) {
859
+ params.push({
860
+ name: "filter[id]",
861
+ in: "query",
862
+ schema: {
863
+ type: "string"
864
+ },
865
+ description: `Filter by ${modelName} ID`
866
+ });
867
+ }
868
+ for (const [fieldName, fieldDef] of Object.entries(modelDef.fields)) {
869
+ if (fieldDef.relation) continue;
870
+ if (idFieldNames.has(fieldName)) continue;
871
+ const type = fieldDef.type;
872
+ if (isFilterKindIncluded(modelName, fieldName, "Equality", this.queryOptions)) {
873
+ params.push({
874
+ name: `filter[${fieldName}]`,
875
+ in: "query",
876
+ schema: {
877
+ type: "string"
878
+ },
879
+ description: `Filter by ${fieldName}`
880
+ });
881
+ }
882
+ if (type === "String" && isFilterKindIncluded(modelName, fieldName, "Like", this.queryOptions)) {
883
+ for (const op of SCALAR_STRING_OPS) {
884
+ params.push({
885
+ name: `filter[${fieldName}][${op}]`,
886
+ in: "query",
887
+ schema: {
888
+ type: "string"
889
+ }
890
+ });
891
+ }
892
+ } else if ((type === "Int" || type === "Float" || type === "BigInt" || type === "Decimal" || type === "DateTime") && isFilterKindIncluded(modelName, fieldName, "Range", this.queryOptions)) {
893
+ for (const op of SCALAR_COMPARABLE_OPS) {
894
+ params.push({
895
+ name: `filter[${fieldName}][${op}]`,
896
+ in: "query",
897
+ schema: {
898
+ type: "string"
899
+ }
900
+ });
901
+ }
902
+ }
903
+ if (fieldDef.array && isFilterKindIncluded(modelName, fieldName, "List", this.queryOptions)) {
904
+ for (const op of SCALAR_ARRAY_OPS) {
905
+ params.push({
906
+ name: `filter[${fieldName}][${op}]`,
907
+ in: "query",
908
+ schema: {
909
+ type: "string"
910
+ }
911
+ });
912
+ }
913
+ }
914
+ }
915
+ return params;
916
+ }
917
+ generateSchemas() {
918
+ const schemas = {};
919
+ Object.assign(schemas, this.buildSharedSchemas());
920
+ if (this.schema.enums) {
921
+ for (const [_enumName, enumDef] of Object.entries(this.schema.enums)) {
922
+ schemas[_enumName] = this.buildEnumSchema(enumDef);
923
+ }
924
+ }
925
+ if (this.schema.typeDefs) {
926
+ for (const [typeName, typeDef] of Object.entries(this.schema.typeDefs)) {
927
+ schemas[typeName] = this.buildTypeDefSchema(typeDef);
928
+ }
929
+ }
930
+ for (const modelName of getIncludedModels(this.schema, this.queryOptions)) {
931
+ const modelDef = this.schema.models[modelName];
932
+ const idFields = this.getIdFields(modelDef);
933
+ if (idFields.length === 0) continue;
934
+ schemas[modelName] = this.buildModelReadSchema(modelName, modelDef);
935
+ schemas[`${modelName}CreateRequest`] = this.buildCreateRequestSchema(modelName, modelDef);
936
+ schemas[`${modelName}UpdateRequest`] = this.buildUpdateRequestSchema(modelDef);
937
+ schemas[`${modelName}Response`] = this.buildModelResponseSchema(modelName);
938
+ schemas[`${modelName}ListResponse`] = this.buildModelListResponseSchema(modelName);
939
+ }
940
+ return schemas;
941
+ }
942
+ buildSharedSchemas() {
943
+ const nullableString = {
944
+ oneOf: [
945
+ {
946
+ type: "string"
947
+ },
948
+ {
949
+ type: "null"
950
+ }
951
+ ]
952
+ };
953
+ return {
954
+ _jsonapi: {
955
+ type: "object",
956
+ properties: {
957
+ version: {
958
+ type: "string"
959
+ },
960
+ meta: {
961
+ type: "object"
962
+ }
963
+ }
964
+ },
965
+ _meta: {
966
+ type: "object",
967
+ additionalProperties: true
968
+ },
969
+ _links: {
970
+ type: "object",
971
+ properties: {
972
+ self: {
973
+ type: "string"
974
+ },
975
+ related: {
976
+ type: "string"
977
+ }
978
+ }
979
+ },
980
+ _pagination: {
981
+ type: "object",
982
+ properties: {
983
+ first: nullableString,
984
+ last: nullableString,
985
+ prev: nullableString,
986
+ next: nullableString
987
+ }
988
+ },
989
+ _errors: {
990
+ type: "array",
991
+ items: {
992
+ type: "object",
993
+ properties: {
994
+ status: {
995
+ type: "integer"
996
+ },
997
+ code: {
998
+ type: "string"
999
+ },
1000
+ title: {
1001
+ type: "string"
1002
+ },
1003
+ detail: {
1004
+ type: "string"
1005
+ }
1006
+ },
1007
+ required: [
1008
+ "status",
1009
+ "title"
1010
+ ]
1011
+ }
1012
+ },
1013
+ _errorResponse: {
1014
+ type: "object",
1015
+ properties: {
1016
+ errors: {
1017
+ $ref: "#/components/schemas/_errors"
1018
+ }
1019
+ },
1020
+ required: [
1021
+ "errors"
1022
+ ]
1023
+ },
1024
+ _resourceIdentifier: {
1025
+ type: "object",
1026
+ properties: {
1027
+ type: {
1028
+ type: "string"
1029
+ },
1030
+ id: {
1031
+ type: "string"
1032
+ }
1033
+ },
1034
+ required: [
1035
+ "type",
1036
+ "id"
1037
+ ]
1038
+ },
1039
+ _resource: {
1040
+ type: "object",
1041
+ properties: {
1042
+ type: {
1043
+ type: "string"
1044
+ },
1045
+ id: {
1046
+ type: "string"
1047
+ },
1048
+ attributes: {
1049
+ type: "object"
1050
+ },
1051
+ relationships: {
1052
+ type: "object"
1053
+ },
1054
+ links: {
1055
+ $ref: "#/components/schemas/_links"
1056
+ },
1057
+ meta: {
1058
+ $ref: "#/components/schemas/_meta"
1059
+ }
1060
+ },
1061
+ required: [
1062
+ "type",
1063
+ "id"
1064
+ ]
1065
+ },
1066
+ _relationLinks: {
1067
+ type: "object",
1068
+ properties: {
1069
+ self: {
1070
+ type: "string"
1071
+ },
1072
+ related: {
1073
+ type: "string"
1074
+ }
1075
+ }
1076
+ },
1077
+ _pagedRelationLinks: {
1078
+ type: "object",
1079
+ allOf: [
1080
+ {
1081
+ $ref: "#/components/schemas/_relationLinks"
1082
+ },
1083
+ {
1084
+ $ref: "#/components/schemas/_pagination"
1085
+ }
1086
+ ]
1087
+ },
1088
+ _toOneRelationship: {
1089
+ type: "object",
1090
+ properties: {
1091
+ data: {
1092
+ oneOf: [
1093
+ {
1094
+ $ref: "#/components/schemas/_resourceIdentifier"
1095
+ },
1096
+ {
1097
+ type: "null"
1098
+ }
1099
+ ]
1100
+ }
1101
+ }
1102
+ },
1103
+ _toManyRelationship: {
1104
+ type: "object",
1105
+ properties: {
1106
+ data: {
1107
+ type: "array",
1108
+ items: {
1109
+ $ref: "#/components/schemas/_resourceIdentifier"
1110
+ }
1111
+ }
1112
+ }
1113
+ },
1114
+ _toOneRelationshipWithLinks: {
1115
+ type: "object",
1116
+ allOf: [
1117
+ {
1118
+ $ref: "#/components/schemas/_toOneRelationship"
1119
+ },
1120
+ {
1121
+ properties: {
1122
+ links: {
1123
+ $ref: "#/components/schemas/_relationLinks"
1124
+ }
1125
+ }
1126
+ }
1127
+ ]
1128
+ },
1129
+ _toManyRelationshipWithLinks: {
1130
+ type: "object",
1131
+ allOf: [
1132
+ {
1133
+ $ref: "#/components/schemas/_toManyRelationship"
1134
+ },
1135
+ {
1136
+ properties: {
1137
+ links: {
1138
+ $ref: "#/components/schemas/_pagedRelationLinks"
1139
+ }
1140
+ }
1141
+ }
1142
+ ]
1143
+ },
1144
+ _toManyRelationshipRequest: {
1145
+ type: "object",
1146
+ properties: {
1147
+ data: {
1148
+ type: "array",
1149
+ items: {
1150
+ $ref: "#/components/schemas/_resourceIdentifier"
1151
+ }
1152
+ }
1153
+ },
1154
+ required: [
1155
+ "data"
1156
+ ]
1157
+ },
1158
+ _toOneRelationshipRequest: {
1159
+ type: "object",
1160
+ properties: {
1161
+ data: {
1162
+ oneOf: [
1163
+ {
1164
+ $ref: "#/components/schemas/_resourceIdentifier"
1165
+ },
1166
+ {
1167
+ type: "null"
1168
+ }
1169
+ ]
1170
+ }
1171
+ },
1172
+ required: [
1173
+ "data"
1174
+ ]
1175
+ },
1176
+ _toManyRelationshipResponse: {
1177
+ type: "object",
1178
+ properties: {
1179
+ data: {
1180
+ type: "array",
1181
+ items: {
1182
+ $ref: "#/components/schemas/_resourceIdentifier"
1183
+ }
1184
+ },
1185
+ links: {
1186
+ $ref: "#/components/schemas/_pagedRelationLinks"
1187
+ },
1188
+ meta: {
1189
+ $ref: "#/components/schemas/_meta"
1190
+ }
1191
+ }
1192
+ },
1193
+ _toOneRelationshipResponse: {
1194
+ type: "object",
1195
+ properties: {
1196
+ data: {
1197
+ oneOf: [
1198
+ {
1199
+ $ref: "#/components/schemas/_resourceIdentifier"
1200
+ },
1201
+ {
1202
+ type: "null"
1203
+ }
1204
+ ]
1205
+ },
1206
+ links: {
1207
+ $ref: "#/components/schemas/_relationLinks"
1208
+ },
1209
+ meta: {
1210
+ $ref: "#/components/schemas/_meta"
1211
+ }
1212
+ }
1213
+ }
1214
+ };
1215
+ }
1216
+ buildEnumSchema(enumDef) {
1217
+ return {
1218
+ type: "string",
1219
+ enum: Object.values(enumDef.values)
1220
+ };
1221
+ }
1222
+ buildTypeDefSchema(typeDef) {
1223
+ const properties = {};
1224
+ const required = [];
1225
+ for (const [fieldName, fieldDef] of Object.entries(typeDef.fields)) {
1226
+ properties[fieldName] = this.fieldToSchema(fieldDef);
1227
+ if (!fieldDef.optional && !fieldDef.array) {
1228
+ required.push(fieldName);
1229
+ }
1230
+ }
1231
+ const result = {
1232
+ type: "object",
1233
+ properties
1234
+ };
1235
+ if (required.length > 0) {
1236
+ result.required = required;
1237
+ }
1238
+ return result;
1239
+ }
1240
+ buildModelReadSchema(modelName, modelDef) {
1241
+ const attrProperties = {};
1242
+ const attrRequired = [];
1243
+ const relProperties = {};
1244
+ for (const [fieldName, fieldDef] of Object.entries(modelDef.fields)) {
1245
+ if (fieldDef.omit) continue;
1246
+ if (isFieldOmitted(modelName, fieldName, this.queryOptions)) continue;
1247
+ if (fieldDef.relation && !isModelIncluded(fieldDef.type, this.queryOptions)) continue;
1248
+ if (fieldDef.relation) {
1249
+ const relRef = fieldDef.array ? {
1250
+ $ref: "#/components/schemas/_toManyRelationshipWithLinks"
1251
+ } : {
1252
+ $ref: "#/components/schemas/_toOneRelationshipWithLinks"
1253
+ };
1254
+ relProperties[fieldName] = fieldDef.optional ? {
1255
+ oneOf: [
1256
+ {
1257
+ type: "null"
1258
+ },
1259
+ relRef
1260
+ ]
1261
+ } : relRef;
1262
+ } else {
1263
+ const schema = this.fieldToSchema(fieldDef);
1264
+ const fieldDescription = getMetaDescription(fieldDef.attributes);
1265
+ if (fieldDescription && !("$ref" in schema)) {
1266
+ schema.description = fieldDescription;
1267
+ }
1268
+ attrProperties[fieldName] = schema;
1269
+ if (!fieldDef.optional && !fieldDef.array) {
1270
+ attrRequired.push(fieldName);
1271
+ }
1272
+ }
1273
+ }
1274
+ const properties = {};
1275
+ if (Object.keys(attrProperties).length > 0) {
1276
+ const attrSchema = {
1277
+ type: "object",
1278
+ properties: attrProperties
1279
+ };
1280
+ if (attrRequired.length > 0) attrSchema.required = attrRequired;
1281
+ properties["attributes"] = attrSchema;
1282
+ }
1283
+ if (Object.keys(relProperties).length > 0) {
1284
+ properties["relationships"] = {
1285
+ type: "object",
1286
+ properties: relProperties
1287
+ };
1288
+ }
1289
+ const result = {
1290
+ type: "object",
1291
+ properties
1292
+ };
1293
+ const description = getMetaDescription(modelDef.attributes);
1294
+ if (description) {
1295
+ result.description = description;
1296
+ }
1297
+ return result;
1298
+ }
1299
+ buildCreateRequestSchema(_modelName, modelDef) {
1300
+ const idFieldNames = new Set(modelDef.idFields);
1301
+ const attributes = {};
1302
+ const attrRequired = [];
1303
+ const relationships = {};
1304
+ for (const [fieldName, fieldDef] of Object.entries(modelDef.fields)) {
1305
+ if (fieldDef.updatedAt) continue;
1306
+ if (fieldDef.foreignKeyFor) continue;
1307
+ if (idFieldNames.has(fieldName) && fieldDef.default !== void 0) continue;
1308
+ if (fieldDef.relation && !isModelIncluded(fieldDef.type, this.queryOptions)) continue;
1309
+ if (fieldDef.relation) {
1310
+ relationships[fieldName] = fieldDef.array ? {
1311
+ type: "object",
1312
+ properties: {
1313
+ data: {
1314
+ type: "array",
1315
+ items: {
1316
+ $ref: "#/components/schemas/_resourceIdentifier"
1317
+ }
1318
+ }
1319
+ }
1320
+ } : {
1321
+ type: "object",
1322
+ properties: {
1323
+ data: {
1324
+ $ref: "#/components/schemas/_resourceIdentifier"
1325
+ }
1326
+ }
1327
+ };
1328
+ } else {
1329
+ attributes[fieldName] = this.fieldToSchema(fieldDef);
1330
+ if (!fieldDef.optional && fieldDef.default === void 0 && !fieldDef.array) {
1331
+ attrRequired.push(fieldName);
1332
+ }
1333
+ }
1334
+ }
1335
+ const dataProperties = {
1336
+ type: {
1337
+ type: "string"
1338
+ }
1339
+ };
1340
+ if (Object.keys(attributes).length > 0) {
1341
+ const attrSchema = {
1342
+ type: "object",
1343
+ properties: attributes
1344
+ };
1345
+ if (attrRequired.length > 0) attrSchema.required = attrRequired;
1346
+ dataProperties["attributes"] = attrSchema;
1347
+ }
1348
+ if (Object.keys(relationships).length > 0) {
1349
+ dataProperties["relationships"] = {
1350
+ type: "object",
1351
+ properties: relationships
1352
+ };
1353
+ }
1354
+ return {
1355
+ type: "object",
1356
+ properties: {
1357
+ data: {
1358
+ type: "object",
1359
+ properties: dataProperties,
1360
+ required: [
1361
+ "type"
1362
+ ]
1363
+ }
1364
+ },
1365
+ required: [
1366
+ "data"
1367
+ ]
1368
+ };
1369
+ }
1370
+ buildUpdateRequestSchema(modelDef) {
1371
+ const attributes = {};
1372
+ const relationships = {};
1373
+ for (const [fieldName, fieldDef] of Object.entries(modelDef.fields)) {
1374
+ if (fieldDef.updatedAt) continue;
1375
+ if (fieldDef.foreignKeyFor) continue;
1376
+ if (fieldDef.relation && !isModelIncluded(fieldDef.type, this.queryOptions)) continue;
1377
+ if (fieldDef.relation) {
1378
+ relationships[fieldName] = fieldDef.array ? {
1379
+ type: "object",
1380
+ properties: {
1381
+ data: {
1382
+ type: "array",
1383
+ items: {
1384
+ $ref: "#/components/schemas/_resourceIdentifier"
1385
+ }
1386
+ }
1387
+ }
1388
+ } : {
1389
+ type: "object",
1390
+ properties: {
1391
+ data: {
1392
+ $ref: "#/components/schemas/_resourceIdentifier"
1393
+ }
1394
+ }
1395
+ };
1396
+ } else {
1397
+ attributes[fieldName] = this.fieldToSchema(fieldDef);
1398
+ }
1399
+ }
1400
+ const dataProperties = {
1401
+ type: {
1402
+ type: "string"
1403
+ },
1404
+ id: {
1405
+ type: "string"
1406
+ }
1407
+ };
1408
+ if (Object.keys(attributes).length > 0) {
1409
+ dataProperties["attributes"] = {
1410
+ type: "object",
1411
+ properties: attributes
1412
+ };
1413
+ }
1414
+ if (Object.keys(relationships).length > 0) {
1415
+ dataProperties["relationships"] = {
1416
+ type: "object",
1417
+ properties: relationships
1418
+ };
1419
+ }
1420
+ return {
1421
+ type: "object",
1422
+ properties: {
1423
+ data: {
1424
+ type: "object",
1425
+ properties: dataProperties,
1426
+ required: [
1427
+ "type",
1428
+ "id"
1429
+ ]
1430
+ }
1431
+ },
1432
+ required: [
1433
+ "data"
1434
+ ]
1435
+ };
1436
+ }
1437
+ buildModelResponseSchema(modelName) {
1438
+ return {
1439
+ type: "object",
1440
+ properties: {
1441
+ jsonapi: {
1442
+ $ref: "#/components/schemas/_jsonapi"
1443
+ },
1444
+ data: {
1445
+ allOf: [
1446
+ {
1447
+ $ref: `#/components/schemas/${modelName}`
1448
+ },
1449
+ {
1450
+ $ref: "#/components/schemas/_resource"
1451
+ }
1452
+ ]
1453
+ },
1454
+ meta: {
1455
+ $ref: "#/components/schemas/_meta"
1456
+ }
1457
+ }
1458
+ };
1459
+ }
1460
+ buildModelListResponseSchema(modelName) {
1461
+ return {
1462
+ type: "object",
1463
+ properties: {
1464
+ jsonapi: {
1465
+ $ref: "#/components/schemas/_jsonapi"
1466
+ },
1467
+ data: {
1468
+ type: "array",
1469
+ items: {
1470
+ allOf: [
1471
+ {
1472
+ $ref: `#/components/schemas/${modelName}`
1473
+ },
1474
+ {
1475
+ $ref: "#/components/schemas/_resource"
1476
+ }
1477
+ ]
1478
+ }
1479
+ },
1480
+ links: {
1481
+ allOf: [
1482
+ {
1483
+ $ref: "#/components/schemas/_pagination"
1484
+ },
1485
+ {
1486
+ $ref: "#/components/schemas/_links"
1487
+ }
1488
+ ]
1489
+ },
1490
+ meta: {
1491
+ $ref: "#/components/schemas/_meta"
1492
+ }
1493
+ }
1494
+ };
1495
+ }
1496
+ generateSharedParams() {
1497
+ return {
1498
+ id: {
1499
+ name: "id",
1500
+ in: "path",
1501
+ required: true,
1502
+ schema: {
1503
+ type: "string"
1504
+ },
1505
+ description: "Resource ID"
1506
+ },
1507
+ include: {
1508
+ name: "include",
1509
+ in: "query",
1510
+ schema: {
1511
+ type: "string"
1512
+ },
1513
+ description: "Comma-separated list of relationships to include"
1514
+ },
1515
+ sort: {
1516
+ name: "sort",
1517
+ in: "query",
1518
+ schema: {
1519
+ type: "string"
1520
+ },
1521
+ description: "Comma-separated list of fields to sort by. Prefix with - for descending"
1522
+ },
1523
+ pageOffset: {
1524
+ name: "page[offset]",
1525
+ in: "query",
1526
+ schema: {
1527
+ type: "integer",
1528
+ minimum: 0
1529
+ },
1530
+ description: "Page offset"
1531
+ },
1532
+ pageLimit: {
1533
+ name: "page[limit]",
1534
+ in: "query",
1535
+ schema: {
1536
+ type: "integer",
1537
+ minimum: 1
1538
+ },
1539
+ description: "Page limit"
1540
+ }
1541
+ };
1542
+ }
1543
+ fieldToSchema(fieldDef) {
1544
+ const baseSchema = this.typeToSchema(fieldDef.type);
1545
+ if (fieldDef.array) {
1546
+ return {
1547
+ type: "array",
1548
+ items: baseSchema
1549
+ };
1550
+ }
1551
+ if (fieldDef.optional) {
1552
+ return {
1553
+ oneOf: [
1554
+ baseSchema,
1555
+ {
1556
+ type: "null"
1557
+ }
1558
+ ]
1559
+ };
1560
+ }
1561
+ return baseSchema;
1562
+ }
1563
+ typeToSchema(type) {
1564
+ switch (type) {
1565
+ case "String":
1566
+ return {
1567
+ type: "string"
1568
+ };
1569
+ case "Int":
1570
+ case "BigInt":
1571
+ return {
1572
+ type: "integer"
1573
+ };
1574
+ case "Float":
1575
+ return {
1576
+ type: "number"
1577
+ };
1578
+ case "Decimal":
1579
+ return {
1580
+ oneOf: [
1581
+ {
1582
+ type: "number"
1583
+ },
1584
+ {
1585
+ type: "string"
1586
+ }
1587
+ ]
1588
+ };
1589
+ case "Boolean":
1590
+ return {
1591
+ type: "boolean"
1592
+ };
1593
+ case "DateTime":
1594
+ return {
1595
+ type: "string",
1596
+ format: "date-time"
1597
+ };
1598
+ case "Bytes":
1599
+ return {
1600
+ type: "string",
1601
+ format: "byte"
1602
+ };
1603
+ case "Json":
1604
+ case "Unsupported":
1605
+ return {};
1606
+ default:
1607
+ return {
1608
+ $ref: `#/components/schemas/${type}`
1609
+ };
1610
+ }
1611
+ }
1612
+ getIdFields(modelDef) {
1613
+ return modelDef.idFields.map((name) => modelDef.fields[name]).filter((f) => f !== void 0);
1614
+ }
1615
+ /**
1616
+ * Checks if an operation on a model may be denied by access policies.
1617
+ * Returns true when `respectAccessPolicies` is enabled and the model's
1618
+ * policies for the given operation are NOT a constant allow (i.e., not
1619
+ * simply `@@allow('...', true)` with no `@@deny` rules).
1620
+ */
1621
+ mayDenyAccess(modelDef, operation) {
1622
+ if (!this.specOptions?.respectAccessPolicies) return false;
1623
+ const policyAttrs = (modelDef.attributes ?? []).filter((attr) => attr.name === "@@allow" || attr.name === "@@deny");
1624
+ if (policyAttrs.length === 0) return true;
1625
+ const getArgByName = /* @__PURE__ */ __name((args, name) => args?.find((a) => a.name === name)?.value, "getArgByName");
1626
+ const matchesOperation = /* @__PURE__ */ __name((args) => {
1627
+ const val = getArgByName(args, "operation");
1628
+ if (!val || val.kind !== "literal" || typeof val.value !== "string") return false;
1629
+ const ops = val.value.split(",").map((s) => s.trim());
1630
+ return ops.includes(operation) || ops.includes("all");
1631
+ }, "matchesOperation");
1632
+ const hasEffectiveDeny = policyAttrs.some((attr) => {
1633
+ if (attr.name !== "@@deny" || !matchesOperation(attr.args)) return false;
1634
+ const condition = getArgByName(attr.args, "condition");
1635
+ return !(condition?.kind === "literal" && condition.value === false);
1636
+ });
1637
+ if (hasEffectiveDeny) return true;
1638
+ const relevantAllow = policyAttrs.filter((attr) => attr.name === "@@allow" && matchesOperation(attr.args));
1639
+ const hasConstantAllow = relevantAllow.some((attr) => {
1640
+ const condition = getArgByName(attr.args, "condition");
1641
+ return condition?.kind === "literal" && condition.value === true;
1642
+ });
1643
+ return !hasConstantAllow;
1644
+ }
1645
+ };
1646
+
232
1647
  // src/api/rest/index.ts
233
1648
  var InvalidValueError = class InvalidValueError2 extends Error {
234
1649
  static {
@@ -411,7 +1826,7 @@ var RestApiHandler = class {
411
1826
  const segmentCharset = options.urlSegmentCharset ?? "a-zA-Z0-9-_~ %";
412
1827
  this.modelNameMapping = options.modelNameMapping ?? {};
413
1828
  this.modelNameMapping = Object.fromEntries(Object.entries(this.modelNameMapping).map(([k, v]) => [
414
- (0, import_common_helpers.lowerCaseFirst)(k),
1829
+ (0, import_common_helpers3.lowerCaseFirst)(k),
415
1830
  v
416
1831
  ]));
417
1832
  this.reverseModelNameMapping = Object.fromEntries(Object.entries(this.modelNameMapping).map(([k, v]) => [
@@ -420,7 +1835,7 @@ var RestApiHandler = class {
420
1835
  ]));
421
1836
  this.externalIdMapping = options.externalIdMapping ?? {};
422
1837
  this.externalIdMapping = Object.fromEntries(Object.entries(this.externalIdMapping).map(([k, v]) => [
423
- (0, import_common_helpers.lowerCaseFirst)(k),
1838
+ (0, import_common_helpers3.lowerCaseFirst)(k),
424
1839
  v
425
1840
  ]));
426
1841
  this.urlPatternMap = this.buildUrlPatternMap(segmentCharset);
@@ -439,7 +1854,8 @@ var RestApiHandler = class {
439
1854
  idDivider: import_zod2.default.string().min(1).optional(),
440
1855
  urlSegmentCharset: import_zod2.default.string().min(1).optional(),
441
1856
  modelNameMapping: import_zod2.default.record(import_zod2.default.string(), import_zod2.default.string()).optional(),
442
- externalIdMapping: import_zod2.default.record(import_zod2.default.string(), import_zod2.default.string()).optional()
1857
+ externalIdMapping: import_zod2.default.record(import_zod2.default.string(), import_zod2.default.string()).optional(),
1858
+ queryOptions: queryOptionsSchema.optional()
443
1859
  });
444
1860
  const parseResult = schema.safeParse(options);
445
1861
  if (!parseResult.success) {
@@ -597,8 +2013,9 @@ var RestApiHandler = class {
597
2013
  }
598
2014
  }
599
2015
  handleGenericError(err) {
600
- return this.makeError("unknownError", err instanceof Error ? `${err.message}
601
- ${err.stack}` : "Unknown error");
2016
+ const resp = this.makeError("unknownError", err instanceof Error ? `${err.message}` : "Unknown error");
2017
+ log(this.options.log, "debug", () => `sending error response: ${(0, import_common_helpers3.safeJSONStringify)(resp)}${err instanceof Error ? "\n" + err.stack : ""}`);
2018
+ return resp;
602
2019
  }
603
2020
  async processProcedureRequest({ client, method, proc, query, requestBody }) {
604
2021
  if (!proc) {
@@ -656,13 +2073,13 @@ ${err.stack}` : "Unknown error");
656
2073
  }
657
2074
  makeProcBadInputErrorResponse(message) {
658
2075
  const resp = this.makeError("invalidPayload", message, 400);
659
- log(this.log, "debug", () => `sending error response: ${JSON.stringify(resp)}`);
2076
+ log(this.log, "debug", () => `sending error response: ${(0, import_common_helpers3.safeJSONStringify)(resp)}`);
660
2077
  return resp;
661
2078
  }
662
2079
  makeProcGenericErrorResponse(err) {
663
2080
  const message = err instanceof Error ? err.message : "unknown error";
664
2081
  const resp = this.makeError("unknownError", message, 500);
665
- log(this.log, "debug", () => `sending error response: ${JSON.stringify(resp)}`);
2082
+ log(this.log, "debug", () => `sending error response: ${(0, import_common_helpers3.safeJSONStringify)(resp)}${err instanceof Error ? "\n" + err.stack : ""}`);
666
2083
  return resp;
667
2084
  }
668
2085
  async processSingleRead(client, type, resourceId, query) {
@@ -735,7 +2152,7 @@ ${err.stack}` : "Unknown error");
735
2152
  select = relationSelect;
736
2153
  }
737
2154
  if (!select) {
738
- const { select: partialFields, error } = this.buildPartialSelect((0, import_common_helpers.lowerCaseFirst)(relationInfo.type), query);
2155
+ const { select: partialFields, error } = this.buildPartialSelect((0, import_common_helpers3.lowerCaseFirst)(relationInfo.type), query);
739
2156
  if (error) return error;
740
2157
  select = partialFields ? {
741
2158
  [relationship]: {
@@ -1030,7 +2447,7 @@ ${err.stack}` : "Unknown error");
1030
2447
  }
1031
2448
  if (relationInfo.isCollection) {
1032
2449
  createPayload.data[key] = {
1033
- connect: (0, import_common_helpers.enumerate)(data.data).map((item) => this.makeIdConnect(relationInfo.idFields, item.id))
2450
+ connect: (0, import_common_helpers3.enumerate)(data.data).map((item) => this.makeIdConnect(relationInfo.idFields, item.id))
1034
2451
  };
1035
2452
  } else {
1036
2453
  if (typeof data.data !== "object") {
@@ -1096,10 +2513,10 @@ ${err.stack}` : "Unknown error");
1096
2513
  }
1097
2514
  if (relationInfo.isCollection) {
1098
2515
  upsertPayload.create[key] = {
1099
- connect: (0, import_common_helpers.enumerate)(data.data).map((item) => this.makeIdConnect(relationInfo.idFields, item.id))
2516
+ connect: (0, import_common_helpers3.enumerate)(data.data).map((item) => this.makeIdConnect(relationInfo.idFields, item.id))
1100
2517
  };
1101
2518
  upsertPayload.update[key] = {
1102
- set: (0, import_common_helpers.enumerate)(data.data).map((item) => this.makeIdConnect(relationInfo.idFields, item.id))
2519
+ set: (0, import_common_helpers3.enumerate)(data.data).map((item) => this.makeIdConnect(relationInfo.idFields, item.id))
1103
2520
  };
1104
2521
  } else {
1105
2522
  if (typeof data.data !== "object") {
@@ -1180,7 +2597,7 @@ ${err.stack}` : "Unknown error");
1180
2597
  const relationVerb = mode === "create" ? "connect" : mode === "delete" ? "disconnect" : "set";
1181
2598
  updateArgs.data = {
1182
2599
  [relationship]: {
1183
- [relationVerb]: (0, import_common_helpers.enumerate)(parsed.data.data).map((item) => this.makeIdFilter(relationInfo.idFields, item.id))
2600
+ [relationVerb]: (0, import_common_helpers3.enumerate)(parsed.data.data).map((item) => this.makeIdFilter(relationInfo.idFields, item.id))
1184
2601
  }
1185
2602
  };
1186
2603
  }
@@ -1223,7 +2640,7 @@ ${err.stack}` : "Unknown error");
1223
2640
  }
1224
2641
  if (relationInfo.isCollection) {
1225
2642
  updatePayload.data[key] = {
1226
- set: (0, import_common_helpers.enumerate)(data.data).map((item) => ({
2643
+ set: (0, import_common_helpers3.enumerate)(data.data).map((item) => ({
1227
2644
  [this.makeDefaultIdKey(relationInfo.idFields)]: item.id
1228
2645
  }))
1229
2646
  };
@@ -1279,7 +2696,7 @@ ${err.stack}` : "Unknown error");
1279
2696
  }
1280
2697
  getIdFields(model) {
1281
2698
  const modelDef = this.requireModel(model);
1282
- const modelLower = (0, import_common_helpers.lowerCaseFirst)(model);
2699
+ const modelLower = (0, import_common_helpers3.lowerCaseFirst)(model);
1283
2700
  if (!(modelLower in this.externalIdMapping)) {
1284
2701
  return Object.values(modelDef.fields).filter((f) => modelDef.idFields.includes(f.name));
1285
2702
  }
@@ -1313,7 +2730,7 @@ ${err.stack}` : "Unknown error");
1313
2730
  log(this.options.log, "warn", `Not including model ${model} in the API because it has no ID field`);
1314
2731
  continue;
1315
2732
  }
1316
- const modelInfo = this.typeMap[(0, import_common_helpers.lowerCaseFirst)(model)] = {
2733
+ const modelInfo = this.typeMap[(0, import_common_helpers3.lowerCaseFirst)(model)] = {
1317
2734
  name: model,
1318
2735
  idFields,
1319
2736
  relationships: {},
@@ -1338,7 +2755,7 @@ ${err.stack}` : "Unknown error");
1338
2755
  }
1339
2756
  }
1340
2757
  getModelInfo(model) {
1341
- return this.typeMap[(0, import_common_helpers.lowerCaseFirst)(model)];
2758
+ return this.typeMap[(0, import_common_helpers3.lowerCaseFirst)(model)];
1342
2759
  }
1343
2760
  makeLinkUrl(path) {
1344
2761
  return `${this.options.endpoint}${path}`;
@@ -1347,7 +2764,7 @@ ${err.stack}` : "Unknown error");
1347
2764
  const linkers = {};
1348
2765
  for (const model of Object.keys(this.schema.models)) {
1349
2766
  const ids = this.getIdFields(model);
1350
- const modelLower = (0, import_common_helpers.lowerCaseFirst)(model);
2767
+ const modelLower = (0, import_common_helpers3.lowerCaseFirst)(model);
1351
2768
  const mappedModel = this.mapModelName(modelLower);
1352
2769
  if (ids.length < 1) {
1353
2770
  continue;
@@ -1376,7 +2793,7 @@ ${err.stack}` : "Unknown error");
1376
2793
  this.serializers.set(modelLower, serializer);
1377
2794
  }
1378
2795
  for (const model of Object.keys(this.schema.models)) {
1379
- const modelLower = (0, import_common_helpers.lowerCaseFirst)(model);
2796
+ const modelLower = (0, import_common_helpers3.lowerCaseFirst)(model);
1380
2797
  const serializer = this.serializers.get(modelLower);
1381
2798
  if (!serializer) {
1382
2799
  continue;
@@ -1387,7 +2804,7 @@ ${err.stack}` : "Unknown error");
1387
2804
  if (!fieldDef.relation) {
1388
2805
  continue;
1389
2806
  }
1390
- const fieldSerializer = this.serializers.get((0, import_common_helpers.lowerCaseFirst)(fieldDef.type));
2807
+ const fieldSerializer = this.serializers.get((0, import_common_helpers3.lowerCaseFirst)(fieldDef.type));
1391
2808
  if (!fieldSerializer) {
1392
2809
  continue;
1393
2810
  }
@@ -1421,12 +2838,12 @@ ${err.stack}` : "Unknown error");
1421
2838
  }
1422
2839
  }
1423
2840
  async serializeItems(model, items, options) {
1424
- model = (0, import_common_helpers.lowerCaseFirst)(model);
2841
+ model = (0, import_common_helpers3.lowerCaseFirst)(model);
1425
2842
  const serializer = this.serializers.get(model);
1426
2843
  if (!serializer) {
1427
2844
  throw new Error(`serializer not found for model ${model}`);
1428
2845
  }
1429
- const itemsWithId = (0, import_common_helpers.clone)(items);
2846
+ const itemsWithId = (0, import_common_helpers3.clone)(items);
1430
2847
  this.injectCompoundId(model, itemsWithId);
1431
2848
  const serialized = await serializer.serialize(itemsWithId, options);
1432
2849
  const plainResult = this.toPlainObject(serialized);
@@ -1445,7 +2862,7 @@ ${err.stack}` : "Unknown error");
1445
2862
  if (!typeInfo) {
1446
2863
  return;
1447
2864
  }
1448
- (0, import_common_helpers.enumerate)(items).forEach((item) => {
2865
+ (0, import_common_helpers3.enumerate)(items).forEach((item) => {
1449
2866
  if (!item) {
1450
2867
  return;
1451
2868
  }
@@ -1620,7 +3037,7 @@ ${err.stack}` : "Unknown error");
1620
3037
  const url = new URL(this.makeLinkUrl(path));
1621
3038
  for (const [key, value] of Object.entries(query ?? {})) {
1622
3039
  if (key.startsWith("filter[") || key.startsWith("sort[") || key === "include" || key.startsWith("include[") || key.startsWith("fields[")) {
1623
- for (const v of (0, import_common_helpers.enumerate)(value)) {
3040
+ for (const v of (0, import_common_helpers3.enumerate)(value)) {
1624
3041
  url.searchParams.append(key, v);
1625
3042
  }
1626
3043
  }
@@ -1692,7 +3109,7 @@ ${err.stack}` : "Unknown error");
1692
3109
  const item = {};
1693
3110
  let curr = item;
1694
3111
  let currType = typeInfo;
1695
- for (const filterValue of (0, import_common_helpers.enumerate)(value)) {
3112
+ for (const filterValue of (0, import_common_helpers3.enumerate)(value)) {
1696
3113
  for (let i = 0; i < filterKeys.length; i++) {
1697
3114
  let filterKey = filterKeys[i];
1698
3115
  let filterOp;
@@ -1771,7 +3188,7 @@ ${err.stack}` : "Unknown error");
1771
3188
  };
1772
3189
  }
1773
3190
  const result = [];
1774
- for (const sortSpec of (0, import_common_helpers.enumerate)(query["sort"])) {
3191
+ for (const sortSpec of (0, import_common_helpers3.enumerate)(query["sort"])) {
1775
3192
  const sortFields = sortSpec.split(",").filter((i) => i);
1776
3193
  for (const sortField of sortFields) {
1777
3194
  const dir = sortField.startsWith("-") ? "desc" : "asc";
@@ -1840,7 +3257,7 @@ ${err.stack}` : "Unknown error");
1840
3257
  }
1841
3258
  const result = {};
1842
3259
  const allIncludes = [];
1843
- for (const includeItem of (0, import_common_helpers.enumerate)(include)) {
3260
+ for (const includeItem of (0, import_common_helpers3.enumerate)(include)) {
1844
3261
  const inclusions = includeItem.split(",").filter((i) => i);
1845
3262
  for (const inclusion of inclusions) {
1846
3263
  allIncludes.push(inclusion);
@@ -1863,7 +3280,7 @@ ${err.stack}` : "Unknown error");
1863
3280
  error: this.makeUnsupportedModelError(relationInfo.type)
1864
3281
  };
1865
3282
  }
1866
- const { select, error } = this.buildPartialSelect((0, import_common_helpers.lowerCaseFirst)(relationInfo.type), query);
3283
+ const { select, error } = this.buildPartialSelect((0, import_common_helpers3.lowerCaseFirst)(relationInfo.type), query);
1867
3284
  if (error) return {
1868
3285
  select: void 0,
1869
3286
  error
@@ -2041,7 +3458,7 @@ ${err.stack}` : "Unknown error");
2041
3458
  status = status ?? this.errors[code]?.status ?? 500;
2042
3459
  const error = {
2043
3460
  status,
2044
- code: (0, import_common_helpers.paramCase)(code),
3461
+ code: (0, import_common_helpers3.paramCase)(code),
2045
3462
  title: this.errors[code]?.title
2046
3463
  };
2047
3464
  if (detail) {
@@ -2063,10 +3480,15 @@ ${err.stack}` : "Unknown error");
2063
3480
  makeUnsupportedRelationshipError(model, relationship, status) {
2064
3481
  return this.makeError("unsupportedRelationship", `Relationship ${model}.${relationship} doesn't exist`, status);
2065
3482
  }
3483
+ //#endregion
3484
+ async generateSpec(options) {
3485
+ const generator = new RestApiSpecGenerator(this.options);
3486
+ return generator.generateSpec(options);
3487
+ }
2066
3488
  };
2067
3489
 
2068
3490
  // src/api/rpc/index.ts
2069
- var import_common_helpers2 = require("@zenstackhq/common-helpers");
3491
+ var import_common_helpers4 = require("@zenstackhq/common-helpers");
2070
3492
  var import_orm3 = require("@zenstackhq/orm");
2071
3493
  var import_superjson4 = __toESM(require("superjson"), 1);
2072
3494
  var import_ts_pattern3 = require("ts-pattern");
@@ -2087,7 +3509,8 @@ var RPCApiHandler = class {
2087
3509
  validateOptions(options) {
2088
3510
  const schema = import_zod3.default.strictObject({
2089
3511
  schema: import_zod3.default.object(),
2090
- log: loggerSchema.optional()
3512
+ log: loggerSchema.optional(),
3513
+ queryOptions: queryOptionsSchema.optional()
2091
3514
  });
2092
3515
  const parseResult = schema.safeParse(options);
2093
3516
  if (!parseResult.success) {
@@ -2124,7 +3547,7 @@ var RPCApiHandler = class {
2124
3547
  requestBody
2125
3548
  });
2126
3549
  }
2127
- model = (0, import_common_helpers2.lowerCaseFirst)(model);
3550
+ model = (0, import_common_helpers4.lowerCaseFirst)(model);
2128
3551
  method = method.toUpperCase();
2129
3552
  let args;
2130
3553
  let resCode = 200;
@@ -2191,7 +3614,7 @@ var RPCApiHandler = class {
2191
3614
  if (!this.isValidModel(client, model)) {
2192
3615
  return this.makeBadInputErrorResponse(`unknown model name: ${model}`);
2193
3616
  }
2194
- log(this.options.log, "debug", () => `handling "${model}.${op}" request with args: ${(0, import_common_helpers2.safeJSONStringify)(processedArgs)}`);
3617
+ log(this.options.log, "debug", () => `handling "${model}.${op}" request with args: ${(0, import_common_helpers4.safeJSONStringify)(processedArgs)}`);
2195
3618
  const clientResult = await client[model][op](processedArgs);
2196
3619
  let responseBody = {
2197
3620
  data: clientResult
@@ -2211,7 +3634,7 @@ var RPCApiHandler = class {
2211
3634
  status: resCode,
2212
3635
  body: responseBody
2213
3636
  };
2214
- log(this.options.log, "debug", () => `sending response for "${model}.${op}" request: ${(0, import_common_helpers2.safeJSONStringify)(response)}`);
3637
+ log(this.options.log, "debug", () => `sending response for "${model}.${op}" request: ${(0, import_common_helpers4.safeJSONStringify)(response)}`);
2215
3638
  return response;
2216
3639
  } catch (err) {
2217
3640
  log(this.options.log, "error", `error occurred when handling "${model}.${op}" request`, err);
@@ -2248,7 +3671,7 @@ var RPCApiHandler = class {
2248
3671
  if (!VALID_OPS.has(itemOp)) {
2249
3672
  return this.makeBadInputErrorResponse(`operation at index ${i} has invalid op: ${itemOp}`);
2250
3673
  }
2251
- if (!this.isValidModel(client, (0, import_common_helpers2.lowerCaseFirst)(itemModel))) {
3674
+ if (!this.isValidModel(client, (0, import_common_helpers4.lowerCaseFirst)(itemModel))) {
2252
3675
  return this.makeBadInputErrorResponse(`operation at index ${i} has unknown model: ${itemModel}`);
2253
3676
  }
2254
3677
  if (itemArgs !== void 0 && itemArgs !== null && (typeof itemArgs !== "object" || Array.isArray(itemArgs))) {
@@ -2259,7 +3682,7 @@ var RPCApiHandler = class {
2259
3682
  return this.makeBadInputErrorResponse(`operation at index ${i}: ${argsError}`);
2260
3683
  }
2261
3684
  processedOps.push({
2262
- model: (0, import_common_helpers2.lowerCaseFirst)(itemModel),
3685
+ model: (0, import_common_helpers4.lowerCaseFirst)(itemModel),
2263
3686
  op: itemOp,
2264
3687
  args: processedArgs
2265
3688
  });
@@ -2283,7 +3706,7 @@ var RPCApiHandler = class {
2283
3706
  status: 200,
2284
3707
  body: responseBody
2285
3708
  };
2286
- log(this.options.log, "debug", () => `sending response for "$transaction" request: ${(0, import_common_helpers2.safeJSONStringify)(response)}`);
3709
+ log(this.options.log, "debug", () => `sending response for "$transaction" request: ${(0, import_common_helpers4.safeJSONStringify)(response)}`);
2287
3710
  return response;
2288
3711
  } catch (err) {
2289
3712
  log(this.options.log, "error", `error occurred when handling "$transaction" request`, err);
@@ -2345,7 +3768,7 @@ var RPCApiHandler = class {
2345
3768
  status: 200,
2346
3769
  body: responseBody
2347
3770
  };
2348
- log(this.options.log, "debug", () => `sending response for "$procs.${proc}" request: ${(0, import_common_helpers2.safeJSONStringify)(response)}`);
3771
+ log(this.options.log, "debug", () => `sending response for "$procs.${proc}" request: ${(0, import_common_helpers4.safeJSONStringify)(response)}`);
2349
3772
  return response;
2350
3773
  } catch (err) {
2351
3774
  log(this.options.log, "error", `error occurred when handling "$procs.${proc}" request`, err);
@@ -2356,7 +3779,7 @@ var RPCApiHandler = class {
2356
3779
  }
2357
3780
  }
2358
3781
  isValidModel(client, model) {
2359
- return Object.keys(client.$schema.models).some((m) => (0, import_common_helpers2.lowerCaseFirst)(m) === (0, import_common_helpers2.lowerCaseFirst)(model));
3782
+ return Object.keys(client.$schema.models).some((m) => (0, import_common_helpers4.lowerCaseFirst)(m) === (0, import_common_helpers4.lowerCaseFirst)(model));
2360
3783
  }
2361
3784
  makeBadInputErrorResponse(message) {
2362
3785
  const resp = {
@@ -2367,7 +3790,7 @@ var RPCApiHandler = class {
2367
3790
  }
2368
3791
  }
2369
3792
  };
2370
- log(this.options.log, "debug", () => `sending error response: ${(0, import_common_helpers2.safeJSONStringify)(resp)}`);
3793
+ log(this.options.log, "debug", () => `sending error response: ${(0, import_common_helpers4.safeJSONStringify)(resp)}`);
2371
3794
  return resp;
2372
3795
  }
2373
3796
  makeGenericErrorResponse(err) {
@@ -2379,7 +3802,7 @@ var RPCApiHandler = class {
2379
3802
  }
2380
3803
  }
2381
3804
  };
2382
- log(this.options.log, "debug", () => `sending error response: ${(0, import_common_helpers2.safeJSONStringify)(resp)}${err instanceof Error ? "\n" + err.stack : ""}`);
3805
+ log(this.options.log, "debug", () => `sending error response: ${(0, import_common_helpers4.safeJSONStringify)(resp)}${err instanceof Error ? "\n" + err.stack : ""}`);
2383
3806
  return resp;
2384
3807
  }
2385
3808
  makeORMErrorResponse(err) {
@@ -2411,7 +3834,7 @@ var RPCApiHandler = class {
2411
3834
  error
2412
3835
  }
2413
3836
  };
2414
- log(this.options.log, "debug", () => `sending error response: ${(0, import_common_helpers2.safeJSONStringify)(resp)}`);
3837
+ log(this.options.log, "debug", () => `sending error response: ${(0, import_common_helpers4.safeJSONStringify)(resp)}`);
2415
3838
  return resp;
2416
3839
  }
2417
3840
  async processRequestPayload(args) {