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