@ghentcdh/crouton-api 0.0.1-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,3787 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc2) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc2 = __getOwnPropDesc(from, key)) || desc2.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+
21
+ // src/index.ts
22
+ var index_exports = {};
23
+ __export(index_exports, {
24
+ CroutonApiModule: () => CroutonApiModule,
25
+ DataSourceRegistry: () => DataSourceRegistry,
26
+ FileSystemResourceConfigLoader: () => FileSystemResourceConfigLoader,
27
+ ResourceConfigLoader: () => ResourceConfigLoader2,
28
+ ResourceConfigRegistry: () => ResourceConfigRegistry,
29
+ isOperationEnabled: () => isOperationEnabled,
30
+ loadDataSourcesFromDir: () => loadDataSourcesFromDir,
31
+ loadResourceConfigsFromDir: () => loadResourceConfigsFromDir,
32
+ resolveDefinition: () => resolveDefinition,
33
+ schemaFor: () => schemaFor,
34
+ upsertOnFor: () => upsertOnFor
35
+ });
36
+ module.exports = __toCommonJS(index_exports);
37
+
38
+ // src/lib/crouton-api.module.ts
39
+ var import_common13 = require("@nestjs/common");
40
+ var import_core = require("@nestjs/core");
41
+
42
+ // src/lib/crud/app-layout.controller.ts
43
+ var import_common2 = require("@nestjs/common");
44
+ var import_swagger = require("@nestjs/swagger");
45
+
46
+ // ../crouton-core/src/lib/request.model.ts
47
+ var import_zod2 = require("zod");
48
+
49
+ // ../crouton-core/src/lib/zod.types.ts
50
+ var import_zod = require("zod");
51
+ var PositiveRequestNumber = /* @__PURE__ */ __name(() => import_zod.z.coerce.number().int().positive().nonnegative(), "PositiveRequestNumber");
52
+ var StringOrArray = /* @__PURE__ */ __name(() => import_zod.z.string().or(import_zod.z.array(import_zod.z.string())).transform((val) => {
53
+ if (Array.isArray(val)) return val;
54
+ return [
55
+ val
56
+ ];
57
+ }), "StringOrArray");
58
+
59
+ // ../crouton-core/src/lib/request.model.ts
60
+ var SortDirEnum = import_zod2.z.enum([
61
+ "asc",
62
+ "desc"
63
+ ]);
64
+ var RequestSchema = import_zod2.z.object({
65
+ page: PositiveRequestNumber().optional().default(1),
66
+ pageSize: PositiveRequestNumber().optional().default(20),
67
+ sort: import_zod2.z.string().optional().default("id"),
68
+ sortDir: SortDirEnum.optional().default("asc"),
69
+ // Filter is of the format key:value:operator (e.g. name:john:eq) operator is optional
70
+ filter: StringOrArray().optional().default([])
71
+ });
72
+ var RequestSchemaWithOffset = RequestSchema.transform((schema) => {
73
+ const { page, pageSize, sort } = schema;
74
+ return {
75
+ ...schema,
76
+ sort: sort || "id",
77
+ offset: (page - 1) * pageSize
78
+ };
79
+ });
80
+
81
+ // ../crouton-core/src/lib/filter.ts
82
+ var Operator = [
83
+ "contains",
84
+ "not_contains",
85
+ "equals",
86
+ "not_equals",
87
+ "gt",
88
+ "lt",
89
+ "isnull",
90
+ "isnotnull"
91
+ ];
92
+ var OperatorLabel = {
93
+ contains: "contains",
94
+ not_contains: "not contains",
95
+ equals: "equals",
96
+ not_equals: "not equals",
97
+ gt: ">",
98
+ lt: "<",
99
+ isnull: "is empty",
100
+ isnotnull: "is not empty"
101
+ };
102
+ var OperatorOptions = Operator.map((k) => ({
103
+ value: k,
104
+ label: OperatorLabel[k]
105
+ }));
106
+ var buildSort = /* @__PURE__ */ __name((key, sortDir) => buildSortKey(key.split("."), SortDirEnum.safeParse(sortDir).data ?? "asc"), "buildSort");
107
+ var buildSortKey = /* @__PURE__ */ __name((keys, sortDir) => {
108
+ if (keys.length === 1) return {
109
+ [keys[0]]: sortDir
110
+ };
111
+ const buildKey = keys.pop();
112
+ return buildSortKey(keys, {
113
+ [buildKey]: sortDir
114
+ });
115
+ }, "buildSortKey");
116
+
117
+ // ../crouton-core/src/lib/response.model.ts
118
+ var import_zod4 = require("zod");
119
+ var ResponseRequestSchema = RequestSchema.extend({
120
+ count: PositiveRequestNumber(),
121
+ totalPages: PositiveRequestNumber()
122
+ });
123
+ var ResponseSchema = import_zod4.z.object({
124
+ data: import_zod4.z.array(import_zod4.z.unknown()),
125
+ request: ResponseRequestSchema
126
+ });
127
+
128
+ // ../crouton-core/src/lib/create-schema.ts
129
+ var import_zod6 = require("zod");
130
+
131
+ // ../crouton-core/src/lib/layout/base.builder.ts
132
+ var Builder = class {
133
+ static {
134
+ __name(this, "Builder");
135
+ }
136
+ type;
137
+ constructor(type) {
138
+ this.type = type;
139
+ }
140
+ };
141
+ var BuilderWithElements = class extends Builder {
142
+ static {
143
+ __name(this, "BuilderWithElements");
144
+ }
145
+ elements = [];
146
+ addControl(control) {
147
+ this.elements.push(control);
148
+ return this;
149
+ }
150
+ addControls(...controls) {
151
+ this.elements.push(...controls);
152
+ return this;
153
+ }
154
+ buildElements() {
155
+ return this.elements.map((e) => e.build());
156
+ }
157
+ };
158
+
159
+ // ../crouton-core/src/lib/layout/control.builder.ts
160
+ var ControlType = {
161
+ number: "number",
162
+ string: "string",
163
+ integer: "Integer",
164
+ autocomplete: "autocomplete",
165
+ textArea: "textarea",
166
+ markdown: "markdown",
167
+ array: "array",
168
+ custom: "custom",
169
+ select: "select",
170
+ mutliSelect: "mutliSelect",
171
+ boolean: "boolean",
172
+ link: "link",
173
+ relation: "relation",
174
+ date: "date",
175
+ dateTime: "dateTime",
176
+ dateRange: "date-range"
177
+ };
178
+ var ControlBuilder = class _ControlBuilder extends Builder {
179
+ static {
180
+ __name(this, "ControlBuilder");
181
+ }
182
+ scope;
183
+ options = {
184
+ format: "Control",
185
+ styles: {}
186
+ };
187
+ _detail;
188
+ constructor(scope, type = "Control") {
189
+ super(type), this.scope = scope;
190
+ }
191
+ static asObject(property) {
192
+ return new _ControlBuilder(`#/properties/${property}`, "Object");
193
+ }
194
+ static properties(property) {
195
+ return new _ControlBuilder(`#/properties/${property}`);
196
+ }
197
+ static asCustom(property, type) {
198
+ const builder = new _ControlBuilder(`#/properties/${property}`);
199
+ builder.addOptions({
200
+ format: ControlType.custom,
201
+ type
202
+ });
203
+ return builder;
204
+ }
205
+ setCustomRender(customRender) {
206
+ this.addOptions({
207
+ customRender
208
+ });
209
+ return this;
210
+ }
211
+ detail(layoutBuilder, label) {
212
+ this._detail = layoutBuilder;
213
+ this.addOptions({
214
+ format: ControlType.array,
215
+ elementLabelProp: label
216
+ });
217
+ return this;
218
+ }
219
+ addAction(action) {
220
+ const actions = this.options?.actions ?? [];
221
+ actions.push(action);
222
+ return this.addOptions({
223
+ actions
224
+ });
225
+ }
226
+ detailFixed(layoutBuilder, options = {}) {
227
+ this._detail = layoutBuilder;
228
+ return this.addOptions({
229
+ hideActions: true,
230
+ format: ControlType.array,
231
+ layout: options.layout ?? "column",
232
+ elementLabelProp: options.label
233
+ });
234
+ }
235
+ labelKey(labelKey) {
236
+ return this.addOptions({
237
+ labelKey
238
+ });
239
+ }
240
+ readonly() {
241
+ return this.addOptions({
242
+ format: ControlType.string,
243
+ readonly: true
244
+ });
245
+ }
246
+ link() {
247
+ return this.addOptions({
248
+ format: ControlType.link
249
+ });
250
+ }
251
+ markdown(options) {
252
+ return this.addOptions({
253
+ format: ControlType.markdown,
254
+ ...options ?? {}
255
+ });
256
+ }
257
+ textArea(options) {
258
+ return this.addOptions({
259
+ format: ControlType.textArea,
260
+ ...options ?? {}
261
+ });
262
+ }
263
+ autocomplete(options) {
264
+ return this.addOptions({
265
+ format: ControlType.autocomplete,
266
+ dataField: "data",
267
+ ...options ?? {}
268
+ });
269
+ }
270
+ control(format, options) {
271
+ return this.addOptions({
272
+ format,
273
+ ...options
274
+ });
275
+ }
276
+ select(options) {
277
+ return this.addOptions({
278
+ format: ControlType.select,
279
+ ...options
280
+ });
281
+ }
282
+ mutliSelect(options) {
283
+ return this.addOptions({
284
+ format: ControlType.mutliSelect,
285
+ ...options
286
+ });
287
+ }
288
+ width(width) {
289
+ return this.addOptions({
290
+ styles: {
291
+ ...this.options?.styles,
292
+ width,
293
+ control: {
294
+ wrapper: `input-${width}`
295
+ }
296
+ }
297
+ });
298
+ }
299
+ customLabel(label) {
300
+ return this.addOptions({
301
+ label
302
+ });
303
+ }
304
+ placeHolder(placeholder) {
305
+ return this.addOptions({
306
+ placeholder
307
+ });
308
+ }
309
+ hideLabel() {
310
+ return this.addOptions({
311
+ hideLabel: true
312
+ });
313
+ }
314
+ addOptions(options) {
315
+ this.options = {
316
+ ...this.options,
317
+ ...options
318
+ };
319
+ return this;
320
+ }
321
+ build() {
322
+ return {
323
+ type: this.type,
324
+ scope: this.scope,
325
+ options: {
326
+ ...this.options,
327
+ detail: this._detail ? this._detail?.build() : void 0
328
+ }
329
+ };
330
+ }
331
+ };
332
+
333
+ // ../crouton-core/src/lib/layout/layout.builder.ts
334
+ var LayoutTypes = {
335
+ HorizontalLayout: "HorizontalLayout",
336
+ VerticalLayout: "VerticalLayout",
337
+ CollapseLayout: "CollapseLayout",
338
+ GridLayout: "GridLayout"
339
+ };
340
+ var LayoutBuilder = class _LayoutBuilder extends BuilderWithElements {
341
+ static {
342
+ __name(this, "LayoutBuilder");
343
+ }
344
+ options;
345
+ constructor(type, options = {}) {
346
+ super(type);
347
+ this.options = options;
348
+ }
349
+ static horizontal() {
350
+ return new _LayoutBuilder(LayoutTypes.HorizontalLayout);
351
+ }
352
+ static collapse() {
353
+ return new _LayoutBuilder(LayoutTypes.CollapseLayout);
354
+ }
355
+ static vertical() {
356
+ return new _LayoutBuilder(LayoutTypes.VerticalLayout);
357
+ }
358
+ static grid() {
359
+ return new _LayoutBuilder(LayoutTypes.GridLayout);
360
+ }
361
+ titleKey(titleKey) {
362
+ return this.addOptions({
363
+ titleKey
364
+ });
365
+ }
366
+ title(title) {
367
+ return this.addOptions({
368
+ title
369
+ });
370
+ }
371
+ addOptions(options) {
372
+ this.options = {
373
+ ...this.options,
374
+ ...options
375
+ };
376
+ return this;
377
+ }
378
+ build() {
379
+ return {
380
+ type: this.type,
381
+ elements: this.buildElements(),
382
+ options: this.options
383
+ };
384
+ }
385
+ };
386
+
387
+ // ../crouton-core/src/lib/json-config.types.ts
388
+ var labelFromId = /* @__PURE__ */ __name((id) => {
389
+ const words = id.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
390
+ return words.charAt(0).toUpperCase() + words.slice(1).toLowerCase();
391
+ }, "labelFromId");
392
+ var normalizeColumns = /* @__PURE__ */ __name((columns) => {
393
+ if (!columns) return void 0;
394
+ const raw = Array.isArray(columns) ? columns : Object.entries(columns).map(([id, col]) => ({
395
+ id,
396
+ ...col
397
+ }));
398
+ return raw.map((col) => ({
399
+ ...col,
400
+ label: col.label ?? labelFromId(col.id)
401
+ }));
402
+ }, "normalizeColumns");
403
+
404
+ // ../crouton-core/src/lib/value-label.ts
405
+ var wrapOne = /* @__PURE__ */ __name((value, values) => {
406
+ const match = values.find((o) => o.value === value);
407
+ return {
408
+ value,
409
+ label: match ? match.label : String(value)
410
+ };
411
+ }, "wrapOne");
412
+ var toValueLabel = /* @__PURE__ */ __name((value, values) => {
413
+ if (value === null || value === void 0) return value;
414
+ if (Array.isArray(value)) return value.map((v) => wrapOne(v, values));
415
+ return wrapOne(value, values);
416
+ }, "toValueLabel");
417
+ var fromValueLabel = /* @__PURE__ */ __name((input) => {
418
+ if (Array.isArray(input)) return input.map(fromValueLabel);
419
+ if (input && typeof input === "object" && "value" in input) {
420
+ return input.value;
421
+ }
422
+ return input;
423
+ }, "fromValueLabel");
424
+
425
+ // ../crouton-core/src/lib/table/table.builder.ts
426
+ var TextCellBuilder = class _TextCellBuilder extends Builder {
427
+ static {
428
+ __name(this, "TextCellBuilder");
429
+ }
430
+ scope;
431
+ options;
432
+ constructor(scope, type = "TextCell") {
433
+ super(type), this.scope = scope;
434
+ }
435
+ static properties(property) {
436
+ return new _TextCellBuilder(`#/properties/${property}`);
437
+ }
438
+ key(key) {
439
+ this.options = {
440
+ format: "keyValue",
441
+ key
442
+ };
443
+ return this;
444
+ }
445
+ setSortId(sortId) {
446
+ this.options = {
447
+ ...this.options ?? {
448
+ format: this.type
449
+ },
450
+ sortId
451
+ };
452
+ return this;
453
+ }
454
+ build() {
455
+ return {
456
+ type: this.type,
457
+ scope: this.scope,
458
+ options: this.options
459
+ };
460
+ }
461
+ };
462
+ var BooleanCellBuilder = class _BooleanCellBuilder extends TextCellBuilder {
463
+ static {
464
+ __name(this, "BooleanCellBuilder");
465
+ }
466
+ constructor(scope) {
467
+ super(scope, "BooleanCell");
468
+ }
469
+ static properties(property) {
470
+ return new _BooleanCellBuilder(`#/properties/${property}`);
471
+ }
472
+ };
473
+ var TableBuilder = class _TableBuilder {
474
+ static {
475
+ __name(this, "TableBuilder");
476
+ }
477
+ builder;
478
+ constructor() {
479
+ this.builder = LayoutBuilder.horizontal();
480
+ }
481
+ static init() {
482
+ return new _TableBuilder();
483
+ }
484
+ addControl(control) {
485
+ this.builder.addControls(control);
486
+ return this;
487
+ }
488
+ addControls(...controls) {
489
+ this.builder.addControls(...controls);
490
+ return this;
491
+ }
492
+ build() {
493
+ return this.builder.build();
494
+ }
495
+ };
496
+
497
+ // src/lib/crud/dev-mode.ts
498
+ var IS_DEV = process.env["NODE_ENV"] !== "production";
499
+
500
+ // src/lib/crud/resource-config.registry.ts
501
+ var import_common = require("@nestjs/common");
502
+ function _ts_decorate(decorators, target, key, desc2) {
503
+ var c = arguments.length, r = c < 3 ? target : desc2 === null ? desc2 = Object.getOwnPropertyDescriptor(target, key) : desc2, d;
504
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc2);
505
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
506
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
507
+ }
508
+ __name(_ts_decorate, "_ts_decorate");
509
+ function _ts_metadata(k, v) {
510
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
511
+ }
512
+ __name(_ts_metadata, "_ts_metadata");
513
+ var ResourceConfigRegistry = class {
514
+ static {
515
+ __name(this, "ResourceConfigRegistry");
516
+ }
517
+ loader;
518
+ configs;
519
+ constructor(loader, initialConfigs) {
520
+ this.loader = loader;
521
+ this.configs = initialConfigs;
522
+ }
523
+ async getAll() {
524
+ if (IS_DEV) {
525
+ this.configs = await this.loader.loadAll();
526
+ }
527
+ return this.configs;
528
+ }
529
+ async getByRoute(route) {
530
+ if (IS_DEV) {
531
+ return this.loader.loadByRoute(route);
532
+ }
533
+ return this.configs.find((c) => c.route === route);
534
+ }
535
+ };
536
+ ResourceConfigRegistry = _ts_decorate([
537
+ (0, import_common.Injectable)(),
538
+ _ts_metadata("design:type", Function),
539
+ _ts_metadata("design:paramtypes", [
540
+ typeof ResourceConfigLoader === "undefined" ? Object : ResourceConfigLoader,
541
+ Array
542
+ ])
543
+ ], ResourceConfigRegistry);
544
+
545
+ // src/lib/crud/app-layout.controller.ts
546
+ function _ts_decorate2(decorators, target, key, desc2) {
547
+ var c = arguments.length, r = c < 3 ? target : desc2 === null ? desc2 = Object.getOwnPropertyDescriptor(target, key) : desc2, d;
548
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc2);
549
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
550
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
551
+ }
552
+ __name(_ts_decorate2, "_ts_decorate");
553
+ function _ts_metadata2(k, v) {
554
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
555
+ }
556
+ __name(_ts_metadata2, "_ts_metadata");
557
+ var byPosition = /* @__PURE__ */ __name((a, b) => {
558
+ if (a.position != null && b.position != null) return a.position - b.position;
559
+ if (a.position != null) return -1;
560
+ if (b.position != null) return 1;
561
+ return a.label.localeCompare(b.label);
562
+ }, "byPosition");
563
+ var buildLayoutPayload = /* @__PURE__ */ __name((configs, sidebarGroups = {}, title, autoSave = true) => {
564
+ const visible = configs.filter((c) => c.sidebar?.hide !== true && c.views?.["table"]);
565
+ const topLevel = [];
566
+ const groupMap = new Map(Object.entries(sidebarGroups).map(([slug, cfg]) => [
567
+ slug,
568
+ {
569
+ label: cfg.label ?? labelFromId(slug),
570
+ position: cfg.position,
571
+ children: []
572
+ }
573
+ ]));
574
+ for (const c of visible) {
575
+ const leaf = {
576
+ kind: "item",
577
+ id: c.name,
578
+ label: c.sidebar?.label ?? c.title ?? c.tag,
579
+ position: c.sidebar?.position
580
+ };
581
+ const groupSlug = c.sidebar?.group;
582
+ if (groupSlug) {
583
+ if (!groupMap.has(groupSlug)) {
584
+ groupMap.set(groupSlug, {
585
+ label: labelFromId(groupSlug),
586
+ children: []
587
+ });
588
+ }
589
+ groupMap.get(groupSlug).children.push(leaf);
590
+ } else {
591
+ topLevel.push(leaf);
592
+ }
593
+ }
594
+ const groups = [
595
+ ...groupMap.entries()
596
+ ].filter(([, g]) => g.children.length > 0).map(([id, g]) => ({
597
+ kind: "group",
598
+ id,
599
+ label: g.label,
600
+ position: g.position,
601
+ children: g.children.sort(byPosition)
602
+ }));
603
+ const sidebar = [
604
+ ...topLevel,
605
+ ...groups
606
+ ].sort(byPosition);
607
+ return {
608
+ sidebar,
609
+ title,
610
+ autoSave
611
+ };
612
+ }, "buildLayoutPayload");
613
+ var createAppLayoutController = /* @__PURE__ */ __name((configs, sidebarGroups = {}, title, autoSave = true) => {
614
+ const layoutPayload = buildLayoutPayload(configs, sidebarGroups, title, autoSave);
615
+ let AppLayoutController = class AppLayoutController {
616
+ static {
617
+ __name(this, "AppLayoutController");
618
+ }
619
+ configRegistry;
620
+ constructor(configRegistry) {
621
+ this.configRegistry = configRegistry;
622
+ }
623
+ async getLayout() {
624
+ if (IS_DEV) {
625
+ const fresh = await this.configRegistry.getAll();
626
+ return buildLayoutPayload(fresh, sidebarGroups, title, autoSave);
627
+ }
628
+ return layoutPayload;
629
+ }
630
+ };
631
+ _ts_decorate2([
632
+ (0, import_common2.Get)("layout"),
633
+ (0, import_swagger.ApiOperation)({
634
+ summary: "Get the application layout (sidebar, \u2026)"
635
+ }),
636
+ (0, import_swagger.ApiResponse)({
637
+ status: 200,
638
+ description: "Application layout metadata"
639
+ }),
640
+ _ts_metadata2("design:type", Function),
641
+ _ts_metadata2("design:paramtypes", []),
642
+ _ts_metadata2("design:returntype", Promise)
643
+ ], AppLayoutController.prototype, "getLayout", null);
644
+ AppLayoutController = _ts_decorate2([
645
+ (0, import_common2.Controller)("_app"),
646
+ (0, import_swagger.ApiTags)("App"),
647
+ _ts_metadata2("design:type", Function),
648
+ _ts_metadata2("design:paramtypes", [
649
+ typeof ResourceConfigRegistry === "undefined" ? Object : ResourceConfigRegistry
650
+ ])
651
+ ], AppLayoutController);
652
+ Reflect.defineMetadata("design:paramtypes", [
653
+ ResourceConfigRegistry
654
+ ], AppLayoutController);
655
+ return AppLayoutController;
656
+ }, "createAppLayoutController");
657
+
658
+ // src/lib/crud/crouton-validation.filter.ts
659
+ var import_common3 = require("@nestjs/common");
660
+
661
+ // src/lib/crud/crouton-validation.error.ts
662
+ var CroutonValidationError = class extends Error {
663
+ static {
664
+ __name(this, "CroutonValidationError");
665
+ }
666
+ errors;
667
+ constructor(errors) {
668
+ super("Validation failed"), this.errors = errors;
669
+ this.name = "CroutonValidationError";
670
+ }
671
+ };
672
+
673
+ // src/lib/crud/crouton-validation.filter.ts
674
+ function _ts_decorate3(decorators, target, key, desc2) {
675
+ var c = arguments.length, r = c < 3 ? target : desc2 === null ? desc2 = Object.getOwnPropertyDescriptor(target, key) : desc2, d;
676
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc2);
677
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
678
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
679
+ }
680
+ __name(_ts_decorate3, "_ts_decorate");
681
+ var CroutonValidationExceptionFilter = class {
682
+ static {
683
+ __name(this, "CroutonValidationExceptionFilter");
684
+ }
685
+ catch(exception, host) {
686
+ const res = host.switchToHttp().getResponse();
687
+ res.status(400).json({
688
+ statusCode: 400,
689
+ message: exception.errors,
690
+ error: "Bad Request"
691
+ });
692
+ }
693
+ };
694
+ CroutonValidationExceptionFilter = _ts_decorate3([
695
+ (0, import_common3.Catch)(CroutonValidationError)
696
+ ], CroutonValidationExceptionFilter);
697
+
698
+ // src/lib/crud/crud-controller.factory.ts
699
+ var import_common11 = require("@nestjs/common");
700
+ var import_common12 = require("@nestjs/common");
701
+ var import_swagger6 = require("@nestjs/swagger");
702
+
703
+ // src/lib/crud/crud.config.ts
704
+ var resolveDefinition = /* @__PURE__ */ __name((config) => {
705
+ const def2 = config.definition;
706
+ return typeof def2 === "function" ? def2() : def2;
707
+ }, "resolveDefinition");
708
+ var isOperationEnabled = /* @__PURE__ */ __name((def2, op) => def2[op] != null, "isOperationEnabled");
709
+ var schemaFor = /* @__PURE__ */ __name((def2, op) => {
710
+ const entry = def2[op];
711
+ if (!entry || entry === true) return void 0;
712
+ return entry.schema;
713
+ }, "schemaFor");
714
+ var upsertOnFor = /* @__PURE__ */ __name((def2) => def2.upsert?.upsertOn, "upsertOnFor");
715
+
716
+ // src/lib/crud/read.repository.ts
717
+ var import_common4 = require("@nestjs/common");
718
+
719
+ // src/lib/crud/sql.helpers.ts
720
+ var castExpression = /* @__PURE__ */ __name((col) => {
721
+ if (col.type === "string") return `(${col.sqlExpression})`;
722
+ if (col.type === "boolean") return `CAST((${col.sqlExpression}) AS BOOLEAN)`;
723
+ return `CAST((${col.sqlExpression}) AS INTEGER)`;
724
+ }, "castExpression");
725
+ var defaultValueForType = /* @__PURE__ */ __name((col) => {
726
+ if (col.type === "boolean") return false;
727
+ if (col.type === "string") return null;
728
+ return 0;
729
+ }, "defaultValueForType");
730
+ var coerceColumnValue = /* @__PURE__ */ __name((col) => {
731
+ if (col.type === "boolean") return (v) => Boolean(v);
732
+ if (col.type === "string") return (v) => v === void 0 || v === null ? null : String(v);
733
+ return (v) => Number(v ?? 0);
734
+ }, "coerceColumnValue");
735
+ var buildCalculatedColumnSql = /* @__PURE__ */ __name((col, tableName, ids) => {
736
+ const alias = col.alias ?? col.id;
737
+ const placeholders = ids.map((_, i) => `$${i + 1}`).join(", ");
738
+ return `SELECT main.id, ${castExpression(col)} AS "${alias}" FROM "${tableName}" main WHERE main.id IN (${placeholders})`;
739
+ }, "buildCalculatedColumnSql");
740
+ var mergeCalculatedColumnsForRows = /* @__PURE__ */ __name(async (rows, calcCols, tableName, prisma) => {
741
+ if (!calcCols.length || !rows.length) return rows;
742
+ const ids = rows.map((r) => r.id);
743
+ const results = await Promise.all(calcCols.map(async (col) => {
744
+ const alias = col.alias ?? col.id;
745
+ const sql = buildCalculatedColumnSql(col, tableName, ids);
746
+ const defaultValue = defaultValueForType(col);
747
+ const coerce = coerceColumnValue(col);
748
+ try {
749
+ const calcRows = await prisma.$queryRawUnsafe(sql, ...ids);
750
+ return {
751
+ id: col.id,
752
+ defaultValue,
753
+ map: Object.fromEntries(calcRows.map((r) => [
754
+ String(r.id),
755
+ coerce(r[alias])
756
+ ]))
757
+ };
758
+ } catch (e) {
759
+ console.error(`[calculatedColumns] Failed for "${tableName}.${col.id}":`, e);
760
+ return {
761
+ id: col.id,
762
+ defaultValue,
763
+ map: {}
764
+ };
765
+ }
766
+ }));
767
+ return rows.map((row) => {
768
+ const extra = {};
769
+ for (const { id, defaultValue, map } of results) {
770
+ extra[id] = map[String(row.id)] ?? defaultValue;
771
+ }
772
+ return {
773
+ ...row,
774
+ ...extra
775
+ };
776
+ });
777
+ }, "mergeCalculatedColumnsForRows");
778
+ var buildIncludeClause = /* @__PURE__ */ __name((include) => {
779
+ if (!include?.length) return void 0;
780
+ const map = /* @__PURE__ */ new Map();
781
+ const orderByMap = /* @__PURE__ */ new Map();
782
+ for (const entry of include) {
783
+ if (typeof entry === "string") {
784
+ const dotIdx = entry.indexOf(".");
785
+ if (dotIdx === -1) {
786
+ if (!map.has(entry)) map.set(entry, []);
787
+ } else {
788
+ const relation = entry.slice(0, dotIdx);
789
+ const rest = entry.slice(dotIdx + 1);
790
+ map.set(relation, [
791
+ ...map.get(relation) ?? [],
792
+ rest
793
+ ]);
794
+ }
795
+ } else {
796
+ map.set(entry.relation, [
797
+ ...map.get(entry.relation) ?? [],
798
+ ...entry.include ?? []
799
+ ]);
800
+ if (entry.orderBy) orderByMap.set(entry.relation, entry.orderBy);
801
+ }
802
+ }
803
+ return Object.fromEntries(Array.from(map.entries()).map(([relation, nestedIncludes]) => {
804
+ const orderBy = orderByMap.get(relation);
805
+ if (!nestedIncludes.length && !orderBy) return [
806
+ relation,
807
+ true
808
+ ];
809
+ const nested = buildIncludeClause(nestedIncludes);
810
+ const clause = {};
811
+ if (nested) clause.include = nested;
812
+ if (orderBy) clause.orderBy = orderBy;
813
+ return [
814
+ relation,
815
+ clause
816
+ ];
817
+ }));
818
+ }, "buildIncludeClause");
819
+ var buildChildSortClause = /* @__PURE__ */ __name((sort, sortDir) => {
820
+ const parts = sort.split(".");
821
+ if (parts.length === 1) return buildSort(sort, sortDir);
822
+ const dir = sortDir ?? "asc";
823
+ return parts.reduceRight((acc, part, i) => i === parts.length - 1 ? {
824
+ [part]: dir
825
+ } : {
826
+ [part]: acc
827
+ }, {});
828
+ }, "buildChildSortClause");
829
+
830
+ // src/lib/crud/read.repository.ts
831
+ var parseFilterString = /* @__PURE__ */ __name((raw) => {
832
+ const parts = raw.split(":");
833
+ if (parts.length < 2 || !parts[0]) return null;
834
+ const field = parts[0];
835
+ const lastPart = parts[parts.length - 1];
836
+ const hasOperator = Operator.includes(lastPart);
837
+ const value = hasOperator ? parts.slice(1, -1).join(":") : parts.slice(1).join(":");
838
+ const operator = hasOperator ? lastPart : "contains";
839
+ return {
840
+ field,
841
+ value,
842
+ operator
843
+ };
844
+ }, "parseFilterString");
845
+ var buildNestedPath = /* @__PURE__ */ __name((path, condition) => {
846
+ if (path.length === 1) return {
847
+ [path[0]]: condition
848
+ };
849
+ const [head, ...rest] = path;
850
+ return {
851
+ [head]: buildNestedPath(rest, condition)
852
+ };
853
+ }, "buildNestedPath");
854
+ var JSON_PATH_SEP = "->";
855
+ var isJsonPath = /* @__PURE__ */ __name((field) => field.includes(JSON_PATH_SEP), "isJsonPath");
856
+ var buildJsonPathCondition = /* @__PURE__ */ __name((field, value, operator) => {
857
+ const [column, ...path] = field.split(JSON_PATH_SEP);
858
+ if (!column || path.length === 0) return null;
859
+ const frag = /* @__PURE__ */ __name((extra) => ({
860
+ [column]: {
861
+ path,
862
+ ...extra
863
+ }
864
+ }), "frag");
865
+ switch (operator) {
866
+ case "equals":
867
+ return frag({
868
+ equals: value
869
+ });
870
+ case "not_equals":
871
+ return frag({
872
+ not: value
873
+ });
874
+ case "gt":
875
+ return frag({
876
+ gt: value
877
+ });
878
+ case "lt":
879
+ return frag({
880
+ lt: value
881
+ });
882
+ case "contains":
883
+ return frag({
884
+ string_contains: value
885
+ });
886
+ case "not_contains":
887
+ return {
888
+ NOT: frag({
889
+ string_contains: value
890
+ })
891
+ };
892
+ // isnull/isnotnull on a json sub-path are not supported — filter on the
893
+ // whole column instead if you need an emptiness check.
894
+ default:
895
+ return null;
896
+ }
897
+ }, "buildJsonPathCondition");
898
+ var operatorToCondition = /* @__PURE__ */ __name((value, operator) => {
899
+ const num = Number(value);
900
+ const numVal = Number.isNaN(num) ? value : num;
901
+ switch (operator) {
902
+ case "contains":
903
+ return {
904
+ contains: value
905
+ };
906
+ case "not_contains":
907
+ return {
908
+ not: {
909
+ contains: value
910
+ }
911
+ };
912
+ case "equals":
913
+ return {
914
+ equals: value
915
+ };
916
+ case "not_equals":
917
+ return {
918
+ not: value
919
+ };
920
+ case "gt":
921
+ return {
922
+ gt: numVal
923
+ };
924
+ case "lt":
925
+ return {
926
+ lt: numVal
927
+ };
928
+ case "isnull":
929
+ return null;
930
+ case "isnotnull":
931
+ return {
932
+ not: null
933
+ };
934
+ default:
935
+ return void 0;
936
+ }
937
+ }, "operatorToCondition");
938
+ var buildFilterWhere = /* @__PURE__ */ __name((filter) => {
939
+ if (!filter?.length) return void 0;
940
+ const conditions = filter.map(parseFilterString).filter((f) => f !== null).map(({ field, value, operator }) => isJsonPath(field) ? buildJsonPathCondition(field, value, operator) : buildNestedPath(field.split("."), operatorToCondition(value, operator))).filter((c) => c !== null);
941
+ return conditions.length ? {
942
+ AND: conditions
943
+ } : void 0;
944
+ }, "buildFilterWhere");
945
+ var sanitizeValueLabelSort = /* @__PURE__ */ __name((sort, cols) => {
946
+ if (!sort || !cols?.length || !sort.endsWith(".label")) return sort;
947
+ const base = sort.slice(0, -".label".length);
948
+ const leaf = base.split(".").pop();
949
+ return cols.some((c) => c.field === base || c.field === leaf) ? base : sort;
950
+ }, "sanitizeValueLabelSort");
951
+ var orderableChildSort = /* @__PURE__ */ __name((sort, childModel, _sub) => {
952
+ if (!sort) return void 0;
953
+ const scalarFields = new Set(Object.keys(childModel?.fields ?? {}));
954
+ if (scalarFields.size === 0) return sort;
955
+ if (sort.includes(".")) return sort;
956
+ return scalarFields.has(sort) ? sort : void 0;
957
+ }, "orderableChildSort");
958
+ var applyValueLabelColumns = /* @__PURE__ */ __name((row, cols) => {
959
+ if (!row || !cols?.length) return row;
960
+ const out = {
961
+ ...row
962
+ };
963
+ for (const { field, values } of cols) {
964
+ if (field in out) out[field] = toValueLabel(out[field], values);
965
+ }
966
+ return out;
967
+ }, "applyValueLabelColumns");
968
+ var ReadRepository = class {
969
+ static {
970
+ __name(this, "ReadRepository");
971
+ }
972
+ prismaModel;
973
+ prisma;
974
+ config;
975
+ listSelect;
976
+ oneSelect;
977
+ constructor(prismaModel, prisma, config, listSelect, oneSelect) {
978
+ this.prismaModel = prismaModel;
979
+ this.prisma = prisma;
980
+ this.config = config;
981
+ this.listSelect = listSelect;
982
+ this.oneSelect = oneSelect;
983
+ }
984
+ toId(id) {
985
+ return (this.config.idType ?? "string") === "number" ? +id : String(id);
986
+ }
987
+ buildWhere(filter) {
988
+ return buildFilterWhere(filter);
989
+ }
990
+ projection(op) {
991
+ const select = op === "findAll" ? this.listSelect : this.oneSelect;
992
+ return select ? {
993
+ select
994
+ } : {};
995
+ }
996
+ safeSort(sort, sortDir) {
997
+ if (!sort) return void 0;
998
+ if (this.listSelect && !(sort in this.listSelect)) return void 0;
999
+ return buildSort(sort, sortDir);
1000
+ }
1001
+ async decorate(rows, op) {
1002
+ const hook = this.config.hooks?.afterRead;
1003
+ const hooked = hook ? await Promise.all(rows.map((row) => hook(row, {
1004
+ prisma: this.prisma,
1005
+ op
1006
+ }))) : rows;
1007
+ const cols = this.config.valueLabelColumns;
1008
+ return cols?.length ? hooked.map((r) => applyValueLabelColumns(r, cols)) : hooked;
1009
+ }
1010
+ async decorateOne(row, op) {
1011
+ const hook = this.config.hooks?.afterRead;
1012
+ return hook ? hook(row, {
1013
+ prisma: this.prisma,
1014
+ op
1015
+ }) : row;
1016
+ }
1017
+ /**
1018
+ * Fetch a paginated, sorted, and filtered list of records.
1019
+ * Sub-resource counts are merged onto each row; calculated columns are resolved via raw SQL.
1020
+ */
1021
+ async findAll(params) {
1022
+ const subResources = this.config.subResources ?? [];
1023
+ const projection = this.projection("findAll");
1024
+ let query = {
1025
+ where: this.buildWhere(params.filter),
1026
+ take: params.pageSize,
1027
+ skip: params.offset ?? (params.page - 1) * params.pageSize,
1028
+ orderBy: this.safeSort(sanitizeValueLabelSort(params.sort, this.config.valueLabelColumns), params.sortDir)
1029
+ };
1030
+ if (subResources.length) {
1031
+ const countClause = {
1032
+ select: Object.fromEntries(subResources.map((s) => [
1033
+ s.relation,
1034
+ true
1035
+ ]))
1036
+ };
1037
+ if (projection.select) {
1038
+ query.select = {
1039
+ ...projection.select,
1040
+ _count: countClause
1041
+ };
1042
+ } else {
1043
+ query = {
1044
+ ...query,
1045
+ _count: countClause
1046
+ };
1047
+ }
1048
+ } else {
1049
+ Object.assign(query, projection);
1050
+ }
1051
+ const rows = await this.prismaModel.findMany(query);
1052
+ const mapped = subResources.length ? rows.map((row) => {
1053
+ const { _count, ...rest } = row;
1054
+ if (!_count) return rest;
1055
+ const counts = Object.fromEntries(subResources.map((s) => [
1056
+ s.column,
1057
+ _count[s.relation] ?? 0
1058
+ ]));
1059
+ return {
1060
+ ...rest,
1061
+ ...counts
1062
+ };
1063
+ }) : rows;
1064
+ const withCalc = await mergeCalculatedColumnsForRows(mapped, this.config.calculatedColumns ?? [], this.config.model, this.prisma);
1065
+ return this.decorate(withCalc, "findAll");
1066
+ }
1067
+ /** Count records matching the given filter strings. */
1068
+ count(filter) {
1069
+ return this.prismaModel.count({
1070
+ where: this.buildWhere(filter)
1071
+ });
1072
+ }
1073
+ /**
1074
+ * Fetch a single record by id.
1075
+ * Sub-resources with `includeInFindOne: true` are eagerly loaded (flat).
1076
+ * `config.include` entries are loaded with full nesting via `buildIncludeClause`.
1077
+ * @throws {NotFoundException} When no record exists for the given id.
1078
+ */
1079
+ async findOne(id) {
1080
+ const formIncludes = (this.config.subResources ?? []).filter((s) => s.includeInFindOne).map((s) => s.relation);
1081
+ const projection = this.projection("findOne");
1082
+ const idField = this.config.idField ?? "id";
1083
+ const query = {
1084
+ where: {
1085
+ [idField]: this.toId(id)
1086
+ },
1087
+ ...projection
1088
+ };
1089
+ const flatIncludes = formIncludes.length ? Object.fromEntries(formIncludes.map((r) => [
1090
+ r,
1091
+ true
1092
+ ])) : void 0;
1093
+ const configInclude = buildIncludeClause(this.config.include);
1094
+ const mergedInclude = flatIncludes || configInclude ? {
1095
+ ...flatIncludes,
1096
+ ...configInclude
1097
+ } : void 0;
1098
+ if (mergedInclude) {
1099
+ if (projection.select) {
1100
+ query.select = {
1101
+ ...projection.select,
1102
+ ...mergedInclude
1103
+ };
1104
+ } else {
1105
+ query.include = mergedInclude;
1106
+ }
1107
+ }
1108
+ const record = await this.prismaModel.findUnique(query);
1109
+ if (!record) throw new import_common4.NotFoundException(`${this.config.name} with id ${id} not found`);
1110
+ const [withCalc] = await mergeCalculatedColumnsForRows([
1111
+ record
1112
+ ], this.config.calculatedColumns ?? [], this.config.model, this.prisma);
1113
+ let enriched = withCalc ?? record;
1114
+ for (const sub of this.config.subResources ?? []) {
1115
+ if (!sub.calculatedColumns?.length) continue;
1116
+ const nested = enriched[sub.relation];
1117
+ if (!Array.isArray(nested) || !nested.length) continue;
1118
+ const enrichedNested = await mergeCalculatedColumnsForRows(nested, sub.calculatedColumns, sub.childModel, this.prisma);
1119
+ enriched = {
1120
+ ...enriched,
1121
+ [sub.relation]: enrichedNested
1122
+ };
1123
+ }
1124
+ return this.decorateOne(enriched, "findOne");
1125
+ }
1126
+ /**
1127
+ * Fetch a paginated list of child records belonging to the given parent.
1128
+ * @param childRoute - Matches the `childRoute` key on a `SubResourceConfig`.
1129
+ * @throws {Error} When no matching sub-resource config or Prisma model is found.
1130
+ */
1131
+ async findAllByParent(parentId, childRoute, params) {
1132
+ const sub = (this.config.subResources ?? []).find((s) => s.childRoute === childRoute);
1133
+ if (!sub) throw new Error(`No sub-resource "${childRoute}" on "${this.config.name}"`);
1134
+ const childModel = this.prisma[sub.childModel];
1135
+ if (!childModel) throw new Error(`Prisma model "${sub.childModel}" not found`);
1136
+ const where = {
1137
+ ...this.buildWhere(params.filter),
1138
+ [sub.foreignKey]: this.toId(parentId)
1139
+ };
1140
+ const includeClause = buildIncludeClause(sub.include);
1141
+ const childSort = orderableChildSort(sanitizeValueLabelSort(params.sort, sub.valueLabelColumns), childModel, sub);
1142
+ const [data, count] = await Promise.all([
1143
+ childModel.findMany({
1144
+ where,
1145
+ take: params.pageSize,
1146
+ skip: params.offset ?? (params.page - 1) * params.pageSize,
1147
+ orderBy: childSort ? buildChildSortClause(childSort, params.sortDir) : void 0,
1148
+ ...includeClause && {
1149
+ include: includeClause
1150
+ }
1151
+ }),
1152
+ childModel.count({
1153
+ where
1154
+ })
1155
+ ]);
1156
+ const withCalc = sub.calculatedColumns?.length ? await mergeCalculatedColumnsForRows(data, sub.calculatedColumns, sub.childModel, this.prisma) : data;
1157
+ const decorated = sub.hooks?.afterRead ? await Promise.all(withCalc.map((row) => sub.hooks.afterRead(row, {
1158
+ prisma: this.prisma,
1159
+ op: "findAll"
1160
+ }))) : withCalc;
1161
+ const labeled = sub.valueLabelColumns?.length ? decorated.map((r) => applyValueLabelColumns(r, sub.valueLabelColumns)) : decorated;
1162
+ return {
1163
+ data: labeled,
1164
+ count
1165
+ };
1166
+ }
1167
+ /**
1168
+ * Fetch a single child record. When `parentId` is supplied the query also filters by the foreign key.
1169
+ * @throws {NotFoundException} When no matching record is found.
1170
+ */
1171
+ async findOneChild(sub, childId, parentId) {
1172
+ const childModel = this.prisma[sub.childModel];
1173
+ if (!childModel) throw new Error(`Prisma model "${sub.childModel}" not found`);
1174
+ const id = (sub.idType ?? "string") === "number" ? +childId : String(childId);
1175
+ const idField = sub.idField ?? "id";
1176
+ const where = {
1177
+ [idField]: id
1178
+ };
1179
+ if (parentId !== void 0) where[sub.foreignKey] = this.toId(parentId);
1180
+ const includeClause = buildIncludeClause(sub.include);
1181
+ const record = await childModel.findFirst({
1182
+ where,
1183
+ ...includeClause && {
1184
+ include: includeClause
1185
+ }
1186
+ });
1187
+ if (!record) throw new import_common4.NotFoundException(`${sub.childRoute} with id ${childId} not found`);
1188
+ const [withCalc] = sub.calculatedColumns?.length ? await mergeCalculatedColumnsForRows([
1189
+ record
1190
+ ], sub.calculatedColumns, sub.childModel, this.prisma) : [
1191
+ record
1192
+ ];
1193
+ if (sub.hooks?.afterRead) return sub.hooks.afterRead(withCalc, {
1194
+ prisma: this.prisma,
1195
+ op: "findOne"
1196
+ });
1197
+ return withCalc;
1198
+ }
1199
+ };
1200
+
1201
+ // src/lib/crud/schema.utils.ts
1202
+ var import_zod7 = require("zod");
1203
+ var dateOverride = /* @__PURE__ */ __name(({ zodSchema, jsonSchema }) => {
1204
+ if (zodSchema instanceof import_zod7.z.ZodDate) {
1205
+ jsonSchema.type = "string";
1206
+ jsonSchema.format = "date-time";
1207
+ }
1208
+ }, "dateOverride");
1209
+ var jsonSchemaOpts = {
1210
+ unrepresentable: "any",
1211
+ override: dateOverride
1212
+ };
1213
+ function isZodSchema(schema) {
1214
+ return schema instanceof import_zod7.ZodObject;
1215
+ }
1216
+ __name(isZodSchema, "isZodSchema");
1217
+ var isNullableProperty = /* @__PURE__ */ __name((property) => {
1218
+ const anyOf = property?.["anyOf"];
1219
+ return Array.isArray(anyOf) && anyOf.some((s) => s?.["type"] === "null");
1220
+ }, "isNullableProperty");
1221
+ var dropNullableFromRequired = /* @__PURE__ */ __name((jsonSchema) => {
1222
+ const { properties, required } = jsonSchema;
1223
+ if (!properties || !Array.isArray(required)) return;
1224
+ jsonSchema.required = required.filter((key) => !isNullableProperty(properties[key]));
1225
+ }, "dropNullableFromRequired");
1226
+ function toJsonSchema(schema) {
1227
+ if (isZodSchema(schema)) {
1228
+ const jsonSchema = (0, import_zod7.toJSONSchema)(schema, {
1229
+ target: "openApi3",
1230
+ ...jsonSchemaOpts
1231
+ });
1232
+ dropNullableFromRequired(jsonSchema);
1233
+ return jsonSchema;
1234
+ }
1235
+ return schema;
1236
+ }
1237
+ __name(toJsonSchema, "toJsonSchema");
1238
+ var unwrap = /* @__PURE__ */ __name((schema) => {
1239
+ let s = schema;
1240
+ let t = s?._zod?.def?.type;
1241
+ while (t === "optional" || t === "nullable" || t === "default" || t === "readonly") {
1242
+ s = s._zod.def.innerType;
1243
+ t = s?._zod?.def?.type;
1244
+ }
1245
+ return s;
1246
+ }, "unwrap");
1247
+ var typeOf = /* @__PURE__ */ __name((schema) => schema?._zod?.def?.type, "typeOf");
1248
+ function toSelectFields(schema) {
1249
+ if (!isZodSchema(schema)) {
1250
+ return Object.keys(schema.properties).reduce((acc, key) => {
1251
+ acc[key] = true;
1252
+ return acc;
1253
+ }, {});
1254
+ }
1255
+ const result = {};
1256
+ for (const [key, value] of Object.entries(schema.shape)) {
1257
+ const inner = unwrap(value);
1258
+ const t = typeOf(inner);
1259
+ if (t === "object") {
1260
+ result[key] = {
1261
+ select: toSelectFields(inner)
1262
+ };
1263
+ continue;
1264
+ }
1265
+ if (t === "array") {
1266
+ const element = unwrap(inner._zod.def.element);
1267
+ if (typeOf(element) === "object") {
1268
+ result[key] = {
1269
+ select: toSelectFields(element)
1270
+ };
1271
+ continue;
1272
+ }
1273
+ }
1274
+ result[key] = true;
1275
+ }
1276
+ return result;
1277
+ }
1278
+ __name(toSelectFields, "toSelectFields");
1279
+
1280
+ // src/lib/crud/write.repository.ts
1281
+ var import_common5 = require("@nestjs/common");
1282
+
1283
+ // src/lib/crud/constants.ts
1284
+ var PRISMA_NOT_FOUND_CODE = "P2025";
1285
+ var DEFAULT_ID_FIELD = "id";
1286
+
1287
+ // src/lib/crud/write.repository.ts
1288
+ var normalizeValueLabels = /* @__PURE__ */ __name((data, cols) => {
1289
+ if (!data || typeof data !== "object" || Array.isArray(data) || !cols?.length) return data;
1290
+ const out = {
1291
+ ...data
1292
+ };
1293
+ for (const { field } of cols) {
1294
+ if (field in out) out[field] = fromValueLabel(out[field]);
1295
+ }
1296
+ return out;
1297
+ }, "normalizeValueLabels");
1298
+ var includeRelationNames = /* @__PURE__ */ __name((include) => new Set((include ?? []).map((e) => typeof e === "string" ? e : e.relation)), "includeRelationNames");
1299
+ var WriteRepository = class {
1300
+ static {
1301
+ __name(this, "WriteRepository");
1302
+ }
1303
+ prismaModel;
1304
+ prisma;
1305
+ config;
1306
+ constructor(prismaModel, prisma, config) {
1307
+ this.prismaModel = prismaModel;
1308
+ this.prisma = prisma;
1309
+ this.config = config;
1310
+ }
1311
+ toId(id) {
1312
+ return (this.config.idType ?? "string") === "number" ? +id : String(id);
1313
+ }
1314
+ notFound(id) {
1315
+ return new import_common5.NotFoundException(`${this.config.name} with id ${id} not found`);
1316
+ }
1317
+ stripSubResourceKeys(data) {
1318
+ if (!data || typeof data !== "object" || Array.isArray(data)) return data;
1319
+ const subKeys = new Set((this.config.subResources ?? []).map((s) => s.column));
1320
+ if (!subKeys.size) return data;
1321
+ return Object.fromEntries(Object.entries(data).filter(([k]) => !subKeys.has(k)));
1322
+ }
1323
+ stripNonCreateableChildFields(data, sub) {
1324
+ if (!data || typeof data !== "object" || Array.isArray(data)) return data;
1325
+ const nonCreateable = new Set((sub.views?.form?.columns ?? []).filter((c) => c.createable === false).map((c) => c.id));
1326
+ if (!nonCreateable.size) return data;
1327
+ return Object.fromEntries(Object.entries(data).filter(([k]) => !nonCreateable.has(k)));
1328
+ }
1329
+ async prepare(data, op, id) {
1330
+ const normalized = normalizeValueLabels(data, this.config.valueLabelColumns);
1331
+ const hook = this.config.hooks?.beforeWrite;
1332
+ return hook ? hook(normalized, {
1333
+ prisma: this.prisma,
1334
+ op,
1335
+ id
1336
+ }) : normalized;
1337
+ }
1338
+ async postWrite(result, op, id) {
1339
+ const hook = this.config.hooks?.afterWrite;
1340
+ return hook ? hook(result, {
1341
+ prisma: this.prisma,
1342
+ op,
1343
+ id
1344
+ }) : result;
1345
+ }
1346
+ upsertWhere(data) {
1347
+ const keys = upsertOnFor(resolveDefinition(this.config));
1348
+ if (!keys) throw new import_common5.BadRequestException(`${this.config.name} has no upsertOn configured`);
1349
+ if (typeof keys === "string") return {
1350
+ [keys]: data[keys]
1351
+ };
1352
+ const composite = keys.join("_");
1353
+ return {
1354
+ [composite]: Object.fromEntries(keys.map((k) => [
1355
+ k,
1356
+ data[k]
1357
+ ]))
1358
+ };
1359
+ }
1360
+ async create(data) {
1361
+ const result = await this.prismaModel.create({
1362
+ data: await this.prepare(this.stripSubResourceKeys(data), "create")
1363
+ });
1364
+ return this.postWrite(result, "create");
1365
+ }
1366
+ async update(id, data) {
1367
+ const idField = this.config.idField ?? "id";
1368
+ try {
1369
+ const result = await this.prismaModel.update({
1370
+ where: {
1371
+ [idField]: this.toId(id)
1372
+ },
1373
+ data: await this.prepare(this.stripSubResourceKeys(data), "update", this.toId(id))
1374
+ });
1375
+ return this.postWrite(result, "update", this.toId(id));
1376
+ } catch (e) {
1377
+ if (e?.code === PRISMA_NOT_FOUND_CODE) throw this.notFound(id);
1378
+ throw e;
1379
+ }
1380
+ }
1381
+ async upsert(data) {
1382
+ const where = this.upsertWhere(data);
1383
+ const existing = await this.prismaModel.findFirst({
1384
+ where
1385
+ });
1386
+ const op = existing ? "update" : "create";
1387
+ const prepared = await this.prepare(this.stripSubResourceKeys(data), op, existing ? existing[this.config.idField ?? DEFAULT_ID_FIELD] : void 0);
1388
+ const result = await this.prismaModel.upsert({
1389
+ where,
1390
+ create: prepared,
1391
+ update: prepared
1392
+ });
1393
+ return this.postWrite(result, op, existing ? existing[this.config.idField ?? DEFAULT_ID_FIELD] : void 0);
1394
+ }
1395
+ /** Upsert multiple rows in parallel. */
1396
+ upsertMany(rows) {
1397
+ return Promise.all(rows.map((r) => this.upsert(r)));
1398
+ }
1399
+ async delete(id) {
1400
+ const idField = this.config.idField ?? "id";
1401
+ try {
1402
+ const result = await this.prismaModel.delete({
1403
+ where: {
1404
+ [idField]: this.toId(id)
1405
+ }
1406
+ });
1407
+ return this.postWrite(result, "delete", this.toId(id));
1408
+ } catch (e) {
1409
+ if (e?.code === PRISMA_NOT_FOUND_CODE) throw this.notFound(id);
1410
+ throw e;
1411
+ }
1412
+ }
1413
+ /**
1414
+ * Create a child record and attach it to the parent via the configured foreign key.
1415
+ * Fields marked `createable: false` in the form view are stripped before writing.
1416
+ */
1417
+ async createChild(parentId, sub, data) {
1418
+ const childModel = this.prisma[sub.childModel];
1419
+ if (!childModel) throw new Error(`Prisma model "${sub.childModel}" not found`);
1420
+ const stripped = this.stripNonCreateableChildFields(data, sub);
1421
+ const normalized = normalizeValueLabels(stripped, sub.valueLabelColumns);
1422
+ const payload = {
1423
+ ...normalized,
1424
+ [sub.foreignKey]: this.toId(parentId)
1425
+ };
1426
+ const prepared = sub.hooks?.beforeWrite ? await sub.hooks.beforeWrite(payload, {
1427
+ prisma: this.prisma,
1428
+ op: "create"
1429
+ }) : payload;
1430
+ const includeKeys = includeRelationNames(sub.include);
1431
+ const prismaData = {
1432
+ ...Object.fromEntries(Object.entries(prepared).filter(([k]) => !includeKeys.has(k))),
1433
+ [sub.foreignKey]: this.toId(parentId)
1434
+ };
1435
+ const result = await childModel.create({
1436
+ data: prismaData
1437
+ });
1438
+ return sub.hooks?.afterWrite ? sub.hooks.afterWrite(result, {
1439
+ prisma: this.prisma,
1440
+ op: "create"
1441
+ }) : result;
1442
+ }
1443
+ /**
1444
+ * Update a child record. Relation include-keys are stripped from the payload so Prisma doesn't
1445
+ * receive non-scalar fields.
1446
+ * @throws {NotFoundException} When the child record does not exist (Prisma P2025).
1447
+ */
1448
+ async updateChild(sub, childId, data) {
1449
+ const childModel = this.prisma[sub.childModel];
1450
+ if (!childModel) throw new Error(`Prisma model "${sub.childModel}" not found`);
1451
+ const id = (sub.idType ?? "string") === "number" ? +childId : String(childId);
1452
+ const normalized = normalizeValueLabels(data, sub.valueLabelColumns);
1453
+ const afterHook = sub.hooks?.beforeWrite ? await sub.hooks.beforeWrite(normalized, {
1454
+ prisma: this.prisma,
1455
+ op: "update",
1456
+ id
1457
+ }) : normalized;
1458
+ const includeKeys = includeRelationNames(sub.include);
1459
+ const prepared = Object.fromEntries(Object.entries(afterHook).filter(([k]) => !includeKeys.has(k)));
1460
+ try {
1461
+ const result = await childModel.update({
1462
+ where: {
1463
+ [sub.idField ?? DEFAULT_ID_FIELD]: id
1464
+ },
1465
+ data: prepared
1466
+ });
1467
+ return sub.hooks?.afterWrite ? sub.hooks.afterWrite(result, {
1468
+ prisma: this.prisma,
1469
+ op: "update",
1470
+ id
1471
+ }) : result;
1472
+ } catch (e) {
1473
+ if (e?.code === PRISMA_NOT_FOUND_CODE) throw new import_common5.NotFoundException(`${sub.childRoute} with id ${childId} not found`);
1474
+ throw e;
1475
+ }
1476
+ }
1477
+ /**
1478
+ * Delete a child record. When `parentId` is supplied the foreign key is included in the `where`
1479
+ * clause to prevent cross-parent deletions.
1480
+ * @throws {NotFoundException} When no matching record is found.
1481
+ */
1482
+ async deleteChild(sub, childId, parentId) {
1483
+ const childModel = this.prisma[sub.childModel];
1484
+ if (!childModel) throw new Error(`Prisma model "${sub.childModel}" not found`);
1485
+ const id = (sub.idType ?? "string") === "number" ? +childId : String(childId);
1486
+ const idField = sub.idField ?? "id";
1487
+ const where = {
1488
+ [idField]: id
1489
+ };
1490
+ if (parentId !== void 0) where[sub.foreignKey] = this.toId(parentId);
1491
+ try {
1492
+ const result = await childModel.deleteMany({
1493
+ where
1494
+ });
1495
+ if (result.count === 0) throw new import_common5.NotFoundException(`${sub.childRoute} with id ${childId} not found`);
1496
+ return sub.hooks?.afterWrite ? sub.hooks.afterWrite(result, {
1497
+ prisma: this.prisma,
1498
+ op: "delete",
1499
+ id
1500
+ }) : result;
1501
+ } catch (e) {
1502
+ if (e?.code === PRISMA_NOT_FOUND_CODE) throw new import_common5.NotFoundException(`${sub.childRoute} with id ${childId} not found`);
1503
+ throw e;
1504
+ }
1505
+ }
1506
+ };
1507
+
1508
+ // src/lib/crud/crud-repository.factory.ts
1509
+ function createCrudRepository(prisma, config) {
1510
+ const model = prisma[config.model];
1511
+ if (!model) {
1512
+ throw new Error(`Model "${config.model}" not found on the provided PrismaClient. Check the resource config for "${config.name}".`);
1513
+ }
1514
+ const definition = resolveDefinition(config);
1515
+ const listSchema = schemaFor(definition, "findAll");
1516
+ const oneSchema = schemaFor(definition, "findOne");
1517
+ const listSelect = listSchema ? toSelectFields(listSchema) : void 0;
1518
+ const oneSelect = oneSchema ? toSelectFields(oneSchema) : listSelect;
1519
+ const reader = new ReadRepository(model, prisma, config, listSelect, oneSelect);
1520
+ const writer = new WriteRepository(model, prisma, config);
1521
+ return {
1522
+ prisma,
1523
+ findAll: reader.findAll.bind(reader),
1524
+ count: reader.count.bind(reader),
1525
+ findOne: reader.findOne.bind(reader),
1526
+ findAllByParent: reader.findAllByParent.bind(reader),
1527
+ findOneChild: reader.findOneChild.bind(reader),
1528
+ create: writer.create.bind(writer),
1529
+ update: writer.update.bind(writer),
1530
+ upsert: writer.upsert.bind(writer),
1531
+ upsertMany: writer.upsertMany.bind(writer),
1532
+ delete: writer.delete.bind(writer),
1533
+ createChild: writer.createChild.bind(writer),
1534
+ updateChild: writer.updateChild.bind(writer),
1535
+ deleteChild: writer.deleteChild.bind(writer)
1536
+ };
1537
+ }
1538
+ __name(createCrudRepository, "createCrudRepository");
1539
+
1540
+ // src/lib/crud/data-source/data-source.registry.ts
1541
+ var import_common6 = require("@nestjs/common");
1542
+ function _ts_decorate4(decorators, target, key, desc2) {
1543
+ var c = arguments.length, r = c < 3 ? target : desc2 === null ? desc2 = Object.getOwnPropertyDescriptor(target, key) : desc2, d;
1544
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc2);
1545
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
1546
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
1547
+ }
1548
+ __name(_ts_decorate4, "_ts_decorate");
1549
+ function _ts_metadata3(k, v) {
1550
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
1551
+ }
1552
+ __name(_ts_metadata3, "_ts_metadata");
1553
+ var DataSourceRegistry = class {
1554
+ static {
1555
+ __name(this, "DataSourceRegistry");
1556
+ }
1557
+ clients = /* @__PURE__ */ new Map();
1558
+ defaultName;
1559
+ constructor(entries) {
1560
+ for (const entry of entries) {
1561
+ this.clients.set(entry.config.name, entry.client);
1562
+ if (entry.config.default) {
1563
+ this.defaultName = entry.config.name;
1564
+ }
1565
+ }
1566
+ if (!this.defaultName && entries.length > 0) {
1567
+ this.defaultName = entries[0].config.name;
1568
+ }
1569
+ }
1570
+ get(name) {
1571
+ const client = this.clients.get(name);
1572
+ if (!client) {
1573
+ throw new Error(`Data source "${name}" not found in registry`);
1574
+ }
1575
+ return client;
1576
+ }
1577
+ getDefault() {
1578
+ if (!this.defaultName) {
1579
+ throw new Error("No default data source configured");
1580
+ }
1581
+ return this.get(this.defaultName);
1582
+ }
1583
+ resolve(database) {
1584
+ return database ? this.get(database) : this.getDefault();
1585
+ }
1586
+ async onModuleDestroy() {
1587
+ for (const client of this.clients.values()) {
1588
+ if (typeof client.$disconnect === "function") {
1589
+ await client.$disconnect();
1590
+ }
1591
+ }
1592
+ }
1593
+ };
1594
+ DataSourceRegistry = _ts_decorate4([
1595
+ (0, import_common6.Injectable)(),
1596
+ _ts_metadata3("design:type", Function),
1597
+ _ts_metadata3("design:paramtypes", [
1598
+ Array
1599
+ ])
1600
+ ], DataSourceRegistry);
1601
+
1602
+ // src/lib/crud/data-source/data-source.loader.ts
1603
+ var import_node_fs = require("fs");
1604
+ var import_node_path = require("path");
1605
+ var loadDataSourcesFromDir = /* @__PURE__ */ __name(async (dirPath) => {
1606
+ if (!(0, import_node_fs.existsSync)(dirPath)) return [];
1607
+ const entries = (0, import_node_fs.readdirSync)(dirPath, {
1608
+ withFileTypes: true
1609
+ });
1610
+ const dirs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
1611
+ const results = [];
1612
+ for (const dir of dirs) {
1613
+ const basePath = (0, import_node_path.join)(dirPath, dir);
1614
+ const jsonFile = (0, import_node_path.join)(basePath, "data-source.json");
1615
+ if (!(0, import_node_fs.existsSync)(jsonFile)) continue;
1616
+ const config = JSON.parse((0, import_node_fs.readFileSync)(jsonFile, "utf-8"));
1617
+ const indexFile = findModule(basePath, "index");
1618
+ if (!indexFile) continue;
1619
+ const mod = await import(indexFile);
1620
+ const client = mod.default;
1621
+ if (!client) continue;
1622
+ results.push({
1623
+ config,
1624
+ client
1625
+ });
1626
+ }
1627
+ return results;
1628
+ }, "loadDataSourcesFromDir");
1629
+ var findModule = /* @__PURE__ */ __name((dir, name) => {
1630
+ for (const ext of [
1631
+ ".ts",
1632
+ ".js"
1633
+ ]) {
1634
+ const p = (0, import_node_path.join)(dir, `${name}${ext}`);
1635
+ if ((0, import_node_fs.existsSync)(p)) return p;
1636
+ }
1637
+ return void 0;
1638
+ }, "findModule");
1639
+
1640
+ // src/lib/crud/operations/register-actions.ts
1641
+ var import_common7 = require("@nestjs/common");
1642
+ var import_swagger2 = require("@nestjs/swagger");
1643
+
1644
+ // src/lib/crud/operations/decorator.utils.ts
1645
+ var def = /* @__PURE__ */ __name((cls, method, fn) => {
1646
+ Object.defineProperty(cls.prototype, method, {
1647
+ value: fn,
1648
+ writable: true,
1649
+ configurable: true
1650
+ });
1651
+ }, "def");
1652
+ var desc = /* @__PURE__ */ __name((cls, method) => Object.getOwnPropertyDescriptor(cls.prototype, method), "desc");
1653
+
1654
+ // src/lib/crud/operations/register-actions.ts
1655
+ var registerActionRoutes = /* @__PURE__ */ __name((ctx) => {
1656
+ const { cls, config } = ctx;
1657
+ const { name } = config;
1658
+ const procedureActions = (config.actions ?? []).filter((a) => a.type !== "link");
1659
+ for (const action of procedureActions) {
1660
+ const methodName = `procedure_${action.id}`;
1661
+ def(cls, methodName, async function(recordId) {
1662
+ return action.procedure(this.repo.prisma, recordId);
1663
+ });
1664
+ const d = desc(cls, methodName);
1665
+ (0, import_common7.Post)(`procedure/${action.id}/:recordId`)(cls.prototype, methodName, d);
1666
+ (0, import_common7.Param)("recordId")(cls.prototype, methodName, 0);
1667
+ (0, import_swagger2.ApiOperation)({
1668
+ summary: `Execute action "${action.label}" on a ${name}`
1669
+ })(cls.prototype, methodName, d);
1670
+ (0, import_swagger2.ApiParam)({
1671
+ name: "recordId",
1672
+ type: "string"
1673
+ })(cls.prototype, methodName, d);
1674
+ (0, import_swagger2.ApiResponse)({
1675
+ status: 200,
1676
+ description: `Action "${action.id}" result`
1677
+ })(cls.prototype, methodName, d);
1678
+ }
1679
+ }, "registerActionRoutes");
1680
+ var registerTableActionRoutes = /* @__PURE__ */ __name((ctx) => {
1681
+ const { cls, config } = ctx;
1682
+ const { name } = config;
1683
+ const procedureActions = (config.tableActions ?? []).filter((a) => a.type !== "link");
1684
+ for (const action of procedureActions) {
1685
+ const methodName = `tableAction_${action.id}`;
1686
+ def(cls, methodName, async function() {
1687
+ return action.procedure(this.repo.prisma);
1688
+ });
1689
+ const d = desc(cls, methodName);
1690
+ (0, import_common7.Post)(`table-action/${action.id}`)(cls.prototype, methodName, d);
1691
+ (0, import_swagger2.ApiOperation)({
1692
+ summary: `Execute table action "${action.label ?? action.id}" on ${name}`
1693
+ })(cls.prototype, methodName, d);
1694
+ (0, import_swagger2.ApiResponse)({
1695
+ status: 200,
1696
+ description: `Table action "${action.id}" result`
1697
+ })(cls.prototype, methodName, d);
1698
+ }
1699
+ }, "registerTableActionRoutes");
1700
+
1701
+ // src/lib/crud/operations/register-crud.ts
1702
+ var import_common8 = require("@nestjs/common");
1703
+ var import_swagger3 = require("@nestjs/swagger");
1704
+
1705
+ // src/lib/crud/request.dto.ts
1706
+ var import_zod_nestjs = require("@anatine/zod-nestjs");
1707
+ var RequestDtoNoOffset = class extends (0, import_zod_nestjs.createZodDto)(RequestSchema) {
1708
+ static {
1709
+ __name(this, "RequestDtoNoOffset");
1710
+ }
1711
+ };
1712
+ var RequestDto = class extends (0, import_zod_nestjs.createZodDto)(RequestSchemaWithOffset) {
1713
+ static {
1714
+ __name(this, "RequestDto");
1715
+ }
1716
+ };
1717
+
1718
+ // src/lib/crud/zod-validation.pipe.ts
1719
+ var ZodValidationPipe = class {
1720
+ static {
1721
+ __name(this, "ZodValidationPipe");
1722
+ }
1723
+ schema;
1724
+ /** Top-level field names that accept `null` (i.e. are nullable). */
1725
+ nullableKeys;
1726
+ /** Whether to coerce `undefined` → `null` on nullable fields (create/upsert only). */
1727
+ coerceUndefinedToNull;
1728
+ constructor(schema, options = {}) {
1729
+ this.schema = schema;
1730
+ this.nullableKeys = Object.entries(schema.shape).filter(([, field]) => field.safeParse(null).success).map(([key]) => key);
1731
+ this.coerceUndefinedToNull = options.coerceNullableUndefinedToNull ?? false;
1732
+ }
1733
+ transform(value) {
1734
+ const stripped = this.stripEmptyStrings(value);
1735
+ const input = this.coerceNullable(stripped);
1736
+ const result = this.schema.safeParse(input);
1737
+ if (!result.success) {
1738
+ throw new CroutonValidationError(this.formatErrors(result.error));
1739
+ }
1740
+ return result.data;
1741
+ }
1742
+ /**
1743
+ * Convert empty strings (`''`) on all top-level fields:
1744
+ * - Nullable fields (`z.string().nullable()`): `""` → `null`
1745
+ * (clearing a nullable field should store null, not fail validation)
1746
+ * - Required/optional fields (`z.string()` / `z.string().optional()`): `""` → `undefined`
1747
+ * (required → fails with invalid_type; optional → passes as absent)
1748
+ */
1749
+ stripEmptyStrings(value) {
1750
+ if (value == null || typeof value !== "object" || Array.isArray(value)) {
1751
+ return value;
1752
+ }
1753
+ const out = {
1754
+ ...value
1755
+ };
1756
+ for (const key of Object.keys(out)) {
1757
+ if (out[key] === "") {
1758
+ out[key] = this.nullableKeys.includes(key) ? null : void 0;
1759
+ }
1760
+ }
1761
+ return out;
1762
+ }
1763
+ /** Replace `undefined` with `null` on nullable fields (create/upsert only). */
1764
+ coerceNullable(value) {
1765
+ if (!this.coerceUndefinedToNull || value == null || typeof value !== "object" || Array.isArray(value)) {
1766
+ return value;
1767
+ }
1768
+ const out = {
1769
+ ...value
1770
+ };
1771
+ for (const key of this.nullableKeys) {
1772
+ if (out[key] === void 0) out[key] = null;
1773
+ }
1774
+ return out;
1775
+ }
1776
+ formatErrors(error) {
1777
+ return error.issues.map((e) => ({
1778
+ field: e.path.join("."),
1779
+ message: e.message
1780
+ }));
1781
+ }
1782
+ };
1783
+
1784
+ // src/lib/crud/operations/register-crud.ts
1785
+ var registerFindAll = /* @__PURE__ */ __name((ctx) => {
1786
+ if (!isOperationEnabled(ctx.definition, "findAll")) return;
1787
+ const { cls, config, listSchema } = ctx;
1788
+ const { name } = config;
1789
+ const lookupLabel = config.lookup?.label;
1790
+ def(cls, "findAll", async function(params, q) {
1791
+ const effectiveParams = {
1792
+ ...params
1793
+ };
1794
+ if (q && lookupLabel) {
1795
+ effectiveParams.filter = [
1796
+ ...params.filter ?? [],
1797
+ `${lookupLabel}:${q}`
1798
+ ];
1799
+ }
1800
+ const [data, count] = await Promise.all([
1801
+ this.repo.findAll(effectiveParams),
1802
+ this.repo.count(effectiveParams.filter)
1803
+ ]);
1804
+ const totalPages = Math.max(1, Math.ceil(count / params.pageSize));
1805
+ return {
1806
+ data,
1807
+ request: {
1808
+ count,
1809
+ page: params.page,
1810
+ pageSize: params.pageSize,
1811
+ totalPages,
1812
+ sort: params.sort,
1813
+ sortDir: params.sortDir,
1814
+ filter: params.filter
1815
+ }
1816
+ };
1817
+ });
1818
+ const d = desc(cls, "findAll");
1819
+ (0, import_common8.Get)()(cls.prototype, "findAll", d);
1820
+ (0, import_common8.Query)(new ZodValidationPipe(RequestDtoNoOffset.zodSchema))(cls.prototype, "findAll", 0);
1821
+ (0, import_common8.Query)("q")(cls.prototype, "findAll", 1);
1822
+ (0, import_swagger3.ApiOperation)({
1823
+ summary: `List all ${name}s`
1824
+ })(cls.prototype, "findAll", d);
1825
+ (0, import_swagger3.ApiResponse)({
1826
+ status: 200,
1827
+ description: `Array of ${name}`,
1828
+ ...listSchema && {
1829
+ schema: {
1830
+ type: "array",
1831
+ items: toJsonSchema(listSchema)
1832
+ }
1833
+ }
1834
+ })(cls.prototype, "findAll", d);
1835
+ }, "registerFindAll");
1836
+ var registerFindOne = /* @__PURE__ */ __name((ctx) => {
1837
+ if (!isOperationEnabled(ctx.definition, "findOne")) return;
1838
+ const { cls, config, oneSchema, idParamMeta } = ctx;
1839
+ const { name } = config;
1840
+ def(cls, "findOne", function(id) {
1841
+ return this.repo.findOne(id);
1842
+ });
1843
+ const d = desc(cls, "findOne");
1844
+ (0, import_common8.Get)(":id")(cls.prototype, "findOne", d);
1845
+ (0, import_common8.Param)("id")(cls.prototype, "findOne", 0);
1846
+ (0, import_swagger3.ApiOperation)({
1847
+ summary: `Get one ${name} by id`
1848
+ })(cls.prototype, "findOne", d);
1849
+ (0, import_swagger3.ApiParam)(idParamMeta)(cls.prototype, "findOne", d);
1850
+ (0, import_swagger3.ApiResponse)({
1851
+ status: 200,
1852
+ description: `The ${name}`,
1853
+ ...oneSchema && {
1854
+ schema: toJsonSchema(oneSchema)
1855
+ }
1856
+ })(cls.prototype, "findOne", d);
1857
+ (0, import_swagger3.ApiNotFoundResponse)({
1858
+ description: "Not found"
1859
+ })(cls.prototype, "findOne", d);
1860
+ }, "registerFindOne");
1861
+ var registerCreate = /* @__PURE__ */ __name((ctx) => {
1862
+ if (!isOperationEnabled(ctx.definition, "create")) return;
1863
+ const { cls, config, createSchema, bodyDecorator } = ctx;
1864
+ const { name } = config;
1865
+ def(cls, "create", function(body) {
1866
+ return this.repo.create(body);
1867
+ });
1868
+ const d = desc(cls, "create");
1869
+ (0, import_common8.Post)()(cls.prototype, "create", d);
1870
+ bodyDecorator(createSchema, {
1871
+ coerceNullableUndefinedToNull: true
1872
+ })(cls.prototype, "create", 0);
1873
+ (0, import_swagger3.ApiOperation)({
1874
+ summary: `Create a ${name}`
1875
+ })(cls.prototype, "create", d);
1876
+ if (createSchema) (0, import_swagger3.ApiBody)({
1877
+ schema: toJsonSchema(createSchema)
1878
+ })(cls.prototype, "create", d);
1879
+ (0, import_swagger3.ApiResponse)({
1880
+ status: 201,
1881
+ description: `${name} created`
1882
+ })(cls.prototype, "create", d);
1883
+ }, "registerCreate");
1884
+ var registerUpdate = /* @__PURE__ */ __name((ctx) => {
1885
+ if (!isOperationEnabled(ctx.definition, "update")) return;
1886
+ const { cls, config, updateSchema, idParamMeta, bodyDecorator } = ctx;
1887
+ const { name } = config;
1888
+ def(cls, "update", function(id, body) {
1889
+ return this.repo.update(id, body);
1890
+ });
1891
+ const d = desc(cls, "update");
1892
+ (0, import_common8.Patch)(":id")(cls.prototype, "update", d);
1893
+ (0, import_common8.Param)("id")(cls.prototype, "update", 0);
1894
+ bodyDecorator(updateSchema)(cls.prototype, "update", 1);
1895
+ (0, import_swagger3.ApiOperation)({
1896
+ summary: `Update a ${name}`
1897
+ })(cls.prototype, "update", d);
1898
+ (0, import_swagger3.ApiParam)(idParamMeta)(cls.prototype, "update", d);
1899
+ if (updateSchema) (0, import_swagger3.ApiBody)({
1900
+ schema: toJsonSchema(updateSchema)
1901
+ })(cls.prototype, "update", d);
1902
+ (0, import_swagger3.ApiResponse)({
1903
+ status: 200,
1904
+ description: `${name} updated`
1905
+ })(cls.prototype, "update", d);
1906
+ (0, import_swagger3.ApiNotFoundResponse)({
1907
+ description: "Not found"
1908
+ })(cls.prototype, "update", d);
1909
+ }, "registerUpdate");
1910
+ var registerUpsert = /* @__PURE__ */ __name((ctx) => {
1911
+ if (!isOperationEnabled(ctx.definition, "upsert")) return;
1912
+ const { cls, config, upsertSchema, bodyDecorator } = ctx;
1913
+ const { name } = config;
1914
+ def(cls, "upsert", function(body) {
1915
+ return this.repo.upsert(body);
1916
+ });
1917
+ const d = desc(cls, "upsert");
1918
+ (0, import_common8.Put)()(cls.prototype, "upsert", d);
1919
+ bodyDecorator(upsertSchema, {
1920
+ coerceNullableUndefinedToNull: true
1921
+ })(cls.prototype, "upsert", 0);
1922
+ (0, import_swagger3.ApiOperation)({
1923
+ summary: `Upsert a ${name}`
1924
+ })(cls.prototype, "upsert", d);
1925
+ if (upsertSchema) (0, import_swagger3.ApiBody)({
1926
+ schema: toJsonSchema(upsertSchema)
1927
+ })(cls.prototype, "upsert", d);
1928
+ (0, import_swagger3.ApiResponse)({
1929
+ status: 200,
1930
+ description: `${name} upserted`
1931
+ })(cls.prototype, "upsert", d);
1932
+ }, "registerUpsert");
1933
+ var registerDelete = /* @__PURE__ */ __name((ctx) => {
1934
+ if (!isOperationEnabled(ctx.definition, "delete")) return;
1935
+ const { cls, config, idParamMeta } = ctx;
1936
+ const { name } = config;
1937
+ def(cls, "delete", function(id) {
1938
+ return this.repo.delete(id);
1939
+ });
1940
+ const d = desc(cls, "delete");
1941
+ (0, import_common8.Delete)(":id")(cls.prototype, "delete", d);
1942
+ (0, import_common8.Param)("id")(cls.prototype, "delete", 0);
1943
+ (0, import_swagger3.ApiOperation)({
1944
+ summary: `Delete a ${name}`
1945
+ })(cls.prototype, "delete", d);
1946
+ (0, import_swagger3.ApiParam)(idParamMeta)(cls.prototype, "delete", d);
1947
+ (0, import_swagger3.ApiResponse)({
1948
+ status: 200,
1949
+ description: `${name} deleted`
1950
+ })(cls.prototype, "delete", d);
1951
+ (0, import_swagger3.ApiNotFoundResponse)({
1952
+ description: "Not found"
1953
+ })(cls.prototype, "delete", d);
1954
+ }, "registerDelete");
1955
+
1956
+ // src/lib/crud/operations/register-schema-endpoints.ts
1957
+ var import_common9 = require("@nestjs/common");
1958
+ var import_swagger4 = require("@nestjs/swagger");
1959
+
1960
+ // src/lib/crud/operations/payload-builders.ts
1961
+ var resolveEnvPlaceholders = /* @__PURE__ */ __name((value) => value.replace(/\{env\.([^}]+)\}/g, (match, varName) => process.env[varName] ?? match), "resolveEnvPlaceholders");
1962
+ var buildSubResourceOperations = /* @__PURE__ */ __name((ops, baseUri, idField = "id") => {
1963
+ if (!ops) return {};
1964
+ const idPlaceholder = `{${idField}}`;
1965
+ return {
1966
+ ...ops.findAll && {
1967
+ findAll: {
1968
+ uri: baseUri,
1969
+ method: "get"
1970
+ }
1971
+ },
1972
+ ...ops.findOne && {
1973
+ findOne: {
1974
+ uri: `${baseUri}/${idPlaceholder}`,
1975
+ method: "get"
1976
+ }
1977
+ },
1978
+ ...ops.create && {
1979
+ create: {
1980
+ uri: baseUri,
1981
+ method: "post"
1982
+ }
1983
+ },
1984
+ ...ops.update && {
1985
+ update: {
1986
+ uri: `${baseUri}/${idPlaceholder}`,
1987
+ method: "patch"
1988
+ }
1989
+ },
1990
+ ...ops.delete && {
1991
+ delete: {
1992
+ uri: `${baseUri}/${idPlaceholder}`,
1993
+ method: "delete"
1994
+ }
1995
+ }
1996
+ };
1997
+ }, "buildSubResourceOperations");
1998
+ var RESOURCE_OPS = [
1999
+ "findAll",
2000
+ "findOne",
2001
+ "create",
2002
+ "update",
2003
+ "delete"
2004
+ ];
2005
+ var OP_METHOD = {
2006
+ findAll: "get",
2007
+ findOne: "get",
2008
+ create: "post",
2009
+ update: "patch",
2010
+ delete: "delete"
2011
+ };
2012
+ var OP_SUFFIX = {
2013
+ findAll: "",
2014
+ findOne: "/{id}",
2015
+ create: "",
2016
+ update: "/{id}",
2017
+ delete: "/{id}"
2018
+ };
2019
+ var buildResourceOperations = /* @__PURE__ */ __name((definition, baseUri) => Object.fromEntries(RESOURCE_OPS.filter((op) => isOperationEnabled(definition, op)).map((op) => [
2020
+ op,
2021
+ {
2022
+ uri: `${baseUri}${OP_SUFFIX[op]}`,
2023
+ method: OP_METHOD[op]
2024
+ }
2025
+ ])), "buildResourceOperations");
2026
+ var buildDefinitionPayload = /* @__PURE__ */ __name((config) => {
2027
+ const { route, name, tag, idType = "string" } = config;
2028
+ const definition = resolveDefinition(config);
2029
+ const listSchema = schemaFor(definition, "findAll");
2030
+ const oneSchema = schemaFor(definition, "findOne") ?? listSchema;
2031
+ const createSchema = schemaFor(definition, "create");
2032
+ const updateSchema = schemaFor(definition, "update");
2033
+ const upsertSchema = schemaFor(definition, "upsert") ?? createSchema;
2034
+ const operations = [
2035
+ "findAll",
2036
+ "findOne",
2037
+ "create",
2038
+ "update",
2039
+ "upsert",
2040
+ "delete"
2041
+ ].filter((op) => isOperationEnabled(definition, op));
2042
+ return {
2043
+ name,
2044
+ route,
2045
+ idType,
2046
+ tag,
2047
+ operations,
2048
+ upsertOn: upsertOnFor(definition),
2049
+ schemas: {
2050
+ ...listSchema && {
2051
+ findAll: toJsonSchema(listSchema)
2052
+ },
2053
+ ...oneSchema && {
2054
+ findOne: toJsonSchema(oneSchema)
2055
+ },
2056
+ ...createSchema && {
2057
+ create: toJsonSchema(createSchema)
2058
+ },
2059
+ ...updateSchema && {
2060
+ update: toJsonSchema(updateSchema)
2061
+ },
2062
+ ...isOperationEnabled(definition, "upsert") && upsertSchema ? {
2063
+ upsert: toJsonSchema(upsertSchema)
2064
+ } : {}
2065
+ }
2066
+ };
2067
+ }, "buildDefinitionPayload");
2068
+ var buildResourceJsonPayload = /* @__PURE__ */ __name((config, baseUrl) => {
2069
+ const { name, route } = config;
2070
+ const definition = resolveDefinition(config);
2071
+ const uri = `${baseUrl}/${route}`;
2072
+ const operations = Object.fromEntries(RESOURCE_OPS.map((op) => [
2073
+ op,
2074
+ isOperationEnabled(definition, op)
2075
+ ]));
2076
+ operations.lookup = `${uri}?q={text}`;
2077
+ const form = config.views?.["form"];
2078
+ const schema = form?.json_schema ? {
2079
+ data: form.json_schema,
2080
+ ui: form.ui_schema
2081
+ } : null;
2082
+ return {
2083
+ id: name,
2084
+ uri,
2085
+ operations,
2086
+ schema
2087
+ };
2088
+ }, "buildResourceJsonPayload");
2089
+ var buildViewsPayload = /* @__PURE__ */ __name((config, baseUrl) => {
2090
+ if (!config.views || !Object.keys(config.views).length) return void 0;
2091
+ const definition = resolveDefinition(config);
2092
+ const baseUri = `${baseUrl}/${config.route}`;
2093
+ const operations = buildResourceOperations(definition, baseUri);
2094
+ if (isOperationEnabled(definition, "findAll")) {
2095
+ operations["lookup"] = `${baseUri}?q={text}`;
2096
+ }
2097
+ const schemas = Object.fromEntries(Object.entries(config.views).map(([key, v]) => [
2098
+ key,
2099
+ {
2100
+ data: v.json_schema,
2101
+ ui: v.ui_schema,
2102
+ ...v.defaultSort !== void 0 && {
2103
+ defaultSort: v.defaultSort
2104
+ }
2105
+ }
2106
+ ]));
2107
+ return {
2108
+ id: config.name,
2109
+ name: config.name,
2110
+ route: config.route,
2111
+ uri: `${baseUrl}/${config.route}`,
2112
+ title: config.title ?? config.tag,
2113
+ idField: config.lookup?.key ?? "id",
2114
+ idType: config.idType ?? "string",
2115
+ ...config.modalSize && {
2116
+ modalSize: config.modalSize
2117
+ },
2118
+ operations,
2119
+ display: config.display,
2120
+ schemas,
2121
+ ...config.actions?.length && {
2122
+ actions: config.actions.map((a) => a.type === "link" ? {
2123
+ type: "link",
2124
+ id: a.id,
2125
+ label: a.label,
2126
+ href: resolveEnvPlaceholders(a.href),
2127
+ ...a.condition && {
2128
+ condition: a.condition
2129
+ }
2130
+ } : {
2131
+ id: a.id,
2132
+ label: a.label,
2133
+ uri: `${baseUrl}/${config.route}/procedure/${a.id}/{id}`,
2134
+ method: a.method ?? "post",
2135
+ ...a.data && {
2136
+ data: a.data
2137
+ },
2138
+ ...a.condition && {
2139
+ condition: a.condition
2140
+ }
2141
+ })
2142
+ },
2143
+ ...config.tableActions?.length && {
2144
+ tableActions: config.tableActions.map((a) => a.type === "link" ? {
2145
+ type: "link",
2146
+ id: a.id,
2147
+ label: a.label,
2148
+ icon: a.icon,
2149
+ tooltip: a.tooltip,
2150
+ href: resolveEnvPlaceholders(a.href)
2151
+ } : {
2152
+ id: a.id,
2153
+ label: a.label,
2154
+ icon: a.icon,
2155
+ tooltip: a.tooltip,
2156
+ uri: `${baseUrl}/${config.route}/table-action/${a.id}`,
2157
+ method: a.method ?? "post",
2158
+ ...a.data && {
2159
+ data: a.data
2160
+ }
2161
+ })
2162
+ }
2163
+ };
2164
+ }, "buildViewsPayload");
2165
+
2166
+ // src/lib/crud/operations/register-schema-endpoints.ts
2167
+ var registerDefinitionEndpoint = /* @__PURE__ */ __name((ctx) => {
2168
+ const { cls, config } = ctx;
2169
+ const { route, name } = config;
2170
+ const definitionPayload = buildDefinitionPayload(config);
2171
+ def(cls, "getDefinition", async function() {
2172
+ if (IS_DEV) {
2173
+ const fresh = await this.configRegistry.getByRoute(route);
2174
+ if (fresh) return buildDefinitionPayload(fresh);
2175
+ }
2176
+ return definitionPayload;
2177
+ });
2178
+ const d = desc(cls, "getDefinition");
2179
+ (0, import_common9.Get)("definition")(cls.prototype, "getDefinition", d);
2180
+ (0, import_swagger4.ApiOperation)({
2181
+ summary: `Get the resource definition for ${name}`
2182
+ })(cls.prototype, "getDefinition", d);
2183
+ (0, import_swagger4.ApiResponse)({
2184
+ status: 200,
2185
+ description: `Definition (operations + schemas) for ${name}`
2186
+ })(cls.prototype, "getDefinition", d);
2187
+ }, "registerDefinitionEndpoint");
2188
+ var registerSchemasEndpoint = /* @__PURE__ */ __name((ctx) => {
2189
+ const { cls, config, baseUrl } = ctx;
2190
+ const { route, name } = config;
2191
+ const viewsPayload = buildViewsPayload(config, baseUrl);
2192
+ def(cls, "getSchemas", async function() {
2193
+ if (IS_DEV) {
2194
+ const fresh = await this.configRegistry.getByRoute(route);
2195
+ if (fresh) return buildViewsPayload(fresh, baseUrl) ?? viewsPayload;
2196
+ }
2197
+ return viewsPayload;
2198
+ });
2199
+ const d = desc(cls, "getSchemas");
2200
+ (0, import_common9.Get)("schemas")(cls.prototype, "getSchemas", d);
2201
+ (0, import_swagger4.ApiOperation)({
2202
+ summary: `Get view schemas (table/form) for ${name}`
2203
+ })(cls.prototype, "getSchemas", d);
2204
+ (0, import_swagger4.ApiResponse)({
2205
+ status: 200,
2206
+ description: `View schemas for ${name}`
2207
+ })(cls.prototype, "getSchemas", d);
2208
+ }, "registerSchemasEndpoint");
2209
+ var registerResourceJsonEndpoint = /* @__PURE__ */ __name((ctx) => {
2210
+ const { cls, config, baseUrl } = ctx;
2211
+ const { route, name } = config;
2212
+ const resourceJsonPayload = buildResourceJsonPayload(config, baseUrl);
2213
+ def(cls, "getResourceJson", async function() {
2214
+ if (IS_DEV) {
2215
+ const fresh = await this.configRegistry.getByRoute(route);
2216
+ if (fresh) return buildResourceJsonPayload(fresh, baseUrl);
2217
+ }
2218
+ return resourceJsonPayload;
2219
+ });
2220
+ const d = desc(cls, "getResourceJson");
2221
+ (0, import_common9.Get)("resource.json")(cls.prototype, "getResourceJson", d);
2222
+ (0, import_swagger4.ApiOperation)({
2223
+ summary: `Get resource descriptor for ${name}`
2224
+ })(cls.prototype, "getResourceJson", d);
2225
+ (0, import_swagger4.ApiResponse)({
2226
+ status: 200,
2227
+ description: `Resource descriptor (operations + JSON Schema) for ${name}`
2228
+ })(cls.prototype, "getResourceJson", d);
2229
+ }, "registerResourceJsonEndpoint");
2230
+
2231
+ // src/lib/crud/operations/register-sub-resources.ts
2232
+ var import_common10 = require("@nestjs/common");
2233
+ var import_swagger5 = require("@nestjs/swagger");
2234
+ var registerSubResourceSchemas = /* @__PURE__ */ __name((ctx, sub) => {
2235
+ if (!sub.views) return;
2236
+ const { cls, config, baseUrl } = ctx;
2237
+ const { route } = config;
2238
+ const methodName = `getSchemas_${sub.childRoute}`;
2239
+ const childUri = `${baseUrl}/${route}/{parent.id}/${sub.childRoute}`;
2240
+ const schemasPayload = {
2241
+ id: sub.name ?? sub.childRoute,
2242
+ name: sub.name ?? sub.childRoute,
2243
+ route: sub.childRoute,
2244
+ uri: childUri,
2245
+ title: sub.title ?? sub.childRoute,
2246
+ idField: sub.idField ?? "id",
2247
+ idType: sub.idType ?? "string",
2248
+ ...sub.modalSize && {
2249
+ modalSize: sub.modalSize
2250
+ },
2251
+ operations: buildSubResourceOperations(sub.operations, childUri, sub.idField ?? "id"),
2252
+ schemas: Object.fromEntries(Object.entries(sub.views).map(([key, v]) => [
2253
+ key,
2254
+ {
2255
+ data: v.json_schema,
2256
+ ui: v.ui_schema,
2257
+ ...v.defaultSort !== void 0 && {
2258
+ defaultSort: v.defaultSort
2259
+ }
2260
+ }
2261
+ ])),
2262
+ ...sub.actions?.length && {
2263
+ actions: sub.actions.map((a) => a.type === "link" ? {
2264
+ type: "link",
2265
+ id: a.id,
2266
+ label: a.label,
2267
+ href: resolveEnvPlaceholders(a.href),
2268
+ ...a.condition && {
2269
+ condition: a.condition
2270
+ }
2271
+ } : {
2272
+ id: a.id,
2273
+ label: a.label,
2274
+ uri: `${baseUrl}/${sub.childRoute}/procedure/${a.id}/{id}`,
2275
+ method: a.method ?? "post",
2276
+ ...a.data && {
2277
+ data: a.data
2278
+ },
2279
+ ...a.condition && {
2280
+ condition: a.condition
2281
+ }
2282
+ })
2283
+ }
2284
+ };
2285
+ def(cls, methodName, async function() {
2286
+ return schemasPayload;
2287
+ });
2288
+ const ds = desc(cls, methodName);
2289
+ (0, import_common10.Get)(`${sub.childRoute}/schemas`)(cls.prototype, methodName, ds);
2290
+ (0, import_swagger5.ApiOperation)({
2291
+ summary: `Get schemas for ${sub.childRoute}`
2292
+ })(cls.prototype, methodName, ds);
2293
+ (0, import_swagger5.ApiResponse)({
2294
+ status: 200,
2295
+ description: `View schemas for ${sub.childRoute}`
2296
+ })(cls.prototype, methodName, ds);
2297
+ }, "registerSubResourceSchemas");
2298
+ var registerSubResourceFindAll = /* @__PURE__ */ __name((ctx, sub) => {
2299
+ if (sub.operations?.findAll === false) return;
2300
+ const { cls, config } = ctx;
2301
+ const { name } = config;
2302
+ const idParamMeta = ctx.idParamMeta;
2303
+ const methodName = `findAllBy_${sub.childRoute}`;
2304
+ def(cls, methodName, async function(id, params) {
2305
+ const { data, count } = await this.repo.findAllByParent(id, sub.childRoute, params);
2306
+ const totalPages = Math.max(1, Math.ceil(count / params.pageSize));
2307
+ return {
2308
+ data,
2309
+ request: {
2310
+ count,
2311
+ page: params.page,
2312
+ pageSize: params.pageSize,
2313
+ totalPages,
2314
+ sort: params.sort,
2315
+ sortDir: params.sortDir,
2316
+ filter: params.filter
2317
+ }
2318
+ };
2319
+ });
2320
+ const d = desc(cls, methodName);
2321
+ (0, import_common10.Get)(`:id/${sub.childRoute}`)(cls.prototype, methodName, d);
2322
+ (0, import_common10.Param)("id")(cls.prototype, methodName, 0);
2323
+ (0, import_common10.Query)(new ZodValidationPipe(RequestDtoNoOffset.zodSchema))(cls.prototype, methodName, 1);
2324
+ (0, import_swagger5.ApiOperation)({
2325
+ summary: `List ${sub.childRoute} for a ${name}`
2326
+ })(cls.prototype, methodName, d);
2327
+ (0, import_swagger5.ApiParam)(idParamMeta)(cls.prototype, methodName, d);
2328
+ (0, import_swagger5.ApiResponse)({
2329
+ status: 200,
2330
+ description: `${sub.childRoute} list`
2331
+ })(cls.prototype, methodName, d);
2332
+ }, "registerSubResourceFindAll");
2333
+ var registerSubResourceCreate = /* @__PURE__ */ __name((ctx, sub) => {
2334
+ if (!sub.operations?.create) return;
2335
+ const { cls, config } = ctx;
2336
+ const { name } = config;
2337
+ const methodName = `createChild_${sub.childRoute}`;
2338
+ def(cls, methodName, async function(id, body) {
2339
+ return this.repo.createChild(id, sub, body);
2340
+ });
2341
+ const d = desc(cls, methodName);
2342
+ (0, import_common10.Post)(`:id/${sub.childRoute}`)(cls.prototype, methodName, d);
2343
+ (0, import_common10.Param)("id")(cls.prototype, methodName, 0);
2344
+ (0, import_common10.Body)()(cls.prototype, methodName, 1);
2345
+ (0, import_swagger5.ApiOperation)({
2346
+ summary: `Create ${sub.childRoute} for a ${name}`
2347
+ })(cls.prototype, methodName, d);
2348
+ (0, import_swagger5.ApiParam)(ctx.idParamMeta)(cls.prototype, methodName, d);
2349
+ (0, import_swagger5.ApiResponse)({
2350
+ status: 201,
2351
+ description: `${sub.childRoute} created`
2352
+ })(cls.prototype, methodName, d);
2353
+ }, "registerSubResourceCreate");
2354
+ var registerSubResourceFindOne = /* @__PURE__ */ __name((ctx, sub) => {
2355
+ if (!sub.operations?.findOne) return;
2356
+ const { cls } = ctx;
2357
+ const methodName = `findOneChild_${sub.childRoute}`;
2358
+ def(cls, methodName, async function(parentId, childId) {
2359
+ return this.repo.findOneChild(sub, childId, parentId);
2360
+ });
2361
+ const d = desc(cls, methodName);
2362
+ (0, import_common10.Get)(`:id/${sub.childRoute}/:childId`)(cls.prototype, methodName, d);
2363
+ (0, import_common10.Param)("id")(cls.prototype, methodName, 0);
2364
+ (0, import_common10.Param)("childId")(cls.prototype, methodName, 1);
2365
+ (0, import_swagger5.ApiOperation)({
2366
+ summary: `Get a ${sub.childRoute} record`
2367
+ })(cls.prototype, methodName, d);
2368
+ (0, import_swagger5.ApiParam)(ctx.idParamMeta)(cls.prototype, methodName, d);
2369
+ (0, import_swagger5.ApiResponse)({
2370
+ status: 200,
2371
+ description: `${sub.childRoute} record`
2372
+ })(cls.prototype, methodName, d);
2373
+ }, "registerSubResourceFindOne");
2374
+ var registerSubResourceUpdate = /* @__PURE__ */ __name((ctx, sub) => {
2375
+ if (!sub.operations?.update) return;
2376
+ const { cls } = ctx;
2377
+ const methodName = `updateChild_${sub.childRoute}`;
2378
+ def(cls, methodName, async function(_id, childId, body) {
2379
+ return this.repo.updateChild(sub, childId, body);
2380
+ });
2381
+ const d = desc(cls, methodName);
2382
+ (0, import_common10.Patch)(`:id/${sub.childRoute}/:childId`)(cls.prototype, methodName, d);
2383
+ (0, import_common10.Param)("id")(cls.prototype, methodName, 0);
2384
+ (0, import_common10.Param)("childId")(cls.prototype, methodName, 1);
2385
+ (0, import_common10.Body)()(cls.prototype, methodName, 2);
2386
+ (0, import_swagger5.ApiOperation)({
2387
+ summary: `Update a ${sub.childRoute} record`
2388
+ })(cls.prototype, methodName, d);
2389
+ (0, import_swagger5.ApiParam)(ctx.idParamMeta)(cls.prototype, methodName, d);
2390
+ (0, import_swagger5.ApiResponse)({
2391
+ status: 200,
2392
+ description: `${sub.childRoute} updated`
2393
+ })(cls.prototype, methodName, d);
2394
+ }, "registerSubResourceUpdate");
2395
+ var registerSubResourceDelete = /* @__PURE__ */ __name((ctx, sub) => {
2396
+ if (!sub.operations?.delete) return;
2397
+ const { cls } = ctx;
2398
+ const methodName = `deleteChild_${sub.childRoute}`;
2399
+ def(cls, methodName, async function(parentId, childId) {
2400
+ return this.repo.deleteChild(sub, childId, parentId);
2401
+ });
2402
+ const d = desc(cls, methodName);
2403
+ (0, import_common10.Delete)(`:id/${sub.childRoute}/:childId`)(cls.prototype, methodName, d);
2404
+ (0, import_common10.Param)("id")(cls.prototype, methodName, 0);
2405
+ (0, import_common10.Param)("childId")(cls.prototype, methodName, 1);
2406
+ (0, import_swagger5.ApiOperation)({
2407
+ summary: `Delete ${sub.childRoute} record`
2408
+ })(cls.prototype, methodName, d);
2409
+ (0, import_swagger5.ApiParam)(ctx.idParamMeta)(cls.prototype, methodName, d);
2410
+ (0, import_swagger5.ApiResponse)({
2411
+ status: 200,
2412
+ description: `${sub.childRoute} deleted`
2413
+ })(cls.prototype, methodName, d);
2414
+ }, "registerSubResourceDelete");
2415
+ var registerSubResourceRoutes = /* @__PURE__ */ __name((ctx) => {
2416
+ for (const sub of ctx.config.subResources ?? []) {
2417
+ registerSubResourceSchemas(ctx, sub);
2418
+ registerSubResourceFindAll(ctx, sub);
2419
+ registerSubResourceCreate(ctx, sub);
2420
+ registerSubResourceFindOne(ctx, sub);
2421
+ registerSubResourceUpdate(ctx, sub);
2422
+ registerSubResourceDelete(ctx, sub);
2423
+ }
2424
+ }, "registerSubResourceRoutes");
2425
+
2426
+ // src/lib/crud/crud-controller.factory.ts
2427
+ function createCrudController(config, baseUrl) {
2428
+ const { route, name, tag, idType = "string" } = config;
2429
+ const definition = resolveDefinition(config);
2430
+ const listSchema = schemaFor(definition, "findAll");
2431
+ const oneSchema = schemaFor(definition, "findOne") ?? listSchema;
2432
+ const createSchema = schemaFor(definition, "create");
2433
+ const updateSchema = schemaFor(definition, "update");
2434
+ const upsertSchema = schemaFor(definition, "upsert") ?? createSchema;
2435
+ if (isOperationEnabled(definition, "upsert") && !upsertOnFor(definition)) {
2436
+ throw new Error(`Resource "${name}" declares 'upsert' but no upsertOn`);
2437
+ }
2438
+ const bodyDecorator = /* @__PURE__ */ __name((schema, options) => {
2439
+ if (!schema) return (0, import_common12.Body)();
2440
+ if (isZodSchema(schema)) return (0, import_common12.Body)(new ZodValidationPipe(schema, options));
2441
+ return (0, import_common12.Body)();
2442
+ }, "bodyDecorator");
2443
+ let CrudControllerBase = class CrudControllerBase {
2444
+ static {
2445
+ __name(this, "CrudControllerBase");
2446
+ }
2447
+ repo;
2448
+ configRegistry;
2449
+ constructor(registry, configRegistry) {
2450
+ const prisma = registry.resolve(config.database);
2451
+ this.repo = createCrudRepository(prisma, config);
2452
+ this.configRegistry = configRegistry;
2453
+ }
2454
+ };
2455
+ const ctx = {
2456
+ cls: CrudControllerBase,
2457
+ config,
2458
+ definition,
2459
+ listSchema,
2460
+ oneSchema,
2461
+ createSchema,
2462
+ updateSchema,
2463
+ upsertSchema,
2464
+ idParamMeta: {
2465
+ name: "id",
2466
+ type: idType === "number" ? "number" : "string"
2467
+ },
2468
+ bodyDecorator,
2469
+ baseUrl
2470
+ };
2471
+ registerFindAll(ctx);
2472
+ registerDefinitionEndpoint(ctx);
2473
+ registerSchemasEndpoint(ctx);
2474
+ registerResourceJsonEndpoint(ctx);
2475
+ registerActionRoutes(ctx);
2476
+ registerTableActionRoutes(ctx);
2477
+ registerSubResourceRoutes(ctx);
2478
+ registerFindOne(ctx);
2479
+ registerCreate(ctx);
2480
+ registerUpdate(ctx);
2481
+ registerUpsert(ctx);
2482
+ registerDelete(ctx);
2483
+ (0, import_common11.Controller)(route)(CrudControllerBase);
2484
+ (0, import_swagger6.ApiTags)(tag)(CrudControllerBase);
2485
+ Object.defineProperty(CrudControllerBase, "name", {
2486
+ value: `${name.charAt(0).toUpperCase() + name.slice(1)}Controller`
2487
+ });
2488
+ Reflect.defineMetadata("design:paramtypes", [
2489
+ DataSourceRegistry,
2490
+ ResourceConfigRegistry
2491
+ ], CrudControllerBase);
2492
+ return CrudControllerBase;
2493
+ }
2494
+ __name(createCrudController, "createCrudController");
2495
+
2496
+ // src/lib/crud/loader/module.loader.ts
2497
+ var import_node_fs2 = require("fs");
2498
+ var import_node_module = require("module");
2499
+ var import_node_path2 = require("path");
2500
+ var import_meta = {};
2501
+ var _require = (0, import_node_module.createRequire)(import_meta.url);
2502
+ var findModule2 = /* @__PURE__ */ __name((dir, name) => {
2503
+ for (const ext of [
2504
+ ".ts",
2505
+ ".js"
2506
+ ]) {
2507
+ const p = (0, import_node_path2.join)(dir, `${name}${ext}`);
2508
+ if ((0, import_node_fs2.existsSync)(p)) return p;
2509
+ }
2510
+ return void 0;
2511
+ }, "findModule");
2512
+ var IS_VITE = typeof globalThis.__vite_ssr_import__ === "function";
2513
+ var importDefault = /* @__PURE__ */ __name(async (filePath) => {
2514
+ try {
2515
+ if (IS_VITE) {
2516
+ const importPath = IS_DEV ? `${filePath}?t=${Date.now()}` : filePath;
2517
+ const mod = await import(importPath);
2518
+ return mod.default ?? mod;
2519
+ } else {
2520
+ const mod = _require(filePath);
2521
+ return mod.default;
2522
+ }
2523
+ } catch {
2524
+ return void 0;
2525
+ }
2526
+ }, "importDefault");
2527
+
2528
+ // src/lib/crud/loader/action.loader.ts
2529
+ var import_node_path3 = require("path");
2530
+ var loadActions = /* @__PURE__ */ __name(async (jsonActions, basePath) => {
2531
+ const results = [];
2532
+ for (const action of jsonActions) {
2533
+ if (action.type === "link") {
2534
+ results.push({
2535
+ type: "link",
2536
+ id: action.id,
2537
+ label: action.label,
2538
+ href: action.href,
2539
+ ...action.condition && {
2540
+ condition: action.condition
2541
+ }
2542
+ });
2543
+ continue;
2544
+ }
2545
+ const file = findModule2((0, import_node_path3.join)(basePath, "actions"), action.procedure);
2546
+ if (!file) {
2547
+ console.warn(`[actions] Procedure file not found for "${action.id}" in ${basePath}/actions/`);
2548
+ continue;
2549
+ }
2550
+ const mod = await importDefault(file);
2551
+ if (!mod) {
2552
+ console.warn(`[actions] No default export in ${file}`);
2553
+ continue;
2554
+ }
2555
+ results.push({
2556
+ id: action.id,
2557
+ label: action.label,
2558
+ method: action.method,
2559
+ data: action.data,
2560
+ ...action.condition && {
2561
+ condition: action.condition
2562
+ },
2563
+ procedure: mod
2564
+ });
2565
+ }
2566
+ return results;
2567
+ }, "loadActions");
2568
+ var loadTableActions = /* @__PURE__ */ __name(async (jsonActions, basePath) => {
2569
+ const results = [];
2570
+ for (const action of jsonActions) {
2571
+ if (action.type === "link") {
2572
+ results.push({
2573
+ type: "link",
2574
+ id: action.id,
2575
+ label: action.label,
2576
+ icon: action.icon,
2577
+ tooltip: action.tooltip,
2578
+ href: action.href
2579
+ });
2580
+ continue;
2581
+ }
2582
+ const file = findModule2((0, import_node_path3.join)(basePath, "actions"), action.procedure);
2583
+ if (!file) {
2584
+ console.warn(`[tableActions] Procedure file not found for "${action.id}" in ${basePath}/actions/`);
2585
+ continue;
2586
+ }
2587
+ const mod = await importDefault(file);
2588
+ if (!mod) {
2589
+ console.warn(`[tableActions] No default export in ${file}`);
2590
+ continue;
2591
+ }
2592
+ results.push({
2593
+ id: action.id,
2594
+ label: action.label,
2595
+ icon: action.icon,
2596
+ tooltip: action.tooltip,
2597
+ method: action.method,
2598
+ data: action.data,
2599
+ procedure: mod
2600
+ });
2601
+ }
2602
+ return results;
2603
+ }, "loadTableActions");
2604
+
2605
+ // src/lib/crud/loader/enum-registry.ts
2606
+ var import_node_fs3 = require("fs");
2607
+ var import_node_path4 = require("path");
2608
+ var ENUMS_FILE = "crouton.enums.json";
2609
+ var loadEnumRegistry = /* @__PURE__ */ __name((startDir, enumsFile) => {
2610
+ let file = enumsFile;
2611
+ if (!file) {
2612
+ let dir = startDir;
2613
+ while (true) {
2614
+ const candidate = (0, import_node_path4.join)(dir, ENUMS_FILE);
2615
+ if ((0, import_node_fs3.existsSync)(candidate)) {
2616
+ file = candidate;
2617
+ break;
2618
+ }
2619
+ const parent = (0, import_node_path4.dirname)(dir);
2620
+ if (parent === dir) break;
2621
+ dir = parent;
2622
+ }
2623
+ }
2624
+ if (!file || !(0, import_node_fs3.existsSync)(file)) return {};
2625
+ try {
2626
+ return JSON.parse((0, import_node_fs3.readFileSync)(file, "utf-8"));
2627
+ } catch {
2628
+ return {};
2629
+ }
2630
+ }, "loadEnumRegistry");
2631
+ var injectEnumValues = /* @__PURE__ */ __name((columns, enums) => {
2632
+ if (!columns) return;
2633
+ for (const col of columns) {
2634
+ if (!col.enum) continue;
2635
+ const values = enums[col.enum];
2636
+ if (!values) continue;
2637
+ col.fieldInput = col.fieldInput ?? {
2638
+ type: "select"
2639
+ };
2640
+ const options = col.fieldInput.options ?? {};
2641
+ if (!("values" in options)) options.values = values;
2642
+ col.fieldInput.options = options;
2643
+ }
2644
+ }, "injectEnumValues");
2645
+
2646
+ // src/lib/crud/loader/json-adapter.ts
2647
+ var import_zod9 = require("zod");
2648
+
2649
+ // src/lib/crud/loader/schema.helpers.ts
2650
+ var pickByColumns = /* @__PURE__ */ __name((schema, columns, filter) => {
2651
+ if (!schema) return void 0;
2652
+ if (!columns?.length) return schema;
2653
+ const isRelation2 = /* @__PURE__ */ __name((c) => c.fieldInput?.format === "relation", "isRelation");
2654
+ const baseFilter = /* @__PURE__ */ __name((c) => !isRelation2(c) && (filter ? filter(c) : true), "baseFilter");
2655
+ const filtered = columns.filter(baseFilter);
2656
+ if (!filtered.length) return void 0;
2657
+ const mask = Object.fromEntries(filtered.map((c) => [
2658
+ c.id,
2659
+ true
2660
+ ]));
2661
+ return schema.pick(mask);
2662
+ }, "pickByColumns");
2663
+ var opWithSchema = /* @__PURE__ */ __name((enabled, schema) => {
2664
+ if (enabled === false) return void 0;
2665
+ return schema ? {
2666
+ schema
2667
+ } : true;
2668
+ }, "opWithSchema");
2669
+ var upsertOp = /* @__PURE__ */ __name((entry, schema) => {
2670
+ if (!entry) return void 0;
2671
+ if (entry === true) {
2672
+ throw new Error("`operations.upsert` must be an object with `upsertOn`, not `true`.");
2673
+ }
2674
+ if (typeof entry === "object") {
2675
+ return {
2676
+ upsertOn: entry.upsertOn,
2677
+ ...schema && {
2678
+ schema
2679
+ }
2680
+ };
2681
+ }
2682
+ return void 0;
2683
+ }, "upsertOp");
2684
+
2685
+ // src/lib/crud/loader/view.builders.ts
2686
+ var import_zod8 = require("zod");
2687
+
2688
+ // src/lib/crud/loader/form-schema.builder.ts
2689
+ var buildConditionSchema = /* @__PURE__ */ __name((when) => {
2690
+ if (when.notExists) return {
2691
+ not: {
2692
+ minLength: 1
2693
+ }
2694
+ };
2695
+ if (when.exists) return {
2696
+ minLength: 1
2697
+ };
2698
+ if (when.neq !== void 0) return {
2699
+ not: {
2700
+ const: when.neq
2701
+ }
2702
+ };
2703
+ return {
2704
+ const: when.eq
2705
+ };
2706
+ }, "buildConditionSchema");
2707
+ var buildRule = /* @__PURE__ */ __name((col) => {
2708
+ if (col.disabledWhen) {
2709
+ return {
2710
+ effect: "DISABLE",
2711
+ condition: {
2712
+ scope: `#/properties/${col.disabledWhen.field}`,
2713
+ schema: buildConditionSchema(col.disabledWhen)
2714
+ }
2715
+ };
2716
+ }
2717
+ const when = col.showWhen ?? col.hideWhen;
2718
+ if (!when) return void 0;
2719
+ const effect = col.showWhen ? "SHOW" : "HIDE";
2720
+ return {
2721
+ effect,
2722
+ condition: {
2723
+ scope: `#/properties/${when.field}`,
2724
+ schema: buildConditionSchema(when)
2725
+ }
2726
+ };
2727
+ }, "buildRule");
2728
+ var buildDetailLayout = /* @__PURE__ */ __name((detail) => {
2729
+ const inner = detail.layout === "collapse" ? LayoutBuilder.collapse() : LayoutBuilder.horizontal();
2730
+ inner.addControls(...detail.controls.map((dc) => {
2731
+ const ctrl = ControlBuilder.properties(dc.property);
2732
+ if (dc.type === "markdown") {
2733
+ ctrl.markdown(dc.options);
2734
+ } else if (dc.type) {
2735
+ ctrl.control(dc.type, dc.options ?? {});
2736
+ }
2737
+ if (dc.hideLabel) ctrl.hideLabel();
2738
+ if (dc.width) ctrl.width(dc.width);
2739
+ return ctrl;
2740
+ }));
2741
+ if (detail.titleKey) inner.titleKey(detail.titleKey);
2742
+ return inner;
2743
+ }, "buildDetailLayout");
2744
+ var buildFormControl = /* @__PURE__ */ __name((col) => {
2745
+ const control = ControlBuilder.properties(col.id);
2746
+ const fieldInput = col.fieldInput;
2747
+ if (fieldInput?.format === "relation") {
2748
+ const options = {
2749
+ ...fieldInput.options
2750
+ };
2751
+ if (!options.colspan) options.colspan = 12;
2752
+ if (fieldInput.relationType) options.relationType = fieldInput.relationType;
2753
+ control.control("relation", options).width("full");
2754
+ } else if (fieldInput?.type === "autocomplete") {
2755
+ const options = {
2756
+ ...fieldInput.options
2757
+ };
2758
+ if (!options.colspan) options.colspan = 12;
2759
+ if (fieldInput.relationType) options.relationType = fieldInput.relationType;
2760
+ const format = fieldInput.format ?? fieldInput.type;
2761
+ control.control(format, options).width("full");
2762
+ } else if (fieldInput?.detail) {
2763
+ const detailLayout = buildDetailLayout(fieldInput.detail);
2764
+ control.detailFixed(detailLayout, {
2765
+ layout: fieldInput.detail.layout === "collapse" ? "row" : void 0
2766
+ });
2767
+ } else if (fieldInput?.format === "date-range") {
2768
+ const options = {
2769
+ ...fieldInput.options
2770
+ };
2771
+ if (!options.colspan) options.colspan = 12;
2772
+ control.control("date-range", options).width("full");
2773
+ } else {
2774
+ const options = fieldInput?.options ?? {};
2775
+ if (!options.colspan) options.colspan = 12;
2776
+ const type = fieldInput?.type ?? "text";
2777
+ control.control(type, options).width("full");
2778
+ }
2779
+ if (fieldInput?.customRender) control.setCustomRender(fieldInput?.customRender);
2780
+ if (col.hideLabel) control.hideLabel();
2781
+ return control;
2782
+ }, "buildFormControl");
2783
+ var buildFormUiSchema = /* @__PURE__ */ __name((cols) => {
2784
+ const layout = LayoutBuilder.grid().addControls(...cols.map(buildFormControl)).build();
2785
+ const colMap = Object.fromEntries(cols.map((c) => [
2786
+ c.id,
2787
+ c
2788
+ ]));
2789
+ layout.elements = layout.elements.map((el) => {
2790
+ const id = el.scope?.replace("#/properties/", "");
2791
+ const col = id ? colMap[id] : void 0;
2792
+ if (!col) return el;
2793
+ const rule = buildRule(col);
2794
+ return {
2795
+ ...el,
2796
+ options: {
2797
+ ...el.options ?? {},
2798
+ label: col.label
2799
+ },
2800
+ ...rule && {
2801
+ rule
2802
+ }
2803
+ };
2804
+ });
2805
+ return layout;
2806
+ }, "buildFormUiSchema");
2807
+
2808
+ // src/lib/crud/loader/schema-transforms.ts
2809
+ var allowAdditionalProperties = /* @__PURE__ */ __name((schema) => {
2810
+ if (schema["type"] === "object") {
2811
+ schema["additionalProperties"] = true;
2812
+ const props = schema["properties"];
2813
+ if (props) {
2814
+ for (const value of Object.values(props)) {
2815
+ allowAdditionalProperties(value);
2816
+ }
2817
+ }
2818
+ }
2819
+ if (schema["type"] === "array" && schema["items"]) {
2820
+ allowAdditionalProperties(schema["items"]);
2821
+ }
2822
+ }, "allowAdditionalProperties");
2823
+ var isNullableProperty2 = /* @__PURE__ */ __name((prop) => {
2824
+ const anyOf = prop?.["anyOf"];
2825
+ return Array.isArray(anyOf) && anyOf.some((s) => s?.["type"] === "null");
2826
+ }, "isNullableProperty");
2827
+ var dropNullableFromRequired2 = /* @__PURE__ */ __name((schema) => {
2828
+ if (schema["type"] !== "object") return;
2829
+ const props = schema["properties"];
2830
+ if (!props) return;
2831
+ const required = schema["required"];
2832
+ if (Array.isArray(required)) {
2833
+ schema["required"] = required.filter((key) => !isNullableProperty2(props[key]));
2834
+ }
2835
+ for (const value of Object.values(props)) {
2836
+ dropNullableFromRequired2(value);
2837
+ }
2838
+ }, "dropNullableFromRequired");
2839
+ var enforceRequiredMinLength = /* @__PURE__ */ __name((schema) => {
2840
+ if (schema["type"] !== "object") return;
2841
+ const props = schema["properties"];
2842
+ if (!props) return;
2843
+ for (const prop of Object.values(props)) {
2844
+ if (prop?.["type"] === "string" && !("minLength" in prop)) {
2845
+ prop["minLength"] = 1;
2846
+ } else if (prop?.["type"] === "object") {
2847
+ enforceRequiredMinLength(prop);
2848
+ }
2849
+ }
2850
+ }, "enforceRequiredMinLength");
2851
+
2852
+ // src/lib/crud/loader/table-schema.builder.ts
2853
+ var isBoolean = /* @__PURE__ */ __name((col) => col.fieldInput?.type === "boolean" || col.columnType === "boolean", "isBoolean");
2854
+ var isRelation = /* @__PURE__ */ __name((col) => col.fieldInput?.format === "relation", "isRelation");
2855
+ var isAutocomplete = /* @__PURE__ */ __name((col) => col.fieldInput?.type === "autocomplete", "isAutocomplete");
2856
+ var isRecordCell = /* @__PURE__ */ __name((col) => isRelation(col) || isAutocomplete(col), "isRecordCell");
2857
+ var isDateRange = /* @__PURE__ */ __name((col) => col.fieldInput?.format === "date-range", "isDateRange");
2858
+ var deriveSortId = /* @__PURE__ */ __name((col) => {
2859
+ if (col.sortable === false) return null;
2860
+ if (col.sortId) return col.sortId;
2861
+ const base = col.column ?? col.id;
2862
+ const fi = col.fieldInput;
2863
+ const opts = fi?.options ?? {};
2864
+ const isRelation2 = fi?.format === "relation" || !!fi?.relationType || fi?.type === "autocomplete" || typeof opts.resource === "string";
2865
+ if (isRelation2) {
2866
+ const key = (typeof col.displayKey === "string" ? col.displayKey : void 0) ?? (typeof opts.displayKey === "string" ? opts.displayKey : void 0) ?? (typeof opts.labelKey === "string" ? opts.labelKey : void 0);
2867
+ if (!key) return null;
2868
+ const path2 = key.includes(".") ? key : `${base}.${key}`;
2869
+ return path2.endsWith(".label") ? path2.slice(0, -".label".length) : path2;
2870
+ }
2871
+ if (!col.displayKey) return base;
2872
+ const path = `${base}.${col.displayKey}`;
2873
+ const isValueLabel = !!col.enum || opts.emitObject === true;
2874
+ if (isValueLabel && path.endsWith(".label")) return path.slice(0, -".label".length);
2875
+ return path;
2876
+ }, "deriveSortId");
2877
+ var resolveDefaultSort = /* @__PURE__ */ __name((tableCols, allColumns) => {
2878
+ const isSortable = /* @__PURE__ */ __name((c) => c.sortable !== false, "isSortable");
2879
+ const sortCol = tableCols.find((c) => c.defaultSort && isSortable(c)) ?? tableCols.find(isSortable) ?? allColumns?.find((c) => c.idField);
2880
+ return sortCol ? deriveSortId(sortCol) ?? sortCol.id : void 0;
2881
+ }, "resolveDefaultSort");
2882
+ var SHARED_CELL_OPTION_KEYS = [
2883
+ "values",
2884
+ "storeValue",
2885
+ "uri",
2886
+ "resourceUri",
2887
+ "schemasUri"
2888
+ ];
2889
+ var pickSharedCellOptions = /* @__PURE__ */ __name((col) => {
2890
+ const options = col.fieldInput?.options ?? {};
2891
+ return Object.fromEntries(SHARED_CELL_OPTION_KEYS.filter((key) => options[key] !== void 0).map((key) => [
2892
+ key,
2893
+ options[key]
2894
+ ]));
2895
+ }, "pickSharedCellOptions");
2896
+ var buildTableUiSchema = /* @__PURE__ */ __name((cols) => {
2897
+ const layout = TableBuilder.init().addControls(...cols.map((col) => {
2898
+ const cellBuilder = isBoolean(col) ? BooleanCellBuilder : TextCellBuilder;
2899
+ let builder = cellBuilder.properties(col.id);
2900
+ if (col.displayKey) builder = builder.key(col.displayKey);
2901
+ if (col.sortId) builder = builder.setSortId(col.sortId);
2902
+ return builder;
2903
+ })).build();
2904
+ const colMap = Object.fromEntries(cols.map((c) => [
2905
+ c.id,
2906
+ c
2907
+ ]));
2908
+ layout.elements = layout.elements.map((el) => {
2909
+ const id = el.scope?.replace("#/properties/", "");
2910
+ const col = id ? colMap[id] : void 0;
2911
+ if (!col) return el;
2912
+ const fieldInputOptions = isRecordCell(col) || isDateRange(col) ? col.fieldInput?.options ?? {} : pickSharedCellOptions(col);
2913
+ const dataPathOption = col.column ? {
2914
+ dataPath: col.column
2915
+ } : {};
2916
+ const derivedSortId = isRecordCell(col) || isDateRange(col) ? null : deriveSortId(col);
2917
+ const sortOptions = derivedSortId ? {
2918
+ sortId: derivedSortId
2919
+ } : {
2920
+ sortable: false
2921
+ };
2922
+ const relationTypeOption = col.fieldInput?.relationType ? {
2923
+ relationType: col.fieldInput.relationType
2924
+ } : {};
2925
+ return {
2926
+ ...el,
2927
+ options: {
2928
+ ...el.options ?? {},
2929
+ ...fieldInputOptions,
2930
+ ...dataPathOption,
2931
+ ...sortOptions,
2932
+ ...relationTypeOption,
2933
+ ...isDateRange(col) && {
2934
+ format: "date-range"
2935
+ },
2936
+ label: col.label
2937
+ },
2938
+ ...isRecordCell(col) && {
2939
+ type: "RecordCell"
2940
+ },
2941
+ ...isDateRange(col) && {
2942
+ type: "Control"
2943
+ }
2944
+ };
2945
+ });
2946
+ return layout;
2947
+ }, "buildTableUiSchema");
2948
+
2949
+ // src/lib/crud/loader/view.builders.ts
2950
+ var patchFilterProperties = /* @__PURE__ */ __name((jsonSchema, columns) => {
2951
+ if (!columns?.length) return;
2952
+ const properties = jsonSchema.properties;
2953
+ if (!properties) return;
2954
+ for (const col of columns) {
2955
+ if (col.fieldInput?.format === "date-range") {
2956
+ delete properties[col.id];
2957
+ const opts = col.fieldInput?.options ?? {};
2958
+ const base = col.label ?? col.id;
2959
+ const fromField = opts["fromField"] ?? "from";
2960
+ const toField = opts["toField"] ?? "to";
2961
+ const dateProp = /* @__PURE__ */ __name((title) => ({
2962
+ type: "string",
2963
+ format: "date",
2964
+ title,
2965
+ "x-field-type": "date"
2966
+ }), "dateProp");
2967
+ properties[`${col.id}->${fromField}`] = dateProp(opts["fromLabel"] ?? `${base} (from)`);
2968
+ properties[`${col.id}->${toField}`] = dateProp(opts["toLabel"] ?? `${base} (to)`);
2969
+ continue;
2970
+ }
2971
+ const prop = properties[col.id];
2972
+ if (!prop) continue;
2973
+ prop.title = col.label ?? col.id;
2974
+ const values = col.fieldInput?.options?.["values"];
2975
+ if (values) {
2976
+ prop["x-values"] = values;
2977
+ }
2978
+ const ft = col.fieldInput?.type;
2979
+ if (ft === "select" || col.enum) {
2980
+ prop["x-field-type"] = "enum";
2981
+ } else if (ft === "number" || prop.type === "number" || prop.type === "integer") {
2982
+ prop["x-field-type"] = "number";
2983
+ } else if (prop.format === "date-time" || prop.format === "date") {
2984
+ prop["x-field-type"] = "date";
2985
+ } else if (prop.type === "boolean") {
2986
+ prop["x-field-type"] = "boolean";
2987
+ }
2988
+ }
2989
+ }, "patchFilterProperties");
2990
+ var colPosition = /* @__PURE__ */ __name((col, i) => col.fieldInput?.position ?? i, "colPosition");
2991
+ var sortByPosition = /* @__PURE__ */ __name((cols) => cols.map((col, i) => ({
2992
+ col,
2993
+ i
2994
+ })).sort((a, b) => colPosition(a.col, a.i) - colPosition(b.col, b.i)).map(({ col }) => col), "sortByPosition");
2995
+ var toViewColumn = /* @__PURE__ */ __name((col) => ({
2996
+ id: col.id,
2997
+ ...col.label && {
2998
+ label: col.label
2999
+ },
3000
+ ...col.sortable != null && {
3001
+ sortable: col.sortable
3002
+ },
3003
+ ...col.searchable != null && {
3004
+ searchable: col.searchable
3005
+ },
3006
+ ...col.fieldInput && {
3007
+ fieldInput: col.fieldInput
3008
+ }
3009
+ }), "toViewColumn");
3010
+ var buildView = /* @__PURE__ */ __name((schema, columns, visible, buildUiSchema, sort = false, schemaVisible) => {
3011
+ if (!schema || !columns?.length) return void 0;
3012
+ const visibleCols = sort ? sortByPosition(columns.filter(visible)) : columns.filter(visible);
3013
+ if (!visibleCols.length) return void 0;
3014
+ const schemaCols = schemaVisible ? columns.filter((c) => visible(c) || schemaVisible(c)) : visibleCols;
3015
+ const schemaIds = schemaCols.map((c) => c.id);
3016
+ const mask = Object.fromEntries(schemaIds.map((id) => [
3017
+ id,
3018
+ true
3019
+ ]));
3020
+ const picked = schema.pick(mask);
3021
+ const jsonSchema = (0, import_zod8.toJSONSchema)(picked, {
3022
+ target: "draft-07",
3023
+ ...jsonSchemaOpts
3024
+ });
3025
+ allowAdditionalProperties(jsonSchema);
3026
+ dropNullableFromRequired2(jsonSchema);
3027
+ enforceRequiredMinLength(jsonSchema);
3028
+ return {
3029
+ json_schema: jsonSchema,
3030
+ ui_schema: buildUiSchema(visibleCols),
3031
+ columns: visibleCols.map(toViewColumn)
3032
+ };
3033
+ }, "buildView");
3034
+ var isVisibleInMode = /* @__PURE__ */ __name((c, mode) => mode === "table" ? !c.hiddenInTable : !c.hiddenInView, "isVisibleInMode");
3035
+ var buildCalculatedElement = /* @__PURE__ */ __name((c, mode) => mode === "table" ? {
3036
+ type: c.type === "boolean" ? "BooleanCell" : "TextCell",
3037
+ scope: `#/properties/${c.id}`,
3038
+ options: {
3039
+ label: c.label ?? c.id
3040
+ }
3041
+ } : {
3042
+ type: "Control",
3043
+ scope: `#/properties/${c.id}`,
3044
+ options: {
3045
+ // Spread fieldInput.options first (e.g. colspan), then layer type-derived options on top.
3046
+ ...c.fieldInput?.options ?? {},
3047
+ label: c.label ?? c.id,
3048
+ ...c.type === "boolean" && {
3049
+ format: "boolean"
3050
+ }
3051
+ }
3052
+ }, "buildCalculatedElement");
3053
+ var calcPosition = /* @__PURE__ */ __name((c) => c.fieldInput?.position ?? c.position, "calcPosition");
3054
+ var injectCalculatedColumnsIntoView = /* @__PURE__ */ __name((viewConfig, calculated, mode) => {
3055
+ if (!calculated.length) return viewConfig;
3056
+ const visible = calculated.filter((c) => isVisibleInMode(c, mode));
3057
+ if (!visible.length) return viewConfig;
3058
+ const jsonSchema = {
3059
+ ...viewConfig.json_schema
3060
+ };
3061
+ jsonSchema.properties = {
3062
+ ...jsonSchema.properties
3063
+ };
3064
+ for (const c of visible) {
3065
+ jsonSchema.properties[c.id] = {
3066
+ type: c.type === "boolean" ? "boolean" : "string",
3067
+ title: c.label ?? c.id
3068
+ };
3069
+ }
3070
+ const uiSchema = {
3071
+ ...viewConfig.ui_schema
3072
+ };
3073
+ const existing = [
3074
+ ...uiSchema.elements ?? []
3075
+ ];
3076
+ const tagged = [
3077
+ ...existing.map((el, i) => ({
3078
+ el,
3079
+ pos: i + 0.5
3080
+ })),
3081
+ ...visible.map((c) => ({
3082
+ el: buildCalculatedElement(c, mode),
3083
+ pos: calcPosition(c) ?? Infinity
3084
+ }))
3085
+ ];
3086
+ tagged.sort((a, b) => a.pos - b.pos);
3087
+ uiSchema.elements = tagged.map((t) => t.el);
3088
+ const columns = [
3089
+ ...viewConfig.columns
3090
+ ];
3091
+ for (const c of visible) {
3092
+ const pos = calcPosition(c);
3093
+ const entry = {
3094
+ id: c.id,
3095
+ label: c.label
3096
+ };
3097
+ if (pos !== void 0) {
3098
+ columns.splice(pos, 0, entry);
3099
+ } else {
3100
+ columns.push(entry);
3101
+ }
3102
+ }
3103
+ return {
3104
+ ...viewConfig,
3105
+ json_schema: jsonSchema,
3106
+ ui_schema: uiSchema,
3107
+ columns
3108
+ };
3109
+ }, "injectCalculatedColumnsIntoView");
3110
+ var injectCalculatedColumns = /* @__PURE__ */ __name((tableView, calculated) => injectCalculatedColumnsIntoView(tableView, calculated, "table"), "injectCalculatedColumns");
3111
+ var injectCalculatedColumnsToView = /* @__PURE__ */ __name((viewConfig, calculated) => injectCalculatedColumnsIntoView(viewConfig, calculated, "view"), "injectCalculatedColumnsToView");
3112
+ var emptyTableView = /* @__PURE__ */ __name(() => ({
3113
+ json_schema: {
3114
+ type: "object",
3115
+ additionalProperties: true,
3116
+ properties: {}
3117
+ },
3118
+ ui_schema: buildTableUiSchema([]),
3119
+ columns: []
3120
+ }), "emptyTableView");
3121
+ var buildViews = /* @__PURE__ */ __name((schema, columns) => {
3122
+ const views = {};
3123
+ const table = buildView(schema, columns, (c) => !c.hiddenInTable, buildTableUiSchema);
3124
+ if (table) {
3125
+ table.defaultSort = resolveDefaultSort(table.columns, columns);
3126
+ views.table = table;
3127
+ } else if (columns?.length) {
3128
+ views.table = emptyTableView();
3129
+ }
3130
+ const form = buildView(schema, columns, (c) => !c.hiddenInForm, buildFormUiSchema, true, (c) => c.createable === true || c.updateable === true);
3131
+ if (form) views.form = form;
3132
+ const filter = buildView(schema, columns, (c) => !!c.filterable, buildFormUiSchema, true);
3133
+ if (filter) {
3134
+ patchFilterProperties(filter.json_schema, columns?.filter((c) => !!c.filterable));
3135
+ views.filter = filter;
3136
+ }
3137
+ const view = buildView(schema, columns, (c) => !c.hiddenInView, buildFormUiSchema, true);
3138
+ if (view) views.view = view;
3139
+ return Object.keys(views).length ? views : void 0;
3140
+ }, "buildViews");
3141
+ var buildViewsFromColumns = /* @__PURE__ */ __name((columns) => {
3142
+ if (!columns?.length) return void 0;
3143
+ const buildJsonSchema = /* @__PURE__ */ __name((cols) => {
3144
+ const properties = {};
3145
+ for (const c of cols) {
3146
+ if (c.column) {
3147
+ if (!properties[c.column]) {
3148
+ properties[c.column] = {
3149
+ type: "object",
3150
+ additionalProperties: true,
3151
+ properties: {}
3152
+ };
3153
+ }
3154
+ const keyPath = (c.displayKey ?? c.id).split(".");
3155
+ let target = properties[c.column].properties;
3156
+ for (let i = 0; i < keyPath.length - 1; i++) {
3157
+ const segment = keyPath[i];
3158
+ if (!target[segment]) {
3159
+ target[segment] = {
3160
+ type: "object",
3161
+ additionalProperties: true,
3162
+ properties: {}
3163
+ };
3164
+ }
3165
+ target = target[segment].properties;
3166
+ }
3167
+ target[keyPath[keyPath.length - 1]] = {
3168
+ type: "string",
3169
+ title: c.label ?? c.id
3170
+ };
3171
+ } else {
3172
+ properties[c.id] = {
3173
+ type: c.columnType ?? "string",
3174
+ title: c.label ?? c.id
3175
+ };
3176
+ }
3177
+ }
3178
+ return {
3179
+ type: "object",
3180
+ additionalProperties: true,
3181
+ properties
3182
+ };
3183
+ }, "buildJsonSchema");
3184
+ const fixNestedScopes = /* @__PURE__ */ __name((uiSchema, cols) => {
3185
+ const colMap = Object.fromEntries(cols.map((c) => [
3186
+ c.id,
3187
+ c
3188
+ ]));
3189
+ const elements = uiSchema.elements;
3190
+ if (!elements) return uiSchema;
3191
+ return {
3192
+ ...uiSchema,
3193
+ elements: elements.map((el) => {
3194
+ const id = el.scope?.replace("#/properties/", "");
3195
+ const col = id ? colMap[id] : void 0;
3196
+ if (!col?.column) return el;
3197
+ const keyPath = (col.displayKey ?? col.id).split(".");
3198
+ const propPath = keyPath.map((k) => `properties/${k}`).join("/");
3199
+ return {
3200
+ ...el,
3201
+ scope: `#/properties/${col.column}/${propPath}`
3202
+ };
3203
+ })
3204
+ };
3205
+ }, "fixNestedScopes");
3206
+ const makeView = /* @__PURE__ */ __name((visible, buildUiSchema) => {
3207
+ if (!visible.length) return void 0;
3208
+ return {
3209
+ json_schema: buildJsonSchema(visible),
3210
+ ui_schema: fixNestedScopes(buildUiSchema(visible), visible),
3211
+ columns: visible.map(toViewColumn)
3212
+ };
3213
+ }, "makeView");
3214
+ const views = {};
3215
+ const tableCols = sortByPosition(columns.filter((c) => !c.hiddenInTable));
3216
+ const table = makeView(tableCols, buildTableUiSchema);
3217
+ if (table) {
3218
+ table.defaultSort = resolveDefaultSort(tableCols, columns);
3219
+ views.table = table;
3220
+ } else {
3221
+ views.table = emptyTableView();
3222
+ }
3223
+ const formCols = sortByPosition(columns.filter((c) => !c.hiddenInForm));
3224
+ const form = makeView(formCols, buildFormUiSchema);
3225
+ if (form) views.form = form;
3226
+ const viewCols = sortByPosition(columns.filter((c) => !c.hiddenInView));
3227
+ const viewView = makeView(viewCols, buildFormUiSchema);
3228
+ if (viewView) views.view = viewView;
3229
+ return Object.keys(views).length ? views : void 0;
3230
+ }, "buildViewsFromColumns");
3231
+
3232
+ // src/lib/crud/loader/json-adapter.ts
3233
+ var import_node_fs4 = require("fs");
3234
+ var import_node_path5 = require("path");
3235
+ var resolveChildResource = /* @__PURE__ */ __name((resourcePath, parentDir) => {
3236
+ const directPath = (0, import_node_path5.resolve)(parentDir, resourcePath);
3237
+ if (resourcePath.endsWith(".json") && (0, import_node_fs4.existsSync)(directPath)) {
3238
+ try {
3239
+ return {
3240
+ json: JSON.parse((0, import_node_fs4.readFileSync)(directPath, "utf-8")),
3241
+ dir: (0, import_node_path5.dirname)(directPath)
3242
+ };
3243
+ } catch {
3244
+ return void 0;
3245
+ }
3246
+ }
3247
+ const childName = resourcePath.replace(/^\.\//, "").replace(/\.resource$/, "");
3248
+ const childJsonPath = (0, import_node_path5.resolve)((0, import_node_path5.dirname)(parentDir), childName, "resource.json");
3249
+ if (!(0, import_node_fs4.existsSync)(childJsonPath)) return void 0;
3250
+ try {
3251
+ return {
3252
+ json: JSON.parse((0, import_node_fs4.readFileSync)(childJsonPath, "utf-8")),
3253
+ dir: (0, import_node_path5.dirname)(childJsonPath)
3254
+ };
3255
+ } catch {
3256
+ return void 0;
3257
+ }
3258
+ }, "resolveChildResource");
3259
+ var expandExtendColumns = /* @__PURE__ */ __name((columns, dirPath) => {
3260
+ if (!dirPath) return columns;
3261
+ const result = [];
3262
+ for (const col of columns) {
3263
+ if (!col.extend) {
3264
+ result.push(col);
3265
+ continue;
3266
+ }
3267
+ const resolved = resolveChildResource(col.extend, dirPath);
3268
+ if (!resolved) {
3269
+ console.warn(`[extend] Could not resolve "${col.extend}" for column "${col.id}" \u2014 keeping as-is`);
3270
+ result.push(col);
3271
+ continue;
3272
+ }
3273
+ const refColumns = normalizeColumns(resolved.json.columns) ?? [];
3274
+ const parentColumnKey = col.column ?? col.id;
3275
+ for (const refCol of refColumns) {
3276
+ if (refCol.idField) continue;
3277
+ const virtualId = `${col.id}_${refCol.id}`;
3278
+ const displayKey = refCol.displayKey ? `${refCol.id}.${refCol.displayKey}` : refCol.id;
3279
+ const hiddenInTable = col.hiddenInTable === true || refCol.hiddenInTable === true ? true : col.hiddenInTable ?? refCol.hiddenInTable;
3280
+ const hiddenInForm = col.hiddenInForm === true || refCol.hiddenInForm === true ? true : col.hiddenInForm ?? refCol.hiddenInForm;
3281
+ const hiddenInView = col.hiddenInView === true || refCol.hiddenInView === true ? true : col.hiddenInView ?? refCol.hiddenInView;
3282
+ const override = col.columns?.[virtualId] ?? col.columns?.[refCol.id] ?? {};
3283
+ const virtualCol = {
3284
+ id: virtualId,
3285
+ column: parentColumnKey,
3286
+ displayKey,
3287
+ label: refCol.label ?? refCol.id,
3288
+ columnType: "object",
3289
+ ...hiddenInTable !== void 0 && {
3290
+ hiddenInTable
3291
+ },
3292
+ ...hiddenInForm !== void 0 && {
3293
+ hiddenInForm
3294
+ },
3295
+ ...hiddenInView !== void 0 && {
3296
+ hiddenInView
3297
+ },
3298
+ ...refCol.sortable != null && {
3299
+ sortable: refCol.sortable
3300
+ },
3301
+ ...refCol.fieldInput && {
3302
+ fieldInput: refCol.fieldInput
3303
+ },
3304
+ ...override
3305
+ };
3306
+ result.push(virtualCol);
3307
+ }
3308
+ }
3309
+ return result;
3310
+ }, "expandExtendColumns");
3311
+ var buildValueLabelColumns = /* @__PURE__ */ __name((columns) => (columns ?? []).flatMap((c) => {
3312
+ const opts = c.fieldInput?.options;
3313
+ if (!opts?.emitObject || !Array.isArray(opts.values)) return [];
3314
+ return [
3315
+ {
3316
+ field: c.column ?? c.id,
3317
+ values: opts.values
3318
+ }
3319
+ ];
3320
+ }), "buildValueLabelColumns");
3321
+ var applyRelationFormatDefault = /* @__PURE__ */ __name((cols) => cols?.map((col) => {
3322
+ const fi = col.fieldInput;
3323
+ if (fi && fi.resource && !fi.format && !fi.type) {
3324
+ return {
3325
+ ...col,
3326
+ fieldInput: {
3327
+ ...fi,
3328
+ format: "relation"
3329
+ }
3330
+ };
3331
+ }
3332
+ return col;
3333
+ }), "applyRelationFormatDefault");
3334
+ var deriveRelationTypeFromColumns = /* @__PURE__ */ __name((col, cols) => {
3335
+ const base = col.column ?? col.id;
3336
+ const fkNames = /* @__PURE__ */ new Set([
3337
+ `${base}_id`,
3338
+ `${col.id}_id`
3339
+ ]);
3340
+ return cols.some((c) => fkNames.has(c.id)) ? "manyToOne" : "oneToMany";
3341
+ }, "deriveRelationTypeFromColumns");
3342
+ var enrichNestedRelationColumns = /* @__PURE__ */ __name((cols, dir, baseUrl) => {
3343
+ if (!cols || !dir) return cols;
3344
+ const base = baseUrl ?? "";
3345
+ return cols.map((col) => {
3346
+ if (col.fieldInput?.format !== "relation" || !col.fieldInput.resource) return col;
3347
+ const resolved = resolveChildResource(col.fieldInput.resource, dir);
3348
+ const targetRoute = resolved?.json?.route ?? col.fieldInput.resource.replace(/^\.\//, "").replace(/\/resource\.json$/, "").replace(/\.resource$/, "");
3349
+ const relationType = col.fieldInput.relationType ?? deriveRelationTypeFromColumns(col, cols);
3350
+ return {
3351
+ ...col,
3352
+ fieldInput: {
3353
+ ...col.fieldInput,
3354
+ relationType,
3355
+ options: {
3356
+ ...col.fieldInput.options,
3357
+ uri: `${base}/${targetRoute}`,
3358
+ resourceUri: `${base}/${targetRoute}`,
3359
+ resource: `${base}/${targetRoute}/schemas`
3360
+ }
3361
+ }
3362
+ };
3363
+ });
3364
+ }, "enrichNestedRelationColumns");
3365
+ var buildSubResources = /* @__PURE__ */ __name((columns, parentRoute, parentModel, parentDir, enums = {}, baseUrl) => {
3366
+ if (!columns || !parentDir) return [];
3367
+ return columns.filter((c) => c.fieldInput?.format === "relation" && c.fieldInput?.resource).map((c) => {
3368
+ const childResolved = resolveChildResource(c.fieldInput.resource, parentDir);
3369
+ const childJson = childResolved?.json;
3370
+ const childDir = childResolved?.dir;
3371
+ const childRoute = childJson?.route ?? c.fieldInput.resource.replace(/^\.\//, "").replace(/\.resource$/, "");
3372
+ const rawChildColumns = childJson ? normalizeColumns(childJson.columns) : void 0;
3373
+ const expandedChildColumns = rawChildColumns ? expandExtendColumns(rawChildColumns, childDir) : void 0;
3374
+ const childColumns = applyRelationFormatDefault(expandedChildColumns) ?? expandedChildColumns;
3375
+ injectEnumValues(childColumns, enums);
3376
+ const enrichedChildColumns = enrichNestedRelationColumns(childColumns, childDir, baseUrl);
3377
+ const childLookupKey = childColumns?.find((col) => col.idField)?.id ?? "id";
3378
+ const childCalculatedColumns = childJson?.calculatedColumns ?? [];
3379
+ let childViews = childJson ? buildViewsFromColumns(enrichedChildColumns) : void 0;
3380
+ if (childViews && childCalculatedColumns.length) {
3381
+ childViews = {
3382
+ ...childViews,
3383
+ table: injectCalculatedColumns(childViews.table, childCalculatedColumns)
3384
+ };
3385
+ if (childViews.view) {
3386
+ childViews = {
3387
+ ...childViews,
3388
+ view: injectCalculatedColumnsToView(childViews.view, childCalculatedColumns)
3389
+ };
3390
+ }
3391
+ }
3392
+ const childOps = childJson?.operations ?? {};
3393
+ return {
3394
+ column: c.id,
3395
+ relation: c.id,
3396
+ childRoute,
3397
+ childModel: c.id,
3398
+ foreignKey: `${parentModel}_id`,
3399
+ name: childJson?.name ?? childRoute,
3400
+ title: childJson?.title ?? childJson?.tag ?? childRoute,
3401
+ idField: childLookupKey,
3402
+ idType: childJson?.idType ?? "string",
3403
+ ...childViews && {
3404
+ views: childViews
3405
+ },
3406
+ operations: {
3407
+ findAll: childOps.findAll !== false,
3408
+ findOne: childOps.findOne !== false,
3409
+ create: childOps.create !== false,
3410
+ update: childOps.update !== false,
3411
+ delete: childOps.delete !== false
3412
+ },
3413
+ ...childJson?.actions?.length && {
3414
+ actions: childJson.actions
3415
+ },
3416
+ ...childJson?.modalSize && {
3417
+ modalSize: childJson.modalSize
3418
+ },
3419
+ ...childJson?.include?.length && {
3420
+ include: childJson.include
3421
+ },
3422
+ ...childJson?.calculatedColumns?.length && {
3423
+ calculatedColumns: childJson.calculatedColumns
3424
+ },
3425
+ ...(c.hiddenInForm === false || c.hiddenInView === false) && {
3426
+ includeInFindOne: true
3427
+ },
3428
+ ...buildValueLabelColumns(childColumns).length && {
3429
+ valueLabelColumns: buildValueLabelColumns(childColumns)
3430
+ }
3431
+ };
3432
+ });
3433
+ }, "buildSubResources");
3434
+ var enrichActionColumns = /* @__PURE__ */ __name((columns, parentRoute, subResources, baseUrl) => {
3435
+ if (!columns) return columns;
3436
+ const base = baseUrl ?? "";
3437
+ return columns.map((col) => {
3438
+ if (col.fieldInput?.format !== "relation") return col;
3439
+ const sub = subResources.find((s) => s.column === col.id);
3440
+ if (!sub) return col;
3441
+ return {
3442
+ ...col,
3443
+ fieldInput: {
3444
+ ...col.fieldInput,
3445
+ options: {
3446
+ ...col.fieldInput.options,
3447
+ uri: `${base}/${parentRoute}/{id}/${sub.childRoute}`,
3448
+ resourceUri: `${base}/${sub.childRoute}`,
3449
+ ...sub.views && {
3450
+ resource: `${base}/${parentRoute}/${sub.childRoute}/schemas`
3451
+ }
3452
+ }
3453
+ }
3454
+ };
3455
+ });
3456
+ }, "enrichActionColumns");
3457
+ var enrichResourceRefColumns = /* @__PURE__ */ __name((columns, parentDir, baseUrl) => {
3458
+ if (!columns || !parentDir) return columns;
3459
+ const base = baseUrl ?? "";
3460
+ return columns.map((col) => {
3461
+ if (!col.fieldInput?.resource || col.fieldInput.format === "relation") return col;
3462
+ const childResolved = resolveChildResource(col.fieldInput.resource, parentDir);
3463
+ const childRoute = childResolved?.json?.route ?? col.fieldInput.resource.replace(/^\.\//, "").replace(/\.resource$/, "");
3464
+ return {
3465
+ ...col,
3466
+ fieldInput: {
3467
+ ...col.fieldInput,
3468
+ options: {
3469
+ ...col.fieldInput.options,
3470
+ resourceUri: `${base}/${childRoute}`,
3471
+ schemasUri: `${base}/${childRoute}/schemas`
3472
+ }
3473
+ }
3474
+ };
3475
+ });
3476
+ }, "enrichResourceRefColumns");
3477
+ var enrichIncludeWithSort = /* @__PURE__ */ __name((include, columns) => {
3478
+ if (!include?.length) return include;
3479
+ return include.map((entry) => {
3480
+ const relationName = typeof entry === "string" ? entry : entry.relation;
3481
+ const col = columns.find((c) => {
3482
+ const opts2 = c.fieldInput?.options;
3483
+ return (c.column ?? c.id) === relationName && opts2?.sort;
3484
+ });
3485
+ if (!col) return entry;
3486
+ const opts = col.fieldInput.options;
3487
+ const orderBy = buildChildSortClause(opts.sort, opts.sortDir ?? "asc");
3488
+ return typeof entry === "string" ? {
3489
+ relation: entry,
3490
+ orderBy
3491
+ } : {
3492
+ ...entry,
3493
+ orderBy
3494
+ };
3495
+ });
3496
+ }, "enrichIncludeWithSort");
3497
+ var fromJson = /* @__PURE__ */ __name((json, schema, hooks, dirPath, baseUrl, actions, tableActions, enums = {}) => {
3498
+ const rawColumns = expandExtendColumns(normalizeColumns(json.columns) ?? [], dirPath);
3499
+ const columns = enrichRelationTypes(applyRelationFormatDefault(rawColumns) ?? rawColumns, schema);
3500
+ injectEnumValues(columns, enums);
3501
+ const subResources = buildSubResources(columns, json.route, json.model, dirPath, enums, baseUrl);
3502
+ const enrichedColumns = enrichResourceRefColumns(enrichActionColumns(columns, json.route, subResources, baseUrl), dirPath, baseUrl) ?? columns;
3503
+ const calculatedColumns = json.calculatedColumns ?? [];
3504
+ const picked = pickByColumns(schema, enrichedColumns);
3505
+ const createSchema = pickByColumns(schema, enrichedColumns, (c) => !c.idField && c.createable !== false);
3506
+ const updateSchema = pickByColumns(schema, enrichedColumns, (c) => !c.idField && c.updateable !== false);
3507
+ let views = buildViews(schema, enrichedColumns);
3508
+ if (views && calculatedColumns.length) {
3509
+ views = {
3510
+ ...views,
3511
+ table: injectCalculatedColumns(views.table, calculatedColumns)
3512
+ };
3513
+ if (views.view) {
3514
+ views = {
3515
+ ...views,
3516
+ view: injectCalculatedColumnsToView(views.view, calculatedColumns)
3517
+ };
3518
+ }
3519
+ }
3520
+ const lookup = buildLookup(enrichedColumns);
3521
+ const enrichedInclude = enrichIncludeWithSort(json.include, enrichedColumns);
3522
+ const definition = {
3523
+ ...opWithSchema(json.operations.findAll, picked) && {
3524
+ findAll: opWithSchema(json.operations.findAll, picked)
3525
+ },
3526
+ ...opWithSchema(json.operations.findOne, picked) && {
3527
+ findOne: opWithSchema(json.operations.findOne, picked)
3528
+ },
3529
+ ...opWithSchema(json.operations.create, createSchema) && {
3530
+ create: opWithSchema(json.operations.create, createSchema)
3531
+ },
3532
+ ...opWithSchema(json.operations.update, updateSchema) && {
3533
+ update: opWithSchema(json.operations.update, updateSchema)
3534
+ },
3535
+ ...upsertOp(json.operations.upsert, createSchema) && {
3536
+ upsert: upsertOp(json.operations.upsert, createSchema)
3537
+ },
3538
+ ...json.operations.delete !== false && {
3539
+ delete: true
3540
+ }
3541
+ };
3542
+ const display = {
3543
+ mode: json.display?.mode === "page" ? "page" : "modal",
3544
+ customComponent: json.display?.customComponent ?? null
3545
+ };
3546
+ return {
3547
+ name: json.name,
3548
+ route: json.route,
3549
+ model: json.model,
3550
+ tag: json.tag,
3551
+ display,
3552
+ ...json.title && {
3553
+ title: json.title
3554
+ },
3555
+ ...json.sidebar && {
3556
+ sidebar: json.sidebar
3557
+ },
3558
+ ...json.idType && {
3559
+ idType: json.idType
3560
+ },
3561
+ ...lookup?.key && lookup.key !== "id" && {
3562
+ idField: lookup.key
3563
+ },
3564
+ ...json.database && {
3565
+ database: json.database
3566
+ },
3567
+ ...hooks && {
3568
+ hooks
3569
+ },
3570
+ definition,
3571
+ ...views && {
3572
+ views
3573
+ },
3574
+ ...lookup && {
3575
+ lookup
3576
+ },
3577
+ ...subResources.length && {
3578
+ subResources
3579
+ },
3580
+ ...calculatedColumns.length && {
3581
+ calculatedColumns
3582
+ },
3583
+ ...actions?.length && {
3584
+ actions
3585
+ },
3586
+ ...tableActions?.length && {
3587
+ tableActions
3588
+ },
3589
+ ...enrichedInclude?.length && {
3590
+ include: enrichedInclude
3591
+ },
3592
+ ...json.modalSize && {
3593
+ modalSize: json.modalSize
3594
+ },
3595
+ ...buildValueLabelColumns(enrichedColumns).length && {
3596
+ valueLabelColumns: buildValueLabelColumns(enrichedColumns)
3597
+ }
3598
+ };
3599
+ }, "fromJson");
3600
+ var unwrapZodType = /* @__PURE__ */ __name((type) => {
3601
+ if (type instanceof import_zod9.ZodOptional || type instanceof import_zod9.ZodNullable) {
3602
+ return unwrapZodType(type.unwrap());
3603
+ }
3604
+ return type;
3605
+ }, "unwrapZodType");
3606
+ var deriveRelationType = /* @__PURE__ */ __name((schema, columnId) => {
3607
+ if (!schema) return void 0;
3608
+ const field = schema.shape[columnId];
3609
+ if (!field) return void 0;
3610
+ const inner = unwrapZodType(field);
3611
+ return inner instanceof import_zod9.ZodArray ? "oneToMany" : "manyToOne";
3612
+ }, "deriveRelationType");
3613
+ var enrichRelationTypes = /* @__PURE__ */ __name((columns, schema) => {
3614
+ if (!schema) return columns;
3615
+ return columns.map((col) => {
3616
+ const fi = col.fieldInput;
3617
+ if (!fi) return col;
3618
+ const isRelationField = fi.type === "autocomplete" || fi.format === "relation";
3619
+ if (!isRelationField) return col;
3620
+ if (fi.relationType) return col;
3621
+ const derived = deriveRelationType(schema, col.id) ?? deriveRelationTypeFromColumns(col, columns);
3622
+ if (!derived) return col;
3623
+ return {
3624
+ ...col,
3625
+ fieldInput: {
3626
+ ...fi,
3627
+ relationType: derived
3628
+ }
3629
+ };
3630
+ });
3631
+ }, "enrichRelationTypes");
3632
+ var buildLookup = /* @__PURE__ */ __name((columns) => {
3633
+ if (!columns) return void 0;
3634
+ const keyCol = columns.find((c) => c.idField);
3635
+ const labelCol = columns.find((c) => c.showInLookup) ?? columns.find((c) => c.searchable);
3636
+ if (!keyCol && !labelCol) return void 0;
3637
+ return {
3638
+ key: keyCol?.id ?? "id",
3639
+ ...labelCol && {
3640
+ label: labelCol.id
3641
+ }
3642
+ };
3643
+ }, "buildLookup");
3644
+
3645
+ // src/lib/crud/loader/index.ts
3646
+ var import_node_fs5 = require("fs");
3647
+ var import_node_path6 = require("path");
3648
+ var loadSubResourceHooks = /* @__PURE__ */ __name(async (subResources, basePath) => {
3649
+ for (const sub of subResources) {
3650
+ const file = sub.name ? findModule2((0, import_node_path6.join)(basePath, "hooks"), sub.name) : void 0;
3651
+ if (!file) continue;
3652
+ const hooks = await importDefault(file);
3653
+ if (hooks) sub.hooks = hooks;
3654
+ }
3655
+ }, "loadSubResourceHooks");
3656
+ var loadResourceConfigsFromDir = /* @__PURE__ */ __name(async (dirPath, baseUrl, enumsFile) => {
3657
+ const enums = loadEnumRegistry(dirPath, enumsFile);
3658
+ const entries = (0, import_node_fs5.readdirSync)(dirPath, {
3659
+ withFileTypes: true
3660
+ });
3661
+ const dirs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
3662
+ const configs = [];
3663
+ for (const dir of dirs) {
3664
+ const basePath = (0, import_node_path6.join)(dirPath, dir);
3665
+ const schemaFile = findModule2(basePath, "schema");
3666
+ const schema = schemaFile ? await importDefault(schemaFile) : void 0;
3667
+ const hooksFile = findModule2(basePath, "hooks");
3668
+ const hooks = hooksFile ? await importDefault(hooksFile) : void 0;
3669
+ const jsonFile = (0, import_node_path6.join)(basePath, "resource.json");
3670
+ if ((0, import_node_fs5.existsSync)(jsonFile)) {
3671
+ const json = JSON.parse((0, import_node_fs5.readFileSync)(jsonFile, "utf-8"));
3672
+ const actions = await loadActions(json.actions ?? [], basePath);
3673
+ const tableActions = await loadTableActions(json.tableActions ?? [], basePath);
3674
+ const config = fromJson(json, schema, hooks, basePath, baseUrl, actions, tableActions, enums);
3675
+ await loadSubResourceHooks(config.subResources ?? [], basePath);
3676
+ configs.push(config);
3677
+ continue;
3678
+ }
3679
+ const tsFile = findModule2(basePath, "resource");
3680
+ if (tsFile) {
3681
+ const config = await importDefault(tsFile);
3682
+ if (config) configs.push(hooks ? {
3683
+ ...config,
3684
+ hooks
3685
+ } : config);
3686
+ }
3687
+ }
3688
+ return configs;
3689
+ }, "loadResourceConfigsFromDir");
3690
+
3691
+ // src/lib/crud/loader/resource-config.loader.ts
3692
+ var ResourceConfigLoader2 = class {
3693
+ static {
3694
+ __name(this, "ResourceConfigLoader");
3695
+ }
3696
+ };
3697
+
3698
+ // src/lib/crud/loader/fs-resource-config.loader.ts
3699
+ var FileSystemResourceConfigLoader = class extends ResourceConfigLoader2 {
3700
+ static {
3701
+ __name(this, "FileSystemResourceConfigLoader");
3702
+ }
3703
+ dirPath;
3704
+ baseUrl;
3705
+ enumsFile;
3706
+ constructor(dirPath, baseUrl, enumsFile) {
3707
+ super(), this.dirPath = dirPath, this.baseUrl = baseUrl, this.enumsFile = enumsFile;
3708
+ }
3709
+ async loadAll() {
3710
+ return loadResourceConfigsFromDir(this.dirPath, this.baseUrl, this.enumsFile);
3711
+ }
3712
+ async loadByRoute(route) {
3713
+ const configs = await this.loadAll();
3714
+ return configs.find((c) => c.route === route);
3715
+ }
3716
+ };
3717
+
3718
+ // src/lib/crouton-api.module.ts
3719
+ function _ts_decorate5(decorators, target, key, desc2) {
3720
+ var c = arguments.length, r = c < 3 ? target : desc2 === null ? desc2 = Object.getOwnPropertyDescriptor(target, key) : desc2, d;
3721
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc2);
3722
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
3723
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
3724
+ }
3725
+ __name(_ts_decorate5, "_ts_decorate");
3726
+ var CroutonApiModule = class _CroutonApiModule {
3727
+ static {
3728
+ __name(this, "CroutonApiModule");
3729
+ }
3730
+ static forResources(configs, dataSources, loader, config) {
3731
+ const dataSourceRegistry = new DataSourceRegistry(dataSources);
3732
+ const configRegistry = new ResourceConfigRegistry(loader, configs);
3733
+ const controllers = [
3734
+ ...configs.map((c) => createCrudController(c, config.baseUrl)),
3735
+ createAppLayoutController(configs, config.sidebarGroups, config.title, config.autoSave ?? true)
3736
+ ];
3737
+ return {
3738
+ module: _CroutonApiModule,
3739
+ controllers,
3740
+ providers: [
3741
+ {
3742
+ provide: import_core.APP_FILTER,
3743
+ useClass: CroutonValidationExceptionFilter
3744
+ },
3745
+ {
3746
+ provide: DataSourceRegistry,
3747
+ useValue: dataSourceRegistry
3748
+ },
3749
+ {
3750
+ provide: ResourceConfigRegistry,
3751
+ useValue: configRegistry
3752
+ }
3753
+ ]
3754
+ };
3755
+ }
3756
+ static async forResourceDir(dirPath, dataSourcesPath, config) {
3757
+ const loader = new FileSystemResourceConfigLoader(dirPath, config.baseUrl, config.enumsFile);
3758
+ const configs = await loadResourceConfigsFromDir(dirPath, config.baseUrl, config.enumsFile);
3759
+ const dataSources = await loadDataSourcesFromDir(dataSourcesPath);
3760
+ return _CroutonApiModule.forResources(configs, dataSources, loader, config);
3761
+ }
3762
+ static forLoader(loader, configs, dataSources, config) {
3763
+ return _CroutonApiModule.forResources(configs, dataSources, loader, config);
3764
+ }
3765
+ };
3766
+ CroutonApiModule = _ts_decorate5([
3767
+ (0, import_common13.Module)({
3768
+ controllers: [],
3769
+ providers: [],
3770
+ exports: []
3771
+ })
3772
+ ], CroutonApiModule);
3773
+ // Annotate the CommonJS export names for ESM import in node:
3774
+ 0 && (module.exports = {
3775
+ CroutonApiModule,
3776
+ DataSourceRegistry,
3777
+ FileSystemResourceConfigLoader,
3778
+ ResourceConfigLoader,
3779
+ ResourceConfigRegistry,
3780
+ isOperationEnabled,
3781
+ loadDataSourcesFromDir,
3782
+ loadResourceConfigsFromDir,
3783
+ resolveDefinition,
3784
+ schemaFor,
3785
+ upsertOnFor
3786
+ });
3787
+ //# sourceMappingURL=index.cjs.map