@ghentcdh/crouton-api 0.0.1-alpha.39 → 0.0.1-alpha.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -34,14 +34,25 @@ __export(index_exports, {
34
34
  CroutonApiModule: () => CroutonApiModule,
35
35
  DataSourceRegistry: () => DataSourceRegistry,
36
36
  FileSystemResourceConfigLoader: () => FileSystemResourceConfigLoader,
37
+ ReadOpSchema: () => ReadOpSchema,
37
38
  ResourceConfigLoader: () => ResourceConfigLoader2,
38
39
  ResourceConfigRegistry: () => ResourceConfigRegistry,
40
+ ResourceHooksSchema: () => ResourceHooksSchema,
41
+ WriteOpSchema: () => WriteOpSchema,
42
+ buildFilterWhere: () => buildFilterWhere,
39
43
  buildViews: () => buildViews,
44
+ decorateRow: () => decorateRow,
45
+ decorateRows: () => decorateRows,
40
46
  isOperationEnabled: () => isOperationEnabled,
41
47
  isRowProcedureAction: () => isRowProcedureAction,
42
48
  isTableProcedureAction: () => isTableProcedureAction,
43
49
  loadDataSourcesFromDir: () => loadDataSourcesFromDir,
44
50
  loadResourceConfigsFromDir: () => loadResourceConfigsFromDir,
51
+ loadResourceHooks: () => loadResourceHooks,
52
+ loadSubResourceHooks: () => loadSubResourceHooks,
53
+ parseFilterString: () => parseFilterString,
54
+ postWrite: () => postWrite,
55
+ prepareWrite: () => prepareWrite,
45
56
  resolveDefinition: () => resolveDefinition,
46
57
  schemaFor: () => schemaFor,
47
58
  upsertOnFor: () => upsertOnFor
@@ -86,6 +97,7 @@ var RequestSchemaWithOffset = RequestSchema.transform((schema) => {
86
97
  offset: (page - 1) * pageSize
87
98
  };
88
99
  });
100
+ var offsetOf = /* @__PURE__ */ __name((params) => params.offset ?? (params.page - 1) * params.pageSize, "offsetOf");
89
101
 
90
102
  // ../crouton-core/src/lib/filter.ts
91
103
  var Operator = [
@@ -240,6 +252,7 @@ var ControlType = {
240
252
  textArea: "textarea",
241
253
  markdown: "markdown",
242
254
  array: "array",
255
+ object: "object",
243
256
  custom: "custom",
244
257
  select: "select",
245
258
  mutliSelect: "mutliSelect",
@@ -329,6 +342,16 @@ var ControlBuilder = class _ControlBuilder extends ElementBuilder {
329
342
  ...options ?? {}
330
343
  });
331
344
  }
345
+ /**
346
+ * Render a datepicker. Defaults to a day-only picker; pass
347
+ * `{ format: 'dateTime' }` (or `{ withTime: true }`) to include a time.
348
+ */
349
+ date(options) {
350
+ return this.opt({
351
+ format: ControlType.date,
352
+ ...options ?? {}
353
+ });
354
+ }
332
355
  textArea(options) {
333
356
  return this.opt({
334
357
  format: ControlType.textArea,
@@ -617,7 +640,7 @@ var JsonOperationsSchema = import_zod8.z.object({
617
640
  });
618
641
 
619
642
  // ../crouton-core/src/lib/resource/ResourceJson.schema.ts
620
- var import_zod15 = require("zod");
643
+ var import_zod18 = require("zod");
621
644
 
622
645
  // ../crouton-core/src/lib/resource/CalculatedColumn.schema.ts
623
646
  var import_zod10 = require("zod");
@@ -771,42 +794,197 @@ var CalculatedColumnSchema = import_zod10.z.object({
771
794
  }).transform(normalizeLabel);
772
795
 
773
796
  // ../crouton-core/src/lib/resource/Column.ts
797
+ var import_zod12 = require("zod");
798
+
799
+ // ../crouton-core/src/lib/resource/ColumnType.schema.ts
774
800
  var import_zod11 = require("zod");
775
- var WhenConditionSchema = import_zod11.z.object({
776
- field: import_zod11.z.string(),
777
- eq: import_zod11.z.unknown().optional(),
778
- neq: import_zod11.z.unknown().optional(),
779
- exists: import_zod11.z.boolean().optional(),
780
- notExists: import_zod11.z.boolean().optional()
801
+ var ColumnTypeShorthandSchema = import_zod11.z.enum([
802
+ "string",
803
+ "number",
804
+ "integer",
805
+ "boolean",
806
+ "date",
807
+ "date-time",
808
+ "object",
809
+ "array"
810
+ ]);
811
+ var JsonSchemaFragmentSchema = import_zod11.z.lazy(() => import_zod11.z.object({
812
+ type: import_zod11.z.union([
813
+ import_zod11.z.string(),
814
+ import_zod11.z.array(import_zod11.z.string())
815
+ ]).optional(),
816
+ format: import_zod11.z.string().optional(),
817
+ enum: import_zod11.z.array(import_zod11.z.unknown()).optional(),
818
+ const: import_zod11.z.unknown().optional(),
819
+ nullable: import_zod11.z.boolean().optional(),
820
+ properties: import_zod11.z.record(import_zod11.z.string(), JsonSchemaFragmentSchema).optional(),
821
+ required: import_zod11.z.array(import_zod11.z.string()).optional(),
822
+ items: JsonSchemaFragmentSchema.optional(),
823
+ additionalProperties: import_zod11.z.union([
824
+ import_zod11.z.boolean(),
825
+ JsonSchemaFragmentSchema
826
+ ]).optional(),
827
+ title: import_zod11.z.string().optional(),
828
+ description: import_zod11.z.string().optional(),
829
+ default: import_zod11.z.unknown().optional(),
830
+ minimum: import_zod11.z.number().optional(),
831
+ maximum: import_zod11.z.number().optional(),
832
+ minLength: import_zod11.z.number().optional(),
833
+ maxLength: import_zod11.z.number().optional(),
834
+ pattern: import_zod11.z.string().optional()
835
+ }).catchall(import_zod11.z.unknown()));
836
+ var ColumnTypeSchema = import_zod11.z.union([
837
+ ColumnTypeShorthandSchema,
838
+ JsonSchemaFragmentSchema
839
+ ]);
840
+ var SHORTHAND_FRAGMENTS = {
841
+ string: {
842
+ type: "string"
843
+ },
844
+ number: {
845
+ type: "number"
846
+ },
847
+ integer: {
848
+ type: "integer"
849
+ },
850
+ boolean: {
851
+ type: "boolean"
852
+ },
853
+ date: {
854
+ type: "string",
855
+ format: "date"
856
+ },
857
+ "date-time": {
858
+ type: "string",
859
+ format: "date-time"
860
+ },
861
+ object: {
862
+ type: "object"
863
+ },
864
+ array: {
865
+ type: "array"
866
+ }
867
+ };
868
+ var isShorthand = /* @__PURE__ */ __name((type) => typeof type === "string", "isShorthand");
869
+ var NESTED_SHORTHANDS = {
870
+ date: SHORTHAND_FRAGMENTS.date,
871
+ "date-time": SHORTHAND_FRAGMENTS["date-time"]
872
+ };
873
+ var normalizeFragment = /* @__PURE__ */ __name((fragment) => {
874
+ const out = {
875
+ ...fragment
876
+ };
877
+ if (typeof out.type === "string") {
878
+ const expanded = NESTED_SHORTHANDS[out.type];
879
+ if (expanded) {
880
+ out.type = expanded.type;
881
+ if (out.format === void 0) out.format = expanded.format;
882
+ }
883
+ }
884
+ if (out.properties) {
885
+ out.properties = Object.fromEntries(Object.entries(out.properties).map(([key, value]) => [
886
+ key,
887
+ normalizeFragment(value)
888
+ ]));
889
+ }
890
+ if (out.items) out.items = normalizeFragment(out.items);
891
+ if (out.additionalProperties && typeof out.additionalProperties === "object") {
892
+ out.additionalProperties = normalizeFragment(out.additionalProperties);
893
+ }
894
+ return out;
895
+ }, "normalizeFragment");
896
+ var columnTypeToJsonSchema = /* @__PURE__ */ __name((type) => {
897
+ if (type === void 0) return {
898
+ type: "string"
899
+ };
900
+ if (isShorthand(type)) return {
901
+ ...SHORTHAND_FRAGMENTS[type]
902
+ };
903
+ return normalizeFragment(type);
904
+ }, "columnTypeToJsonSchema");
905
+ var columnTypeName = /* @__PURE__ */ __name((type) => {
906
+ const fragment = columnTypeToJsonSchema(type);
907
+ const raw = fragment.type;
908
+ if (Array.isArray(raw)) return raw.find((t) => t !== "null") ?? "string";
909
+ if (typeof raw === "string") return raw;
910
+ if (fragment.properties) return "object";
911
+ if (fragment.items) return "array";
912
+ return "string";
913
+ }, "columnTypeName");
914
+ var isObjectColumnType = /* @__PURE__ */ __name((type) => columnTypeName(type) === "object", "isObjectColumnType");
915
+ var isArrayColumnType = /* @__PURE__ */ __name((type) => columnTypeName(type) === "array", "isArrayColumnType");
916
+
917
+ // ../crouton-core/src/lib/resource/Column.ts
918
+ var WhenConditionSchema = import_zod12.z.object({
919
+ field: import_zod12.z.string(),
920
+ eq: import_zod12.z.unknown().optional(),
921
+ neq: import_zod12.z.unknown().optional(),
922
+ exists: import_zod12.z.boolean().optional(),
923
+ notExists: import_zod12.z.boolean().optional()
781
924
  });
782
- var JsonColumnSchema = import_zod11.z.object({
783
- id: import_zod11.z.string(),
784
- column: import_zod11.z.string().optional(),
785
- label: import_zod11.z.string().optional(),
786
- hiddenInTable: import_zod11.z.boolean().default(false),
787
- hiddenInForm: import_zod11.z.boolean().default(false),
788
- hiddenInView: import_zod11.z.boolean().default(false),
789
- sortable: import_zod11.z.boolean().default(false),
790
- defaultSort: import_zod11.z.boolean().default(false),
791
- searchable: import_zod11.z.boolean().default(false),
792
- filterable: import_zod11.z.boolean().default(false),
793
- createable: import_zod11.z.boolean().default(true),
794
- updateable: import_zod11.z.boolean().default(true),
795
- hideLabel: import_zod11.z.boolean().default(false),
925
+ var JsonColumnSchema = import_zod12.z.object({
926
+ id: import_zod12.z.string(),
927
+ column: import_zod12.z.string().optional(),
928
+ label: import_zod12.z.string().optional(),
929
+ hiddenInTable: import_zod12.z.boolean().default(false),
930
+ hiddenInForm: import_zod12.z.boolean().default(false),
931
+ hiddenInView: import_zod12.z.boolean().default(false),
932
+ sortable: import_zod12.z.boolean().default(false),
933
+ defaultSort: import_zod12.z.boolean().default(false),
934
+ searchable: import_zod12.z.boolean().default(false),
935
+ filterable: import_zod12.z.boolean().default(false),
936
+ createable: import_zod12.z.boolean().default(true),
937
+ updateable: import_zod12.z.boolean().default(true),
938
+ hideLabel: import_zod12.z.boolean().default(false),
796
939
  showWhen: WhenConditionSchema.optional(),
797
940
  hideWhen: WhenConditionSchema.optional(),
798
941
  disabledWhen: WhenConditionSchema.optional(),
799
- displayKey: import_zod11.z.string().optional(),
800
- sortId: import_zod11.z.string().optional(),
942
+ displayKey: import_zod12.z.string().optional(),
943
+ sortId: import_zod12.z.string().optional(),
801
944
  /**
802
945
  * Name of a shared enum in the project enum registry (`crouton.enums.json`).
803
946
  * At load time the loader injects that enum's `{ value, label }[]` into
804
947
  * `fieldInput.options.values`, so columns don't duplicate the option list.
805
948
  */
806
- enum: import_zod11.z.string().optional(),
807
- idField: import_zod11.z.boolean().default(false),
808
- showInLookup: import_zod11.z.boolean().default(false),
809
- columnType: import_zod11.z.string().default("string"),
949
+ enum: import_zod12.z.string().optional(),
950
+ idField: import_zod12.z.boolean().default(false),
951
+ showInLookup: import_zod12.z.boolean().default(false),
952
+ /**
953
+ * Data type of the column, as a shorthand name (`"string"`, `"integer"`, …)
954
+ * or a full JSON Schema fragment:
955
+ *
956
+ * ```json
957
+ * "type": {
958
+ * "type": "object",
959
+ * "properties": { "id": { "type": "string" }, "name": { "type": "string" } }
960
+ * }
961
+ * ```
962
+ *
963
+ * Required on every column of a `kind: "custom"` resource, where it is the
964
+ * only source of the resource's json_schema. Optional on a `prisma`
965
+ * resource, whose schema is derived from the Zod model.
966
+ */
967
+ type: ColumnTypeSchema.optional(),
968
+ /**
969
+ * Whether the form requires a value.
970
+ *
971
+ * A prisma resource derives `required` from its Zod model, so this is an
972
+ * override in either direction: `true` adds the column to the form schema's
973
+ * `required` array, `false` removes it, and omitting it leaves the model's
974
+ * answer alone. A `kind: "custom"` resource has no model, so this is the only
975
+ * way to mark one of its fields required.
976
+ *
977
+ * Applies to the **form** view only — a required filter input would make the
978
+ * filter panel unsubmittable, and the table and view schemas are read-only.
979
+ * Ignored on the id column and on columns that are neither createable nor
980
+ * updateable, since the form cannot supply a value for those.
981
+ */
982
+ required: import_zod12.z.boolean().optional(),
983
+ /**
984
+ * @deprecated Use `type` instead. Retained because it is still consumed by
985
+ * the boolean predicate and the schema-less sub-resource view builder.
986
+ */
987
+ columnType: import_zod12.z.string().default("string"),
810
988
  fieldInput: FieldInputSchema.optional(),
811
989
  /**
812
990
  * Optional per-context override for the read-only VIEW ui. Same shape as
@@ -830,40 +1008,65 @@ var JsonColumnSchema = import_zod11.z.object({
830
1008
  * Visibility (`hiddenInTable/Form/View`) on this column is inherited by all sub-columns as a
831
1009
  * default; the referenced resource's own column visibility further restricts it.
832
1010
  */
833
- extend: import_zod11.z.string().optional(),
1011
+ extend: import_zod12.z.string().optional(),
834
1012
  /**
835
1013
  * Per-sub-column overrides when using `extend`.
836
1014
  * Key: the virtual column id (`"{extendId}_{refColId}"`) or just the referenced column id.
837
1015
  * Value: any `JsonColumn` fields to merge over the generated virtual column.
838
1016
  */
839
- columns: import_zod11.z.record(import_zod11.z.string(), import_zod11.z.record(import_zod11.z.string(), import_zod11.z.unknown())).optional()
1017
+ columns: import_zod12.z.record(import_zod12.z.string(), import_zod12.z.record(import_zod12.z.string(), import_zod12.z.unknown())).optional()
1018
+ });
1019
+
1020
+ // ../crouton-core/src/lib/resource/ParentRef.schema.ts
1021
+ var import_zod13 = require("zod");
1022
+ var ParentRefSchema = import_zod13.z.object({
1023
+ /** Route segment of the parent, e.g. `"group"`. */
1024
+ route: import_zod13.z.string(),
1025
+ /**
1026
+ * Name of the path parameter carrying the parent id, e.g. `"groupId"`.
1027
+ * Avoid `"id"` — that is the child's own id in `/:id` routes.
1028
+ */
1029
+ param: import_zod13.z.string().default("parentId"),
1030
+ /** Type of the parent id, used to coerce the path param. Defaults to `'string'`. */
1031
+ idType: import_zod13.z.enum([
1032
+ "string",
1033
+ "number"
1034
+ ]).optional()
840
1035
  });
1036
+ var resourceControllerPath = /* @__PURE__ */ __name((route, parent) => parent ? `${parent.route}/:${parent.param}/${route}` : route, "resourceControllerPath");
1037
+
1038
+ // ../crouton-core/src/lib/resource/ResourceKind.ts
1039
+ var import_zod14 = require("zod");
1040
+ var ResourceKindSchema = import_zod14.z.enum([
1041
+ "prisma",
1042
+ "custom"
1043
+ ]).default("prisma");
841
1044
 
842
1045
  // ../crouton-core/src/lib/resource/Sidebar.schema.ts
843
- var import_zod12 = __toESM(require("zod"), 1);
844
- var SidebarGroupSchema = import_zod12.default.object({
1046
+ var import_zod15 = __toESM(require("zod"), 1);
1047
+ var SidebarGroupSchema = import_zod15.default.object({
845
1048
  /** Human-readable heading shown in the sidebar. Defaults to a title-cased version of the slug. */
846
- label: import_zod12.default.string().optional(),
1049
+ label: import_zod15.default.string().optional(),
847
1050
  /** Controls the order of this group among top-level sidebar items. */
848
- position: import_zod12.default.number().optional()
1051
+ position: import_zod15.default.number().optional()
849
1052
  });
850
- var SidebarSchema = import_zod12.default.object({
851
- hide: import_zod12.default.boolean().default(false),
852
- position: import_zod12.default.number().optional(),
853
- label: import_zod12.default.string().optional(),
1053
+ var SidebarSchema = import_zod15.default.object({
1054
+ hide: import_zod15.default.boolean().default(false),
1055
+ position: import_zod15.default.number().optional(),
1056
+ label: import_zod15.default.string().optional(),
854
1057
  /**
855
1058
  * Slug of the group this resource belongs to.
856
1059
  * Must match a key in `sidebarGroups` in `crouton.json`.
857
1060
  * Resources with the same `group` are nested under a shared collapsible section.
858
1061
  */
859
- group: import_zod12.default.string().optional()
1062
+ group: import_zod15.default.string().optional()
860
1063
  });
861
1064
 
862
1065
  // ../crouton-core/src/lib/resource/TableAction.schema.ts
863
- var import_zod13 = require("zod");
864
- var ActionConditionSchema = import_zod13.z.object({
1066
+ var import_zod16 = require("zod");
1067
+ var ActionConditionSchema = import_zod16.z.object({
865
1068
  /** Row field to evaluate. */
866
- field: import_zod13.z.string(),
1069
+ field: import_zod16.z.string(),
867
1070
  /**
868
1071
  * Comparison operator. Defaults to `"eq"`.
869
1072
  * - `eq` / `neq` — strict equality
@@ -871,7 +1074,7 @@ var ActionConditionSchema = import_zod13.z.object({
871
1074
  * - `exists` — field is not null / undefined / empty string
872
1075
  * - `notExists` — field is null / undefined / empty string
873
1076
  */
874
- op: import_zod13.z.enum([
1077
+ op: import_zod16.z.enum([
875
1078
  "eq",
876
1079
  "neq",
877
1080
  "gt",
@@ -882,38 +1085,38 @@ var ActionConditionSchema = import_zod13.z.object({
882
1085
  "notExists"
883
1086
  ]).default("eq"),
884
1087
  /** Comparison value. Not required for `exists` / `notExists`. */
885
- value: import_zod13.z.unknown().optional()
1088
+ value: import_zod16.z.unknown().optional()
886
1089
  });
887
- var ActionSchema = import_zod13.z.object({
888
- type: import_zod13.z.string(),
1090
+ var ActionSchema = import_zod16.z.object({
1091
+ type: import_zod16.z.string(),
889
1092
  /** Unique identifier for the action. */
890
- id: import_zod13.z.string(),
1093
+ id: import_zod16.z.string(),
891
1094
  /** Human-readable button label. */
892
- label: import_zod13.z.string(),
1095
+ label: import_zod16.z.string(),
893
1096
  /** MDI icon name, e.g. `"mdi:open-in-new"`. */
894
- icon: import_zod13.z.string().optional(),
1097
+ icon: import_zod16.z.string().optional(),
895
1098
  /** Tooltip text. Falls back to `label` when omitted. */
896
- tooltip: import_zod13.z.string().optional(),
1099
+ tooltip: import_zod16.z.string().optional(),
897
1100
  condition: ActionConditionSchema.optional()
898
1101
  });
899
1102
  var JsonProcedureActionSchema = ActionSchema.extend({
900
- type: import_zod13.z.literal("procedure").default("procedure"),
1103
+ type: import_zod16.z.literal("procedure").default("procedure"),
901
1104
  /**
902
1105
  * Filename (without extension) inside the resource's `actions/` directory
903
1106
  * that exports the table-action procedure, e.g. `"syncZotero"`.
904
1107
  */
905
- procedure: import_zod13.z.string(),
1108
+ procedure: import_zod16.z.string(),
906
1109
  /** HTTP method for the frontend. Defaults to `"post"`. */
907
- method: import_zod13.z.string().optional().default("post"),
1110
+ method: import_zod16.z.string().optional().default("post"),
908
1111
  /** Static query / body params passed to the endpoint. */
909
- data: import_zod13.z.record(import_zod13.z.string(), import_zod13.z.unknown()).optional()
1112
+ data: import_zod16.z.record(import_zod16.z.string(), import_zod16.z.unknown()).optional()
910
1113
  });
911
1114
  var JsonLinkActionSchema = ActionSchema.extend({
912
1115
  /** Optional condition evaluated per row. Button is hidden when the condition is false. */
913
- type: import_zod13.z.literal("link").default("link"),
1116
+ type: import_zod16.z.literal("link").default("link"),
914
1117
  /** URL to open. May contain `{env.VAR}` placeholders. */
915
- href: import_zod13.z.string(),
916
- blank: import_zod13.z.boolean().optional().default(true)
1118
+ href: import_zod16.z.string(),
1119
+ blank: import_zod16.z.boolean().optional().default(true)
917
1120
  });
918
1121
  var transformAction = /* @__PURE__ */ __name((action) => {
919
1122
  const label = action.label;
@@ -922,20 +1125,20 @@ var transformAction = /* @__PURE__ */ __name((action) => {
922
1125
  ...action
923
1126
  };
924
1127
  }, "transformAction");
925
- var JsonActionSchema = import_zod13.z.union([
1128
+ var JsonActionSchema = import_zod16.z.union([
926
1129
  JsonProcedureActionSchema,
927
1130
  JsonLinkActionSchema
928
1131
  ]).transform(transformAction);
929
1132
 
930
1133
  // ../crouton-core/src/lib/resource/include.schema.ts
931
- var import_zod14 = require("zod");
932
- var JsonIncludeEntrySchema = import_zod14.z.lazy(() => import_zod14.z.union([
933
- import_zod14.z.string(),
934
- import_zod14.z.object({
935
- relation: import_zod14.z.string(),
936
- include: import_zod14.z.array(JsonIncludeEntrySchema).optional(),
1134
+ var import_zod17 = require("zod");
1135
+ var JsonIncludeEntrySchema = import_zod17.z.lazy(() => import_zod17.z.union([
1136
+ import_zod17.z.string(),
1137
+ import_zod17.z.object({
1138
+ relation: import_zod17.z.string(),
1139
+ include: import_zod17.z.array(JsonIncludeEntrySchema).optional(),
937
1140
  /** Prisma-format orderBy clause applied to the included records. */
938
- orderBy: import_zod14.z.record(import_zod14.z.string(), import_zod14.z.unknown()).optional()
1141
+ orderBy: import_zod17.z.record(import_zod17.z.string(), import_zod17.z.unknown()).optional()
939
1142
  })
940
1143
  ]));
941
1144
 
@@ -944,16 +1147,16 @@ var BASELINE_RESOURCE_VERSION = 1;
944
1147
  var CURRENT_RESOURCE_VERSION = 1;
945
1148
 
946
1149
  // ../crouton-core/src/lib/resource/ResourceJson.schema.ts
947
- var JsonColumnsMapSchema = import_zod15.z.record(import_zod15.z.string(), JsonColumnSchema.omit({
1150
+ var JsonColumnsMapSchema = import_zod18.z.record(import_zod18.z.string(), JsonColumnSchema.omit({
948
1151
  id: true
949
- }).catchall(import_zod15.z.unknown()));
1152
+ }).catchall(import_zod18.z.unknown()));
950
1153
  var ColumnsSchema = JsonColumnsMapSchema;
951
- var JsonDisplaySchema = import_zod15.z.object({
952
- mode: import_zod15.z.enum([
1154
+ var JsonDisplaySchema = import_zod18.z.object({
1155
+ mode: import_zod18.z.enum([
953
1156
  "page",
954
1157
  "modal"
955
1158
  ]).default("modal"),
956
- customComponent: import_zod15.z.string().nullable().optional().default(null)
1159
+ customComponent: import_zod18.z.string().nullable().optional().default(null)
957
1160
  });
958
1161
  var normalizeColumns = /* @__PURE__ */ __name((columns) => {
959
1162
  if (!columns) return void 0;
@@ -966,37 +1169,61 @@ var normalizeColumns = /* @__PURE__ */ __name((columns) => {
966
1169
  label: col.label ?? labelFromId(col.id)
967
1170
  }));
968
1171
  }, "normalizeColumns");
969
- var ResourceJsonShape = import_zod15.z.object({
1172
+ var ResourceJsonShape = import_zod18.z.object({
970
1173
  /**
971
1174
  * URL of the generated JSON Schema, for editor autocomplete/validation. Declared so the
972
1175
  * key is *allowed* (not stripped, and not flagged by the very schema it points at).
973
1176
  * Ignored at runtime.
974
1177
  */
975
- $schema: import_zod15.z.string().optional(),
1178
+ $schema: import_zod18.z.string().optional(),
976
1179
  /**
977
1180
  * resource.json shape version. Missing ⇒ baseline (see `./version`). Auto-migrated
978
1181
  * toward `CURRENT_RESOURCE_VERSION` on load in the dev environment.
979
1182
  */
980
- schemaVersion: import_zod15.z.number().int().positive().optional(),
1183
+ schemaVersion: import_zod18.z.number().int().positive().optional(),
981
1184
  /** When `true`, the resource lives in the repo but is NOT loaded/served (work in progress). */
982
- draft: import_zod15.z.boolean().optional().default(false),
983
- name: import_zod15.z.string(),
984
- route: import_zod15.z.string(),
985
- model: import_zod15.z.string(),
986
- tag: import_zod15.z.string(),
987
- title: import_zod15.z.string().optional(),
988
- table: import_zod15.z.string().optional(),
989
- database: import_zod15.z.string().optional(),
1185
+ draft: import_zod18.z.boolean().optional().default(false),
1186
+ /**
1187
+ * Where the data comes from. `prisma` (the default) is backed by a Prisma
1188
+ * model plus a `schema.ts`; `custom` is configuration only and the developer
1189
+ * supplies a `repository.ts`. See `./ResourceKind`.
1190
+ */
1191
+ kind: ResourceKindSchema,
1192
+ name: import_zod18.z.string(),
1193
+ route: import_zod18.z.string(),
1194
+ /**
1195
+ * Prisma model name. Required when `kind` is `prisma` (enforced by the
1196
+ * refinement on `ResourceJsonSchema`), and must be absent when `kind` is
1197
+ * `custom` — there is no Prisma delegate to address.
1198
+ */
1199
+ model: import_zod18.z.string().optional(),
1200
+ tag: import_zod18.z.string(),
1201
+ title: import_zod18.z.string().optional(),
1202
+ table: import_zod18.z.string().optional(),
1203
+ /**
1204
+ * Type of the resource's primary key, used to coerce `:id` route params.
1205
+ * Written by codegen from the Prisma model; defaults to `'string'`.
1206
+ */
1207
+ idType: import_zod18.z.enum([
1208
+ "string",
1209
+ "number"
1210
+ ]).optional(),
1211
+ database: import_zod18.z.string().optional(),
1212
+ /**
1213
+ * Mount this resource under a parent route instead of at the top level —
1214
+ * see `./ParentRef.schema`. Only valid on a `kind: "custom"` resource.
1215
+ */
1216
+ parent: ParentRefSchema.optional(),
990
1217
  sidebar: SidebarSchema.default(SidebarSchema.parse({})),
991
1218
  display: JsonDisplaySchema.default(JsonDisplaySchema.parse({})),
992
1219
  operations: JsonOperationsSchema,
993
1220
  columns: ColumnsSchema.default(ColumnsSchema.parse({})),
994
- calculatedColumns: import_zod15.z.array(CalculatedColumnSchema).default([]),
995
- actions: import_zod15.z.array(JsonActionSchema).default([]),
1221
+ calculatedColumns: import_zod18.z.array(CalculatedColumnSchema).default([]),
1222
+ actions: import_zod18.z.array(JsonActionSchema).default([]),
996
1223
  /** Global table-level actions (no record id). Shown as toolbar buttons. */
997
- tableActions: import_zod15.z.array(JsonActionSchema).default([]),
1224
+ tableActions: import_zod18.z.array(JsonActionSchema).default([]),
998
1225
  /** Modal width when opening a form for this resource. */
999
- modalSize: import_zod15.z.enum([
1226
+ modalSize: import_zod18.z.enum([
1000
1227
  "xs",
1001
1228
  "sm",
1002
1229
  "lg",
@@ -1009,9 +1236,73 @@ var ResourceJsonShape = import_zod15.z.object({
1009
1236
  * `{ "relation": "text_author", "include": ["author"] }` →
1010
1237
  * `include: { text_author: { include: { author: true } } }`
1011
1238
  */
1012
- include: import_zod15.z.array(JsonIncludeEntrySchema).default([])
1239
+ include: import_zod18.z.array(JsonIncludeEntrySchema).default([])
1013
1240
  });
1014
- var ResourceJsonSchema = ResourceJsonShape.transform((obj) => {
1241
+ var refineByKind = /* @__PURE__ */ __name((obj, ctx) => {
1242
+ if (obj.kind === "custom") {
1243
+ if (obj.model !== void 0) {
1244
+ ctx.addIssue({
1245
+ code: "custom",
1246
+ path: [
1247
+ "model"
1248
+ ],
1249
+ message: 'A custom resource has no Prisma model. Remove "model" \u2014 data access comes from repository.ts.'
1250
+ });
1251
+ }
1252
+ if (obj.calculatedColumns?.length) {
1253
+ ctx.addIssue({
1254
+ code: "custom",
1255
+ path: [
1256
+ "calculatedColumns"
1257
+ ],
1258
+ message: "calculatedColumns run raw SQL against a database table and are not supported on a custom resource. Compute the value in repository.ts instead."
1259
+ });
1260
+ }
1261
+ for (const [id, col] of Object.entries(obj.columns ?? {})) {
1262
+ if (col.fieldInput?.format === "relation" || col.fieldInput?.type === "autocomplete") {
1263
+ continue;
1264
+ }
1265
+ if (col.type === void 0) {
1266
+ ctx.addIssue({
1267
+ code: "custom",
1268
+ path: [
1269
+ "columns",
1270
+ id,
1271
+ "type"
1272
+ ],
1273
+ message: `Column "${id}" needs a "type": a custom resource has no schema.ts, so its json_schema is built from the column types.`
1274
+ });
1275
+ }
1276
+ }
1277
+ if (obj.parent && obj.parent.param === "id") {
1278
+ ctx.addIssue({
1279
+ code: "custom",
1280
+ path: [
1281
+ "parent",
1282
+ "param"
1283
+ ],
1284
+ message: `parent.param cannot be "id" \u2014 that is the child's own id in /:id routes. Use something like "groupId".`
1285
+ });
1286
+ }
1287
+ } else if (obj.parent !== void 0) {
1288
+ ctx.addIssue({
1289
+ code: "custom",
1290
+ path: [
1291
+ "parent"
1292
+ ],
1293
+ message: '"parent" is only supported on a custom resource. A prisma resource is nested by declaring a relation column on its parent.'
1294
+ });
1295
+ } else if (obj.model === void 0) {
1296
+ ctx.addIssue({
1297
+ code: "custom",
1298
+ path: [
1299
+ "model"
1300
+ ],
1301
+ message: '"model" is required for a prisma-backed resource. Set "kind": "custom" for a resource with no Prisma model.'
1302
+ });
1303
+ }
1304
+ }, "refineByKind");
1305
+ var ResourceJsonSchema = ResourceJsonShape.superRefine(refineByKind).transform((obj) => {
1015
1306
  const title = obj.title ?? labelFromId(obj.name);
1016
1307
  const schemaVersion = obj.schemaVersion ?? BASELINE_RESOURCE_VERSION;
1017
1308
  return {
@@ -1090,50 +1381,50 @@ var resolveViewField = /* @__PURE__ */ __name((c) => mergeFieldVariant(c.fieldIn
1090
1381
  var resolveTableField = /* @__PURE__ */ __name((c) => mergeFieldVariant(resolveViewField(c), c.fieldTable), "resolveTableField");
1091
1382
 
1092
1383
  // ../crouton-core/src/lib/config/CroutonConfig.schema.ts
1093
- var import_zod16 = require("zod");
1094
- var RulesetSchema = import_zod16.z.object({
1095
- hideIdInTable: import_zod16.z.boolean().default(true),
1096
- hideIdInForm: import_zod16.z.boolean().default(true),
1097
- hideIdInView: import_zod16.z.boolean().default(true),
1098
- hideTimestamps: import_zod16.z.boolean().default(true),
1099
- hideForeignKeys: import_zod16.z.boolean().default(true),
1100
- includeRelations: import_zod16.z.boolean().default(false),
1101
- hideRelationsInTable: import_zod16.z.boolean().default(true),
1102
- showRelationsInForm: import_zod16.z.boolean().default(true),
1103
- enumValueLabel: import_zod16.z.boolean().default(true),
1104
- sharedEnums: import_zod16.z.boolean().default(true),
1384
+ var import_zod19 = require("zod");
1385
+ var RulesetSchema = import_zod19.z.object({
1386
+ hideIdInTable: import_zod19.z.boolean().default(true),
1387
+ hideIdInForm: import_zod19.z.boolean().default(true),
1388
+ hideIdInView: import_zod19.z.boolean().default(true),
1389
+ hideTimestamps: import_zod19.z.boolean().default(true),
1390
+ hideForeignKeys: import_zod19.z.boolean().default(true),
1391
+ includeRelations: import_zod19.z.boolean().default(true),
1392
+ hideRelationsInTable: import_zod19.z.boolean().default(true),
1393
+ showRelationsInForm: import_zod19.z.boolean().default(true),
1394
+ enumValueLabel: import_zod19.z.boolean().default(true),
1395
+ sharedEnums: import_zod19.z.boolean().default(true),
1105
1396
  defaultOperations: JsonOperationsSchema.default(JsonOperationsSchema.parse({}))
1106
1397
  });
1107
- var CroutonConfigSchema = import_zod16.z.object({
1398
+ var CroutonConfigSchema = import_zod19.z.object({
1108
1399
  /**
1109
1400
  * Application title served to the frontend via `GET /_app/layout`.
1110
1401
  * Displayed in the admin sidebar header.
1111
1402
  */
1112
- title: import_zod16.z.string(),
1403
+ title: import_zod19.z.string(),
1113
1404
  /** Where resource directories live, relative to the project root. */
1114
- resourcesDir: import_zod16.z.string(),
1405
+ resourcesDir: import_zod19.z.string(),
1115
1406
  /** Where datasource folders live (each with a `data-source.json`). */
1116
- dataSourcesDir: import_zod16.z.string(),
1407
+ dataSourcesDir: import_zod19.z.string(),
1117
1408
  /**
1118
1409
  * Template for a model's Zod export name. `{Model}` → Prisma model name.
1119
1410
  * Defaults to `{Model}WithRelationsSchema` (the relations-aware schema
1120
1411
  * emitted by zod-prisma-types when `createRelationValuesTypes` is on).
1121
1412
  */
1122
- schemaExportName: import_zod16.z.string().default("{Model}WithRelationsSchema"),
1413
+ schemaExportName: import_zod19.z.string().default("{Model}WithRelationsSchema"),
1123
1414
  /** Path to the shared enum registry, relative to project root. Default `crouton.enums.json`. */
1124
- enumsFile: import_zod16.z.string().default("crouton.enums.json"),
1415
+ enumsFile: import_zod19.z.string().default("crouton.enums.json"),
1125
1416
  /** Optional overrides of the default visibility ruleset. */
1126
1417
  rules: RulesetSchema.default(RulesetSchema.parse({})),
1127
1418
  /**
1128
1419
  * Sidebar group definitions, keyed by group slug (e.g. `"metadata"`).
1129
1420
  * Resources reference a group via `sidebar.group` in their `resource.json`.
1130
1421
  */
1131
- sidebarGroups: import_zod16.z.record(import_zod16.z.string(), SidebarGroupSchema).default({}),
1422
+ sidebarGroups: import_zod19.z.record(import_zod19.z.string(), SidebarGroupSchema).default({}),
1132
1423
  /**
1133
1424
  * Whether form fields are saved automatically as the user edits them.
1134
1425
  * @default true
1135
1426
  */
1136
- autoSave: import_zod16.z.boolean().default(true)
1427
+ autoSave: import_zod19.z.boolean().default(true)
1137
1428
  });
1138
1429
 
1139
1430
  // ../crouton-core/src/lib/config/readConfig.ts
@@ -1149,6 +1440,9 @@ var isRelation = /* @__PURE__ */ __name((col) => col.fieldInput?.format === "rel
1149
1440
  var isAutocomplete = /* @__PURE__ */ __name((col) => col.fieldInput?.type === "autocomplete", "isAutocomplete");
1150
1441
  var isRecordCell = /* @__PURE__ */ __name((col) => isRelation(col) || isAutocomplete(col), "isRecordCell");
1151
1442
  var isDateRange = /* @__PURE__ */ __name((col) => col.fieldInput?.format === "date-range", "isDateRange");
1443
+ var isObjectColumn = /* @__PURE__ */ __name((col) => isObjectColumnType(col.type), "isObjectColumn");
1444
+ var isArrayColumn = /* @__PURE__ */ __name((col) => isArrayColumnType(col.type), "isArrayColumn");
1445
+ var isObjectCell = /* @__PURE__ */ __name((col) => isObjectColumn(col) && !!col.displayKey, "isObjectCell");
1152
1446
 
1153
1447
  // ../crouton-core/src/lib/view/column.utils.ts
1154
1448
  var colPosition = /* @__PURE__ */ __name((col, i) => col.fieldInput?.position ?? i, "colPosition");
@@ -1290,7 +1584,7 @@ var buildTableUiSchema = /* @__PURE__ */ __name((cols) => {
1290
1584
  const id = el.scope?.replace("#/properties/", "");
1291
1585
  const col = id ? colMap[id] : void 0;
1292
1586
  if (!col) return el;
1293
- const fieldInputOptions = isRecordCell(col) || isDateRange(col) ? col.fieldInput?.options ?? {} : pickSharedCellOptions(col);
1587
+ const fieldInputOptions = isRecordCell(col) || isDateRange(col) || isObjectCell(col) ? col.fieldInput?.options ?? {} : pickSharedCellOptions(col);
1294
1588
  const dataPathOption = col.column ? {
1295
1589
  dataPath: col.column
1296
1590
  } : {};
@@ -1316,7 +1610,9 @@ var buildTableUiSchema = /* @__PURE__ */ __name((cols) => {
1316
1610
  },
1317
1611
  label: col.label
1318
1612
  },
1319
- ...isRecordCell(col) && {
1613
+ // An object column with a displayKey renders the same way as a relation:
1614
+ // one nested key out of an object value.
1615
+ ...(isRecordCell(col) || isObjectCell(col)) && {
1320
1616
  type: "RecordCell"
1321
1617
  },
1322
1618
  ...isDateRange(col) && {
@@ -1374,6 +1670,11 @@ var buildDetailLayout = /* @__PURE__ */ __name((detail) => {
1374
1670
  if (detail.titleKey) inner.titleKey(detail.titleKey);
1375
1671
  return inner;
1376
1672
  }, "buildDetailLayout");
1673
+ var defaultControlFormat = /* @__PURE__ */ __name((col) => {
1674
+ if (isObjectColumn(col)) return "object";
1675
+ if (isArrayColumn(col)) return "array";
1676
+ return "text";
1677
+ }, "defaultControlFormat");
1377
1678
  var buildFormControl = /* @__PURE__ */ __name((col) => {
1378
1679
  const control = ControlBuilder.properties(col.id);
1379
1680
  const fieldInput = col.fieldInput;
@@ -1397,6 +1698,13 @@ var buildFormControl = /* @__PURE__ */ __name((col) => {
1397
1698
  control.detailFixed(detailLayout, {
1398
1699
  layout: fieldInput.detail.layout === "collapse" ? "row" : void 0
1399
1700
  });
1701
+ } else if (fieldInput?.format !== "date-range" && (fieldInput?.type === "date" || fieldInput?.format === "dateTime")) {
1702
+ const options = {
1703
+ ...fieldInput.options
1704
+ };
1705
+ if (!options.colspan) options.colspan = 12;
1706
+ const format = fieldInput.format === "dateTime" ? "dateTime" : "date";
1707
+ control.control(format, options).width("full");
1400
1708
  } else if (fieldInput?.format === "date-range") {
1401
1709
  const options = {
1402
1710
  ...fieldInput.options
@@ -1406,7 +1714,7 @@ var buildFormControl = /* @__PURE__ */ __name((col) => {
1406
1714
  } else {
1407
1715
  const options = fieldInput?.options ?? {};
1408
1716
  if (!options.colspan) options.colspan = 12;
1409
- const type = fieldInput?.type ?? "text";
1717
+ const type = fieldInput?.type ?? defaultControlFormat(col);
1410
1718
  control.control(type, options).width("full");
1411
1719
  }
1412
1720
  if (fieldInput?.customRender) control.setCustomRender(fieldInput?.customRender);
@@ -1502,19 +1810,66 @@ var injectCalculatedColumns = /* @__PURE__ */ __name((tableView, calculated) =>
1502
1810
  var injectCalculatedColumnsToView = /* @__PURE__ */ __name((viewConfig, calculated) => injectCalculatedColumnsIntoView(viewConfig, calculated, "view"), "injectCalculatedColumnsToView");
1503
1811
 
1504
1812
  // ../crouton-core/src/lib/view/view.builder.ts
1505
- var import_zod18 = require("zod");
1813
+ var import_zod21 = require("zod");
1814
+
1815
+ // ../crouton-core/src/lib/view/column-type-schema.source.ts
1816
+ var optionValues = /* @__PURE__ */ __name((col) => {
1817
+ const values = col.fieldInput?.options?.["values"];
1818
+ if (!Array.isArray(values)) return void 0;
1819
+ const unwrapped = values.map((v) => v && typeof v === "object" && "value" in v ? v.value : v);
1820
+ return unwrapped.length ? unwrapped : void 0;
1821
+ }, "optionValues");
1822
+ var columnToJsonSchemaProperty = /* @__PURE__ */ __name((col) => {
1823
+ const property = col.type === void 0 && isAutocomplete(col) ? {} : {
1824
+ ...columnTypeToJsonSchema(col.type)
1825
+ };
1826
+ if (property.title === void 0) property.title = col.label ?? col.id;
1827
+ if (col.fieldInput?.defaultValue !== void 0 && property.default === void 0) {
1828
+ property.default = col.fieldInput.defaultValue;
1829
+ }
1830
+ if (property.enum === void 0) {
1831
+ const values = optionValues(col);
1832
+ if (values) property.enum = values;
1833
+ }
1834
+ return property;
1835
+ }, "columnToJsonSchemaProperty");
1836
+ var columnTypeSchemaSource = /* @__PURE__ */ __name((schemaCols) => {
1837
+ if (!schemaCols.length) return void 0;
1838
+ const properties = {};
1839
+ for (const col of schemaCols) {
1840
+ properties[col.id] = columnToJsonSchemaProperty(col);
1841
+ }
1842
+ return {
1843
+ type: "object",
1844
+ properties
1845
+ };
1846
+ }, "columnTypeSchemaSource");
1506
1847
 
1507
1848
  // ../crouton-core/src/lib/view/json-schema.opts.ts
1508
- var import_zod17 = require("zod");
1509
- var dateOverride = /* @__PURE__ */ __name(({ zodSchema, jsonSchema }) => {
1510
- if (zodSchema instanceof import_zod17.z.ZodDate) {
1849
+ var import_zod20 = require("zod");
1850
+ var isInstanceOf = /* @__PURE__ */ __name((zodSchema, className) => {
1851
+ if (!(zodSchema instanceof import_zod20.z.ZodCustom)) return false;
1852
+ const result = zodSchema.safeParse(null);
1853
+ return !result.success && result.error.issues[0]?.expected === className;
1854
+ }, "isInstanceOf");
1855
+ var jsonSchemaOverride = /* @__PURE__ */ __name(({ zodSchema, jsonSchema }) => {
1856
+ if (zodSchema instanceof import_zod20.z.ZodDate) {
1511
1857
  jsonSchema.type = "string";
1512
1858
  jsonSchema.format = "date-time";
1513
1859
  }
1514
- }, "dateOverride");
1860
+ if (zodSchema instanceof import_zod20.z.ZodBigInt) {
1861
+ jsonSchema.type = "integer";
1862
+ }
1863
+ if (isInstanceOf(zodSchema, "Decimal")) {
1864
+ jsonSchema.type = "number";
1865
+ }
1866
+ if (isInstanceOf(zodSchema, "Buffer")) {
1867
+ jsonSchema.type = "string";
1868
+ }
1869
+ }, "jsonSchemaOverride");
1515
1870
  var jsonSchemaOpts = {
1516
1871
  unrepresentable: "any",
1517
- override: dateOverride
1872
+ override: jsonSchemaOverride
1518
1873
  };
1519
1874
 
1520
1875
  // ../crouton-core/src/lib/view/view.builder.ts
@@ -1567,11 +1922,7 @@ var injectFieldDefaults = /* @__PURE__ */ __name((jsonSchema, columns) => {
1567
1922
  if (prop) prop.default = col.fieldInput.defaultValue;
1568
1923
  }
1569
1924
  }, "injectFieldDefaults");
1570
- var buildView = /* @__PURE__ */ __name((schema, columns, visible, buildUiSchema, sort = false, schemaVisible) => {
1571
- if (!schema || !columns?.length) return void 0;
1572
- const visibleCols = sort ? sortByPosition(columns.filter(visible)) : columns.filter(visible);
1573
- if (!visibleCols.length) return void 0;
1574
- const schemaCols = (schemaVisible ? columns.filter((c) => visible(c) || schemaVisible(c)) : visibleCols).filter((c) => !isRelation(c));
1925
+ var zodSchemaSource = /* @__PURE__ */ __name((schema) => (schemaCols) => {
1575
1926
  const schemaKeys = new Set(Object.keys(schema.shape));
1576
1927
  const schemaIds = schemaCols.map((c) => c.id).filter((id) => schemaKeys.has(id));
1577
1928
  if (!schemaIds.length) return void 0;
@@ -1580,10 +1931,61 @@ var buildView = /* @__PURE__ */ __name((schema, columns, visible, buildUiSchema,
1580
1931
  true
1581
1932
  ]));
1582
1933
  const picked = schema.pick(mask);
1583
- const jsonSchema = (0, import_zod18.toJSONSchema)(picked, {
1934
+ return (0, import_zod21.toJSONSchema)(picked, {
1584
1935
  target: "draft-07",
1585
1936
  ...jsonSchemaOpts
1586
1937
  });
1938
+ }, "zodSchemaSource");
1939
+ var NON_NULL_TYPES = [
1940
+ "object",
1941
+ "array",
1942
+ "string",
1943
+ "number",
1944
+ "boolean"
1945
+ ];
1946
+ var SHAPE_KEYS = [
1947
+ "type",
1948
+ "enum",
1949
+ "const",
1950
+ "anyOf",
1951
+ "oneOf",
1952
+ "allOf",
1953
+ "$ref"
1954
+ ];
1955
+ var applyRequiredColumns = /* @__PURE__ */ __name((jsonSchema, columns) => {
1956
+ if (!columns?.length) return;
1957
+ const properties = jsonSchema["properties"];
1958
+ if (!properties) return;
1959
+ const required = new Set(Array.isArray(jsonSchema["required"]) ? jsonSchema["required"] : []);
1960
+ for (const col of columns) {
1961
+ if (col.required === void 0) continue;
1962
+ if (!(col.id in properties)) continue;
1963
+ if (!col.required) {
1964
+ required.delete(col.id);
1965
+ continue;
1966
+ }
1967
+ if (col.idField) continue;
1968
+ if (col.createable === false && col.updateable === false) continue;
1969
+ required.add(col.id);
1970
+ const property = properties[col.id];
1971
+ if (property && typeof property === "object" && !SHAPE_KEYS.some((key) => key in property)) {
1972
+ property["type"] = [
1973
+ ...NON_NULL_TYPES
1974
+ ];
1975
+ }
1976
+ }
1977
+ if (required.size) jsonSchema["required"] = [
1978
+ ...required
1979
+ ];
1980
+ else delete jsonSchema["required"];
1981
+ }, "applyRequiredColumns");
1982
+ var buildView = /* @__PURE__ */ __name((source, columns, visible, buildUiSchema, sort = false, schemaVisible) => {
1983
+ if (!source || !columns?.length) return void 0;
1984
+ const visibleCols = sort ? sortByPosition(columns.filter(visible)) : columns.filter(visible);
1985
+ if (!visibleCols.length) return void 0;
1986
+ const schemaCols = (schemaVisible ? columns.filter((c) => visible(c) || schemaVisible(c)) : visibleCols).filter((c) => !isRelation(c));
1987
+ const jsonSchema = source(schemaCols);
1988
+ if (!jsonSchema) return void 0;
1587
1989
  applySchemaTransforms(jsonSchema);
1588
1990
  injectFieldDefaults(jsonSchema, visibleCols);
1589
1991
  if (schemaVisible) {
@@ -1608,26 +2010,31 @@ var emptyTableView = /* @__PURE__ */ __name(() => ({
1608
2010
  ui_schema: buildTableUiSchema([]),
1609
2011
  columns: []
1610
2012
  }), "emptyTableView");
1611
- var buildViews = /* @__PURE__ */ __name((schema, columns) => {
2013
+ var buildViewsWithSource = /* @__PURE__ */ __name((source, columns) => {
1612
2014
  const views = {};
1613
- const table = buildView(schema, columns?.map((c) => columnForContext(c, "table")), (c) => !c.hiddenInTable, buildTableUiSchema, true);
2015
+ const table = buildView(source, columns?.map((c) => columnForContext(c, "table")), (c) => !c.hiddenInTable, buildTableUiSchema, true);
1614
2016
  if (table) {
1615
2017
  table.defaultSort = resolveDefaultSort(table.columns, columns);
1616
2018
  views.table = table;
1617
2019
  } else if (columns?.length) {
1618
2020
  views.table = emptyTableView();
1619
2021
  }
1620
- const form = buildView(schema, columns, (c) => !c.hiddenInForm, buildFormUiSchema, true, (c) => !c.idField && (c.createable === true || c.updateable === true));
1621
- if (form) views.form = form;
1622
- const filter = buildView(schema, columns, (c) => !!c.filterable, buildFormUiSchema, true);
2022
+ const form = buildView(source, columns, (c) => !c.hiddenInForm, buildFormUiSchema, true, (c) => !c.idField && (c.createable === true || c.updateable === true));
2023
+ if (form) {
2024
+ applyRequiredColumns(form.json_schema, columns);
2025
+ views.form = form;
2026
+ }
2027
+ const filter = buildView(source, columns, (c) => !!c.filterable, buildFormUiSchema, true);
1623
2028
  if (filter) {
1624
2029
  patchFilterProperties(filter.json_schema, columns?.filter((c) => !!c.filterable));
1625
2030
  views.filter = filter;
1626
2031
  }
1627
- const view = buildView(schema, columns?.map((c) => columnForContext(c, "view")), (c) => !c.hiddenInView, buildFormUiSchema, true);
2032
+ const view = buildView(source, columns?.map((c) => columnForContext(c, "view")), (c) => !c.hiddenInView, buildFormUiSchema, true);
1628
2033
  if (view) views.view = view;
1629
2034
  return Object.keys(views).length ? views : void 0;
1630
- }, "buildViews");
2035
+ }, "buildViewsWithSource");
2036
+ var buildViews = /* @__PURE__ */ __name((schema, columns) => buildViewsWithSource(schema ? zodSchemaSource(schema) : void 0, columns), "buildViews");
2037
+ var buildViewsFromColumnTypes = /* @__PURE__ */ __name((columns) => buildViewsWithSource(columnTypeSchemaSource, columns), "buildViewsFromColumnTypes");
1631
2038
  var buildViewsFromColumns = /* @__PURE__ */ __name((columns) => {
1632
2039
  if (!columns?.length) return void 0;
1633
2040
  const buildJsonSchema = /* @__PURE__ */ __name((cols) => {
@@ -1656,6 +2063,8 @@ var buildViewsFromColumns = /* @__PURE__ */ __name((columns) => {
1656
2063
  type: "string",
1657
2064
  title: c.label ?? c.id
1658
2065
  };
2066
+ } else if (c.type !== void 0) {
2067
+ properties[c.id] = columnToJsonSchemaProperty(c);
1659
2068
  } else {
1660
2069
  const opts = c.fieldInput?.options;
1661
2070
  const isObject = opts?.emitObject === true || c.fieldInput?.type === "autocomplete";
@@ -1718,7 +2127,10 @@ var buildViewsFromColumns = /* @__PURE__ */ __name((columns) => {
1718
2127
  }
1719
2128
  const formCols = sortByPosition(columns.filter((c) => !c.hiddenInForm));
1720
2129
  const form = makeView(formCols, buildFormUiSchema);
1721
- if (form) views.form = form;
2130
+ if (form) {
2131
+ applyRequiredColumns(form.json_schema, formCols);
2132
+ views.form = form;
2133
+ }
1722
2134
  const viewCols = sortByPosition(columns.filter((c) => !c.hiddenInView).map((c) => columnForContext(c, "view")));
1723
2135
  const viewView = makeView(viewCols, buildFormUiSchema);
1724
2136
  if (viewView) views.view = viewView;
@@ -1726,41 +2138,41 @@ var buildViewsFromColumns = /* @__PURE__ */ __name((columns) => {
1726
2138
  }, "buildViewsFromColumns");
1727
2139
 
1728
2140
  // ../crouton-core/src/lib/view/view.schema.ts
1729
- var import_zod19 = require("zod");
1730
- var ViewColumnConfigSchema = import_zod19.z.object({
1731
- id: import_zod19.z.string(),
1732
- label: import_zod19.z.string().optional(),
1733
- sortable: import_zod19.z.boolean().optional(),
1734
- searchable: import_zod19.z.boolean().optional(),
2141
+ var import_zod22 = require("zod");
2142
+ var ViewColumnConfigSchema = import_zod22.z.object({
2143
+ id: import_zod22.z.string(),
2144
+ label: import_zod22.z.string().optional(),
2145
+ sortable: import_zod22.z.boolean().optional(),
2146
+ searchable: import_zod22.z.boolean().optional(),
1735
2147
  fieldInput: FieldInputSchema.optional()
1736
2148
  });
1737
- var ViewConfigSchema = import_zod19.z.object({
1738
- json_schema: import_zod19.z.record(import_zod19.z.string(), import_zod19.z.unknown()),
1739
- ui_schema: import_zod19.z.record(import_zod19.z.string(), import_zod19.z.unknown()),
1740
- columns: import_zod19.z.array(ViewColumnConfigSchema),
1741
- defaultSort: import_zod19.z.string().optional()
2149
+ var ViewConfigSchema = import_zod22.z.object({
2150
+ json_schema: import_zod22.z.record(import_zod22.z.string(), import_zod22.z.unknown()),
2151
+ ui_schema: import_zod22.z.record(import_zod22.z.string(), import_zod22.z.unknown()),
2152
+ columns: import_zod22.z.array(ViewColumnConfigSchema),
2153
+ defaultSort: import_zod22.z.string().optional()
1742
2154
  });
1743
2155
 
1744
2156
  // src/lib/crouton-api.module.ts
1745
- var import_common19 = require("@nestjs/common");
2157
+ var import_common21 = require("@nestjs/common");
1746
2158
  var import_core = require("@nestjs/core");
1747
2159
 
1748
2160
  // src/lib/crud/app-layout/app-layout.types.ts
1749
- var import_zod20 = require("zod");
1750
- var SidebarLeafSchema = import_zod20.z.object({
1751
- kind: import_zod20.z.literal("item").default("item"),
1752
- id: import_zod20.z.string(),
1753
- label: import_zod20.z.string(),
1754
- position: import_zod20.z.number().optional()
2161
+ var import_zod23 = require("zod");
2162
+ var SidebarLeafSchema = import_zod23.z.object({
2163
+ kind: import_zod23.z.literal("item").default("item"),
2164
+ id: import_zod23.z.string(),
2165
+ label: import_zod23.z.string(),
2166
+ position: import_zod23.z.number().optional()
1755
2167
  });
1756
- var SidebarGroupSchema2 = import_zod20.z.object({
1757
- kind: import_zod20.z.literal("group").default("group"),
1758
- id: import_zod20.z.string(),
1759
- label: import_zod20.z.string(),
1760
- position: import_zod20.z.number().optional(),
1761
- children: import_zod20.z.array(SidebarLeafSchema).default([])
2168
+ var SidebarGroupSchema2 = import_zod23.z.object({
2169
+ kind: import_zod23.z.literal("group").default("group"),
2170
+ id: import_zod23.z.string(),
2171
+ label: import_zod23.z.string(),
2172
+ position: import_zod23.z.number().optional(),
2173
+ children: import_zod23.z.array(SidebarLeafSchema).default([])
1762
2174
  });
1763
- var SidebarNodeSchema = import_zod20.z.discriminatedUnion("kind", [
2175
+ var SidebarNodeSchema = import_zod23.z.discriminatedUnion("kind", [
1764
2176
  SidebarLeafSchema,
1765
2177
  SidebarGroupSchema2
1766
2178
  ]);
@@ -1773,7 +2185,10 @@ var byPosition = /* @__PURE__ */ __name((a, b) => {
1773
2185
  return a.label.localeCompare(b.label);
1774
2186
  }, "byPosition");
1775
2187
  var buildLayoutPayload = /* @__PURE__ */ __name((configs, sidebarGroups = {}, title, autoSave = true, isDev = false) => {
1776
- const visible = configs.filter((c) => c.sidebar?.hide !== true && c.views?.["table"]);
2188
+ const visible = configs.filter((c) => c.sidebar?.hide !== true && c.views?.["table"] && // A resource nested under a parent has no standalone route, so a sidebar
2189
+ // entry for it would resolve to `<name>/schemas` and 404. It is reached
2190
+ // from its parent's detail view instead.
2191
+ !c.parent);
1777
2192
  const topLevel = [];
1778
2193
  const groupMap = new Map(Object.entries(sidebarGroups).map(([slug, cfg]) => [
1779
2194
  slug,
@@ -2026,51 +2441,51 @@ CroutonValidationExceptionFilter = _ts_decorate3([
2026
2441
  ], CroutonValidationExceptionFilter);
2027
2442
 
2028
2443
  // src/lib/crud/crud-controller.factory.ts
2029
- var import_common16 = require("@nestjs/common");
2444
+ var import_common18 = require("@nestjs/common");
2030
2445
  var import_swagger11 = require("@nestjs/swagger");
2031
2446
 
2032
2447
  // src/lib/crud/action/action.types.ts
2033
- var import_zod21 = require("zod");
2034
- var ActionMetadataSchema = import_zod21.z.object({
2448
+ var import_zod24 = require("zod");
2449
+ var ActionMetadataSchema = import_zod24.z.object({
2035
2450
  /** URL segment used in the endpoint. */
2036
- id: import_zod21.z.string(),
2451
+ id: import_zod24.z.string(),
2037
2452
  /** Human-readable label shown as a button. */
2038
- label: import_zod21.z.string().optional(),
2453
+ label: import_zod24.z.string().optional(),
2039
2454
  /** MDI icon name, e.g. `"mdi:open-in-new"`. */
2040
- icon: import_zod21.z.string().optional(),
2455
+ icon: import_zod24.z.string().optional(),
2041
2456
  /** Tooltip text. Falls back to `label` when omitted. */
2042
- tooltip: import_zod21.z.string().optional(),
2457
+ tooltip: import_zod24.z.string().optional(),
2043
2458
  /** Per-row condition — button is hidden when false. */
2044
- condition: import_zod21.z.custom().optional()
2459
+ condition: import_zod24.z.custom().optional()
2045
2460
  });
2046
2461
  var ResourceLinkActionSchema = ActionMetadataSchema.extend({
2047
- type: import_zod21.z.literal("link"),
2462
+ type: import_zod24.z.literal("link"),
2048
2463
  /** URL to open. May contain `{id}` or `{env.VAR}` placeholders. */
2049
- href: import_zod21.z.string()
2464
+ href: import_zod24.z.string()
2050
2465
  });
2051
2466
  var ResourceRowProcedureActionSchema = ActionMetadataSchema.extend({
2052
- type: import_zod21.z.literal("procedure").optional(),
2467
+ type: import_zod24.z.literal("procedure").optional(),
2053
2468
  /** HTTP method for the endpoint. Defaults to `"post"`. */
2054
- method: import_zod21.z.string().optional(),
2469
+ method: import_zod24.z.string().optional(),
2055
2470
  /** Static data payload merged into the request body by the frontend. */
2056
- data: import_zod21.z.record(import_zod21.z.string(), import_zod21.z.unknown()).optional(),
2471
+ data: import_zod24.z.record(import_zod24.z.string(), import_zod24.z.unknown()).optional(),
2057
2472
  /** Procedure called with `(prisma, recordId)`. */
2058
- procedure: import_zod21.z.custom((v) => typeof v === "function")
2473
+ procedure: import_zod24.z.custom((v) => typeof v === "function")
2059
2474
  });
2060
2475
  var ResourceTableProcedureActionSchema = ActionMetadataSchema.extend({
2061
- type: import_zod21.z.literal("procedure").optional(),
2476
+ type: import_zod24.z.literal("procedure").optional(),
2062
2477
  /** HTTP method for the endpoint. Defaults to `"post"`. */
2063
- method: import_zod21.z.string().optional(),
2478
+ method: import_zod24.z.string().optional(),
2064
2479
  /** Static data payload merged into the request body by the frontend. */
2065
- data: import_zod21.z.record(import_zod21.z.string(), import_zod21.z.unknown()).optional(),
2480
+ data: import_zod24.z.record(import_zod24.z.string(), import_zod24.z.unknown()).optional(),
2066
2481
  /** Procedure called with `(prisma)` — no record id. */
2067
- procedure: import_zod21.z.custom((v) => typeof v === "function")
2482
+ procedure: import_zod24.z.custom((v) => typeof v === "function")
2068
2483
  });
2069
- var ResourceRowActionSchema = import_zod21.z.union([
2484
+ var ResourceRowActionSchema = import_zod24.z.union([
2070
2485
  ResourceRowProcedureActionSchema,
2071
2486
  ResourceLinkActionSchema
2072
2487
  ]);
2073
- var ResourceTableActionSchema = import_zod21.z.union([
2488
+ var ResourceTableActionSchema = import_zod24.z.union([
2074
2489
  ResourceTableProcedureActionSchema,
2075
2490
  ResourceLinkActionSchema
2076
2491
  ]);
@@ -2093,7 +2508,7 @@ var findModule = /* @__PURE__ */ __name((dir, name) => {
2093
2508
  return void 0;
2094
2509
  }, "findModule");
2095
2510
  var IS_VITE = typeof globalThis.__vite_ssr_import__ === "function";
2096
- var importDefault = /* @__PURE__ */ __name(async (filePath) => {
2511
+ var importDefault = /* @__PURE__ */ __name(async (filePath, onError) => {
2097
2512
  try {
2098
2513
  if (IS_VITE) {
2099
2514
  const importPath = IS_DEV ? `${filePath}?t=${Date.now()}` : filePath;
@@ -2103,7 +2518,9 @@ var importDefault = /* @__PURE__ */ __name(async (filePath) => {
2103
2518
  const mod = _require(filePath);
2104
2519
  return mod.default;
2105
2520
  }
2106
- } catch {
2521
+ } catch (error) {
2522
+ if (onError) onError(error, filePath);
2523
+ else console.error(`[crouton] Failed to import ${filePath}:`, error);
2107
2524
  return void 0;
2108
2525
  }
2109
2526
  }, "importDefault");
@@ -2149,9 +2566,422 @@ var schemaFor = /* @__PURE__ */ __name((def2, op) => {
2149
2566
  }, "schemaFor");
2150
2567
  var upsertOnFor = /* @__PURE__ */ __name((def2) => def2.upsert?.upsertOn, "upsertOnFor");
2151
2568
 
2152
- // src/lib/crud/read.repository.ts
2569
+ // src/lib/crud/custom-repository/custom-repository.types.ts
2570
+ var import_zod25 = require("zod");
2571
+ var CUSTOM_OPS = [
2572
+ "findAll",
2573
+ "findOne",
2574
+ "create",
2575
+ "update",
2576
+ "patch",
2577
+ "delete"
2578
+ ];
2579
+ var PARENT_METHOD = {
2580
+ findAll: "findAllByParent",
2581
+ findOne: "findOneByParent",
2582
+ create: "createByParent",
2583
+ update: "updateByParent",
2584
+ patch: "patchByParent",
2585
+ delete: "deleteByParent"
2586
+ };
2587
+ var CustomRepositorySchema = import_zod25.z.custom((value) => typeof value === "object" && value !== null);
2588
+
2589
+ // src/lib/crud/resource/resource-load-errors.registry.ts
2590
+ var ResourceLoadErrorsRegistry = class ResourceLoadErrorsRegistry2 {
2591
+ static {
2592
+ __name(this, "ResourceLoadErrorsRegistry");
2593
+ }
2594
+ errors = [];
2595
+ record(e) {
2596
+ this.errors.push(e);
2597
+ }
2598
+ getAll() {
2599
+ return [
2600
+ ...this.errors
2601
+ ];
2602
+ }
2603
+ clear() {
2604
+ this.errors = [];
2605
+ }
2606
+ };
2607
+ var resourceLoadErrorsRegistry = new ResourceLoadErrorsRegistry();
2608
+
2609
+ // src/lib/crud/custom-repository/custom-repository.loader.ts
2610
+ var REPOSITORY_MODULE = "repository";
2611
+ var loadCustomRepository = /* @__PURE__ */ __name(async (basePath, resourceName) => {
2612
+ const file = findModule(basePath, REPOSITORY_MODULE);
2613
+ if (!file) return void 0;
2614
+ let failure;
2615
+ const repository = await importDefault(file, (error) => {
2616
+ failure = error;
2617
+ });
2618
+ if (failure !== void 0) {
2619
+ resourceLoadErrorsRegistry.record({
2620
+ name: resourceName,
2621
+ path: file,
2622
+ error: `Failed to import ${REPOSITORY_MODULE}: ${failure instanceof Error ? failure.message : String(failure)}`
2623
+ });
2624
+ return void 0;
2625
+ }
2626
+ if (repository && typeof repository !== "object") {
2627
+ resourceLoadErrorsRegistry.record({
2628
+ name: resourceName,
2629
+ path: file,
2630
+ error: `${REPOSITORY_MODULE} must default-export an object of operation functions.`
2631
+ });
2632
+ return void 0;
2633
+ }
2634
+ return repository;
2635
+ }, "loadCustomRepository");
2636
+ var loadSubResourceRepositories = /* @__PURE__ */ __name(async (subResources, parentName) => {
2637
+ for (const sub of subResources) {
2638
+ if (sub.childKind !== "custom") continue;
2639
+ if (!sub.childDir) {
2640
+ resourceLoadErrorsRegistry.record({
2641
+ name: parentName,
2642
+ path: sub.childRoute,
2643
+ error: `Sub-resource "${sub.childRoute}" is a custom resource but its directory could not be resolved, so its repository.ts cannot be loaded. Check the relation column's "resource" path.`
2644
+ });
2645
+ continue;
2646
+ }
2647
+ const repository = await loadCustomRepository(sub.childDir, `${parentName}.${sub.childRoute}`);
2648
+ if (repository) sub.repository = repository;
2649
+ }
2650
+ }, "loadSubResourceRepositories");
2651
+
2652
+ // src/lib/crud/custom-repository/custom-repository.adapter.ts
2153
2653
  var import_common4 = require("@nestjs/common");
2154
2654
 
2655
+ // src/lib/crud/constants.ts
2656
+ var PRISMA_NOT_FOUND_CODE = "P2025";
2657
+ var DEFAULT_ID_TYPE = "string";
2658
+ var DEFAULT_ID_FIELD = "id";
2659
+
2660
+ // src/lib/crud/hooks/hooks.types.ts
2661
+ var import_zod26 = require("zod");
2662
+ var WriteOpSchema = import_zod26.z.enum([
2663
+ "create",
2664
+ "update",
2665
+ "patch",
2666
+ "upsert",
2667
+ "delete"
2668
+ ]);
2669
+ var ReadOpSchema = import_zod26.z.enum([
2670
+ "findAll",
2671
+ "findOne"
2672
+ ]);
2673
+ var ResourceHooksSchema = import_zod26.z.object({
2674
+ beforeWrite: import_zod26.z.custom().optional(),
2675
+ afterWrite: import_zod26.z.custom().optional(),
2676
+ afterRead: import_zod26.z.custom().optional()
2677
+ });
2678
+
2679
+ // src/lib/crud/hooks/hooks.loader.ts
2680
+ var import_node_path4 = require("path");
2681
+ var loadResourceHooks = /* @__PURE__ */ __name(async (basePath) => {
2682
+ const file = findModule(basePath, "hooks");
2683
+ return file ? importDefault(file) : void 0;
2684
+ }, "loadResourceHooks");
2685
+ var loadSubResourceHooks = /* @__PURE__ */ __name(async (subResources, basePath) => {
2686
+ for (const sub of subResources) {
2687
+ const file = (sub.name ? findModule((0, import_node_path4.join)(basePath, "hooks"), sub.name) : void 0) ?? (sub.childDir ? findModule(sub.childDir, "hooks") : void 0);
2688
+ if (!file) continue;
2689
+ const hooks = await importDefault(file);
2690
+ if (hooks) sub.hooks = hooks;
2691
+ }
2692
+ }, "loadSubResourceHooks");
2693
+
2694
+ // src/lib/crud/resource/valueLabel.apply.ts
2695
+ var applyValueLabelColumns = /* @__PURE__ */ __name((row, cols) => {
2696
+ if (!row || !cols?.length) return row;
2697
+ const out = {
2698
+ ...row
2699
+ };
2700
+ for (const { field, values } of cols) {
2701
+ if (field in out) out[field] = toValueLabel(out[field], values);
2702
+ }
2703
+ return out;
2704
+ }, "applyValueLabelColumns");
2705
+ var normalizeValueLabels = /* @__PURE__ */ __name((data, cols) => {
2706
+ if (!data || typeof data !== "object" || Array.isArray(data) || !cols?.length) return data;
2707
+ const out = {
2708
+ ...data
2709
+ };
2710
+ for (const { field } of cols) {
2711
+ if (field in out) out[field] = fromValueLabel(out[field]);
2712
+ }
2713
+ return out;
2714
+ }, "normalizeValueLabels");
2715
+
2716
+ // src/lib/crud/hooks/hooks.apply.ts
2717
+ var decorateRows = /* @__PURE__ */ __name(async (rows, op, target, prisma, request, parent) => {
2718
+ const hook = target.hooks?.afterRead;
2719
+ const hooked = hook ? await Promise.all(rows.map((row) => hook(row, {
2720
+ prisma,
2721
+ op,
2722
+ request,
2723
+ ...parent && {
2724
+ parent
2725
+ }
2726
+ }))) : rows;
2727
+ const cols = target.valueLabelColumns;
2728
+ return cols?.length ? hooked.map((r) => applyValueLabelColumns(r, cols)) : hooked;
2729
+ }, "decorateRows");
2730
+ var decorateRow = /* @__PURE__ */ __name(async (row, op, target, prisma, request, parent) => {
2731
+ const hook = target.hooks?.afterRead;
2732
+ return hook ? hook(row, {
2733
+ prisma,
2734
+ op,
2735
+ request,
2736
+ ...parent && {
2737
+ parent
2738
+ }
2739
+ }) : row;
2740
+ }, "decorateRow");
2741
+ var prepareWrite = /* @__PURE__ */ __name(async (data, op, target, prisma, id, request, parent) => {
2742
+ const normalized = normalizeValueLabels(data, target.valueLabelColumns);
2743
+ const hook = target.hooks?.beforeWrite;
2744
+ return hook ? hook(normalized, {
2745
+ prisma,
2746
+ op,
2747
+ id,
2748
+ request,
2749
+ ...parent && {
2750
+ parent
2751
+ }
2752
+ }) : normalized;
2753
+ }, "prepareWrite");
2754
+ var postWrite = /* @__PURE__ */ __name(async (result, op, target, prisma, id, request, parent) => {
2755
+ const hook = target.hooks?.afterWrite;
2756
+ return hook ? hook(result, {
2757
+ prisma,
2758
+ op,
2759
+ id,
2760
+ request,
2761
+ ...parent && {
2762
+ parent
2763
+ }
2764
+ }) : result;
2765
+ }, "postWrite");
2766
+
2767
+ // src/lib/crud/custom-repository/custom-repository.adapter.ts
2768
+ var unsupported = /* @__PURE__ */ __name((config, op) => {
2769
+ const method = config.parent ? PARENT_METHOD[op] : op;
2770
+ throw new import_common4.NotImplementedException(`Resource "${config.name}" enables "${op}" but its repository.ts does not implement "${method}".`);
2771
+ }, "unsupported");
2772
+ var createCustomRepository = /* @__PURE__ */ __name((prisma, config, dataSources, repository) => {
2773
+ const repo = repository ?? {};
2774
+ const idField = config.idField ?? DEFAULT_ID_FIELD;
2775
+ const toId = /* @__PURE__ */ __name((id) => (config.idType ?? DEFAULT_ID_TYPE) === "number" ? +id : String(id), "toId");
2776
+ const parentRef = config.parent;
2777
+ const parentIdFrom = /* @__PURE__ */ __name((request) => {
2778
+ const raw = request?.params?.[parentRef.param];
2779
+ if (raw === void 0 || raw === null || raw === "") {
2780
+ throw new import_common4.BadRequestException(`Resource "${config.name}" is nested under "${parentRef.route}" but no "${parentRef.param}" was supplied.`);
2781
+ }
2782
+ return (parentRef.idType ?? "string") === "number" ? +raw : String(raw);
2783
+ }, "parentIdFrom");
2784
+ const parentCtx = /* @__PURE__ */ __name((request, parentId2) => ({
2785
+ parent: {
2786
+ route: parentRef.route,
2787
+ param: parentRef.param,
2788
+ id: parentId2
2789
+ }
2790
+ }), "parentCtx");
2791
+ const parentHookCtx = /* @__PURE__ */ __name((request) => {
2792
+ if (!parentRef) return void 0;
2793
+ const raw = request?.params?.[parentRef.param];
2794
+ if (raw === void 0 || raw === null || raw === "") return void 0;
2795
+ return {
2796
+ route: parentRef.route,
2797
+ param: parentRef.param,
2798
+ id: (parentRef.idType ?? "string") === "number" ? +raw : String(raw)
2799
+ };
2800
+ }, "parentHookCtx");
2801
+ const ctx = /* @__PURE__ */ __name((op, params, id, request) => ({
2802
+ prisma,
2803
+ dataSources,
2804
+ config,
2805
+ op,
2806
+ offset: params ? offsetOf(params) : 0,
2807
+ ...id !== void 0 && {
2808
+ id
2809
+ },
2810
+ ...request !== void 0 && {
2811
+ request
2812
+ }
2813
+ }), "ctx");
2814
+ const findAllWithCount = /* @__PURE__ */ __name(async (params, request) => {
2815
+ let result;
2816
+ if (parentRef) {
2817
+ if (!repo.findAllByParent) unsupported(config, "findAll");
2818
+ const parentId2 = parentIdFrom(request);
2819
+ result = await repo.findAllByParent(parentId2, params, {
2820
+ ...ctx("findAll", params, void 0, request),
2821
+ ...parentCtx(request, parentId2)
2822
+ });
2823
+ } else {
2824
+ if (!repo.findAll) unsupported(config, "findAll");
2825
+ result = await repo.findAll(params, ctx("findAll", params, void 0, request));
2826
+ }
2827
+ const data = await decorateRows(result?.data ?? [], "findAll", config, prisma, request, parentHookCtx(request));
2828
+ return {
2829
+ data,
2830
+ count: result?.count ?? data.length
2831
+ };
2832
+ }, "findAllWithCount");
2833
+ const findOne = /* @__PURE__ */ __name(async (id, request) => {
2834
+ let row;
2835
+ if (parentRef) {
2836
+ if (!repo.findOneByParent) unsupported(config, "findOne");
2837
+ const parentId2 = parentIdFrom(request);
2838
+ row = await repo.findOneByParent(parentId2, toId(id), {
2839
+ ...ctx("findOne", void 0, id, request),
2840
+ ...parentCtx(request, parentId2)
2841
+ });
2842
+ } else {
2843
+ if (!repo.findOne) unsupported(config, "findOne");
2844
+ row = await repo.findOne(toId(id), ctx("findOne", void 0, id, request));
2845
+ }
2846
+ if (row === null || row === void 0) {
2847
+ throw new import_common4.NotFoundException(`${config.name} with id ${id} not found`);
2848
+ }
2849
+ return decorateRow(row, "findOne", config, prisma, request, parentHookCtx(request));
2850
+ }, "findOne");
2851
+ const write = /* @__PURE__ */ __name(async (op, data, id, request) => {
2852
+ const coercedId = id !== void 0 ? toId(id) : void 0;
2853
+ const prepared = /* @__PURE__ */ __name(() => prepareWrite(data, op, config, prisma, coercedId, request, parentHookCtx(request)), "prepared");
2854
+ let result;
2855
+ if (parentRef) {
2856
+ const fn = op === "create" ? repo.createByParent : op === "update" ? repo.updateByParent : repo.patchByParent ?? repo.updateByParent;
2857
+ if (!fn) unsupported(config, op);
2858
+ const parentId2 = parentIdFrom(request);
2859
+ const nestedCtx = {
2860
+ ...ctx(op, void 0, coercedId, request),
2861
+ ...parentCtx(request, parentId2)
2862
+ };
2863
+ const body = await prepared();
2864
+ result = op === "create" ? await fn(parentId2, body, nestedCtx) : await fn(parentId2, coercedId, body, nestedCtx);
2865
+ } else {
2866
+ const fn = op === "create" ? repo.create : op === "update" ? repo.update : repo.patch ?? repo.update;
2867
+ if (!fn) unsupported(config, op);
2868
+ const body = await prepared();
2869
+ result = op === "create" ? await fn(body, ctx("create", void 0, void 0, request)) : await fn(coercedId, body, ctx(op, void 0, coercedId, request));
2870
+ }
2871
+ return postWrite(result, op, config, prisma, coercedId, request, parentHookCtx(request));
2872
+ }, "write");
2873
+ const notAChildRepository = /* @__PURE__ */ __name(async () => {
2874
+ throw new import_common4.NotImplementedException(`Resource "${config.name}" is a custom resource; nested sub-resource routes are not supported. Expose the child collection as its own resource instead.`);
2875
+ }, "notAChildRepository");
2876
+ return {
2877
+ prisma,
2878
+ // Preferred by register-findall: one round trip returns rows and count.
2879
+ findAllWithCount,
2880
+ findAll: /* @__PURE__ */ __name(async (params, request) => (await findAllWithCount(params, request)).data, "findAll"),
2881
+ count: /* @__PURE__ */ __name(async (filter) => (await findAllWithCount({
2882
+ page: 1,
2883
+ pageSize: 1,
2884
+ sort: idField,
2885
+ sortDir: "asc",
2886
+ filter: filter ?? []
2887
+ })).count, "count"),
2888
+ findOne,
2889
+ create: /* @__PURE__ */ __name((data, request) => write("create", data, void 0, request), "create"),
2890
+ update: /* @__PURE__ */ __name((id, data, request) => write("update", data, id, request), "update"),
2891
+ patch: /* @__PURE__ */ __name((id, data, request) => write("patch", data, id, request), "patch"),
2892
+ delete: /* @__PURE__ */ __name(async (id, request) => {
2893
+ const coercedId = toId(id);
2894
+ let result;
2895
+ if (parentRef) {
2896
+ if (!repo.deleteByParent) unsupported(config, "delete");
2897
+ const parentId2 = parentIdFrom(request);
2898
+ result = await repo.deleteByParent(parentId2, coercedId, {
2899
+ ...ctx("delete", void 0, coercedId, request),
2900
+ ...parentCtx(request, parentId2)
2901
+ });
2902
+ } else {
2903
+ if (!repo.delete) unsupported(config, "delete");
2904
+ result = await repo.delete(coercedId, ctx("delete", void 0, coercedId, request));
2905
+ }
2906
+ return postWrite(result, "delete", config, prisma, coercedId, request, parentHookCtx(request));
2907
+ }, "delete"),
2908
+ upsert: /* @__PURE__ */ __name(async () => {
2909
+ throw new import_common4.NotImplementedException(`Resource "${config.name}" is a custom resource; upsert is not part of the repository contract.`);
2910
+ }, "upsert"),
2911
+ upsertMany: /* @__PURE__ */ __name(async () => {
2912
+ throw new import_common4.NotImplementedException(`Resource "${config.name}" is a custom resource; upsert is not part of the repository contract.`);
2913
+ }, "upsertMany"),
2914
+ findAllByParent: notAChildRepository,
2915
+ findOneChild: notAChildRepository,
2916
+ createChild: notAChildRepository,
2917
+ updateChild: notAChildRepository,
2918
+ deleteChild: notAChildRepository
2919
+ };
2920
+ }, "createCustomRepository");
2921
+
2922
+ // src/lib/crud/custom-repository/custom-repository.validate.ts
2923
+ var validateCustomRepository = /* @__PURE__ */ __name((config, repository) => {
2924
+ const definition = resolveDefinition(config);
2925
+ const enabled = CUSTOM_OPS.filter((op) => isOperationEnabled(definition, op));
2926
+ const nested = !!config.parent;
2927
+ const methodFor = /* @__PURE__ */ __name((op) => nested ? PARENT_METHOD[op] : op, "methodFor");
2928
+ if (!repository) {
2929
+ return `No repository.ts found. A custom resource implements its own data access; create ${config.name}/repository.ts with a default export implementing: ${enabled.map(methodFor).join(", ") || "no operations"}.`;
2930
+ }
2931
+ const implemented = /* @__PURE__ */ __name((name) => typeof repository[name] === "function", "implemented");
2932
+ const missing = enabled.filter((op) => op === "patch" ? !implemented(methodFor("patch")) && !implemented(methodFor("update")) : !implemented(methodFor(op)));
2933
+ if (missing.length) {
2934
+ const names = missing.map(methodFor);
2935
+ return `repository.ts does not implement ${names.join(", ")}` + (nested ? ` (this resource is nested under "${config.parent.route}", so it implements the parent-aware operations). ` : ". ") + `Either implement them or disable the operation in resource.json ("operations": { "${missing[0]}": false }).`;
2936
+ }
2937
+ return void 0;
2938
+ }, "validateCustomRepository");
2939
+
2940
+ // src/lib/crud/read.repository.ts
2941
+ var import_common6 = require("@nestjs/common");
2942
+
2943
+ // src/lib/crud/custom-repository/child-delegate.ts
2944
+ var import_common5 = require("@nestjs/common");
2945
+ var childRepositoryFn = /* @__PURE__ */ __name((sub, op, parentName) => {
2946
+ const repo = sub.repository;
2947
+ const method = PARENT_METHOD[op];
2948
+ if (!repo) {
2949
+ throw new import_common5.NotImplementedException(`Sub-resource "${sub.childRoute}" of "${parentName}" is a custom resource but no repository.ts was loaded for it.`);
2950
+ }
2951
+ const fn = repo[method] ?? (op === "patch" ? repo[PARENT_METHOD.update] : void 0);
2952
+ if (typeof fn !== "function") {
2953
+ throw new import_common5.NotImplementedException(`Sub-resource "${sub.childRoute}" of "${parentName}" does not implement "${method}" in its repository.ts.`);
2954
+ }
2955
+ return fn.bind(repo);
2956
+ }, "childRepositoryFn");
2957
+ var childCtx = /* @__PURE__ */ __name(({ parentConfig, prisma, op, parentId: parentId2, params, id, request }) => ({
2958
+ prisma,
2959
+ dataSources: {
2960
+ resolve: /* @__PURE__ */ __name(() => prisma, "resolve"),
2961
+ entries: /* @__PURE__ */ __name(() => [], "entries")
2962
+ },
2963
+ config: parentConfig,
2964
+ op,
2965
+ offset: params ? offsetOf(params) : 0,
2966
+ ...id !== void 0 && {
2967
+ id
2968
+ },
2969
+ ...request !== void 0 && {
2970
+ request
2971
+ },
2972
+ // Served under the parent's `:id`, so that is the param name here — unlike a
2973
+ // top-level nested resource, which names its own param.
2974
+ parent: {
2975
+ route: parentConfig.route,
2976
+ param: "id",
2977
+ id: parentId2
2978
+ }
2979
+ }), "childCtx");
2980
+ var parentIdFromRequest = /* @__PURE__ */ __name((request, fallback) => {
2981
+ const raw = request?.params?.id;
2982
+ return raw === void 0 || raw === null || raw === "" ? fallback : raw;
2983
+ }, "parentIdFromRequest");
2984
+
2155
2985
  // src/lib/crud/sql.helpers.ts
2156
2986
  var castExpression = /* @__PURE__ */ __name((col) => {
2157
2987
  if (col.type === "string") return `(${col.sqlExpression})`;
@@ -2168,17 +2998,17 @@ var coerceColumnValue = /* @__PURE__ */ __name((col) => {
2168
2998
  if (col.type === "string") return (v) => v === void 0 || v === null ? null : String(v);
2169
2999
  return (v) => Number(v ?? 0);
2170
3000
  }, "coerceColumnValue");
2171
- var buildCalculatedColumnSql = /* @__PURE__ */ __name((col, tableName, ids) => {
3001
+ var buildCalculatedColumnSql = /* @__PURE__ */ __name((col, tableName, ids, idField) => {
2172
3002
  const alias = col.alias ?? col.id;
2173
3003
  const placeholders = ids.map((_, i) => `$${i + 1}`).join(", ");
2174
- return `SELECT main.id, ${castExpression(col)} AS "${alias}" FROM "${tableName}" main WHERE main.id IN (${placeholders})`;
3004
+ return `SELECT main."${idField}" AS id, ${castExpression(col)} AS "${alias}" FROM "${tableName}" main WHERE main."${idField}" IN (${placeholders})`;
2175
3005
  }, "buildCalculatedColumnSql");
2176
- var mergeCalculatedColumnsForRows = /* @__PURE__ */ __name(async (rows, calcCols, tableName, prisma) => {
3006
+ var mergeCalculatedColumnsForRows = /* @__PURE__ */ __name(async (rows, calcCols, tableName, prisma, idField = "id") => {
2177
3007
  if (!calcCols.length || !rows.length) return rows;
2178
- const ids = rows.map((r) => r.id);
3008
+ const ids = rows.map((r) => r[idField]);
2179
3009
  const results = await Promise.all(calcCols.map(async (col) => {
2180
3010
  const alias = col.alias ?? col.id;
2181
- const sql = buildCalculatedColumnSql(col, tableName, ids);
3011
+ const sql = buildCalculatedColumnSql(col, tableName, ids, idField);
2182
3012
  const defaultValue = defaultValueForType(col);
2183
3013
  const coerce = coerceColumnValue(col);
2184
3014
  try {
@@ -2203,7 +3033,7 @@ var mergeCalculatedColumnsForRows = /* @__PURE__ */ __name(async (rows, calcCols
2203
3033
  return rows.map((row) => {
2204
3034
  const extra = {};
2205
3035
  for (const { id, defaultValue, map } of results) {
2206
- extra[id] = map[String(row.id)] ?? defaultValue;
3036
+ extra[id] = map[String(row[idField])] ?? defaultValue;
2207
3037
  }
2208
3038
  return {
2209
3039
  ...row,
@@ -2263,7 +3093,7 @@ var buildChildSortClause = /* @__PURE__ */ __name((sort, sortDir) => {
2263
3093
  }, {});
2264
3094
  }, "buildChildSortClause");
2265
3095
  var buildFindOneIncludes = /* @__PURE__ */ __name((subResources, configInclude) => {
2266
- const autoSubs = subResources.filter((s) => s.includeInFindOne);
3096
+ const autoSubs = subResources.filter((s) => s.includeInFindOne && s.childKind !== "custom");
2267
3097
  const flatIncludes = autoSubs.length ? Object.fromEntries(autoSubs.map((s) => s.findOneOrderBy ? [
2268
3098
  s.relation,
2269
3099
  {
@@ -2408,16 +3238,6 @@ var orderableChildSort = /* @__PURE__ */ __name((sort, childModel, _sub) => {
2408
3238
  if (sort.includes(".")) return sort;
2409
3239
  return scalarFields.has(sort) ? sort : void 0;
2410
3240
  }, "orderableChildSort");
2411
- var applyValueLabelColumns = /* @__PURE__ */ __name((row, cols) => {
2412
- if (!row || !cols?.length) return row;
2413
- const out = {
2414
- ...row
2415
- };
2416
- for (const { field, values } of cols) {
2417
- if (field in out) out[field] = toValueLabel(out[field], values);
2418
- }
2419
- return out;
2420
- }, "applyValueLabelColumns");
2421
3241
  var ReadRepository = class {
2422
3242
  static {
2423
3243
  __name(this, "ReadRepository");
@@ -2434,6 +3254,15 @@ var ReadRepository = class {
2434
3254
  this.listSelect = listSelect;
2435
3255
  this.oneSelect = oneSelect;
2436
3256
  }
3257
+ /**
3258
+ * Physical table name for raw SQL (calculated columns).
3259
+ *
3260
+ * `table` overrides `model` for a `@@map`-ed Prisma model. Empty for a custom
3261
+ * resource, which cannot have calculated columns.
3262
+ */
3263
+ get tableName() {
3264
+ return this.config.table ?? this.config.model ?? "";
3265
+ }
2437
3266
  toId(id) {
2438
3267
  return (this.config.idType ?? "string") === "number" ? +id : String(id);
2439
3268
  }
@@ -2451,43 +3280,55 @@ var ReadRepository = class {
2451
3280
  if (this.listSelect && !(sort in this.listSelect)) return void 0;
2452
3281
  return buildSort(sort, sortDir);
2453
3282
  }
2454
- async decorate(rows, op) {
2455
- const hook = this.config.hooks?.afterRead;
2456
- const hooked = hook ? await Promise.all(rows.map((row) => hook(row, {
2457
- prisma: this.prisma,
2458
- op
2459
- }))) : rows;
2460
- const cols = this.config.valueLabelColumns;
2461
- return cols?.length ? hooked.map((r) => applyValueLabelColumns(r, cols)) : hooked;
2462
- }
2463
- async decorateOne(row, op) {
2464
- const hook = this.config.hooks?.afterRead;
2465
- return hook ? hook(row, {
2466
- prisma: this.prisma,
2467
- op
2468
- }) : row;
3283
+ /**
3284
+ * The parent a *child* read is scoped to, for the hook context.
3285
+ *
3286
+ * A sub-resource is served by the parent's controller, so its parent id arrives
3287
+ * as the parent's own `:id` — hence `param: 'id'`. `undefined` when no parent id
3288
+ * was supplied, so a hook is never handed a fabricated one.
3289
+ */
3290
+ parentHookContext(parentId2) {
3291
+ if (parentId2 === void 0 || parentId2 === null || parentId2 === "") {
3292
+ return void 0;
3293
+ }
3294
+ return {
3295
+ route: this.config.route,
3296
+ param: "id",
3297
+ id: this.toId(parentId2)
3298
+ };
3299
+ }
3300
+ async decorate(rows, op, request) {
3301
+ return decorateRows(rows, op, this.config, this.prisma, request);
3302
+ }
3303
+ async decorateOne(row, op, request) {
3304
+ return decorateRow(row, op, this.config, this.prisma, request);
2469
3305
  }
2470
3306
  /**
2471
3307
  * Fetch a paginated, sorted, and filtered list of records.
2472
3308
  * Sub-resource counts are merged onto each row; calculated columns are resolved via raw SQL.
2473
3309
  */
2474
- async findAll(params) {
3310
+ async findAll(params, request) {
2475
3311
  const subResources = this.config.subResources ?? [];
2476
3312
  const projection = this.projection("findAll");
2477
3313
  const query = {
2478
3314
  where: this.buildWhere(params.filter),
2479
3315
  take: params.pageSize,
2480
- skip: params.offset ?? (params.page - 1) * params.pageSize,
3316
+ skip: offsetOf(params),
2481
3317
  orderBy: this.safeSort(sanitizeValueLabelSort(params.sort, this.config.valueLabelColumns), params.sortDir)
2482
3318
  };
2483
- const countableSubResources = subResources.filter((s) => s.relationType !== "manyToOne");
2484
- const manyToOneIncludes = subResources.filter((s) => s.relationType === "manyToOne").map((s) => s.relation);
3319
+ const prismaSubResources = subResources.filter((s) => s.childKind !== "custom");
3320
+ const oneToManySubResources = prismaSubResources.filter((s) => s.relationType !== "manyToOne");
3321
+ const countableSubResources = oneToManySubResources.filter((s) => !s.hiddenInTable);
3322
+ const manyToOneIncludes = prismaSubResources.filter((s) => s.relationType === "manyToOne").map((s) => s.relation);
2485
3323
  const flatIncludes = manyToOneIncludes.length ? Object.fromEntries(manyToOneIncludes.map((r) => [
2486
3324
  r,
2487
3325
  true
2488
3326
  ])) : void 0;
2489
3327
  const configInclude = buildIncludeClause(this.config.include);
2490
- const countableRelations = new Set(countableSubResources.map((s) => s.relation));
3328
+ const countableRelations = /* @__PURE__ */ new Set([
3329
+ ...oneToManySubResources.map((s) => s.relation),
3330
+ ...subResources.filter((s) => s.childKind === "custom").map((s) => s.relation)
3331
+ ]);
2491
3332
  const filteredConfigInclude = configInclude ? Object.fromEntries(Object.entries(configInclude).filter(([key]) => !countableRelations.has(key))) : void 0;
2492
3333
  const safeConfigInclude = filteredConfigInclude && Object.keys(filteredConfigInclude).length ? filteredConfigInclude : void 0;
2493
3334
  const mergedInclude = flatIncludes || safeConfigInclude ? {
@@ -2525,8 +3366,8 @@ var ReadRepository = class {
2525
3366
  ...counts
2526
3367
  };
2527
3368
  }) : rows;
2528
- const withCalc = await mergeCalculatedColumnsForRows(mapped, this.config.calculatedColumns ?? [], this.config.model, this.prisma);
2529
- return this.decorate(withCalc, "findAll");
3369
+ const withCalc = await mergeCalculatedColumnsForRows(mapped, this.config.calculatedColumns ?? [], this.tableName, this.prisma, this.config.idField ?? "id");
3370
+ return this.decorate(withCalc, "findAll", request);
2530
3371
  }
2531
3372
  /** Count records matching the given filter strings. */
2532
3373
  count(filter) {
@@ -2540,7 +3381,7 @@ var ReadRepository = class {
2540
3381
  * `config.include` entries are loaded with full nesting via `buildIncludeClause`.
2541
3382
  * @throws {NotFoundException} When no record exists for the given id.
2542
3383
  */
2543
- async findOne(id) {
3384
+ async findOne(id, request) {
2544
3385
  const projection = this.projection("findOne");
2545
3386
  const idField = this.config.idField ?? "id";
2546
3387
  const query = {
@@ -2561,36 +3402,59 @@ var ReadRepository = class {
2561
3402
  }
2562
3403
  }
2563
3404
  const record = await this.prismaModel.findUnique(query);
2564
- if (!record) throw new import_common4.NotFoundException(`${this.config.name} with id ${id} not found`);
3405
+ if (!record) throw new import_common6.NotFoundException(`${this.config.name} with id ${id} not found`);
2565
3406
  const [withCalc] = await mergeCalculatedColumnsForRows([
2566
3407
  record
2567
- ], this.config.calculatedColumns ?? [], this.config.model, this.prisma);
3408
+ ], this.config.calculatedColumns ?? [], this.tableName, this.prisma, this.config.idField ?? "id");
2568
3409
  let enriched = withCalc ?? record;
2569
3410
  for (const sub of this.config.subResources ?? []) {
2570
3411
  if (!sub.calculatedColumns?.length) continue;
2571
3412
  const nested = enriched[sub.relation];
2572
3413
  if (!Array.isArray(nested) || !nested.length) continue;
2573
- const enrichedNested = await mergeCalculatedColumnsForRows(nested, sub.calculatedColumns, sub.childModel, this.prisma);
3414
+ const enrichedNested = await mergeCalculatedColumnsForRows(nested, sub.calculatedColumns, sub.childModel, this.prisma, sub.idField ?? "id");
2574
3415
  enriched = {
2575
3416
  ...enriched,
2576
3417
  [sub.relation]: enrichedNested
2577
3418
  };
2578
3419
  }
2579
- return this.decorateOne(enriched, "findOne");
3420
+ return this.decorateOne(enriched, "findOne", request);
2580
3421
  }
2581
3422
  /**
2582
3423
  * Fetch a paginated list of child records belonging to the given parent.
2583
3424
  * @param childRoute - Matches the `childRoute` key on a `SubResourceConfig`.
2584
3425
  * @throws {Error} When no matching sub-resource config or Prisma model is found.
2585
3426
  */
2586
- async findAllByParent(parentId, childRoute, params) {
3427
+ async findAllByParent(parentId2, childRoute, params, request) {
2587
3428
  const sub = (this.config.subResources ?? []).find((s) => s.childRoute === childRoute);
2588
3429
  if (!sub) throw new Error(`No sub-resource "${childRoute}" on "${this.config.name}"`);
3430
+ if (sub.childKind === "custom") {
3431
+ const findAll = childRepositoryFn(sub, "findAll", this.config.name);
3432
+ const result = await findAll(this.toId(parentId2), params, childCtx({
3433
+ parentConfig: this.config,
3434
+ prisma: this.prisma,
3435
+ op: "findAll",
3436
+ parentId: this.toId(parentId2),
3437
+ params,
3438
+ request
3439
+ }));
3440
+ const rows = result?.data ?? [];
3441
+ const decorated2 = sub.hooks?.afterRead ? await Promise.all(rows.map((row) => sub.hooks.afterRead(row, {
3442
+ prisma: this.prisma,
3443
+ op: "findAll",
3444
+ request,
3445
+ parent: this.parentHookContext(parentId2)
3446
+ }))) : rows;
3447
+ const labeled2 = sub.valueLabelColumns?.length ? decorated2.map((r) => applyValueLabelColumns(r, sub.valueLabelColumns)) : decorated2;
3448
+ return {
3449
+ data: labeled2,
3450
+ count: result?.count ?? labeled2.length
3451
+ };
3452
+ }
2589
3453
  const childModel = this.prisma[sub.childModel];
2590
3454
  if (!childModel) throw new Error(`Prisma model "${sub.childModel}" not found`);
2591
3455
  const where = {
2592
3456
  ...this.buildWhere(params.filter),
2593
- [sub.foreignKey]: this.toId(parentId)
3457
+ [sub.foreignKey]: this.toId(parentId2)
2594
3458
  };
2595
3459
  const includeClause = buildIncludeClause(sub.include);
2596
3460
  const childSort = orderableChildSort(sanitizeValueLabelSort(params.sort, sub.valueLabelColumns), childModel, sub);
@@ -2598,7 +3462,7 @@ var ReadRepository = class {
2598
3462
  childModel.findMany({
2599
3463
  where,
2600
3464
  take: params.pageSize,
2601
- skip: params.offset ?? (params.page - 1) * params.pageSize,
3465
+ skip: offsetOf(params),
2602
3466
  orderBy: childSort ? buildChildSortClause(childSort, params.sortDir) : void 0,
2603
3467
  ...includeClause && {
2604
3468
  include: includeClause
@@ -2608,10 +3472,12 @@ var ReadRepository = class {
2608
3472
  where
2609
3473
  })
2610
3474
  ]);
2611
- const withCalc = sub.calculatedColumns?.length ? await mergeCalculatedColumnsForRows(data, sub.calculatedColumns, sub.childModel, this.prisma) : data;
3475
+ const withCalc = sub.calculatedColumns?.length ? await mergeCalculatedColumnsForRows(data, sub.calculatedColumns, sub.childModel, this.prisma, sub.idField ?? "id") : data;
2612
3476
  const decorated = sub.hooks?.afterRead ? await Promise.all(withCalc.map((row) => sub.hooks.afterRead(row, {
2613
3477
  prisma: this.prisma,
2614
- op: "findAll"
3478
+ op: "findAll",
3479
+ request,
3480
+ parent: this.parentHookContext(parentId2)
2615
3481
  }))) : withCalc;
2616
3482
  const labeled = sub.valueLabelColumns?.length ? decorated.map((r) => applyValueLabelColumns(r, sub.valueLabelColumns)) : decorated;
2617
3483
  return {
@@ -2623,7 +3489,30 @@ var ReadRepository = class {
2623
3489
  * Fetch a single child record. When `parentId` is supplied the query also filters by the foreign key.
2624
3490
  * @throws {NotFoundException} When no matching record is found.
2625
3491
  */
2626
- async findOneChild(sub, childId, parentId) {
3492
+ async findOneChild(sub, childId, parentId2, request) {
3493
+ if (sub.childKind === "custom") {
3494
+ if (parentId2 === void 0) {
3495
+ throw new import_common6.BadRequestException(`Sub-resource "${sub.childRoute}" of "${this.config.name}" requires a parent id.`);
3496
+ }
3497
+ const findOne = childRepositoryFn(sub, "findOne", this.config.name);
3498
+ const row = await findOne(this.toId(parentId2), (sub.idType ?? "string") === "number" ? +childId : String(childId), childCtx({
3499
+ parentConfig: this.config,
3500
+ prisma: this.prisma,
3501
+ op: "findOne",
3502
+ parentId: this.toId(parentId2),
3503
+ id: childId,
3504
+ request
3505
+ }));
3506
+ if (row === null || row === void 0) {
3507
+ throw new import_common6.NotFoundException(`${sub.childRoute} with id ${childId} not found`);
3508
+ }
3509
+ return sub.hooks?.afterRead ? sub.hooks.afterRead(row, {
3510
+ prisma: this.prisma,
3511
+ op: "findOne",
3512
+ request,
3513
+ parent: this.parentHookContext(parentId2)
3514
+ }) : row;
3515
+ }
2627
3516
  const childModel = this.prisma[sub.childModel];
2628
3517
  if (!childModel) throw new Error(`Prisma model "${sub.childModel}" not found`);
2629
3518
  const id = (sub.idType ?? "string") === "number" ? +childId : String(childId);
@@ -2631,7 +3520,7 @@ var ReadRepository = class {
2631
3520
  const where = {
2632
3521
  [idField]: id
2633
3522
  };
2634
- if (parentId !== void 0) where[sub.foreignKey] = this.toId(parentId);
3523
+ if (parentId2 !== void 0) where[sub.foreignKey] = this.toId(parentId2);
2635
3524
  const includeClause = buildIncludeClause(sub.include);
2636
3525
  const record = await childModel.findFirst({
2637
3526
  where,
@@ -2639,24 +3528,26 @@ var ReadRepository = class {
2639
3528
  include: includeClause
2640
3529
  }
2641
3530
  });
2642
- if (!record) throw new import_common4.NotFoundException(`${sub.childRoute} with id ${childId} not found`);
3531
+ if (!record) throw new import_common6.NotFoundException(`${sub.childRoute} with id ${childId} not found`);
2643
3532
  const [withCalc] = sub.calculatedColumns?.length ? await mergeCalculatedColumnsForRows([
2644
3533
  record
2645
- ], sub.calculatedColumns, sub.childModel, this.prisma) : [
3534
+ ], sub.calculatedColumns, sub.childModel, this.prisma, sub.idField ?? "id") : [
2646
3535
  record
2647
3536
  ];
2648
3537
  if (sub.hooks?.afterRead) return sub.hooks.afterRead(withCalc, {
2649
3538
  prisma: this.prisma,
2650
- op: "findOne"
3539
+ op: "findOne",
3540
+ request,
3541
+ parent: this.parentHookContext(parentId2)
2651
3542
  });
2652
3543
  return withCalc;
2653
3544
  }
2654
3545
  };
2655
3546
 
2656
3547
  // src/lib/crud/schema.utils.ts
2657
- var import_zod22 = require("zod");
3548
+ var import_zod27 = require("zod");
2658
3549
  function isZodSchema(schema) {
2659
- return schema instanceof import_zod22.ZodObject;
3550
+ return schema instanceof import_zod27.ZodObject;
2660
3551
  }
2661
3552
  __name(isZodSchema, "isZodSchema");
2662
3553
  var isNullableProperty2 = /* @__PURE__ */ __name((property) => {
@@ -2670,7 +3561,7 @@ var dropNullableFromRequired2 = /* @__PURE__ */ __name((jsonSchema) => {
2670
3561
  }, "dropNullableFromRequired");
2671
3562
  function toJsonSchema(schema) {
2672
3563
  if (isZodSchema(schema)) {
2673
- const jsonSchema = (0, import_zod22.toJSONSchema)(schema, {
3564
+ const jsonSchema = (0, import_zod27.toJSONSchema)(schema, {
2674
3565
  target: "openApi3",
2675
3566
  ...jsonSchemaOpts
2676
3567
  });
@@ -2723,24 +3614,25 @@ function toSelectFields(schema) {
2723
3614
  __name(toSelectFields, "toSelectFields");
2724
3615
 
2725
3616
  // src/lib/crud/write.repository.ts
2726
- var import_common5 = require("@nestjs/common");
2727
-
2728
- // src/lib/crud/constants.ts
2729
- var PRISMA_NOT_FOUND_CODE = "P2025";
2730
- var DEFAULT_ID_FIELD = "id";
2731
-
2732
- // src/lib/crud/write.repository.ts
2733
- var normalizeValueLabels = /* @__PURE__ */ __name((data, cols) => {
2734
- if (!data || typeof data !== "object" || Array.isArray(data) || !cols?.length) return data;
2735
- const out = {
2736
- ...data
2737
- };
2738
- for (const { field } of cols) {
2739
- if (field in out) out[field] = fromValueLabel(out[field]);
2740
- }
2741
- return out;
2742
- }, "normalizeValueLabels");
3617
+ var import_common7 = require("@nestjs/common");
2743
3618
  var includeRelationNames = /* @__PURE__ */ __name((include) => new Set((include ?? []).map((e) => typeof e === "string" ? e : e.relation)), "includeRelationNames");
3619
+ var PRISMA_RELATION_WRITE_KEYS = /* @__PURE__ */ new Set([
3620
+ "connect",
3621
+ "connectOrCreate",
3622
+ "create",
3623
+ "createMany",
3624
+ "set",
3625
+ "disconnect",
3626
+ "update",
3627
+ "updateMany",
3628
+ "upsert",
3629
+ "delete",
3630
+ "deleteMany"
3631
+ ]);
3632
+ var isPrismaRelationWrite = /* @__PURE__ */ __name((value) => {
3633
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
3634
+ return Object.keys(value).some((k) => PRISMA_RELATION_WRITE_KEYS.has(k));
3635
+ }, "isPrismaRelationWrite");
2744
3636
  var WriteRepository = class {
2745
3637
  static {
2746
3638
  __name(this, "WriteRepository");
@@ -2757,7 +3649,7 @@ var WriteRepository = class {
2757
3649
  return (this.config.idType ?? "string") === "number" ? +id : String(id);
2758
3650
  }
2759
3651
  notFound(id) {
2760
- return new import_common5.NotFoundException(`${this.config.name} with id ${id} not found`);
3652
+ return new import_common7.NotFoundException(`${this.config.name} with id ${id} not found`);
2761
3653
  }
2762
3654
  stripSubResourceKeys(data) {
2763
3655
  if (!data || typeof data !== "object" || Array.isArray(data)) return data;
@@ -2771,26 +3663,29 @@ var WriteRepository = class {
2771
3663
  if (!nonCreateable.size) return data;
2772
3664
  return Object.fromEntries(Object.entries(data).filter(([k]) => !nonCreateable.has(k)));
2773
3665
  }
2774
- async prepare(data, op, id) {
2775
- const normalized = normalizeValueLabels(data, this.config.valueLabelColumns);
2776
- const hook = this.config.hooks?.beforeWrite;
2777
- return hook ? hook(normalized, {
2778
- prisma: this.prisma,
2779
- op,
2780
- id
2781
- }) : normalized;
3666
+ /**
3667
+ * The parent a *child* write is scoped to, for the hook context.
3668
+ *
3669
+ * A sub-resource is served by the parent's controller, so its parent id arrives
3670
+ * as the parent's own `:id` — hence `param: 'id'`. A resource that declares
3671
+ * `parent` names its own param and goes through the custom adapter instead.
3672
+ */
3673
+ parentHookContext(parentId1) {
3674
+ return {
3675
+ route: this.config.route,
3676
+ param: "id",
3677
+ id: this.toId(parentId1)
3678
+ };
2782
3679
  }
2783
- async postWrite(result, op, id) {
2784
- const hook = this.config.hooks?.afterWrite;
2785
- return hook ? hook(result, {
2786
- prisma: this.prisma,
2787
- op,
2788
- id
2789
- }) : result;
3680
+ async prepare(data, op, id, request) {
3681
+ return prepareWrite(data, op, this.config, this.prisma, id, request);
3682
+ }
3683
+ async postWrite(result, op, id, request) {
3684
+ return postWrite(result, op, this.config, this.prisma, id, request);
2790
3685
  }
2791
3686
  upsertWhere(data) {
2792
3687
  const keys = upsertOnFor(resolveDefinition(this.config));
2793
- if (!keys) throw new import_common5.BadRequestException(`${this.config.name} has no upsertOn configured`);
3688
+ if (!keys) throw new import_common7.BadRequestException(`${this.config.name} has no upsertOn configured`);
2794
3689
  if (typeof keys === "string") return {
2795
3690
  [keys]: data[keys]
2796
3691
  };
@@ -2802,61 +3697,62 @@ var WriteRepository = class {
2802
3697
  ]))
2803
3698
  };
2804
3699
  }
2805
- async create(data) {
3700
+ async create(data, request) {
2806
3701
  const result = await this.prismaModel.create({
2807
- data: await this.prepare(this.stripSubResourceKeys(data), "create")
3702
+ data: await this.prepare(this.stripSubResourceKeys(data), "create", void 0, request)
2808
3703
  });
2809
- return this.postWrite(result, "create");
3704
+ return this.postWrite(result, "create", void 0, request);
2810
3705
  }
2811
- async update(id, data) {
3706
+ async update(id, data, request) {
2812
3707
  const idField = this.config.idField ?? "id";
2813
3708
  try {
2814
3709
  const result = await this.prismaModel.update({
2815
3710
  where: {
2816
3711
  [idField]: this.toId(id)
2817
3712
  },
2818
- data: await this.prepare(this.stripSubResourceKeys(data), "update", this.toId(id))
3713
+ data: await this.prepare(this.stripSubResourceKeys(data), "update", this.toId(id), request)
2819
3714
  });
2820
- return this.postWrite(result, "update", this.toId(id));
3715
+ return this.postWrite(result, "update", this.toId(id), request);
2821
3716
  } catch (e) {
2822
3717
  if (e?.code === PRISMA_NOT_FOUND_CODE) throw this.notFound(id);
2823
3718
  throw e;
2824
3719
  }
2825
3720
  }
2826
- async patch(id, data) {
3721
+ async patch(id, data, request) {
2827
3722
  const idField = this.config.idField ?? "id";
2828
3723
  try {
2829
3724
  const result = await this.prismaModel.update({
2830
3725
  where: {
2831
3726
  [idField]: this.toId(id)
2832
3727
  },
2833
- data: await this.prepare(this.stripSubResourceKeys(data), "patch", this.toId(id))
3728
+ data: await this.prepare(this.stripSubResourceKeys(data), "patch", this.toId(id), request)
2834
3729
  });
2835
- return this.postWrite(result, "patch", this.toId(id));
3730
+ return this.postWrite(result, "patch", this.toId(id), request);
2836
3731
  } catch (e) {
2837
3732
  if (e?.code === PRISMA_NOT_FOUND_CODE) throw this.notFound(id);
2838
3733
  throw e;
2839
3734
  }
2840
3735
  }
2841
- async upsert(data) {
3736
+ async upsert(data, request) {
2842
3737
  const where = this.upsertWhere(data);
2843
3738
  const existing = await this.prismaModel.findFirst({
2844
3739
  where
2845
3740
  });
2846
3741
  const op = existing ? "update" : "create";
2847
- const prepared = await this.prepare(this.stripSubResourceKeys(data), op, existing ? existing[this.config.idField ?? DEFAULT_ID_FIELD] : void 0);
3742
+ const existingId = existing ? existing[this.config.idField ?? DEFAULT_ID_FIELD] : void 0;
3743
+ const prepared = await this.prepare(this.stripSubResourceKeys(data), op, existingId, request);
2848
3744
  const result = await this.prismaModel.upsert({
2849
3745
  where,
2850
3746
  create: prepared,
2851
3747
  update: prepared
2852
3748
  });
2853
- return this.postWrite(result, op, existing ? existing[this.config.idField ?? DEFAULT_ID_FIELD] : void 0);
3749
+ return this.postWrite(result, op, existingId, request);
2854
3750
  }
2855
3751
  /** Upsert multiple rows in parallel. */
2856
- upsertMany(rows) {
2857
- return Promise.all(rows.map((r) => this.upsert(r)));
3752
+ upsertMany(rows, request) {
3753
+ return Promise.all(rows.map((r) => this.upsert(r, request)));
2858
3754
  }
2859
- async delete(id) {
3755
+ async delete(id, request) {
2860
3756
  const idField = this.config.idField ?? "id";
2861
3757
  try {
2862
3758
  const result = await this.prismaModel.delete({
@@ -2864,40 +3760,96 @@ var WriteRepository = class {
2864
3760
  [idField]: this.toId(id)
2865
3761
  }
2866
3762
  });
2867
- return this.postWrite(result, "delete", this.toId(id));
3763
+ return this.postWrite(result, "delete", this.toId(id), request);
2868
3764
  } catch (e) {
2869
3765
  if (e?.code === PRISMA_NOT_FOUND_CODE) throw this.notFound(id);
2870
3766
  throw e;
2871
3767
  }
2872
3768
  }
2873
3769
  /**
3770
+ * Write to a **custom** sub-resource by delegating to the child's own
3771
+ * repository.
3772
+ *
3773
+ * The child has no Prisma model, so `prisma[childModel]` is not an option.
3774
+ * Value-label normalisation and the child's `beforeWrite`/`afterWrite` hooks
3775
+ * still apply, so a custom child behaves like a Prisma one from the caller's
3776
+ * point of view.
3777
+ */
3778
+ async delegateChildWrite(sub, op, parentId1, childId, data, request) {
3779
+ if (parentId1 === void 0) {
3780
+ throw new import_common7.BadRequestException(`Sub-resource "${sub.childRoute}" of "${this.config.name}" requires a parent id.`);
3781
+ }
3782
+ const fn = childRepositoryFn(sub, op, this.config.name);
3783
+ const id = childId === void 0 ? void 0 : (sub.idType ?? "string") === "number" ? +childId : String(childId);
3784
+ const ctx = childCtx({
3785
+ parentConfig: this.config,
3786
+ prisma: this.prisma,
3787
+ op,
3788
+ parentId: this.toId(parentId1),
3789
+ id,
3790
+ request
3791
+ });
3792
+ let payload;
3793
+ if (op !== "delete") {
3794
+ const stripped = op === "create" ? this.stripNonCreateableChildFields(data, sub) : data;
3795
+ const normalized = normalizeValueLabels(stripped, sub.valueLabelColumns);
3796
+ payload = sub.hooks?.beforeWrite ? await sub.hooks.beforeWrite(normalized, {
3797
+ prisma: this.prisma,
3798
+ op: op === "patch" ? "patch" : op,
3799
+ ...id !== void 0 && {
3800
+ id
3801
+ },
3802
+ request,
3803
+ parent: ctx.parent
3804
+ }) : normalized;
3805
+ }
3806
+ const result = op === "create" ? await fn(this.toId(parentId1), payload, ctx) : op === "delete" ? await fn(this.toId(parentId1), id, ctx) : await fn(this.toId(parentId1), id, payload, ctx);
3807
+ return sub.hooks?.afterWrite ? sub.hooks.afterWrite(result, {
3808
+ prisma: this.prisma,
3809
+ op: op === "patch" ? "patch" : op,
3810
+ ...id !== void 0 && {
3811
+ id
3812
+ },
3813
+ request
3814
+ }) : result;
3815
+ }
3816
+ /**
2874
3817
  * Create a child record and attach it to the parent via the configured foreign key.
2875
3818
  * Fields marked `createable: false` in the form view are stripped before writing.
2876
3819
  */
2877
- async createChild(parentId, sub, data) {
3820
+ async createChild(parentId1, sub, data, request) {
3821
+ if (sub.childKind === "custom") {
3822
+ return this.delegateChildWrite(sub, "create", parentId1, void 0, data, request);
3823
+ }
2878
3824
  const childModel = this.prisma[sub.childModel];
2879
3825
  if (!childModel) throw new Error(`Prisma model "${sub.childModel}" not found`);
2880
3826
  const stripped = this.stripNonCreateableChildFields(data, sub);
2881
3827
  const normalized = normalizeValueLabels(stripped, sub.valueLabelColumns);
2882
3828
  const payload = {
2883
3829
  ...normalized,
2884
- [sub.foreignKey]: this.toId(parentId)
3830
+ [sub.foreignKey]: this.toId(parentId1)
2885
3831
  };
2886
3832
  const prepared = sub.hooks?.beforeWrite ? await sub.hooks.beforeWrite(payload, {
2887
3833
  prisma: this.prisma,
2888
- op: "create"
3834
+ op: "create",
3835
+ request,
3836
+ parent: this.parentHookContext(parentId1)
2889
3837
  }) : payload;
2890
3838
  const includeKeys = includeRelationNames(sub.include);
3839
+ const preparedEntries = prepared;
2891
3840
  const prismaData = {
2892
- ...Object.fromEntries(Object.entries(prepared).filter(([k]) => !includeKeys.has(k))),
2893
- [sub.foreignKey]: this.toId(parentId)
3841
+ ...Object.fromEntries(Object.entries(preparedEntries).filter(([k, v]) => !includeKeys.has(k) || isPrismaRelationWrite(v))),
3842
+ ...sub.foreignKey in preparedEntries ? {
3843
+ [sub.foreignKey]: this.toId(parentId1)
3844
+ } : {}
2894
3845
  };
2895
3846
  const result = await childModel.create({
2896
3847
  data: prismaData
2897
3848
  });
2898
3849
  return sub.hooks?.afterWrite ? sub.hooks.afterWrite(result, {
2899
3850
  prisma: this.prisma,
2900
- op: "create"
3851
+ op: "create",
3852
+ request
2901
3853
  }) : result;
2902
3854
  }
2903
3855
  /**
@@ -2905,7 +3857,10 @@ var WriteRepository = class {
2905
3857
  * receive non-scalar fields.
2906
3858
  * @throws {NotFoundException} When the child record does not exist (Prisma P2025).
2907
3859
  */
2908
- async updateChild(sub, childId, data) {
3860
+ async updateChild(sub, childId, data, request) {
3861
+ if (sub.childKind === "custom") {
3862
+ return this.delegateChildWrite(sub, "update", parentIdFromRequest(request), childId, data, request);
3863
+ }
2909
3864
  const childModel = this.prisma[sub.childModel];
2910
3865
  if (!childModel) throw new Error(`Prisma model "${sub.childModel}" not found`);
2911
3866
  const id = (sub.idType ?? "string") === "number" ? +childId : String(childId);
@@ -2913,10 +3868,12 @@ var WriteRepository = class {
2913
3868
  const afterHook = sub.hooks?.beforeWrite ? await sub.hooks.beforeWrite(normalized, {
2914
3869
  prisma: this.prisma,
2915
3870
  op: "update",
2916
- id
3871
+ id,
3872
+ request,
3873
+ parent: this.parentHookContext(parentId)
2917
3874
  }) : normalized;
2918
3875
  const includeKeys = includeRelationNames(sub.include);
2919
- const prepared = Object.fromEntries(Object.entries(afterHook).filter(([k]) => !includeKeys.has(k)));
3876
+ const prepared = Object.fromEntries(Object.entries(afterHook).filter(([k, v]) => !includeKeys.has(k) || isPrismaRelationWrite(v)));
2920
3877
  try {
2921
3878
  const result = await childModel.update({
2922
3879
  where: {
@@ -2927,10 +3884,11 @@ var WriteRepository = class {
2927
3884
  return sub.hooks?.afterWrite ? sub.hooks.afterWrite(result, {
2928
3885
  prisma: this.prisma,
2929
3886
  op: "update",
2930
- id
3887
+ id,
3888
+ request
2931
3889
  }) : result;
2932
3890
  } catch (e) {
2933
- if (e?.code === PRISMA_NOT_FOUND_CODE) throw new import_common5.NotFoundException(`${sub.childRoute} with id ${childId} not found`);
3891
+ if (e?.code === PRISMA_NOT_FOUND_CODE) throw new import_common7.NotFoundException(`${sub.childRoute} with id ${childId} not found`);
2934
3892
  throw e;
2935
3893
  }
2936
3894
  }
@@ -2939,7 +3897,10 @@ var WriteRepository = class {
2939
3897
  * clause to prevent cross-parent deletions.
2940
3898
  * @throws {NotFoundException} When no matching record is found.
2941
3899
  */
2942
- async deleteChild(sub, childId, parentId) {
3900
+ async deleteChild(sub, childId, parentId1, request) {
3901
+ if (sub.childKind === "custom") {
3902
+ return this.delegateChildWrite(sub, "delete", parentIdFromRequest(request, parentId1), childId, void 0, request);
3903
+ }
2943
3904
  const childModel = this.prisma[sub.childModel];
2944
3905
  if (!childModel) throw new Error(`Prisma model "${sub.childModel}" not found`);
2945
3906
  const id = (sub.idType ?? "string") === "number" ? +childId : String(childId);
@@ -2947,26 +3908,36 @@ var WriteRepository = class {
2947
3908
  const where = {
2948
3909
  [idField]: id
2949
3910
  };
2950
- if (parentId !== void 0) where[sub.foreignKey] = this.toId(parentId);
3911
+ if (parentId1 !== void 0) where[sub.foreignKey] = this.toId(parentId1);
2951
3912
  try {
2952
3913
  const result = await childModel.deleteMany({
2953
3914
  where
2954
3915
  });
2955
- if (result.count === 0) throw new import_common5.NotFoundException(`${sub.childRoute} with id ${childId} not found`);
3916
+ if (result.count === 0) throw new import_common7.NotFoundException(`${sub.childRoute} with id ${childId} not found`);
2956
3917
  return sub.hooks?.afterWrite ? sub.hooks.afterWrite(result, {
2957
3918
  prisma: this.prisma,
2958
3919
  op: "delete",
2959
- id
3920
+ id,
3921
+ request
2960
3922
  }) : result;
2961
3923
  } catch (e) {
2962
- if (e?.code === PRISMA_NOT_FOUND_CODE) throw new import_common5.NotFoundException(`${sub.childRoute} with id ${childId} not found`);
3924
+ if (e?.code === PRISMA_NOT_FOUND_CODE) throw new import_common7.NotFoundException(`${sub.childRoute} with id ${childId} not found`);
2963
3925
  throw e;
2964
3926
  }
2965
3927
  }
2966
3928
  };
2967
3929
 
2968
3930
  // src/lib/crud/crud-repository.factory.ts
2969
- function createCrudRepository(prisma, config) {
3931
+ function createCrudRepository(prisma, config, dataSources) {
3932
+ if (config.kind === "custom") {
3933
+ return createCustomRepository(prisma, config, dataSources ?? {
3934
+ resolve: /* @__PURE__ */ __name(() => prisma, "resolve"),
3935
+ entries: /* @__PURE__ */ __name(() => [], "entries")
3936
+ }, config.repository);
3937
+ }
3938
+ if (!config.model) {
3939
+ throw new Error(`Resource "${config.name}" has no "model". A prisma-backed resource must name its Prisma model; set "kind": "custom" for a resource with no model.`);
3940
+ }
2970
3941
  const model = prisma[config.model];
2971
3942
  if (!model) {
2972
3943
  throw new Error(`Model "${config.model}" not found on the provided PrismaClient. Check the resource config for "${config.name}".`);
@@ -2999,7 +3970,7 @@ function createCrudRepository(prisma, config) {
2999
3970
  __name(createCrudRepository, "createCrudRepository");
3000
3971
 
3001
3972
  // src/lib/crud/data-source/data-source.registry.ts
3002
- var import_common6 = require("@nestjs/common");
3973
+ var import_common8 = require("@nestjs/common");
3003
3974
  function _ts_decorate4(decorators, target, key, desc2) {
3004
3975
  var c = arguments.length, r = c < 3 ? target : desc2 === null ? desc2 = Object.getOwnPropertyDescriptor(target, key) : desc2, d;
3005
3976
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc2);
@@ -3070,36 +4041,16 @@ var DataSourceRegistry = class {
3070
4041
  }
3071
4042
  };
3072
4043
  DataSourceRegistry = _ts_decorate4([
3073
- (0, import_common6.Injectable)(),
4044
+ (0, import_common8.Injectable)(),
3074
4045
  _ts_metadata3("design:type", Function),
3075
4046
  _ts_metadata3("design:paramtypes", [
3076
4047
  Array
3077
4048
  ])
3078
4049
  ], DataSourceRegistry);
3079
4050
 
3080
- // src/lib/crud/resource/resource-load-errors.registry.ts
3081
- var ResourceLoadErrorsRegistry = class ResourceLoadErrorsRegistry2 {
3082
- static {
3083
- __name(this, "ResourceLoadErrorsRegistry");
3084
- }
3085
- errors = [];
3086
- record(e) {
3087
- this.errors.push(e);
3088
- }
3089
- getAll() {
3090
- return [
3091
- ...this.errors
3092
- ];
3093
- }
3094
- clear() {
3095
- this.errors = [];
3096
- }
3097
- };
3098
- var resourceLoadErrorsRegistry = new ResourceLoadErrorsRegistry();
3099
-
3100
4051
  // src/lib/crud/data-source/data-source.loader.ts
3101
4052
  var import_node_fs2 = require("fs");
3102
- var import_node_path4 = require("path");
4053
+ var import_node_path5 = require("path");
3103
4054
  var loadDataSourcesFromDir = /* @__PURE__ */ __name(async (dirPath) => {
3104
4055
  if (!(0, import_node_fs2.existsSync)(dirPath)) return [];
3105
4056
  const entries = (0, import_node_fs2.readdirSync)(dirPath, {
@@ -3108,8 +4059,8 @@ var loadDataSourcesFromDir = /* @__PURE__ */ __name(async (dirPath) => {
3108
4059
  const dirs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
3109
4060
  const results = [];
3110
4061
  for (const dir of dirs) {
3111
- const basePath = (0, import_node_path4.join)(dirPath, dir);
3112
- const jsonFile = (0, import_node_path4.join)(basePath, "data-source.json");
4062
+ const basePath = (0, import_node_path5.join)(dirPath, dir);
4063
+ const jsonFile = (0, import_node_path5.join)(basePath, "data-source.json");
3113
4064
  if (!(0, import_node_fs2.existsSync)(jsonFile)) continue;
3114
4065
  let _config;
3115
4066
  try {
@@ -3149,14 +4100,14 @@ var findModule2 = /* @__PURE__ */ __name((dir, name) => {
3149
4100
  ".ts",
3150
4101
  ".js"
3151
4102
  ]) {
3152
- const p = (0, import_node_path4.join)(dir, `${name}${ext}`);
4103
+ const p = (0, import_node_path5.join)(dir, `${name}${ext}`);
3153
4104
  if ((0, import_node_fs2.existsSync)(p)) return p;
3154
4105
  }
3155
4106
  return void 0;
3156
4107
  }, "findModule");
3157
4108
 
3158
4109
  // src/lib/crud/operations/register-actions.ts
3159
- var import_common7 = require("@nestjs/common");
4110
+ var import_common9 = require("@nestjs/common");
3160
4111
  var import_swagger2 = require("@nestjs/swagger");
3161
4112
 
3162
4113
  // src/lib/crud/operations/decorator.utils.ts
@@ -3180,8 +4131,8 @@ var registerActionRoutes = /* @__PURE__ */ __name((ctx) => {
3180
4131
  return action.procedure(this.repo.prisma, recordId);
3181
4132
  });
3182
4133
  const d = desc(cls, methodName);
3183
- (0, import_common7.Post)(`procedure/${action.id}/:recordId`)(cls.prototype, methodName, d);
3184
- (0, import_common7.Param)("recordId")(cls.prototype, methodName, 0);
4134
+ (0, import_common9.Post)(`procedure/${action.id}/:recordId`)(cls.prototype, methodName, d);
4135
+ (0, import_common9.Param)("recordId")(cls.prototype, methodName, 0);
3185
4136
  (0, import_swagger2.ApiOperation)({
3186
4137
  summary: `Execute action "${action.label}" on a ${name}`
3187
4138
  })(cls.prototype, methodName, d);
@@ -3205,7 +4156,7 @@ var registerTableActionRoutes = /* @__PURE__ */ __name((ctx) => {
3205
4156
  return action.procedure(this.repo.prisma);
3206
4157
  });
3207
4158
  const d = desc(cls, methodName);
3208
- (0, import_common7.Post)(`table-action/${action.id}`)(cls.prototype, methodName, d);
4159
+ (0, import_common9.Post)(`table-action/${action.id}`)(cls.prototype, methodName, d);
3209
4160
  (0, import_swagger2.ApiOperation)({
3210
4161
  summary: `Execute table action "${action.label ?? action.id}" on ${name}`
3211
4162
  })(cls.prototype, methodName, d);
@@ -3217,7 +4168,7 @@ var registerTableActionRoutes = /* @__PURE__ */ __name((ctx) => {
3217
4168
  }, "registerTableActionRoutes");
3218
4169
 
3219
4170
  // src/lib/crud/operations/register-create.ts
3220
- var import_common8 = require("@nestjs/common");
4171
+ var import_common10 = require("@nestjs/common");
3221
4172
  var import_swagger3 = require("@nestjs/swagger");
3222
4173
  var defaultCreate = /* @__PURE__ */ __name((ctx) => {
3223
4174
  if (!isOperationEnabled(ctx.definition, "create")) return null;
@@ -3227,13 +4178,14 @@ var defaultCreate = /* @__PURE__ */ __name((ctx) => {
3227
4178
  route: "",
3228
4179
  methodName,
3229
4180
  name: config.name,
3230
- createFn: /* @__PURE__ */ __name(function(body) {
3231
- return this.repo.create(body);
4181
+ createFn: /* @__PURE__ */ __name(function(body, req) {
4182
+ return this.repo.create(body, req);
3232
4183
  }, "createFn"),
3233
4184
  decorators: /* @__PURE__ */ __name(() => {
3234
4185
  bodyDecorator(createSchema, {
3235
4186
  coerceNullableUndefinedToNull: true
3236
4187
  })(cls.prototype, methodName, 0);
4188
+ (0, import_common10.Req)()(cls.prototype, methodName, 1);
3237
4189
  }, "decorators")
3238
4190
  };
3239
4191
  }, "defaultCreate");
@@ -3241,12 +4193,13 @@ var childCreate = /* @__PURE__ */ __name((sub) => (ctx) => {
3241
4193
  if (!isOperationEnabled(sub.operations, "create")) return null;
3242
4194
  const { cls } = ctx;
3243
4195
  const methodName = `createChild_${sub.childRoute}`;
3244
- const createFn = /* @__PURE__ */ __name(async function(id, body) {
3245
- return this.repo.createChild(id, sub, body);
4196
+ const createFn = /* @__PURE__ */ __name(async function(id, body, req) {
4197
+ return this.repo.createChild(id, sub, body, req);
3246
4198
  }, "createFn");
3247
4199
  const decorators = /* @__PURE__ */ __name(() => {
3248
- (0, import_common8.Param)("id")(cls.prototype, methodName, 0);
3249
- (0, import_common8.Body)()(cls.prototype, methodName, 1);
4200
+ (0, import_common10.Param)("id")(cls.prototype, methodName, 0);
4201
+ (0, import_common10.Body)()(cls.prototype, methodName, 1);
4202
+ (0, import_common10.Req)()(cls.prototype, methodName, 2);
3250
4203
  }, "decorators");
3251
4204
  return {
3252
4205
  route: `:id/${sub.childRoute}`,
@@ -3264,7 +4217,7 @@ var registerCreate = /* @__PURE__ */ __name((ctx, sub) => {
3264
4217
  const { cls } = ctx;
3265
4218
  def(cls, methodName, properties.createFn);
3266
4219
  const d = desc(cls, methodName);
3267
- (0, import_common8.Post)(route)(cls.prototype, methodName, d);
4220
+ (0, import_common10.Post)(route)(cls.prototype, methodName, d);
3268
4221
  (0, import_swagger3.ApiOperation)({
3269
4222
  summary: `Create a ${name}`
3270
4223
  })(cls.prototype, methodName, d);
@@ -3276,7 +4229,7 @@ var registerCreate = /* @__PURE__ */ __name((ctx, sub) => {
3276
4229
  }, "registerCreate");
3277
4230
 
3278
4231
  // src/lib/crud/operations/register-delete.ts
3279
- var import_common9 = require("@nestjs/common");
4232
+ var import_common11 = require("@nestjs/common");
3280
4233
  var import_swagger4 = require("@nestjs/swagger");
3281
4234
  var defaultDelete = /* @__PURE__ */ __name((ctx) => {
3282
4235
  if (!isOperationEnabled(ctx.definition, "delete")) return null;
@@ -3289,8 +4242,8 @@ var defaultDelete = /* @__PURE__ */ __name((ctx) => {
3289
4242
  name,
3290
4243
  decorators: /* @__PURE__ */ __name(() => {
3291
4244
  }, "decorators"),
3292
- deleteFn: /* @__PURE__ */ __name(function(id) {
3293
- return this.repo.delete(id);
4245
+ deleteFn: /* @__PURE__ */ __name(function(id, req) {
4246
+ return this.repo.delete(id, req);
3294
4247
  }, "deleteFn")
3295
4248
  };
3296
4249
  }, "defaultDelete");
@@ -3298,11 +4251,13 @@ var deleteChild = /* @__PURE__ */ __name((sub) => (ctx) => {
3298
4251
  if (!isOperationEnabled(sub.operations, "delete")) return null;
3299
4252
  const { cls } = ctx;
3300
4253
  const methodName = `deleteChild_${sub.childRoute}`;
3301
- const deleteFn = /* @__PURE__ */ __name(async function(childId, parentId) {
3302
- return this.repo.deleteChild(sub, childId, parentId);
4254
+ const deleteFn = /* @__PURE__ */ __name(async function(childId, parentId2, req) {
4255
+ return this.repo.deleteChild(sub, childId, parentId2, req);
3303
4256
  }, "deleteFn");
3304
4257
  const decorators = /* @__PURE__ */ __name(() => {
3305
- (0, import_common9.Param)("childId")(cls.prototype, methodName, 0);
4258
+ (0, import_common11.Param)("childId")(cls.prototype, methodName, 0);
4259
+ (0, import_common11.Param)("id")(cls.prototype, methodName, 1);
4260
+ (0, import_common11.Req)()(cls.prototype, methodName, 2);
3306
4261
  }, "decorators");
3307
4262
  return {
3308
4263
  route: `:id/${sub.childRoute}/:childId`,
@@ -3320,8 +4275,8 @@ var registerDelete = /* @__PURE__ */ __name((ctx, sub) => {
3320
4275
  const { methodName, route, name } = properties;
3321
4276
  def(cls, methodName, properties.deleteFn);
3322
4277
  const d = desc(cls, methodName);
3323
- (0, import_common9.Delete)(route)(cls.prototype, methodName, d);
3324
- (0, import_common9.Param)("id")(cls.prototype, methodName, 0);
4278
+ (0, import_common11.Delete)(route)(cls.prototype, methodName, d);
4279
+ (0, import_common11.Param)("id")(cls.prototype, methodName, 0);
3325
4280
  (0, import_swagger4.ApiOperation)({
3326
4281
  summary: `Delete ${name} record`
3327
4282
  })(cls.prototype, methodName, d);
@@ -3330,10 +4285,11 @@ var registerDelete = /* @__PURE__ */ __name((ctx, sub) => {
3330
4285
  status: 200
3331
4286
  })(cls.prototype, methodName, d);
3332
4287
  properties.decorators();
4288
+ if (!sub) (0, import_common11.Req)()(cls.prototype, methodName, 1);
3333
4289
  }, "registerDelete");
3334
4290
 
3335
4291
  // src/lib/crud/operations/register-findall.ts
3336
- var import_common10 = require("@nestjs/common");
4292
+ var import_common12 = require("@nestjs/common");
3337
4293
  var import_swagger5 = require("@nestjs/swagger");
3338
4294
 
3339
4295
  // src/lib/crud/request.dto.ts
@@ -3416,7 +4372,7 @@ var ZodValidationPipe = class {
3416
4372
  };
3417
4373
 
3418
4374
  // src/lib/crud/operations/register-findall.ts
3419
- var _findAll = /* @__PURE__ */ __name(async (repo, params, q, lookupLabel) => {
4375
+ var _findAll = /* @__PURE__ */ __name(async (repo, params, q, lookupLabel, request) => {
3420
4376
  const effectiveParams = {
3421
4377
  ...params
3422
4378
  };
@@ -3426,10 +4382,13 @@ var _findAll = /* @__PURE__ */ __name(async (repo, params, q, lookupLabel) => {
3426
4382
  `${lookupLabel}:${q}`
3427
4383
  ];
3428
4384
  }
3429
- const [data, count] = await Promise.all([
3430
- repo.findAll(effectiveParams),
4385
+ const { data, count } = repo.findAllWithCount ? await repo.findAllWithCount(effectiveParams, request) : await Promise.all([
4386
+ repo.findAll(effectiveParams, request),
3431
4387
  repo.count(effectiveParams.filter)
3432
- ]);
4388
+ ]).then(([data2, count2]) => ({
4389
+ data: data2,
4390
+ count: count2
4391
+ }));
3433
4392
  const totalPages = Math.max(1, Math.ceil(count / params.pageSize));
3434
4393
  return {
3435
4394
  data,
@@ -3444,8 +4403,8 @@ var _findAll = /* @__PURE__ */ __name(async (repo, params, q, lookupLabel) => {
3444
4403
  }
3445
4404
  };
3446
4405
  }, "_findAll");
3447
- var findAllByParent = /* @__PURE__ */ __name(async (repo, id, childRoute, params) => {
3448
- const { data, count } = await repo.findAllByParent(id, childRoute, params);
4406
+ var findAllByParent = /* @__PURE__ */ __name(async (repo, id, childRoute, params, request) => {
4407
+ const { data, count } = await repo.findAllByParent(id, childRoute, params, request);
3449
4408
  const totalPages = Math.max(1, Math.ceil(count / params.pageSize));
3450
4409
  return {
3451
4410
  data,
@@ -3464,8 +4423,8 @@ var defaultFindAll = /* @__PURE__ */ __name((ctx) => {
3464
4423
  if (!isOperationEnabled(ctx.definition, "findAll")) return;
3465
4424
  const { config } = ctx;
3466
4425
  const lookupLabel = config.lookup?.label;
3467
- const findAll = /* @__PURE__ */ __name(async function(params, q) {
3468
- return _findAll(this.repo, params, q, lookupLabel);
4426
+ const findAll = /* @__PURE__ */ __name(async function(params, q, req) {
4427
+ return _findAll(this.repo, params, q, lookupLabel, req);
3469
4428
  }, "findAll");
3470
4429
  return {
3471
4430
  route: "",
@@ -3481,11 +4440,12 @@ var childFindAll = /* @__PURE__ */ __name((sub) => (ctx) => {
3481
4440
  if (!isOperationEnabled(sub.operations, "findAll")) return;
3482
4441
  const { cls } = ctx;
3483
4442
  const methodName = `findAllBy_${sub.childRoute}`;
3484
- const findAll = /* @__PURE__ */ __name(async function(params, q, id) {
3485
- return findAllByParent(this.repo, id, sub.childRoute, params);
4443
+ const findAll = /* @__PURE__ */ __name(async function(params, q, id, req) {
4444
+ return findAllByParent(this.repo, id, sub.childRoute, params, req);
3486
4445
  }, "findAll");
3487
4446
  const decorators = /* @__PURE__ */ __name(() => {
3488
- (0, import_common10.Param)("id")(cls.prototype, methodName, 2);
4447
+ (0, import_common12.Param)("id")(cls.prototype, methodName, 2);
4448
+ (0, import_common12.Req)()(cls.prototype, methodName, 3);
3489
4449
  }, "decorators");
3490
4450
  return {
3491
4451
  name: sub.childRoute,
@@ -3504,9 +4464,10 @@ var registerFindAll = /* @__PURE__ */ __name((ctx, sub) => {
3504
4464
  const { cls } = ctx;
3505
4465
  def(cls, methodName, properties.findAll);
3506
4466
  const d = desc(cls, methodName);
3507
- (0, import_common10.Get)(route)(cls.prototype, methodName, d);
3508
- (0, import_common10.Query)(new ZodValidationPipe(RequestDtoNoOffset.zodSchema))(cls.prototype, methodName, 0);
3509
- (0, import_common10.Query)("q")(cls.prototype, methodName, 1);
4467
+ (0, import_common12.Get)(route)(cls.prototype, methodName, d);
4468
+ (0, import_common12.Query)(new ZodValidationPipe(RequestDtoNoOffset.zodSchema))(cls.prototype, methodName, 0);
4469
+ (0, import_common12.Query)("q")(cls.prototype, methodName, 1);
4470
+ if (!sub) (0, import_common12.Req)()(cls.prototype, methodName, 2);
3510
4471
  (0, import_swagger5.ApiOperation)({
3511
4472
  summary: `List all ${name}s`
3512
4473
  })(cls.prototype, methodName, d);
@@ -3524,7 +4485,7 @@ var registerFindAll = /* @__PURE__ */ __name((ctx, sub) => {
3524
4485
  }, "registerFindAll");
3525
4486
 
3526
4487
  // src/lib/crud/operations/register-findone.ts
3527
- var import_common11 = require("@nestjs/common");
4488
+ var import_common13 = require("@nestjs/common");
3528
4489
  var import_swagger6 = require("@nestjs/swagger");
3529
4490
  var defaultFindOne = /* @__PURE__ */ __name((ctx) => {
3530
4491
  if (!isOperationEnabled(ctx.definition, "findOne")) return null;
@@ -3534,11 +4495,12 @@ var defaultFindOne = /* @__PURE__ */ __name((ctx) => {
3534
4495
  route: ":id",
3535
4496
  methodName,
3536
4497
  name: config.name,
3537
- findOneFn: /* @__PURE__ */ __name(function(id) {
3538
- return this.repo.findOne(id);
4498
+ findOneFn: /* @__PURE__ */ __name(function(id, req) {
4499
+ return this.repo.findOne(id, req);
3539
4500
  }, "findOneFn"),
3540
4501
  decorators: /* @__PURE__ */ __name(() => {
3541
- (0, import_common11.Param)("id")(cls.prototype, methodName, 0);
4502
+ (0, import_common13.Param)("id")(cls.prototype, methodName, 0);
4503
+ (0, import_common13.Req)()(cls.prototype, methodName, 1);
3542
4504
  }, "decorators")
3543
4505
  };
3544
4506
  }, "defaultFindOne");
@@ -3546,12 +4508,13 @@ var childFindOne = /* @__PURE__ */ __name((sub) => (ctx) => {
3546
4508
  if (!isOperationEnabled(sub.operations, "findOne")) return null;
3547
4509
  const { cls } = ctx;
3548
4510
  const methodName = `findOneChild_${sub.childRoute}`;
3549
- const findOneFn = /* @__PURE__ */ __name(async function(parentId, childId) {
3550
- return this.repo.findOneChild(sub, childId, parentId);
4511
+ const findOneFn = /* @__PURE__ */ __name(async function(parentId2, childId, req) {
4512
+ return this.repo.findOneChild(sub, childId, parentId2, req);
3551
4513
  }, "findOneFn");
3552
4514
  const decorators = /* @__PURE__ */ __name(() => {
3553
- (0, import_common11.Param)("id")(cls.prototype, methodName, 0);
3554
- (0, import_common11.Param)("childId")(cls.prototype, methodName, 1);
4515
+ (0, import_common13.Param)("id")(cls.prototype, methodName, 0);
4516
+ (0, import_common13.Param)("childId")(cls.prototype, methodName, 1);
4517
+ (0, import_common13.Req)()(cls.prototype, methodName, 2);
3555
4518
  }, "decorators");
3556
4519
  return {
3557
4520
  route: `:id/${sub.childRoute}/:childId`,
@@ -3569,7 +4532,7 @@ var registerFindOne = /* @__PURE__ */ __name((ctx, sub) => {
3569
4532
  const { cls } = ctx;
3570
4533
  def(cls, methodName, properties.findOneFn);
3571
4534
  const d = desc(cls, methodName);
3572
- (0, import_common11.Get)(route)(cls.prototype, methodName, d);
4535
+ (0, import_common13.Get)(route)(cls.prototype, methodName, d);
3573
4536
  (0, import_swagger6.ApiOperation)({
3574
4537
  summary: `Get one ${name} by id`
3575
4538
  })(cls.prototype, methodName, d);
@@ -3588,7 +4551,7 @@ var registerFindOne = /* @__PURE__ */ __name((ctx, sub) => {
3588
4551
  }, "registerFindOne");
3589
4552
 
3590
4553
  // src/lib/crud/operations/register-patch.ts
3591
- var import_common12 = require("@nestjs/common");
4554
+ var import_common14 = require("@nestjs/common");
3592
4555
  var import_swagger7 = require("@nestjs/swagger");
3593
4556
  var defaultPatch = /* @__PURE__ */ __name((ctx) => {
3594
4557
  if (!isOperationEnabled(ctx.definition, "patch")) return null;
@@ -3598,12 +4561,13 @@ var defaultPatch = /* @__PURE__ */ __name((ctx) => {
3598
4561
  route: ":id",
3599
4562
  methodName,
3600
4563
  name: config.name,
3601
- patchFn: /* @__PURE__ */ __name(function(id, body) {
3602
- return this.repo.patch(id, body);
4564
+ patchFn: /* @__PURE__ */ __name(function(id, body, req) {
4565
+ return this.repo.patch(id, body, req);
3603
4566
  }, "patchFn"),
3604
4567
  decorators: /* @__PURE__ */ __name(() => {
3605
- (0, import_common12.Param)("id")(cls.prototype, methodName, 0);
4568
+ (0, import_common14.Param)("id")(cls.prototype, methodName, 0);
3606
4569
  bodyDecorator(patchSchema)(cls.prototype, methodName, 1);
4570
+ (0, import_common14.Req)()(cls.prototype, methodName, 2);
3607
4571
  }, "decorators")
3608
4572
  };
3609
4573
  }, "defaultPatch");
@@ -3611,13 +4575,14 @@ var childPatch = /* @__PURE__ */ __name((sub) => (ctx) => {
3611
4575
  if (!isOperationEnabled(sub.operations, "patch")) return null;
3612
4576
  const { cls } = ctx;
3613
4577
  const methodName = `patchChild_${sub.childRoute}`;
3614
- const patchFn = /* @__PURE__ */ __name(async function(_id, childId, body) {
3615
- return this.repo.updateChild(sub, childId, body);
4578
+ const patchFn = /* @__PURE__ */ __name(async function(_id, childId, body, req) {
4579
+ return this.repo.updateChild(sub, childId, body, req);
3616
4580
  }, "patchFn");
3617
4581
  const decorators = /* @__PURE__ */ __name(() => {
3618
- (0, import_common12.Param)("id")(cls.prototype, methodName, 0);
3619
- (0, import_common12.Param)("childId")(cls.prototype, methodName, 1);
3620
- (0, import_common12.Body)()(cls.prototype, methodName, 2);
4582
+ (0, import_common14.Param)("id")(cls.prototype, methodName, 0);
4583
+ (0, import_common14.Param)("childId")(cls.prototype, methodName, 1);
4584
+ (0, import_common14.Body)()(cls.prototype, methodName, 2);
4585
+ (0, import_common14.Req)()(cls.prototype, methodName, 3);
3621
4586
  }, "decorators");
3622
4587
  return {
3623
4588
  route: `:id/${sub.childRoute}/:childId`,
@@ -3635,7 +4600,7 @@ var registerPatch = /* @__PURE__ */ __name((ctx, sub) => {
3635
4600
  const { cls } = ctx;
3636
4601
  def(cls, methodName, properties.patchFn);
3637
4602
  const d = desc(cls, methodName);
3638
- (0, import_common12.Patch)(route)(cls.prototype, methodName, d);
4603
+ (0, import_common14.Patch)(route)(cls.prototype, methodName, d);
3639
4604
  (0, import_swagger7.ApiOperation)({
3640
4605
  summary: `Update a ${name}`
3641
4606
  })(cls.prototype, methodName, d);
@@ -3650,30 +4615,30 @@ var registerPatch = /* @__PURE__ */ __name((ctx, sub) => {
3650
4615
  }, "registerPatch");
3651
4616
 
3652
4617
  // src/lib/crud/operations/register-schema-endpoints.ts
3653
- var import_common13 = require("@nestjs/common");
4618
+ var import_common15 = require("@nestjs/common");
3654
4619
  var import_swagger8 = require("@nestjs/swagger");
3655
4620
 
3656
4621
  // src/lib/crud/resource/PatchResourceJson.schema.ts
3657
- var import_zod23 = require("zod");
3658
- var FieldVariantPatchSchema = import_zod23.z.object({
3659
- type: import_zod23.z.string().nullable().optional(),
3660
- format: import_zod23.z.string().nullable().optional(),
3661
- resource: import_zod23.z.string().nullable().optional(),
3662
- position: import_zod23.z.number().nullable().optional(),
3663
- options: import_zod23.z.record(import_zod23.z.string(), import_zod23.z.unknown().nullable()).optional()
4622
+ var import_zod28 = require("zod");
4623
+ var FieldVariantPatchSchema = import_zod28.z.object({
4624
+ type: import_zod28.z.string().nullable().optional(),
4625
+ format: import_zod28.z.string().nullable().optional(),
4626
+ resource: import_zod28.z.string().nullable().optional(),
4627
+ position: import_zod28.z.number().nullable().optional(),
4628
+ options: import_zod28.z.record(import_zod28.z.string(), import_zod28.z.unknown().nullable()).optional()
3664
4629
  }).partial();
3665
- var PatchColumnSchema = import_zod23.z.object({
3666
- label: import_zod23.z.string().optional(),
3667
- column: import_zod23.z.string().optional(),
3668
- hiddenInTable: import_zod23.z.boolean().optional(),
3669
- hiddenInForm: import_zod23.z.boolean().optional(),
3670
- hiddenInView: import_zod23.z.boolean().optional(),
4630
+ var PatchColumnSchema = import_zod28.z.object({
4631
+ label: import_zod28.z.string().optional(),
4632
+ column: import_zod28.z.string().optional(),
4633
+ hiddenInTable: import_zod28.z.boolean().optional(),
4634
+ hiddenInForm: import_zod28.z.boolean().optional(),
4635
+ hiddenInView: import_zod28.z.boolean().optional(),
3671
4636
  fieldInput: FieldVariantPatchSchema.optional(),
3672
4637
  fieldView: FieldVariantPatchSchema.optional(),
3673
4638
  fieldTable: FieldVariantPatchSchema.optional()
3674
4639
  }).partial();
3675
- var PatchResourceJsonSchema = import_zod23.z.object({
3676
- columns: import_zod23.z.record(import_zod23.z.string(), PatchColumnSchema)
4640
+ var PatchResourceJsonSchema = import_zod28.z.object({
4641
+ columns: import_zod28.z.record(import_zod28.z.string(), PatchColumnSchema)
3677
4642
  });
3678
4643
 
3679
4644
  // src/lib/crud/resource/WriteResourceJson.ts
@@ -3906,7 +4871,7 @@ var resolveActions = /* @__PURE__ */ __name((baseUrl, actions) => {
3906
4871
  var buildViewsPayload = /* @__PURE__ */ __name((config, baseUrl) => {
3907
4872
  if (!config.views || !Object.keys(config.views).length) return void 0;
3908
4873
  const definition = resolveDefinition(config);
3909
- const baseUri = `${baseUrl}/${config.route}`;
4874
+ const baseUri = config.parent ? `${baseUrl}/${config.parent.route}/{${config.parent.param}}/${config.route}` : `${baseUrl}/${config.route}`;
3910
4875
  const operations = buildResourceOperations(definition, baseUri);
3911
4876
  if (isOperationEnabled(definition, "findAll")) {
3912
4877
  operations["lookup"] = `${baseUri}?q={text}`;
@@ -3926,7 +4891,7 @@ var buildViewsPayload = /* @__PURE__ */ __name((config, baseUrl) => {
3926
4891
  id: config.name,
3927
4892
  name: config.name,
3928
4893
  route: config.route,
3929
- uri: `${baseUrl}/${config.route}`,
4894
+ uri: baseUri,
3930
4895
  title: config.title ?? config.tag,
3931
4896
  idField: config.lookup?.key ?? "id",
3932
4897
  idType: config.idType ?? "string",
@@ -3995,7 +4960,7 @@ var buildEditableColumnsPayload = /* @__PURE__ */ __name((config) => {
3995
4960
  }, "buildEditableColumnsPayload");
3996
4961
 
3997
4962
  // src/lib/crud/operations/register-schema-endpoints.ts
3998
- var import_node_path5 = require("path");
4963
+ var import_node_path6 = require("path");
3999
4964
  var registerDefinitionEndpoint = /* @__PURE__ */ __name((ctx) => {
4000
4965
  const { cls, config } = ctx;
4001
4966
  const { route, name } = config;
@@ -4008,7 +4973,7 @@ var registerDefinitionEndpoint = /* @__PURE__ */ __name((ctx) => {
4008
4973
  return definitionPayload;
4009
4974
  });
4010
4975
  const d = desc(cls, "getDefinition");
4011
- (0, import_common13.Get)("definition")(cls.prototype, "getDefinition", d);
4976
+ (0, import_common15.Get)("definition")(cls.prototype, "getDefinition", d);
4012
4977
  (0, import_swagger8.ApiOperation)({
4013
4978
  summary: `Get the resource definition for ${name}`
4014
4979
  })(cls.prototype, "getDefinition", d);
@@ -4029,7 +4994,7 @@ var registerResourceJsonEndpoint = /* @__PURE__ */ __name((ctx) => {
4029
4994
  return resourceJsonPayload;
4030
4995
  });
4031
4996
  const d = desc(cls, "getResourceJson");
4032
- (0, import_common13.Get)("resource.json")(cls.prototype, "getResourceJson", d);
4997
+ (0, import_common15.Get)("resource.json")(cls.prototype, "getResourceJson", d);
4033
4998
  (0, import_swagger8.ApiOperation)({
4034
4999
  summary: `Get resource descriptor for ${name}`
4035
5000
  })(cls.prototype, "getResourceJson", d);
@@ -4043,13 +5008,13 @@ var registerResourceColumnsEndpoint = /* @__PURE__ */ __name((ctx) => {
4043
5008
  const { route, name } = config;
4044
5009
  def(cls, "getResourceColumns", async function() {
4045
5010
  if (!IS_DEV) {
4046
- throw new import_common13.ForbiddenException("The resource schema editor is only available when the backend is running in local dev mode.");
5011
+ throw new import_common15.ForbiddenException("The resource schema editor is only available when the backend is running in local dev mode.");
4047
5012
  }
4048
5013
  const fresh = await this.configRegistry.getByRoute(route);
4049
5014
  return buildEditableColumnsPayload(fresh ?? config);
4050
5015
  });
4051
5016
  const d = desc(cls, "getResourceColumns");
4052
- (0, import_common13.Get)("resource-columns")(cls.prototype, "getResourceColumns", d);
5017
+ (0, import_common15.Get)("resource-columns")(cls.prototype, "getResourceColumns", d);
4053
5018
  (0, import_swagger8.ApiOperation)({
4054
5019
  summary: `Dev-only: get the editable column list for ${name}`
4055
5020
  })(cls.prototype, "getResourceColumns", d);
@@ -4063,30 +5028,30 @@ var registerResourceJsonPatchEndpoint = /* @__PURE__ */ __name((ctx) => {
4063
5028
  const { route, name } = config;
4064
5029
  def(cls, "patchResourceJson", async function(body) {
4065
5030
  if (!IS_DEV) {
4066
- throw new import_common13.ForbiddenException("Editing resource.json is only available when the backend is running in local dev mode.");
5031
+ throw new import_common15.ForbiddenException("Editing resource.json is only available when the backend is running in local dev mode.");
4067
5032
  }
4068
5033
  await this.configRegistry.getByRoute(route);
4069
5034
  const dir = this.configRegistry.getResourceDir(route);
4070
5035
  if (!dir) {
4071
- throw new import_common13.NotFoundException(`No resource.json on disk for "${name}" (route "${route}").`);
5036
+ throw new import_common15.NotFoundException(`No resource.json on disk for "${name}" (route "${route}").`);
4072
5037
  }
4073
- const jsonPath = (0, import_node_path5.join)(dir, "resource.json");
5038
+ const jsonPath = (0, import_node_path6.join)(dir, "resource.json");
4074
5039
  const raw = readRawResourceJson(jsonPath);
4075
5040
  if (!raw) {
4076
- throw new import_common13.NotFoundException(`resource.json not found at ${jsonPath}`);
5041
+ throw new import_common15.NotFoundException(`resource.json not found at ${jsonPath}`);
4077
5042
  }
4078
5043
  const merged = applyColumnPatch(raw, body);
4079
5044
  const validated = validateResourceJson(merged);
4080
5045
  if (!validated.success) {
4081
- throw new import_common13.BadRequestException(validated.error.issues);
5046
+ throw new import_common15.BadRequestException(validated.error.issues);
4082
5047
  }
4083
5048
  writeRawResourceJson(jsonPath, merged);
4084
5049
  const fresh = await this.configRegistry.getByRoute(route);
4085
5050
  return buildEditableColumnsPayload(fresh ?? config);
4086
5051
  });
4087
5052
  const d = desc(cls, "patchResourceJson");
4088
- (0, import_common13.Patch)("resource.json")(cls.prototype, "patchResourceJson", d);
4089
- (0, import_common13.Body)(new ZodValidationPipe(PatchResourceJsonSchema))(cls.prototype, "patchResourceJson", 0);
5053
+ (0, import_common15.Patch)("resource.json")(cls.prototype, "patchResourceJson", d);
5054
+ (0, import_common15.Body)(new ZodValidationPipe(PatchResourceJsonSchema))(cls.prototype, "patchResourceJson", 0);
4090
5055
  (0, import_swagger8.ApiOperation)({
4091
5056
  summary: `Dev-only: patch column layout in resource.json for ${name}`
4092
5057
  })(cls.prototype, "patchResourceJson", d);
@@ -4100,22 +5065,22 @@ var registerResourceJsonRawGetEndpoint = /* @__PURE__ */ __name((ctx) => {
4100
5065
  const { route, name } = config;
4101
5066
  def(cls, "getResourceJsonRaw", async function() {
4102
5067
  if (!IS_DEV) {
4103
- throw new import_common13.ForbiddenException("The resource JSON editor is only available when the backend is running in local dev mode.");
5068
+ throw new import_common15.ForbiddenException("The resource JSON editor is only available when the backend is running in local dev mode.");
4104
5069
  }
4105
5070
  await this.configRegistry.getByRoute(route);
4106
5071
  const dir = this.configRegistry.getResourceDir(route);
4107
5072
  if (!dir) {
4108
- throw new import_common13.NotFoundException(`No resource.json on disk for "${name}" (route "${route}").`);
5073
+ throw new import_common15.NotFoundException(`No resource.json on disk for "${name}" (route "${route}").`);
4109
5074
  }
4110
- const jsonPath = (0, import_node_path5.join)(dir, "resource.json");
5075
+ const jsonPath = (0, import_node_path6.join)(dir, "resource.json");
4111
5076
  const raw = readRawResourceJson(jsonPath);
4112
5077
  if (!raw) {
4113
- throw new import_common13.NotFoundException(`resource.json not found at ${jsonPath}`);
5078
+ throw new import_common15.NotFoundException(`resource.json not found at ${jsonPath}`);
4114
5079
  }
4115
5080
  return raw;
4116
5081
  });
4117
5082
  const d = desc(cls, "getResourceJsonRaw");
4118
- (0, import_common13.Get)("resource-json-raw")(cls.prototype, "getResourceJsonRaw", d);
5083
+ (0, import_common15.Get)("resource-json-raw")(cls.prototype, "getResourceJsonRaw", d);
4119
5084
  (0, import_swagger8.ApiOperation)({
4120
5085
  summary: `Dev-only: get the raw resource.json for ${name}`
4121
5086
  })(cls.prototype, "getResourceJsonRaw", d);
@@ -4129,24 +5094,24 @@ var registerResourceJsonRawPutEndpoint = /* @__PURE__ */ __name((ctx) => {
4129
5094
  const { route, name } = config;
4130
5095
  def(cls, "putResourceJsonRaw", async function(body) {
4131
5096
  if (!IS_DEV) {
4132
- throw new import_common13.ForbiddenException("Editing resource.json is only available when the backend is running in local dev mode.");
5097
+ throw new import_common15.ForbiddenException("Editing resource.json is only available when the backend is running in local dev mode.");
4133
5098
  }
4134
5099
  await this.configRegistry.getByRoute(route);
4135
5100
  const dir = this.configRegistry.getResourceDir(route);
4136
5101
  if (!dir) {
4137
- throw new import_common13.NotFoundException(`No resource.json on disk for "${name}" (route "${route}").`);
5102
+ throw new import_common15.NotFoundException(`No resource.json on disk for "${name}" (route "${route}").`);
4138
5103
  }
4139
5104
  const validated = validateResourceJson(body);
4140
5105
  if (!validated.success) {
4141
- throw new import_common13.BadRequestException(validated.error.issues);
5106
+ throw new import_common15.BadRequestException(validated.error.issues);
4142
5107
  }
4143
- const jsonPath = (0, import_node_path5.join)(dir, "resource.json");
5108
+ const jsonPath = (0, import_node_path6.join)(dir, "resource.json");
4144
5109
  writeRawResourceJson(jsonPath, body);
4145
5110
  return body;
4146
5111
  });
4147
5112
  const d = desc(cls, "putResourceJsonRaw");
4148
- (0, import_common13.Put)("resource-json-raw")(cls.prototype, "putResourceJsonRaw", d);
4149
- (0, import_common13.Body)()(cls.prototype, "putResourceJsonRaw", 0);
5113
+ (0, import_common15.Put)("resource-json-raw")(cls.prototype, "putResourceJsonRaw", d);
5114
+ (0, import_common15.Body)()(cls.prototype, "putResourceJsonRaw", 0);
4150
5115
  (0, import_swagger8.ApiOperation)({
4151
5116
  summary: `Dev-only: replace the full resource.json for ${name}`
4152
5117
  })(cls.prototype, "putResourceJsonRaw", d);
@@ -4157,7 +5122,7 @@ var registerResourceJsonRawPutEndpoint = /* @__PURE__ */ __name((ctx) => {
4157
5122
  }, "registerResourceJsonRawPutEndpoint");
4158
5123
 
4159
5124
  // src/lib/crud/operations/register-schemas.ts
4160
- var import_common14 = require("@nestjs/common");
5125
+ var import_common16 = require("@nestjs/common");
4161
5126
  var import_swagger9 = require("@nestjs/swagger");
4162
5127
  var defaultSchemas = /* @__PURE__ */ __name((ctx) => {
4163
5128
  const { config, baseUrl } = ctx;
@@ -4201,7 +5166,7 @@ var registerSchemas = /* @__PURE__ */ __name((ctx, sub) => {
4201
5166
  const { cls } = ctx;
4202
5167
  def(cls, methodName, properties.schemasFn);
4203
5168
  const d = desc(cls, methodName);
4204
- (0, import_common14.Get)(route)(cls.prototype, methodName, d);
5169
+ (0, import_common16.Get)(route)(cls.prototype, methodName, d);
4205
5170
  (0, import_swagger9.ApiOperation)({
4206
5171
  summary: `Get view schemas for ${name}`
4207
5172
  })(cls.prototype, methodName, d);
@@ -4213,7 +5178,7 @@ var registerSchemas = /* @__PURE__ */ __name((ctx, sub) => {
4213
5178
  }, "registerSchemas");
4214
5179
 
4215
5180
  // src/lib/crud/operations/register-update.ts
4216
- var import_common15 = require("@nestjs/common");
5181
+ var import_common17 = require("@nestjs/common");
4217
5182
  var import_swagger10 = require("@nestjs/swagger");
4218
5183
  var defaultUpdate = /* @__PURE__ */ __name((ctx) => {
4219
5184
  if (!isOperationEnabled(ctx.definition, "update")) return null;
@@ -4223,12 +5188,13 @@ var defaultUpdate = /* @__PURE__ */ __name((ctx) => {
4223
5188
  route: ":id",
4224
5189
  methodName,
4225
5190
  name: config.name,
4226
- updateFn: /* @__PURE__ */ __name(function(id, body) {
4227
- return this.repo.update(id, body);
5191
+ updateFn: /* @__PURE__ */ __name(function(id, body, req) {
5192
+ return this.repo.update(id, body, req);
4228
5193
  }, "updateFn"),
4229
5194
  decorators: /* @__PURE__ */ __name(() => {
4230
- (0, import_common15.Param)("id")(cls.prototype, methodName, 0);
5195
+ (0, import_common17.Param)("id")(cls.prototype, methodName, 0);
4231
5196
  bodyDecorator(updateSchema)(cls.prototype, methodName, 1);
5197
+ (0, import_common17.Req)()(cls.prototype, methodName, 2);
4232
5198
  }, "decorators")
4233
5199
  };
4234
5200
  }, "defaultUpdate");
@@ -4236,13 +5202,14 @@ var childUpdate = /* @__PURE__ */ __name((sub) => (ctx) => {
4236
5202
  if (!isOperationEnabled(sub.operations, "update")) return null;
4237
5203
  const { cls } = ctx;
4238
5204
  const methodName = `updateChild_${sub.childRoute}`;
4239
- const updateFn = /* @__PURE__ */ __name(async function(_id, childId, body) {
4240
- return this.repo.updateChild(sub, childId, body);
5205
+ const updateFn = /* @__PURE__ */ __name(async function(_id, childId, body, req) {
5206
+ return this.repo.updateChild(sub, childId, body, req);
4241
5207
  }, "updateFn");
4242
5208
  const decorators = /* @__PURE__ */ __name(() => {
4243
- (0, import_common15.Param)("id")(cls.prototype, methodName, 0);
4244
- (0, import_common15.Param)("childId")(cls.prototype, methodName, 1);
4245
- (0, import_common15.Body)()(cls.prototype, methodName, 2);
5209
+ (0, import_common17.Param)("id")(cls.prototype, methodName, 0);
5210
+ (0, import_common17.Param)("childId")(cls.prototype, methodName, 1);
5211
+ (0, import_common17.Body)()(cls.prototype, methodName, 2);
5212
+ (0, import_common17.Req)()(cls.prototype, methodName, 3);
4246
5213
  }, "decorators");
4247
5214
  return {
4248
5215
  route: `:id/${sub.childRoute}/:childId`,
@@ -4260,7 +5227,7 @@ var registerUpdate = /* @__PURE__ */ __name((ctx, sub) => {
4260
5227
  const { cls } = ctx;
4261
5228
  def(cls, methodName, properties.updateFn);
4262
5229
  const d = desc(cls, methodName);
4263
- (0, import_common15.Put)(route)(cls.prototype, methodName, d);
5230
+ (0, import_common17.Put)(route)(cls.prototype, methodName, d);
4264
5231
  (0, import_swagger10.ApiOperation)({
4265
5232
  summary: `Replace a ${name}`
4266
5233
  })(cls.prototype, methodName, d);
@@ -4318,10 +5285,18 @@ function createCrudController(config, baseUrl) {
4318
5285
  throw new Error(`Resource "${name}" declares 'upsert' but no upsertOn`);
4319
5286
  }
4320
5287
  const bodyDecorator = /* @__PURE__ */ __name((schema, options) => {
4321
- if (!schema) return (0, import_common16.Body)();
4322
- if (isZodSchema(schema)) return (0, import_common16.Body)(new ZodValidationPipe(schema, options));
4323
- return (0, import_common16.Body)();
5288
+ if (!schema) return (0, import_common18.Body)();
5289
+ if (isZodSchema(schema)) return (0, import_common18.Body)(new ZodValidationPipe(schema, options));
5290
+ return (0, import_common18.Body)();
4324
5291
  }, "bodyDecorator");
5292
+ const resolveClient = /* @__PURE__ */ __name((registry, resource) => {
5293
+ try {
5294
+ return registry.resolve(resource.database);
5295
+ } catch (e) {
5296
+ if (resource.kind === "custom") return void 0;
5297
+ throw e;
5298
+ }
5299
+ }, "resolveClient");
4325
5300
  let CrudControllerBase = class CrudControllerBase {
4326
5301
  static {
4327
5302
  __name(this, "CrudControllerBase");
@@ -4329,8 +5304,8 @@ function createCrudController(config, baseUrl) {
4329
5304
  repo;
4330
5305
  configRegistry;
4331
5306
  constructor(registry, configRegistry) {
4332
- const prisma = registry.resolve(config.database);
4333
- this.repo = createCrudRepository(prisma, config);
5307
+ const prisma = resolveClient(registry, config);
5308
+ this.repo = createCrudRepository(prisma, config, registry);
4334
5309
  this.configRegistry = configRegistry;
4335
5310
  }
4336
5311
  };
@@ -4352,7 +5327,7 @@ function createCrudController(config, baseUrl) {
4352
5327
  baseUrl
4353
5328
  };
4354
5329
  registerEndpoints(ctx);
4355
- (0, import_common16.Controller)(route)(CrudControllerBase);
5330
+ (0, import_common18.Controller)(resourceControllerPath(route, config.parent))(CrudControllerBase);
4356
5331
  (0, import_swagger11.ApiTags)(tag)(CrudControllerBase);
4357
5332
  Object.defineProperty(CrudControllerBase, "name", {
4358
5333
  value: `${name.charAt(0).toUpperCase() + name.slice(1)}Controller`
@@ -4366,7 +5341,7 @@ function createCrudController(config, baseUrl) {
4366
5341
  __name(createCrudController, "createCrudController");
4367
5342
 
4368
5343
  // src/lib/crud/dev-tools/dev-resources.controller.ts
4369
- var import_common17 = require("@nestjs/common");
5344
+ var import_common19 = require("@nestjs/common");
4370
5345
  var import_swagger12 = require("@nestjs/swagger");
4371
5346
 
4372
5347
  // ../crouton-codegen/src/naming.ts
@@ -4595,9 +5570,14 @@ var classify = /* @__PURE__ */ __name((model, ctx = {}) => {
4595
5570
  };
4596
5571
  } else if (field.kind === "relation") {
4597
5572
  const target = ctx.resolveRelationResource?.(field.relationModel ?? "");
5573
+ const isCollectionSide = field.relationType === "oneToMany" || field.relationType === "manyToMany";
4598
5574
  if (ruleset.showRelationsInForm && target) {
4599
5575
  col = {
4600
5576
  hiddenInTable: ruleset.hideRelationsInTable,
5577
+ ...isCollectionSide ? {
5578
+ hiddenInForm: true,
5579
+ hiddenInView: true
5580
+ } : {},
4601
5581
  fieldInput: {
4602
5582
  format: "relation",
4603
5583
  resource: target,
@@ -4793,7 +5773,8 @@ var diff = /* @__PURE__ */ __name(({ draft, existing, hasSchemaFile = false }) =
4793
5773
  }
4794
5774
  return {
4795
5775
  name: draft.name,
4796
- model: draft.config.model,
5776
+ // Codegen only ever drafts prisma-backed resources, so `model` is set.
5777
+ model: draft.config.model ?? draft.name,
4797
5778
  isNew,
4798
5779
  decisions,
4799
5780
  draft,
@@ -4825,7 +5806,7 @@ var resolve2 = /* @__PURE__ */ __name(async (diff2, resolver = recommendedResolv
4825
5806
  // ../crouton-codegen/src/serialize.ts
4826
5807
  var RESOURCE_SCHEMA_URL = `https://ghentcdh.github.io/crouton/schema/v${CURRENT_RESOURCE_VERSION}/resource.schema.json`;
4827
5808
  var withResourceHeader = /* @__PURE__ */ __name((config, opts = {}) => {
4828
- const { $schema: _schema, schemaVersion: _version, draft: existingDraft, ...rest } = config;
5809
+ const { $schema: _schema, schemaVersion: _version, draft: existingDraft, kind: existingKind, ...rest } = config;
4829
5810
  const draft = opts.draft !== void 0 ? opts.draft : existingDraft;
4830
5811
  return {
4831
5812
  $schema: RESOURCE_SCHEMA_URL,
@@ -4833,6 +5814,9 @@ var withResourceHeader = /* @__PURE__ */ __name((config, opts = {}) => {
4833
5814
  ...draft !== void 0 ? {
4834
5815
  draft
4835
5816
  } : {},
5817
+ ...existingKind !== void 0 ? {
5818
+ kind: existingKind
5819
+ } : {},
4836
5820
  ...rest
4837
5821
  };
4838
5822
  }, "withResourceHeader");
@@ -4933,7 +5917,7 @@ var apply = /* @__PURE__ */ __name((resolved, ctx) => {
4933
5917
 
4934
5918
  // ../crouton-codegen/src/commit.ts
4935
5919
  var import_promises3 = require("fs/promises");
4936
- var import_node_path6 = require("path");
5920
+ var import_node_path7 = require("path");
4937
5921
  var commit = /* @__PURE__ */ __name(async (plan, opts = {}) => {
4938
5922
  const written = [];
4939
5923
  const skipped = [];
@@ -4943,7 +5927,7 @@ var commit = /* @__PURE__ */ __name(async (plan, opts = {}) => {
4943
5927
  continue;
4944
5928
  }
4945
5929
  if (!opts.dryRun) {
4946
- await (0, import_promises3.mkdir)((0, import_node_path6.dirname)(file.path), {
5930
+ await (0, import_promises3.mkdir)((0, import_node_path7.dirname)(file.path), {
4947
5931
  recursive: true
4948
5932
  });
4949
5933
  await (0, import_promises3.writeFile)(file.path, file.contents, "utf-8");
@@ -4958,15 +5942,15 @@ var commit = /* @__PURE__ */ __name(async (plan, opts = {}) => {
4958
5942
 
4959
5943
  // ../crouton-codegen/src/config.ts
4960
5944
  var import_promises4 = require("fs/promises");
4961
- var import_node_path7 = require("path");
5945
+ var import_node_path8 = require("path");
4962
5946
  var findConfigPath2 = /* @__PURE__ */ __name(async (cwd) => {
4963
- let dir = (0, import_node_path7.resolve)(cwd);
5947
+ let dir = (0, import_node_path8.resolve)(cwd);
4964
5948
  while (true) {
4965
5949
  for (const name of CONFIG_FILES) {
4966
- const candidate = (0, import_node_path7.join)(dir, name);
5950
+ const candidate = (0, import_node_path8.join)(dir, name);
4967
5951
  if (await fileExists2(candidate)) return candidate;
4968
5952
  }
4969
- const parent = (0, import_node_path7.dirname)(dir);
5953
+ const parent = (0, import_node_path8.dirname)(dir);
4970
5954
  if (parent === dir) return void 0;
4971
5955
  dir = parent;
4972
5956
  }
@@ -4987,7 +5971,7 @@ var loadConfig2 = /* @__PURE__ */ __name(async (cwd) => {
4987
5971
  return {
4988
5972
  config,
4989
5973
  path,
4990
- root: (0, import_node_path7.dirname)(path)
5974
+ root: (0, import_node_path8.dirname)(path)
4991
5975
  };
4992
5976
  }, "loadConfig");
4993
5977
  var validateConfig = /* @__PURE__ */ __name((config, path = "<config>") => {
@@ -5004,7 +5988,7 @@ var loadDatasources = /* @__PURE__ */ __name(async (loaded) => {
5004
5988
  const datasources = [];
5005
5989
  for (const e of entries) {
5006
5990
  if (!e.isDirectory()) continue;
5007
- const jsonPath = (0, import_node_path7.join)(base, e.name, "data-source.json");
5991
+ const jsonPath = (0, import_node_path8.join)(base, e.name, "data-source.json");
5008
5992
  if (!await fileExists2(jsonPath)) continue;
5009
5993
  const ds_json = JSON.parse(await (0, import_promises4.readFile)(jsonPath, "utf-8"));
5010
5994
  const name = ds_json.name ?? e.name;
@@ -5050,34 +6034,42 @@ var makeSchemaExportName = /* @__PURE__ */ __name((config) => {
5050
6034
  const template = config.schemaExportName ?? "{Model}WithRelationsSchema";
5051
6035
  return (prismaName) => template.replace("{Model}", prismaName);
5052
6036
  }, "makeSchemaExportName");
5053
- var resolveFromRoot = /* @__PURE__ */ __name((root, p) => (0, import_node_path7.isAbsolute)(p) ? p : (0, import_node_path7.join)(root, p), "resolveFromRoot");
6037
+ var resolveFromRoot = /* @__PURE__ */ __name((root, p) => (0, import_node_path8.isAbsolute)(p) ? p : (0, import_node_path8.join)(root, p), "resolveFromRoot");
5054
6038
 
5055
6039
  // ../crouton-codegen/src/scaffold.ts
5056
- var import_zod24 = require("zod");
6040
+ var import_zod29 = require("zod");
5057
6041
  var import_promises5 = require("fs/promises");
5058
- var import_node_path8 = require("path");
6042
+ var import_node_path9 = require("path");
5059
6043
  var ScallfoldDatasourceSchema = DataSourceShape.extend({
5060
6044
  /** Folder name under `dataSourcesDir`. */
5061
- folder: import_zod24.z.string().default("default")
6045
+ folder: import_zod29.z.string().default("default")
5062
6046
  }).transform(transformDataSource);
5063
6047
 
5064
6048
  // ../crouton-codegen/src/datasource-scaffold.ts
5065
- var import_node_path9 = require("path");
6049
+ var import_node_path10 = require("path");
5066
6050
 
5067
6051
  // ../crouton-codegen/src/project.ts
5068
6052
  var import_promises6 = require("fs/promises");
5069
- var import_node_path10 = require("path");
5070
- var resourceDir = /* @__PURE__ */ __name((loaded, name) => (0, import_node_path10.join)(resolveFromRoot(loaded.root, loaded.config.resourcesDir), name), "resourceDir");
6053
+ var import_node_path11 = require("path");
6054
+ var resourceDir = /* @__PURE__ */ __name((loaded, name) => (0, import_node_path11.join)(resolveFromRoot(loaded.root, loaded.config.resourcesDir), name), "resourceDir");
5071
6055
  var readExistingResource = /* @__PURE__ */ __name(async (loaded, name) => {
5072
6056
  const dir = resourceDir(loaded, name);
5073
- const jsonPath = (0, import_node_path10.join)(dir, "resource.json");
6057
+ const jsonPath = (0, import_node_path11.join)(dir, "resource.json");
5074
6058
  const config = await fileExists2(jsonPath) ? JSON.parse(await (0, import_promises6.readFile)(jsonPath, "utf-8")) : null;
5075
- const hasSchemaFile = await fileExists2((0, import_node_path10.join)(dir, "schema.ts")) || await fileExists2((0, import_node_path10.join)(dir, "schema.js"));
6059
+ const hasSchemaFile = await fileExists2((0, import_node_path11.join)(dir, "schema.ts")) || await fileExists2((0, import_node_path11.join)(dir, "schema.js"));
5076
6060
  return {
5077
6061
  config,
5078
6062
  hasSchemaFile
5079
6063
  };
5080
6064
  }, "readExistingResource");
6065
+ var isCustomResourceFile = /* @__PURE__ */ __name(async (jsonPath) => {
6066
+ try {
6067
+ const parsed = JSON.parse(await (0, import_promises6.readFile)(jsonPath, "utf-8"));
6068
+ return parsed?.kind === "custom";
6069
+ } catch {
6070
+ return false;
6071
+ }
6072
+ }, "isCustomResourceFile");
5081
6073
  var listResourceNames = /* @__PURE__ */ __name(async (loaded) => {
5082
6074
  const base = resolveFromRoot(loaded.root, loaded.config.resourcesDir);
5083
6075
  if (!await fileExists2(base)) return [];
@@ -5086,9 +6078,11 @@ var listResourceNames = /* @__PURE__ */ __name(async (loaded) => {
5086
6078
  });
5087
6079
  const names = [];
5088
6080
  for (const e of entries) {
5089
- if (e.isDirectory() && await fileExists2((0, import_node_path10.join)(base, e.name, "resource.json"))) {
5090
- names.push(e.name);
5091
- }
6081
+ if (!e.isDirectory()) continue;
6082
+ const jsonPath = (0, import_node_path11.join)(base, e.name, "resource.json");
6083
+ if (!await fileExists2(jsonPath)) continue;
6084
+ if (await isCustomResourceFile(jsonPath)) continue;
6085
+ names.push(e.name);
5092
6086
  }
5093
6087
  return names;
5094
6088
  }, "listResourceNames");
@@ -5126,7 +6120,7 @@ var buildResourceDiffs = /* @__PURE__ */ __name(async (models, deps) => {
5126
6120
  var import_node_child_process = require("child_process");
5127
6121
  var import_node_fs4 = require("fs");
5128
6122
  var import_promises7 = require("fs/promises");
5129
- var import_node_path11 = require("path");
6123
+ var import_node_path12 = require("path");
5130
6124
  var run = /* @__PURE__ */ __name((cmd, args, cwd) => new Promise((resolve7) => {
5131
6125
  const child = (0, import_node_child_process.spawn)(cmd, args, {
5132
6126
  cwd,
@@ -5213,7 +6207,7 @@ var fixZodImports = /* @__PURE__ */ __name(async (zodOutputDir) => {
5213
6207
  return;
5214
6208
  }
5215
6209
  for (const entry of entries) {
5216
- const full = (0, import_node_path11.join)(dir, entry.name);
6210
+ const full = (0, import_node_path12.join)(dir, entry.name);
5217
6211
  if (entry.isDirectory()) {
5218
6212
  await walk(full);
5219
6213
  } else if (entry.name.endsWith(".ts")) {
@@ -5262,8 +6256,8 @@ var pullAndGenerate = /* @__PURE__ */ __name(async (input) => {
5262
6256
  };
5263
6257
  }, "pullAndGenerate");
5264
6258
  var normalizeSchema = /* @__PURE__ */ __name(async (schemaPath, configDir) => {
5265
- const dir = configDir ?? (0, import_node_path11.dirname)(schemaPath);
5266
- const configPath = (0, import_node_path11.join)(dir, "normalize-schema.json");
6259
+ const dir = configDir ?? (0, import_node_path12.dirname)(schemaPath);
6260
+ const configPath = (0, import_node_path12.join)(dir, "normalize-schema.json");
5267
6261
  if (!(0, import_node_fs4.existsSync)(configPath)) return {
5268
6262
  renamed: 0
5269
6263
  };
@@ -5293,7 +6287,7 @@ var normalizeSchema = /* @__PURE__ */ __name(async (schemaPath, configDir) => {
5293
6287
  }, "normalizeSchema");
5294
6288
 
5295
6289
  // src/lib/crud/resource/ResourceFlags.ts
5296
- var import_node_path12 = require("path");
6290
+ var import_node_path13 = require("path");
5297
6291
  var applyResourceFlagPatch = /* @__PURE__ */ __name((raw, patch) => {
5298
6292
  const result = {
5299
6293
  ...raw
@@ -5334,8 +6328,8 @@ var applyResourceFlagPatch = /* @__PURE__ */ __name((raw, patch) => {
5334
6328
  return result;
5335
6329
  }, "applyResourceFlagPatch");
5336
6330
  var resolveResourcePath = /* @__PURE__ */ __name((resourcesDir, name) => {
5337
- const resolved = (0, import_node_path12.resolve)(resourcesDir, name, "resource.json");
5338
- if (!resolved.startsWith((0, import_node_path12.resolve)(resourcesDir) + "/")) {
6331
+ const resolved = (0, import_node_path13.resolve)(resourcesDir, name, "resource.json");
6332
+ if (!resolved.startsWith((0, import_node_path13.resolve)(resourcesDir) + "/")) {
5339
6333
  throw new Error(`Invalid resource name: "${name}"`);
5340
6334
  }
5341
6335
  return resolved;
@@ -5394,7 +6388,7 @@ var DevResourcesController = class {
5394
6388
  }
5395
6389
  assertDev() {
5396
6390
  if (!IS_DEV) {
5397
- throw new import_common17.ForbiddenException("The database sync tools are only available when CROUTON_SCHEMA_EDITOR is enabled.");
6391
+ throw new import_common19.ForbiddenException("The database sync tools are only available when CROUTON_SCHEMA_EDITOR is enabled.");
5398
6392
  }
5399
6393
  }
5400
6394
  /** Loads project config + resolves the datasource + Prisma schema path. Throws 404/400 with a clear message on misconfiguration. */
@@ -5403,14 +6397,14 @@ var DevResourcesController = class {
5403
6397
  try {
5404
6398
  loaded = await loadConfig2(process.cwd());
5405
6399
  } catch (e) {
5406
- throw new import_common17.NotFoundException(e.message ?? "No crouton.json config found.");
6400
+ throw new import_common19.NotFoundException(e.message ?? "No crouton.json config found.");
5407
6401
  }
5408
6402
  const datasources = await loadDatasources(loaded);
5409
6403
  let ds;
5410
6404
  try {
5411
6405
  ds = resolveDatasource(datasources, datasourceName);
5412
6406
  } catch (e) {
5413
- throw new import_common17.BadRequestException(e.message);
6407
+ throw new import_common19.BadRequestException(e.message);
5414
6408
  }
5415
6409
  const schemaPath = resolveFromRoot(loaded.root, ds.prismaSchema);
5416
6410
  return {
@@ -5425,7 +6419,7 @@ var DevResourcesController = class {
5425
6419
  schemaPath
5426
6420
  });
5427
6421
  } catch (e) {
5428
- throw new import_common17.BadRequestException(`Failed to read Prisma schema at ${schemaPath}: ${e.message}`);
6422
+ throw new import_common19.BadRequestException(`Failed to read Prisma schema at ${schemaPath}: ${e.message}`);
5429
6423
  }
5430
6424
  }
5431
6425
  buildApplyContext(loaded, ds) {
@@ -5495,7 +6489,7 @@ var DevResourcesController = class {
5495
6489
  zodOutputDir: zodDir
5496
6490
  });
5497
6491
  if (!result.ok) {
5498
- throw new import_common17.BadRequestException(`prisma db pull failed:
6492
+ throw new import_common19.BadRequestException(`prisma db pull failed:
5499
6493
  ${result.dbPull.output}`);
5500
6494
  }
5501
6495
  return {
@@ -5512,13 +6506,13 @@ ${result.dbPull.output}`);
5512
6506
  async sync(body) {
5513
6507
  this.assertDev();
5514
6508
  if (!body?.model) {
5515
- throw new import_common17.BadRequestException('"model" is required.');
6509
+ throw new import_common19.BadRequestException('"model" is required.');
5516
6510
  }
5517
6511
  const { loaded, ds, schemaPath } = await this.loadProject(body.datasource);
5518
6512
  const models = await this.introspectModels(schemaPath);
5519
6513
  const model = models.find((m) => m.prismaName === body.model || m.clientAccessor === body.model);
5520
6514
  if (!model) {
5521
- throw new import_common17.NotFoundException(`Model "${body.model}" not found in ${schemaPath}.`);
6515
+ throw new import_common19.NotFoundException(`Model "${body.model}" not found in ${schemaPath}.`);
5522
6516
  }
5523
6517
  const resolveRelationResource = await makeRelationResolver(loaded);
5524
6518
  const diff2 = await buildResourceDiff(model, {
@@ -5640,14 +6634,14 @@ ${result.dbPull.output}`);
5640
6634
  const resourcesDir = resolveFromRoot(loaded.root, loaded.config.resourcesDir);
5641
6635
  const jsonPath = resolveResourcePath(resourcesDir, name);
5642
6636
  const raw = readRawResourceJson(jsonPath);
5643
- if (!raw) throw new import_common17.NotFoundException(`Resource "${name}" not found.`);
5644
- if (jsonPath.endsWith(".ts")) throw new import_common17.ForbiddenException("TypeScript resources cannot be edited.");
6637
+ if (!raw) throw new import_common19.NotFoundException(`Resource "${name}" not found.`);
6638
+ if (jsonPath.endsWith(".ts")) throw new import_common19.ForbiddenException("TypeScript resources cannot be edited.");
5645
6639
  const patched = applyResourceFlagPatch(raw, {
5646
6640
  draft: false
5647
6641
  });
5648
6642
  const result = validateResourceJson(patched);
5649
6643
  if (!result.success) {
5650
- throw new import_common17.BadRequestException(`Validation failed: ${result.error.message}`);
6644
+ throw new import_common19.BadRequestException(`Validation failed: ${result.error.message}`);
5651
6645
  }
5652
6646
  writeRawResourceJson(jsonPath, patched);
5653
6647
  return {
@@ -5660,8 +6654,8 @@ ${result.dbPull.output}`);
5660
6654
  const resourcesDir = resolveFromRoot(loaded.root, loaded.config.resourcesDir);
5661
6655
  const jsonPath = resolveResourcePath(resourcesDir, name);
5662
6656
  const raw = readRawResourceJson(jsonPath);
5663
- if (!raw) throw new import_common17.NotFoundException(`Resource "${name}" not found.`);
5664
- if (jsonPath.endsWith(".ts")) throw new import_common17.ForbiddenException("TypeScript resources cannot be edited.");
6657
+ if (!raw) throw new import_common19.NotFoundException(`Resource "${name}" not found.`);
6658
+ if (jsonPath.endsWith(".ts")) throw new import_common19.ForbiddenException("TypeScript resources cannot be edited.");
5665
6659
  const patched = applyResourceFlagPatch(raw, {
5666
6660
  sidebar: {
5667
6661
  hide: true
@@ -5669,7 +6663,7 @@ ${result.dbPull.output}`);
5669
6663
  });
5670
6664
  const result = validateResourceJson(patched);
5671
6665
  if (!result.success) {
5672
- throw new import_common17.BadRequestException(`Validation failed: ${result.error.message}`);
6666
+ throw new import_common19.BadRequestException(`Validation failed: ${result.error.message}`);
5673
6667
  }
5674
6668
  writeRawResourceJson(jsonPath, patched);
5675
6669
  return {
@@ -5682,8 +6676,8 @@ ${result.dbPull.output}`);
5682
6676
  const resourcesDir = resolveFromRoot(loaded.root, loaded.config.resourcesDir);
5683
6677
  const jsonPath = resolveResourcePath(resourcesDir, name);
5684
6678
  const raw = readRawResourceJson(jsonPath);
5685
- if (!raw) throw new import_common17.NotFoundException(`Resource "${name}" not found.`);
5686
- if (jsonPath.endsWith(".ts")) throw new import_common17.ForbiddenException("TypeScript resources cannot be edited.");
6679
+ if (!raw) throw new import_common19.NotFoundException(`Resource "${name}" not found.`);
6680
+ if (jsonPath.endsWith(".ts")) throw new import_common19.ForbiddenException("TypeScript resources cannot be edited.");
5687
6681
  const sidebarPatch = {
5688
6682
  hide: false
5689
6683
  };
@@ -5696,7 +6690,7 @@ ${result.dbPull.output}`);
5696
6690
  });
5697
6691
  const result = validateResourceJson(patched);
5698
6692
  if (!result.success) {
5699
- throw new import_common17.BadRequestException(`Validation failed: ${result.error.message}`);
6693
+ throw new import_common19.BadRequestException(`Validation failed: ${result.error.message}`);
5700
6694
  }
5701
6695
  writeRawResourceJson(jsonPath, patched);
5702
6696
  return {
@@ -5709,12 +6703,12 @@ ${result.dbPull.output}`);
5709
6703
  const resourcesDir = resolveFromRoot(loaded.root, loaded.config.resourcesDir);
5710
6704
  const jsonPath = resolveResourcePath(resourcesDir, name);
5711
6705
  const raw = readRawResourceJson(jsonPath);
5712
- if (!raw) throw new import_common17.NotFoundException(`Resource "${name}" not found.`);
5713
- if (jsonPath.endsWith(".ts")) throw new import_common17.ForbiddenException("TypeScript resources cannot be edited.");
6706
+ if (!raw) throw new import_common19.NotFoundException(`Resource "${name}" not found.`);
6707
+ if (jsonPath.endsWith(".ts")) throw new import_common19.ForbiddenException("TypeScript resources cannot be edited.");
5714
6708
  const patched = applyResourceFlagPatch(raw, body);
5715
6709
  const result = validateResourceJson(patched);
5716
6710
  if (!result.success) {
5717
- throw new import_common17.BadRequestException(`Validation failed: ${result.error.message}`);
6711
+ throw new import_common19.BadRequestException(`Validation failed: ${result.error.message}`);
5718
6712
  }
5719
6713
  writeRawResourceJson(jsonPath, patched);
5720
6714
  return {
@@ -5723,7 +6717,7 @@ ${result.dbPull.output}`);
5723
6717
  }
5724
6718
  };
5725
6719
  _ts_decorate5([
5726
- (0, import_common17.Get)("models"),
6720
+ (0, import_common19.Get)("models"),
5727
6721
  (0, import_swagger12.ApiOperation)({
5728
6722
  summary: "Dev-only: list DB models from the Prisma schema, flagging which already have a resource.json and whether the running backend can actually use them yet"
5729
6723
  }),
@@ -5736,7 +6730,7 @@ _ts_decorate5([
5736
6730
  _ts_metadata4("design:returntype", Promise)
5737
6731
  ], DevResourcesController.prototype, "listModels", null);
5738
6732
  _ts_decorate5([
5739
- (0, import_common17.Post)("restart"),
6733
+ (0, import_common19.Post)("restart"),
5740
6734
  (0, import_swagger12.ApiOperation)({
5741
6735
  summary: "Dev-only: exit this process so a dev watcher/process manager (nodemon, `nest start --watch`, pm2, a Docker restart policy, ...) restarts it with a fresh Prisma client. Does nothing useful if this process isn't supervised by one of those."
5742
6736
  }),
@@ -5749,7 +6743,7 @@ _ts_decorate5([
5749
6743
  _ts_metadata4("design:returntype", Object)
5750
6744
  ], DevResourcesController.prototype, "restart", null);
5751
6745
  _ts_decorate5([
5752
- (0, import_common17.Post)("pull"),
6746
+ (0, import_common19.Post)("pull"),
5753
6747
  (0, import_swagger12.ApiOperation)({
5754
6748
  summary: "Dev-only: run `prisma db pull` + case-format + `prisma generate` for a datasource, refreshing schema.prisma and the generated Prisma client/Zod types from the live database"
5755
6749
  }),
@@ -5757,7 +6751,7 @@ _ts_decorate5([
5757
6751
  status: 200,
5758
6752
  description: "Result of each step, or requiresConfirmation if schema.prisma has uncommitted changes"
5759
6753
  }),
5760
- _ts_param(0, (0, import_common17.Body)()),
6754
+ _ts_param(0, (0, import_common19.Body)()),
5761
6755
  _ts_metadata4("design:type", Function),
5762
6756
  _ts_metadata4("design:paramtypes", [
5763
6757
  Object
@@ -5765,7 +6759,7 @@ _ts_decorate5([
5765
6759
  _ts_metadata4("design:returntype", Promise)
5766
6760
  ], DevResourcesController.prototype, "pull", null);
5767
6761
  _ts_decorate5([
5768
- (0, import_common17.Post)("sync"),
6762
+ (0, import_common19.Post)("sync"),
5769
6763
  (0, import_swagger12.ApiOperation)({
5770
6764
  summary: "Dev-only: generate or update a single resource.json from its DB model, using recommended defaults (non-interactive)"
5771
6765
  }),
@@ -5773,7 +6767,7 @@ _ts_decorate5([
5773
6767
  status: 200,
5774
6768
  description: "Files written for this resource"
5775
6769
  }),
5776
- _ts_param(0, (0, import_common17.Body)()),
6770
+ _ts_param(0, (0, import_common19.Body)()),
5777
6771
  _ts_metadata4("design:type", Function),
5778
6772
  _ts_metadata4("design:paramtypes", [
5779
6773
  Object
@@ -5781,7 +6775,7 @@ _ts_decorate5([
5781
6775
  _ts_metadata4("design:returntype", Promise)
5782
6776
  ], DevResourcesController.prototype, "sync", null);
5783
6777
  _ts_decorate5([
5784
- (0, import_common17.Post)("plan"),
6778
+ (0, import_common19.Post)("plan"),
5785
6779
  (0, import_swagger12.ApiOperation)({
5786
6780
  summary: "Dev-only: dry-run introspect + diff across all (or selected) DB models using recommended defaults \u2014 computes what would change, writes nothing"
5787
6781
  }),
@@ -5789,7 +6783,7 @@ _ts_decorate5([
5789
6783
  status: 200,
5790
6784
  description: "Proposed per-resource changes"
5791
6785
  }),
5792
- _ts_param(0, (0, import_common17.Body)()),
6786
+ _ts_param(0, (0, import_common19.Body)()),
5793
6787
  _ts_metadata4("design:type", Function),
5794
6788
  _ts_metadata4("design:paramtypes", [
5795
6789
  Object
@@ -5797,7 +6791,7 @@ _ts_decorate5([
5797
6791
  _ts_metadata4("design:returntype", Promise)
5798
6792
  ], DevResourcesController.prototype, "plan", null);
5799
6793
  _ts_decorate5([
5800
- (0, import_common17.Post)("apply"),
6794
+ (0, import_common19.Post)("apply"),
5801
6795
  (0, import_swagger12.ApiOperation)({
5802
6796
  summary: "Dev-only: commit resource.json/schema.ts changes to disk for the given resources (or all, if omitted), using recommended defaults"
5803
6797
  }),
@@ -5805,7 +6799,7 @@ _ts_decorate5([
5805
6799
  status: 200,
5806
6800
  description: "Per-resource commit results"
5807
6801
  }),
5808
- _ts_param(0, (0, import_common17.Body)()),
6802
+ _ts_param(0, (0, import_common19.Body)()),
5809
6803
  _ts_metadata4("design:type", Function),
5810
6804
  _ts_metadata4("design:paramtypes", [
5811
6805
  Object
@@ -5813,7 +6807,7 @@ _ts_decorate5([
5813
6807
  _ts_metadata4("design:returntype", Promise)
5814
6808
  ], DevResourcesController.prototype, "apply", null);
5815
6809
  _ts_decorate5([
5816
- (0, import_common17.Get)("visibility"),
6810
+ (0, import_common19.Get)("visibility"),
5817
6811
  (0, import_swagger12.ApiOperation)({
5818
6812
  summary: "Dev-only: list all resources with their menu visibility state (in-menu, hidden, draft, error)"
5819
6813
  }),
@@ -5826,7 +6820,7 @@ _ts_decorate5([
5826
6820
  _ts_metadata4("design:returntype", Promise)
5827
6821
  ], DevResourcesController.prototype, "visibility", null);
5828
6822
  _ts_decorate5([
5829
- (0, import_common17.Post)(":name/publish"),
6823
+ (0, import_common19.Post)(":name/publish"),
5830
6824
  (0, import_swagger12.ApiOperation)({
5831
6825
  summary: "Dev-only: publish a draft resource (removes `draft: true` from resource.json)"
5832
6826
  }),
@@ -5834,7 +6828,7 @@ _ts_decorate5([
5834
6828
  status: 200,
5835
6829
  description: "Resource published"
5836
6830
  }),
5837
- _ts_param(0, (0, import_common17.Param)("name")),
6831
+ _ts_param(0, (0, import_common19.Param)("name")),
5838
6832
  _ts_metadata4("design:type", Function),
5839
6833
  _ts_metadata4("design:paramtypes", [
5840
6834
  String
@@ -5842,7 +6836,7 @@ _ts_decorate5([
5842
6836
  _ts_metadata4("design:returntype", Promise)
5843
6837
  ], DevResourcesController.prototype, "publish", null);
5844
6838
  _ts_decorate5([
5845
- (0, import_common17.Post)(":name/remove-from-menu"),
6839
+ (0, import_common19.Post)(":name/remove-from-menu"),
5846
6840
  (0, import_swagger12.ApiOperation)({
5847
6841
  summary: "Dev-only: hide a resource from the sidebar menu (sets `sidebar.hide: true` in resource.json)"
5848
6842
  }),
@@ -5850,7 +6844,7 @@ _ts_decorate5([
5850
6844
  status: 200,
5851
6845
  description: "Resource removed from menu"
5852
6846
  }),
5853
- _ts_param(0, (0, import_common17.Param)("name")),
6847
+ _ts_param(0, (0, import_common19.Param)("name")),
5854
6848
  _ts_metadata4("design:type", Function),
5855
6849
  _ts_metadata4("design:paramtypes", [
5856
6850
  String
@@ -5858,7 +6852,7 @@ _ts_decorate5([
5858
6852
  _ts_metadata4("design:returntype", Promise)
5859
6853
  ], DevResourcesController.prototype, "removeFromMenu", null);
5860
6854
  _ts_decorate5([
5861
- (0, import_common17.Post)(":name/add-to-menu"),
6855
+ (0, import_common19.Post)(":name/add-to-menu"),
5862
6856
  (0, import_swagger12.ApiOperation)({
5863
6857
  summary: "Dev-only: publish + un-hide a resource and optionally set sidebar group/position/label"
5864
6858
  }),
@@ -5866,8 +6860,8 @@ _ts_decorate5([
5866
6860
  status: 200,
5867
6861
  description: "Resource added to menu"
5868
6862
  }),
5869
- _ts_param(0, (0, import_common17.Param)("name")),
5870
- _ts_param(1, (0, import_common17.Body)()),
6863
+ _ts_param(0, (0, import_common19.Param)("name")),
6864
+ _ts_param(1, (0, import_common19.Body)()),
5871
6865
  _ts_metadata4("design:type", Function),
5872
6866
  _ts_metadata4("design:paramtypes", [
5873
6867
  String,
@@ -5876,7 +6870,7 @@ _ts_decorate5([
5876
6870
  _ts_metadata4("design:returntype", Promise)
5877
6871
  ], DevResourcesController.prototype, "addToMenu", null);
5878
6872
  _ts_decorate5([
5879
- (0, import_common17.Patch)(":name/flags"),
6873
+ (0, import_common19.Patch)(":name/flags"),
5880
6874
  (0, import_swagger12.ApiOperation)({
5881
6875
  summary: "Dev-only: set arbitrary resource flags (draft, sidebar.hide, group, position, label)"
5882
6876
  }),
@@ -5884,8 +6878,8 @@ _ts_decorate5([
5884
6878
  status: 200,
5885
6879
  description: "Flags updated"
5886
6880
  }),
5887
- _ts_param(0, (0, import_common17.Param)("name")),
5888
- _ts_param(1, (0, import_common17.Body)()),
6881
+ _ts_param(0, (0, import_common19.Param)("name")),
6882
+ _ts_param(1, (0, import_common19.Body)()),
5889
6883
  _ts_metadata4("design:type", Function),
5890
6884
  _ts_metadata4("design:paramtypes", [
5891
6885
  String,
@@ -5894,7 +6888,7 @@ _ts_decorate5([
5894
6888
  _ts_metadata4("design:returntype", Promise)
5895
6889
  ], DevResourcesController.prototype, "updateFlags", null);
5896
6890
  DevResourcesController = _ts_decorate5([
5897
- (0, import_common17.Controller)("_app/resources"),
6891
+ (0, import_common19.Controller)("_app/resources"),
5898
6892
  (0, import_swagger12.ApiTags)("Dev tools"),
5899
6893
  _ts_metadata4("design:type", Function),
5900
6894
  _ts_metadata4("design:paramtypes", [
@@ -5904,28 +6898,28 @@ DevResourcesController = _ts_decorate5([
5904
6898
  ], DevResourcesController);
5905
6899
 
5906
6900
  // src/lib/crud/enum-registry/enum-registry.types.ts
5907
- var import_zod25 = require("zod");
5908
- var EnumEntrySchema = import_zod25.z.object({
5909
- value: import_zod25.z.unknown(),
5910
- label: import_zod25.z.string()
6901
+ var import_zod30 = require("zod");
6902
+ var EnumEntrySchema = import_zod30.z.object({
6903
+ value: import_zod30.z.unknown(),
6904
+ label: import_zod30.z.string()
5911
6905
  });
5912
- var EnumRegistrySchema = import_zod25.z.record(import_zod25.z.string(), import_zod25.z.array(EnumEntrySchema)).default({});
6906
+ var EnumRegistrySchema = import_zod30.z.record(import_zod30.z.string(), import_zod30.z.array(EnumEntrySchema)).default({});
5913
6907
 
5914
6908
  // src/lib/crud/enum-registry/enum-registry.loader.ts
5915
6909
  var import_node_fs5 = require("fs");
5916
- var import_node_path13 = require("path");
6910
+ var import_node_path14 = require("path");
5917
6911
  var ENUMS_FILE = "crouton.enums.json";
5918
6912
  var loadEnumRegistry = /* @__PURE__ */ __name((startDir, enumsFile) => {
5919
6913
  let file = enumsFile;
5920
6914
  if (!file) {
5921
6915
  let dir = startDir;
5922
6916
  while (true) {
5923
- const candidate = (0, import_node_path13.join)(dir, ENUMS_FILE);
6917
+ const candidate = (0, import_node_path14.join)(dir, ENUMS_FILE);
5924
6918
  if ((0, import_node_fs5.existsSync)(candidate)) {
5925
6919
  file = candidate;
5926
6920
  break;
5927
6921
  }
5928
- const parent = (0, import_node_path13.dirname)(dir);
6922
+ const parent = (0, import_node_path14.dirname)(dir);
5929
6923
  if (parent === dir) break;
5930
6924
  dir = parent;
5931
6925
  }
@@ -5991,9 +6985,9 @@ var upsertOp = /* @__PURE__ */ __name((entry, schema) => {
5991
6985
  }, "upsertOp");
5992
6986
 
5993
6987
  // src/lib/crud/adapter/relation-type.ts
5994
- var import_zod26 = require("zod");
6988
+ var import_zod31 = require("zod");
5995
6989
  var unwrapZodType = /* @__PURE__ */ __name((type) => {
5996
- if (type instanceof import_zod26.ZodOptional || type instanceof import_zod26.ZodNullable) {
6990
+ if (type instanceof import_zod31.ZodOptional || type instanceof import_zod31.ZodNullable) {
5997
6991
  return unwrapZodType(type.unwrap());
5998
6992
  }
5999
6993
  return type;
@@ -6003,7 +6997,7 @@ var deriveRelationType = /* @__PURE__ */ __name((schema, columnId) => {
6003
6997
  const field = schema.shape[columnId];
6004
6998
  if (!field) return void 0;
6005
6999
  const inner = unwrapZodType(field);
6006
- return inner instanceof import_zod26.ZodArray ? "oneToMany" : "manyToOne";
7000
+ return inner instanceof import_zod31.ZodArray ? "oneToMany" : "manyToOne";
6007
7001
  }, "deriveRelationType");
6008
7002
  var deriveRelationTypeFromColumns = /* @__PURE__ */ __name((col, cols) => {
6009
7003
  const base = col.column ?? col.id;
@@ -6035,7 +7029,7 @@ var enrichRelationTypes = /* @__PURE__ */ __name((columns, schema) => {
6035
7029
 
6036
7030
  // src/lib/crud/resource/ReadResourceJson.ts
6037
7031
  var import_node_fs6 = require("fs");
6038
- var import_node_path14 = require("path");
7032
+ var import_node_path15 = require("path");
6039
7033
  var readResourceJson = /* @__PURE__ */ __name((jsonPath) => {
6040
7034
  if (!(0, import_node_fs6.existsSync)(jsonPath)) return void 0;
6041
7035
  let fileContent;
@@ -6058,30 +7052,62 @@ var readResourceJson = /* @__PURE__ */ __name((jsonPath) => {
6058
7052
  success: true,
6059
7053
  data: {
6060
7054
  json: resource.data,
6061
- dir: (0, import_node_path14.dirname)(jsonPath)
7055
+ dir: (0, import_node_path15.dirname)(jsonPath)
6062
7056
  }
6063
7057
  };
6064
7058
  }, "readResourceJson");
6065
7059
 
6066
7060
  // src/lib/crud/adapter/resource-resolver.ts
6067
7061
  var import_node_fs7 = require("fs");
6068
- var import_node_path15 = require("path");
6069
- var unwrap2 = /* @__PURE__ */ __name((result) => {
6070
- if (result?.success) return result.data;
6071
- return void 0;
6072
- }, "unwrap");
6073
- var resolveChildResource = /* @__PURE__ */ __name((resourcePath, parentDir) => {
6074
- const directPath = (0, import_node_path15.resolve)(parentDir, resourcePath);
7062
+ var import_node_path16 = require("path");
7063
+ var resolveChildResourceDetailed = /* @__PURE__ */ __name((resourcePath, parentDir) => {
7064
+ const attempted = [];
6075
7065
  try {
6076
- if (resourcePath.endsWith(".json") && (0, import_node_fs7.existsSync)(directPath)) {
6077
- return unwrap2(readResourceJson(directPath));
7066
+ if (resourcePath.endsWith(".json")) {
7067
+ const directPath = (0, import_node_path16.resolve)(parentDir, resourcePath);
7068
+ attempted.push(directPath);
7069
+ if ((0, import_node_fs7.existsSync)(directPath)) {
7070
+ const result2 = readResourceJson(directPath);
7071
+ if (result2?.success) return {
7072
+ ok: true,
7073
+ value: result2.data
7074
+ };
7075
+ return {
7076
+ ok: false,
7077
+ reason: "invalid",
7078
+ error: result2?.error ?? `Could not read ${directPath}`
7079
+ };
7080
+ }
6078
7081
  }
6079
7082
  const childName = resourcePath.replace(/^\.\//, "").replace(/\.resource$/, "");
6080
- const childJsonPath = (0, import_node_path15.resolve)((0, import_node_path15.dirname)(parentDir), childName, "resource.json");
6081
- return unwrap2(readResourceJson(childJsonPath));
6082
- } catch {
6083
- return void 0;
7083
+ const childJsonPath = (0, import_node_path16.resolve)((0, import_node_path16.dirname)(parentDir), childName, "resource.json");
7084
+ attempted.push(childJsonPath);
7085
+ const result = readResourceJson(childJsonPath);
7086
+ if (result?.success) return {
7087
+ ok: true,
7088
+ value: result.data
7089
+ };
7090
+ if (result) return {
7091
+ ok: false,
7092
+ reason: "invalid",
7093
+ error: result.error
7094
+ };
7095
+ return {
7096
+ ok: false,
7097
+ reason: "missing",
7098
+ attempted
7099
+ };
7100
+ } catch (err) {
7101
+ return {
7102
+ ok: false,
7103
+ reason: "invalid",
7104
+ error: err.message
7105
+ };
6084
7106
  }
7107
+ }, "resolveChildResourceDetailed");
7108
+ var resolveChildResource = /* @__PURE__ */ __name((resourcePath, parentDir) => {
7109
+ const resolution = resolveChildResourceDetailed(resourcePath, parentDir);
7110
+ return resolution.ok ? resolution.value : void 0;
6085
7111
  }, "resolveChildResource");
6086
7112
 
6087
7113
  // src/lib/crud/adapter/column-enrichment.ts
@@ -6261,18 +7287,44 @@ var resolveColumnFieldVariants = /* @__PURE__ */ __name((cols) => cols?.map((col
6261
7287
  }), "resolveColumnFieldVariants");
6262
7288
 
6263
7289
  // src/lib/crud/adapter/sub-resource.builder.ts
7290
+ var REMOTE_RESOURCE = /^https?:\/\//i;
6264
7291
  var buildSubResources = /* @__PURE__ */ __name((columns, parentRoute, parentModel, parentDir, enums = {}, baseUrl) => {
6265
7292
  if (!columns || !parentDir) return [];
6266
- return columns.filter((c) => c.fieldInput?.format === "relation" && c.fieldInput?.resource).map((c) => {
6267
- const childResolved = resolveChildResource(c.fieldInput.resource, parentDir);
6268
- const childJson = childResolved?.json;
6269
- const childDir = childResolved?.dir;
6270
- const childRoute = childJson?.route ?? c.fieldInput.resource.replace(/^\.\.?\//, "").replace(/\/resource\.json$/, "").replace(/\.resource$/, "");
7293
+ return columns.filter((c) => c.fieldInput?.format === "relation" && c.fieldInput?.resource).flatMap((c) => {
7294
+ const resourcePath = c.fieldInput.resource;
7295
+ if (REMOTE_RESOURCE.test(resourcePath)) return [];
7296
+ const resolution = resolveChildResourceDetailed(resourcePath, parentDir);
7297
+ if (!resolution.ok) {
7298
+ resourceLoadErrorsRegistry.record({
7299
+ name: parentRoute,
7300
+ path: resourcePath,
7301
+ error: resolution.reason === "missing" ? `Relation column "${c.id}" points at "${resourcePath}", but no resource.json was found there (looked in: ${resolution.attempted.join(", ")}). Sub-resource routes for it were not registered.` : `Relation column "${c.id}" points at "${resourcePath}", which could not be read: ${resolution.error} Sub-resource routes for it were not registered.`
7302
+ });
7303
+ return [];
7304
+ }
7305
+ const childJson = resolution.value.json;
7306
+ const childDir = resolution.value.dir;
7307
+ if (childJson.parent) {
7308
+ resourceLoadErrorsRegistry.record({
7309
+ name: parentRoute,
7310
+ path: resourcePath,
7311
+ error: `Relation column "${c.id}" declares "${childJson.name}" as a sub-resource, but that resource also declares "parent": { "route": "${childJson.parent.route}" }. The two ways of nesting are mutually exclusive \u2014 remove the "parent" block to embed it in this resource, or remove this relation column to keep its own nested controller. Sub-resource routes for it were not registered.`
7312
+ });
7313
+ return [];
7314
+ }
7315
+ const childRoute = childJson.route ?? resourcePath.replace(/^\.\.?\//, "").replace(/\/resource\.json$/, "").replace(/\.resource$/, "");
6271
7316
  const rawChildColumns = childJson?.columns;
6272
7317
  const expandedChildColumns = rawChildColumns ? expandExtendColumns(rawChildColumns, childDir) : void 0;
6273
7318
  const childColumns = applyRelationFormatDefault(expandedChildColumns) ?? expandedChildColumns;
6274
7319
  injectEnumValues(childColumns, enums);
6275
7320
  const enrichedChildColumns = enrichNestedRelationColumns(childColumns, childDir, baseUrl);
7321
+ const autoIncludes = (enrichedChildColumns ?? []).filter((col) => col.fieldInput?.format === "relation" && (col.fieldInput.relationType ?? deriveRelationTypeFromColumns(col, enrichedChildColumns)) === "manyToOne").map((col) => col.fieldInput?.relation ?? col.id);
7322
+ const explicitIncludes = childJson?.include ?? [];
7323
+ const explicitNames = new Set(explicitIncludes.map((e) => typeof e === "string" ? e : e.relation));
7324
+ const mergedIncludes = [
7325
+ ...explicitIncludes,
7326
+ ...autoIncludes.filter((name) => !explicitNames.has(name))
7327
+ ];
6276
7328
  const childLookupKey = childColumns?.find((col) => col.idField)?.id ?? "id";
6277
7329
  const childCalculatedColumns = childJson?.calculatedColumns ?? [];
6278
7330
  let childViews = childJson ? buildViewsFromColumns(enrichedChildColumns) : void 0;
@@ -6289,16 +7341,27 @@ var buildSubResources = /* @__PURE__ */ __name((columns, parentRoute, parentMode
6289
7341
  }
6290
7342
  }
6291
7343
  const childOps = childJson?.operations ?? {};
7344
+ const childKind = childJson?.kind === "custom" ? "custom" : "prisma";
6292
7345
  return {
6293
7346
  column: c.id,
6294
7347
  relation: c.fieldInput?.relation ?? c.id,
6295
7348
  childRoute,
6296
- childModel: c.id,
7349
+ childKind,
7350
+ ...childDir && {
7351
+ childDir
7352
+ },
7353
+ // A custom child has no Prisma model. Leave it empty rather than
7354
+ // falling back to the column id, which would produce a bogus
7355
+ // `prisma[<column>]` lookup at query time.
7356
+ childModel: childKind === "custom" ? "" : c.id,
6297
7357
  foreignKey: c.fieldInput?.foreignKey ?? `${parentModel}Id`,
6298
7358
  name: childJson?.name ?? childRoute,
6299
7359
  title: childJson?.title ?? childJson?.tag ?? childRoute,
6300
7360
  idField: childLookupKey,
6301
7361
  idType: childJson?.idType ?? "string",
7362
+ ...c.hiddenInTable && {
7363
+ hiddenInTable: true
7364
+ },
6302
7365
  ...childViews && {
6303
7366
  views: childViews
6304
7367
  },
@@ -6317,8 +7380,8 @@ var buildSubResources = /* @__PURE__ */ __name((columns, parentRoute, parentMode
6317
7380
  ...childJson?.modalSize && {
6318
7381
  modalSize: childJson.modalSize
6319
7382
  },
6320
- ...childJson?.include?.length && {
6321
- include: childJson.include
7383
+ ...mergedIncludes.length && {
7384
+ include: mergedIncludes
6322
7385
  },
6323
7386
  ...childJson?.calculatedColumns?.length && {
6324
7387
  calculatedColumns: childJson.calculatedColumns
@@ -6342,17 +7405,18 @@ var buildSubResources = /* @__PURE__ */ __name((columns, parentRoute, parentMode
6342
7405
  }, "buildSubResources");
6343
7406
 
6344
7407
  // src/lib/crud/adapter/json-adapter.ts
6345
- var fromJson = /* @__PURE__ */ __name((json, schema, hooks, dirPath, baseUrl, actions, tableActions, enums = {}) => {
7408
+ var fromJson = /* @__PURE__ */ __name((json, schema, hooks, dirPath, baseUrl, actions, tableActions, enums = {}, repository) => {
7409
+ const isCustom = json.kind === "custom";
6346
7410
  const rawColumns = expandExtendColumns(json.columns, dirPath);
6347
7411
  const columns = enrichRelationTypes(applyRelationFormatDefault(rawColumns) ?? rawColumns, schema);
6348
7412
  injectEnumValues(columns, enums);
6349
- const subResources = buildSubResources(columns, json.route, json.model, dirPath, enums, baseUrl);
7413
+ const subResources = isCustom ? [] : buildSubResources(columns, json.route, json.model ?? "", dirPath, enums, baseUrl);
6350
7414
  const enrichedColumns = resolveColumnFieldVariants(enrichResourceRefColumns(enrichActionColumns(columns, json.route, subResources, baseUrl), dirPath, baseUrl) ?? columns);
6351
7415
  const calculatedColumns = json.calculatedColumns ?? [];
6352
7416
  const picked = pickByColumns(schema, enrichedColumns);
6353
7417
  const createSchema = pickByColumns(schema, enrichedColumns, (c) => !c.idField && c.createable !== false);
6354
7418
  const updateSchema = pickByColumns(schema, enrichedColumns, (c) => !c.idField && c.updateable !== false);
6355
- let views = buildViews(schema, enrichedColumns);
7419
+ let views = isCustom ? buildViewsFromColumnTypes(enrichedColumns) : buildViews(schema, enrichedColumns);
6356
7420
  if (views && calculatedColumns.length) {
6357
7421
  views = {
6358
7422
  ...views,
@@ -6410,6 +7474,9 @@ var fromJson = /* @__PURE__ */ __name((json, schema, hooks, dirPath, baseUrl, ac
6410
7474
  ...hooks && {
6411
7475
  hooks
6412
7476
  },
7477
+ ...repository && {
7478
+ repository
7479
+ },
6413
7480
  definition,
6414
7481
  ...views && {
6415
7482
  views
@@ -6453,40 +7520,6 @@ var buildLookup = /* @__PURE__ */ __name((columns) => {
6453
7520
  };
6454
7521
  }, "buildLookup");
6455
7522
 
6456
- // src/lib/crud/hooks/hooks.types.ts
6457
- var import_zod27 = require("zod");
6458
- var WriteOpSchema = import_zod27.z.enum([
6459
- "create",
6460
- "update",
6461
- "patch",
6462
- "upsert",
6463
- "delete"
6464
- ]);
6465
- var ReadOpSchema = import_zod27.z.enum([
6466
- "findAll",
6467
- "findOne"
6468
- ]);
6469
- var ResourceHooksSchema = import_zod27.z.object({
6470
- beforeWrite: import_zod27.z.custom().optional(),
6471
- afterWrite: import_zod27.z.custom().optional(),
6472
- afterRead: import_zod27.z.custom().optional()
6473
- });
6474
-
6475
- // src/lib/crud/hooks/hooks.loader.ts
6476
- var import_node_path16 = require("path");
6477
- var loadResourceHooks = /* @__PURE__ */ __name(async (basePath) => {
6478
- const file = findModule(basePath, "hooks");
6479
- return file ? importDefault(file) : void 0;
6480
- }, "loadResourceHooks");
6481
- var loadSubResourceHooks = /* @__PURE__ */ __name(async (subResources, basePath) => {
6482
- for (const sub of subResources) {
6483
- const file = sub.name ? findModule((0, import_node_path16.join)(basePath, "hooks"), sub.name) : void 0;
6484
- if (!file) continue;
6485
- const hooks = await importDefault(file);
6486
- if (hooks) sub.hooks = hooks;
6487
- }
6488
- }, "loadSubResourceHooks");
6489
-
6490
7523
  // src/lib/crud/resource/MigrateResourceJson.ts
6491
7524
  var import_node_fs8 = require("fs");
6492
7525
  var migrateResourceJsonFile = /* @__PURE__ */ __name((jsonPath, opts) => {
@@ -6609,10 +7642,12 @@ var loadResourceConfigsFromDir = /* @__PURE__ */ __name(async (dirPath, baseUrl,
6609
7642
  });
6610
7643
  continue;
6611
7644
  }
7645
+ const repository = json.kind === "custom" ? await loadCustomRepository(basePath, json.name) : void 0;
6612
7646
  const actions = await loadActions(json.actions ?? [], basePath, "row");
6613
7647
  const tableActions = await loadActions(json.tableActions ?? [], basePath, "table");
6614
- const config = fromJson(json, schema, hooks, basePath, baseUrl, actions, tableActions, enums);
7648
+ const config = fromJson(json, schema, hooks, basePath, baseUrl, actions, tableActions, enums, repository);
6615
7649
  await loadSubResourceHooks(config.subResources ?? [], basePath);
7650
+ await loadSubResourceRepositories(config.subResources ?? [], config.name);
6616
7651
  onResourceDir?.(config.route, basePath);
6617
7652
  configs.push(config);
6618
7653
  continue;
@@ -6736,6 +7771,13 @@ var getResourceStatus = /* @__PURE__ */ __name((loadedConfigs) => {
6736
7771
  path: c.route,
6737
7772
  valid: true,
6738
7773
  version: c.schemaVersion ?? CURRENT_RESOURCE_VERSION,
7774
+ kind: c.kind ?? "prisma",
7775
+ // Which operations the user's repository.ts actually implements. A resource
7776
+ // only reaches this list after validateCustomRepository passed, so this is
7777
+ // informational rather than a warning.
7778
+ ...c.kind === "custom" && c.repository ? {
7779
+ customOperations: CUSTOM_OPS.filter((op) => typeof c.repository?.[op] === "function")
7780
+ } : {},
6739
7781
  ...c.sidebar?.hide ? {
6740
7782
  hidden: true
6741
7783
  } : {}
@@ -6893,7 +7935,7 @@ var buildStatus = /* @__PURE__ */ __name(async (registry, loadedConfigs, enumReg
6893
7935
  }, "buildStatus");
6894
7936
 
6895
7937
  // src/lib/crud/status/status.controller.ts
6896
- var import_common18 = require("@nestjs/common");
7938
+ var import_common20 = require("@nestjs/common");
6897
7939
  var import_swagger13 = require("@nestjs/swagger");
6898
7940
  function _ts_decorate6(decorators, target, key, desc2) {
6899
7941
  var c = arguments.length, r = c < 3 ? target : desc2 === null ? desc2 = Object.getOwnPropertyDescriptor(target, key) : desc2, d;
@@ -6923,7 +7965,7 @@ var createStatusController = /* @__PURE__ */ __name((enumRegistry) => {
6923
7965
  }
6924
7966
  };
6925
7967
  _ts_decorate6([
6926
- (0, import_common18.Get)("status.json"),
7968
+ (0, import_common20.Get)("status.json"),
6927
7969
  (0, import_swagger13.ApiOperation)({
6928
7970
  summary: "Crouton system status (db, resources, version)"
6929
7971
  }),
@@ -6936,7 +7978,7 @@ var createStatusController = /* @__PURE__ */ __name((enumRegistry) => {
6936
7978
  _ts_metadata5("design:returntype", Promise)
6937
7979
  ], StatusController.prototype, "getStatus", null);
6938
7980
  StatusController = _ts_decorate6([
6939
- (0, import_common18.Controller)("crouton"),
7981
+ (0, import_common20.Controller)("crouton"),
6940
7982
  (0, import_swagger13.ApiTags)("Status"),
6941
7983
  _ts_metadata5("design:type", Function),
6942
7984
  _ts_metadata5("design:paramtypes", [
@@ -6978,9 +8020,34 @@ var CroutonApiModule = class _CroutonApiModule {
6978
8020
  const enumRegistry = loadEnumRegistry(startDir, config.enumsFile);
6979
8021
  const validConfigs = [];
6980
8022
  for (const c of configs) {
8023
+ if (c.kind === "custom") {
8024
+ if (c.database) {
8025
+ try {
8026
+ dataSourceRegistry.resolve(c.database);
8027
+ } catch (e) {
8028
+ resourceLoadErrorsRegistry.record({
8029
+ name: c.name,
8030
+ path: c.route,
8031
+ error: e.message ?? String(e)
8032
+ });
8033
+ continue;
8034
+ }
8035
+ }
8036
+ const problem = validateCustomRepository(c, c.repository);
8037
+ if (problem) {
8038
+ resourceLoadErrorsRegistry.record({
8039
+ name: c.name,
8040
+ path: c.route,
8041
+ error: problem
8042
+ });
8043
+ continue;
8044
+ }
8045
+ validConfigs.push(c);
8046
+ continue;
8047
+ }
6981
8048
  try {
6982
8049
  const prisma = dataSourceRegistry.resolve(c.database);
6983
- if (!prisma[c.model]) {
8050
+ if (!c.model || !prisma[c.model]) {
6984
8051
  resourceLoadErrorsRegistry.record({
6985
8052
  name: c.name,
6986
8053
  path: c.route,
@@ -7043,7 +8110,7 @@ var CroutonApiModule = class _CroutonApiModule {
7043
8110
  }
7044
8111
  };
7045
8112
  CroutonApiModule = _ts_decorate7([
7046
- (0, import_common19.Module)({
8113
+ (0, import_common21.Module)({
7047
8114
  controllers: [],
7048
8115
  providers: [],
7049
8116
  exports: []
@@ -7054,14 +8121,25 @@ CroutonApiModule = _ts_decorate7([
7054
8121
  CroutonApiModule,
7055
8122
  DataSourceRegistry,
7056
8123
  FileSystemResourceConfigLoader,
8124
+ ReadOpSchema,
7057
8125
  ResourceConfigLoader,
7058
8126
  ResourceConfigRegistry,
8127
+ ResourceHooksSchema,
8128
+ WriteOpSchema,
8129
+ buildFilterWhere,
7059
8130
  buildViews,
8131
+ decorateRow,
8132
+ decorateRows,
7060
8133
  isOperationEnabled,
7061
8134
  isRowProcedureAction,
7062
8135
  isTableProcedureAction,
7063
8136
  loadDataSourcesFromDir,
7064
8137
  loadResourceConfigsFromDir,
8138
+ loadResourceHooks,
8139
+ loadSubResourceHooks,
8140
+ parseFilterString,
8141
+ postWrite,
8142
+ prepareWrite,
7065
8143
  resolveDefinition,
7066
8144
  schemaFor,
7067
8145
  upsertOnFor