@ghentcdh/crouton-api 0.0.1-alpha.34 → 0.0.1-alpha.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -52,7 +52,7 @@ var getImportMetaUrl = /* @__PURE__ */ __name(() => typeof document === "undefin
52
52
  var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
53
53
 
54
54
  // src/lib/crouton-api.module.ts
55
- var import_common12 = require("@nestjs/common");
55
+ var import_common13 = require("@nestjs/common");
56
56
  var import_core = require("@nestjs/core");
57
57
 
58
58
  // src/lib/crud/app-layout/app-layout.types.ts
@@ -595,6 +595,7 @@ var DetailConfigSchema = import_zod10.z.object({
595
595
  controls: import_zod10.z.array(DetailControlSchema)
596
596
  });
597
597
  var RelationFieldInputOptionsSchema = import_zod10.z.object({
598
+ colspan: import_zod10.z.number().optional().default(12),
598
599
  /** Field to sort related records by, e.g. `"title"` or `"author.name"`. */
599
600
  sort: import_zod10.z.string().optional(),
600
601
  /** Sort direction. Defaults to `"asc"` when omitted. */
@@ -639,6 +640,28 @@ var FieldInputSchema = import_zod10.z.object({
639
640
  /** Nested array detail layout (renders via `detailFixed`). */
640
641
  detail: DetailConfigSchema.optional()
641
642
  });
643
+ var FieldVariantSchema = FieldInputSchema;
644
+ var stripNull = /* @__PURE__ */ __name((obj) => {
645
+ const out = {};
646
+ for (const [k, v] of Object.entries(obj)) if (v !== null) out[k] = v;
647
+ return out;
648
+ }, "stripNull");
649
+ var mergeFieldVariant = /* @__PURE__ */ __name((base, override) => {
650
+ if (!base) return override;
651
+ if (!override) return base;
652
+ const merged = stripNull({
653
+ ...base,
654
+ ...override
655
+ });
656
+ if (base.options || override.options) {
657
+ const mergedOptions = stripNull({
658
+ ...base.options,
659
+ ...override.options
660
+ });
661
+ merged.options = mergedOptions;
662
+ }
663
+ return merged;
664
+ }, "mergeFieldVariant");
642
665
 
643
666
  // ../crouton-core/src/lib/schema/label.helper.ts
644
667
  var labelFromId = /* @__PURE__ */ __name((id) => {
@@ -723,6 +746,18 @@ var JsonColumnSchema = import_zod12.z.object({
723
746
  columnType: import_zod12.z.string().default("string"),
724
747
  fieldInput: FieldInputSchema.optional(),
725
748
  /**
749
+ * Optional per-context override for the read-only VIEW ui. Same shape as
750
+ * `fieldInput`; every key optional. Falls back to `fieldInput` (deep-merged
751
+ * one level into `options`). Resolved at read time by the transformer.
752
+ */
753
+ fieldView: FieldVariantSchema.optional(),
754
+ /**
755
+ * Optional per-context override for the TABLE ui. Same shape as `fieldInput`;
756
+ * every key optional. Falls back to `fieldView`, then `fieldInput`.
757
+ * Resolved at read time by the transformer.
758
+ */
759
+ fieldTable: FieldVariantSchema.optional(),
760
+ /**
726
761
  * Path to another `resource.json` whose columns are expanded as nested sub-columns
727
762
  * under this column's object key.
728
763
  *
@@ -904,6 +939,10 @@ var ResourceJsonSchema = ResourceJsonShape.transform((obj) => {
904
939
  };
905
940
  });
906
941
 
942
+ // ../crouton-core/src/lib/resource/fieldVariants.ts
943
+ var resolveViewField = /* @__PURE__ */ __name((c) => mergeFieldVariant(c.fieldInput, c.fieldView), "resolveViewField");
944
+ var resolveTableField = /* @__PURE__ */ __name((c) => mergeFieldVariant(resolveViewField(c), c.fieldTable), "resolveTableField");
945
+
907
946
  // ../crouton-core/src/lib/config/CroutonConfig.schema.ts
908
947
  var import_zod17 = require("zod");
909
948
  var RulesetSchema = import_zod17.z.object({
@@ -1661,33 +1700,40 @@ var ReadRepository = class {
1661
1700
  async findAll(params) {
1662
1701
  const subResources = this.config.subResources ?? [];
1663
1702
  const projection = this.projection("findAll");
1664
- let query = {
1703
+ const query = {
1665
1704
  where: this.buildWhere(params.filter),
1666
1705
  take: params.pageSize,
1667
1706
  skip: params.offset ?? (params.page - 1) * params.pageSize,
1668
1707
  orderBy: this.safeSort(sanitizeValueLabelSort(params.sort, this.config.valueLabelColumns), params.sortDir)
1669
1708
  };
1670
1709
  const countableSubResources = subResources.filter((s) => s.relationType !== "manyToOne");
1671
- if (countableSubResources.length) {
1672
- const countClause = {
1673
- select: Object.fromEntries(countableSubResources.map((s) => [
1674
- s.relation,
1675
- true
1676
- ]))
1677
- };
1678
- if (projection.select) {
1679
- query.select = {
1680
- ...projection.select,
1681
- _count: countClause
1682
- };
1683
- } else {
1684
- query = {
1685
- ...query,
1710
+ const manyToOneIncludes = subResources.filter((s) => s.relationType === "manyToOne").map((s) => s.relation);
1711
+ const flatIncludes = manyToOneIncludes.length ? Object.fromEntries(manyToOneIncludes.map((r) => [
1712
+ r,
1713
+ true
1714
+ ])) : void 0;
1715
+ const configInclude = buildIncludeClause(this.config.include);
1716
+ const mergedInclude = flatIncludes || configInclude ? {
1717
+ ...flatIncludes,
1718
+ ...configInclude
1719
+ } : void 0;
1720
+ const countClause = countableSubResources.length ? {
1721
+ select: Object.fromEntries(countableSubResources.map((s) => [
1722
+ s.relation,
1723
+ true
1724
+ ]))
1725
+ } : void 0;
1726
+ if (projection.select) {
1727
+ query.select = {
1728
+ ...projection.select,
1729
+ ...countClause && {
1686
1730
  _count: countClause
1687
- };
1688
- }
1731
+ },
1732
+ ...mergedInclude
1733
+ };
1689
1734
  } else {
1690
- Object.assign(query, projection);
1735
+ if (countClause) query._count = countClause;
1736
+ if (mergedInclude) query.include = mergedInclude;
1691
1737
  }
1692
1738
  const rows = await this.prismaModel.findMany(query);
1693
1739
  const mapped = countableSubResources.length ? rows.map((row) => {
@@ -2240,6 +2286,14 @@ var DataSourceRegistry = class {
2240
2286
  resolve(database) {
2241
2287
  return database ? this.get(database) : this.getDefault();
2242
2288
  }
2289
+ entries() {
2290
+ return [
2291
+ ...this.clients.entries()
2292
+ ].map(([name, client]) => ({
2293
+ name,
2294
+ client
2295
+ }));
2296
+ }
2243
2297
  async onModuleDestroy() {
2244
2298
  for (const client of this.clients.values()) {
2245
2299
  if (typeof client.$disconnect === "function") {
@@ -2256,6 +2310,26 @@ DataSourceRegistry = _ts_decorate4([
2256
2310
  ])
2257
2311
  ], DataSourceRegistry);
2258
2312
 
2313
+ // src/lib/crud/resource/resource-load-errors.registry.ts
2314
+ var ResourceLoadErrorsRegistry = class ResourceLoadErrorsRegistry2 {
2315
+ static {
2316
+ __name(this, "ResourceLoadErrorsRegistry");
2317
+ }
2318
+ errors = [];
2319
+ record(e) {
2320
+ this.errors.push(e);
2321
+ }
2322
+ getAll() {
2323
+ return [
2324
+ ...this.errors
2325
+ ];
2326
+ }
2327
+ clear() {
2328
+ this.errors = [];
2329
+ }
2330
+ };
2331
+ var resourceLoadErrorsRegistry = new ResourceLoadErrorsRegistry();
2332
+
2259
2333
  // src/lib/crud/data-source/data-source.loader.ts
2260
2334
  var import_node_fs2 = require("fs");
2261
2335
  var import_node_path4 = require("path");
@@ -2270,11 +2344,25 @@ var loadDataSourcesFromDir = /* @__PURE__ */ __name(async (dirPath) => {
2270
2344
  const basePath = (0, import_node_path4.join)(dirPath, dir);
2271
2345
  const jsonFile = (0, import_node_path4.join)(basePath, "data-source.json");
2272
2346
  if (!(0, import_node_fs2.existsSync)(jsonFile)) continue;
2273
- const _config = JSON.parse((0, import_node_fs2.readFileSync)(jsonFile, "utf-8"));
2347
+ let _config;
2348
+ try {
2349
+ _config = JSON.parse((0, import_node_fs2.readFileSync)(jsonFile, "utf-8"));
2350
+ } catch (err) {
2351
+ resourceLoadErrorsRegistry.record({
2352
+ name: dir,
2353
+ path: jsonFile,
2354
+ error: `Invalid JSON in ${jsonFile}: ${err.message}`
2355
+ });
2356
+ continue;
2357
+ }
2274
2358
  const datasource = DataSourceSchema.safeParse(_config);
2275
2359
  if (!datasource.success) {
2276
- console.error(datasource.error);
2277
- throw new Error(`Invalid datasource schema: ${jsonFile}`);
2360
+ resourceLoadErrorsRegistry.record({
2361
+ name: dir,
2362
+ path: jsonFile,
2363
+ error: `Invalid datasource schema: ${jsonFile}: ${datasource.error.message}`
2364
+ });
2365
+ continue;
2278
2366
  }
2279
2367
  const config = datasource.data;
2280
2368
  const indexFile = findModule2(basePath, "index");
@@ -3290,6 +3378,17 @@ var sortByPosition = /* @__PURE__ */ __name((cols) => cols.map((col, i) => ({
3290
3378
  col,
3291
3379
  i
3292
3380
  })).sort((a, b) => colPosition(a.col, a.i) - colPosition(b.col, b.i)).map(({ col }) => col), "sortByPosition");
3381
+ var columnForContext = /* @__PURE__ */ __name((col, ctx) => {
3382
+ if (ctx === "view") return {
3383
+ ...col,
3384
+ fieldInput: col.fieldView ?? col.fieldInput
3385
+ };
3386
+ if (ctx === "table") return {
3387
+ ...col,
3388
+ fieldInput: col.fieldTable ?? col.fieldInput
3389
+ };
3390
+ return col;
3391
+ }, "columnForContext");
3293
3392
  var toViewColumn = /* @__PURE__ */ __name((col) => ({
3294
3393
  id: col.id,
3295
3394
  ...col.label && {
@@ -3386,7 +3485,8 @@ var pickByColumns = /* @__PURE__ */ __name((schema, columns, filter) => {
3386
3485
  if (!schema) return void 0;
3387
3486
  if (!columns?.length) return schema;
3388
3487
  const baseFilter = /* @__PURE__ */ __name((c) => !isRelation(c) && (filter ? filter(c) : true), "baseFilter");
3389
- const filtered = columns.filter(baseFilter);
3488
+ const schemaKeys = new Set(Object.keys(schema.shape));
3489
+ const filtered = columns.filter((c) => baseFilter(c) && schemaKeys.has(c.id));
3390
3490
  if (!filtered.length) return void 0;
3391
3491
  const mask = Object.fromEntries(filtered.map((c) => [
3392
3492
  c.id,
@@ -3731,7 +3831,9 @@ var buildView = /* @__PURE__ */ __name((schema, columns, visible, buildUiSchema,
3731
3831
  const visibleCols = sort ? sortByPosition(columns.filter(visible)) : columns.filter(visible);
3732
3832
  if (!visibleCols.length) return void 0;
3733
3833
  const schemaCols = (schemaVisible ? columns.filter((c) => visible(c) || schemaVisible(c)) : visibleCols).filter((c) => !isRelation(c));
3734
- const schemaIds = schemaCols.map((c) => c.id);
3834
+ const schemaKeys = new Set(Object.keys(schema.shape));
3835
+ const schemaIds = schemaCols.map((c) => c.id).filter((id) => schemaKeys.has(id));
3836
+ if (!schemaIds.length) return void 0;
3735
3837
  const mask = Object.fromEntries(schemaIds.map((id) => [
3736
3838
  id,
3737
3839
  true
@@ -3766,7 +3868,7 @@ var emptyTableView = /* @__PURE__ */ __name(() => ({
3766
3868
  }), "emptyTableView");
3767
3869
  var buildViews = /* @__PURE__ */ __name((schema, columns) => {
3768
3870
  const views = {};
3769
- const table = buildView(schema, columns, (c) => !c.hiddenInTable, buildTableUiSchema);
3871
+ const table = buildView(schema, columns?.map((c) => columnForContext(c, "table")), (c) => !c.hiddenInTable, buildTableUiSchema);
3770
3872
  if (table) {
3771
3873
  table.defaultSort = resolveDefaultSort(table.columns, columns);
3772
3874
  views.table = table;
@@ -3780,7 +3882,7 @@ var buildViews = /* @__PURE__ */ __name((schema, columns) => {
3780
3882
  patchFilterProperties(filter.json_schema, columns?.filter((c) => !!c.filterable));
3781
3883
  views.filter = filter;
3782
3884
  }
3783
- const view = buildView(schema, columns, (c) => !c.hiddenInView, buildFormUiSchema, true);
3885
+ const view = buildView(schema, columns?.map((c) => columnForContext(c, "view")), (c) => !c.hiddenInView, buildFormUiSchema, true);
3784
3886
  if (view) views.view = view;
3785
3887
  return Object.keys(views).length ? views : void 0;
3786
3888
  }, "buildViews");
@@ -3861,7 +3963,7 @@ var buildViewsFromColumns = /* @__PURE__ */ __name((columns) => {
3861
3963
  };
3862
3964
  }, "makeView");
3863
3965
  const views = {};
3864
- const tableCols = sortByPosition(columns.filter((c) => !c.hiddenInTable));
3966
+ const tableCols = sortByPosition(columns.filter((c) => !c.hiddenInTable).map((c) => columnForContext(c, "table")));
3865
3967
  const table = makeView(tableCols, buildTableUiSchema);
3866
3968
  if (table) {
3867
3969
  table.defaultSort = resolveDefaultSort(tableCols, columns);
@@ -3872,7 +3974,7 @@ var buildViewsFromColumns = /* @__PURE__ */ __name((columns) => {
3872
3974
  const formCols = sortByPosition(columns.filter((c) => !c.hiddenInForm));
3873
3975
  const form = makeView(formCols, buildFormUiSchema);
3874
3976
  if (form) views.form = form;
3875
- const viewCols = sortByPosition(columns.filter((c) => !c.hiddenInView));
3977
+ const viewCols = sortByPosition(columns.filter((c) => !c.hiddenInView).map((c) => columnForContext(c, "view")));
3876
3978
  const viewView = makeView(viewCols, buildFormUiSchema);
3877
3979
  if (viewView) views.view = viewView;
3878
3980
  return Object.keys(views).length ? views : void 0;
@@ -3977,15 +4079,28 @@ var import_node_fs4 = require("fs");
3977
4079
  var import_node_path6 = require("path");
3978
4080
  var readResourceJson = /* @__PURE__ */ __name((jsonPath) => {
3979
4081
  if (!(0, import_node_fs4.existsSync)(jsonPath)) return void 0;
3980
- const fileContent = JSON.parse((0, import_node_fs4.readFileSync)(jsonPath, "utf-8"));
4082
+ let fileContent;
4083
+ try {
4084
+ fileContent = JSON.parse((0, import_node_fs4.readFileSync)(jsonPath, "utf-8"));
4085
+ } catch (err) {
4086
+ return {
4087
+ success: false,
4088
+ error: `Invalid JSON in ${jsonPath}: ${err.message}`
4089
+ };
4090
+ }
3981
4091
  const resource = ResourceJsonSchema.safeParse(fileContent);
3982
4092
  if (resource.error) {
3983
- console.error(resource.error);
3984
- throw new Error(`Resource cannot be parsed ${jsonPath}`);
4093
+ return {
4094
+ success: false,
4095
+ error: `Resource cannot be parsed ${jsonPath}: ${resource.error.message}`
4096
+ };
3985
4097
  }
3986
4098
  return {
3987
- json: resource.data,
3988
- dir: (0, import_node_path6.dirname)(jsonPath)
4099
+ success: true,
4100
+ data: {
4101
+ json: resource.data,
4102
+ dir: (0, import_node_path6.dirname)(jsonPath)
4103
+ }
3989
4104
  };
3990
4105
  }, "readResourceJson");
3991
4106
 
@@ -4023,7 +4138,7 @@ var enrichActionColumns = /* @__PURE__ */ __name((columns, parentRoute, subResou
4023
4138
  uri: `${base}/${parentRoute}/{id}/${sub.childRoute}`,
4024
4139
  resourceUri: `${base}/${sub.childRoute}`,
4025
4140
  ...sub.views && {
4026
- resource: `${base}/${parentRoute}/${sub.childRoute}/schemas`
4141
+ resource: sub.relationType === "manyToOne" ? `${base}/${sub.childRoute}/schemas` : `${base}/${parentRoute}/${sub.childRoute}/schemas`
4027
4142
  }
4028
4143
  }
4029
4144
  }
@@ -4170,6 +4285,19 @@ var applyRelationFormatDefault = /* @__PURE__ */ __name((cols) => cols?.map((col
4170
4285
  }
4171
4286
  return col;
4172
4287
  }), "applyRelationFormatDefault");
4288
+ var resolveColumnFieldVariants = /* @__PURE__ */ __name((cols) => cols?.map((col) => {
4289
+ const fieldView = resolveViewField(col);
4290
+ const fieldTable = resolveTableField(col);
4291
+ return {
4292
+ ...col,
4293
+ ...fieldView && {
4294
+ fieldView
4295
+ },
4296
+ ...fieldTable && {
4297
+ fieldTable
4298
+ }
4299
+ };
4300
+ }), "resolveColumnFieldVariants");
4173
4301
 
4174
4302
  // src/lib/crud/adapter/sub-resource.builder.ts
4175
4303
  var buildSubResources = /* @__PURE__ */ __name((columns, parentRoute, parentModel, parentDir, enums = {}, baseUrl) => {
@@ -4251,7 +4379,7 @@ var fromJson = /* @__PURE__ */ __name((json, schema, hooks, dirPath, baseUrl, ac
4251
4379
  const columns = enrichRelationTypes(applyRelationFormatDefault(rawColumns) ?? rawColumns, schema);
4252
4380
  injectEnumValues(columns, enums);
4253
4381
  const subResources = buildSubResources(columns, json.route, json.model, dirPath, enums, baseUrl);
4254
- const enrichedColumns = enrichResourceRefColumns(enrichActionColumns(columns, json.route, subResources, baseUrl), dirPath, baseUrl) ?? columns;
4382
+ const enrichedColumns = resolveColumnFieldVariants(enrichResourceRefColumns(enrichActionColumns(columns, json.route, subResources, baseUrl), dirPath, baseUrl) ?? columns);
4255
4383
  const calculatedColumns = json.calculatedColumns ?? [];
4256
4384
  const picked = pickByColumns(schema, enrichedColumns);
4257
4385
  const createSchema = pickByColumns(schema, enrichedColumns, (c) => !c.idField && c.createable !== false);
@@ -4287,6 +4415,9 @@ var fromJson = /* @__PURE__ */ __name((json, schema, hooks, dirPath, baseUrl, ac
4287
4415
  ...upsertOp(json.operations.upsert, createSchema) && {
4288
4416
  upsert: upsertOp(json.operations.upsert, createSchema)
4289
4417
  },
4418
+ ...opWithSchema(json.operations.patch, createSchema) && {
4419
+ patch: opWithSchema(json.operations.patch, createSchema)
4420
+ },
4290
4421
  ...json.operations.delete !== false && {
4291
4422
  delete: true
4292
4423
  }
@@ -4393,6 +4524,7 @@ var import_node_fs6 = require("fs");
4393
4524
  var import_node_path9 = require("path");
4394
4525
  var loadResourceConfigsFromDir = /* @__PURE__ */ __name(async (dirPath, baseUrl, enumsFile) => {
4395
4526
  if (!(0, import_node_fs6.existsSync)(dirPath)) return [];
4527
+ resourceLoadErrorsRegistry.clear();
4396
4528
  const enums = loadEnumRegistry(dirPath, enumsFile);
4397
4529
  const entries = (0, import_node_fs6.readdirSync)(dirPath, {
4398
4530
  withFileTypes: true
@@ -4406,7 +4538,16 @@ var loadResourceConfigsFromDir = /* @__PURE__ */ __name(async (dirPath, baseUrl,
4406
4538
  const hooks = await loadResourceHooks(basePath);
4407
4539
  const jsonFile = (0, import_node_path9.join)(basePath, "resource.json");
4408
4540
  if ((0, import_node_fs6.existsSync)(jsonFile)) {
4409
- const json = readResourceJson(jsonFile).json;
4541
+ const result = readResourceJson(jsonFile);
4542
+ if (!result || !result.success) {
4543
+ resourceLoadErrorsRegistry.record({
4544
+ name: dir,
4545
+ path: jsonFile,
4546
+ error: result?.error ?? `Failed to read ${jsonFile}`
4547
+ });
4548
+ continue;
4549
+ }
4550
+ const json = result.data.json;
4410
4551
  const actions = await loadActions(json.actions ?? [], basePath, "row");
4411
4552
  const tableActions = await loadActions(json.tableActions ?? [], basePath, "table");
4412
4553
  const config = fromJson(json, schema, hooks, basePath, baseUrl, actions, tableActions, enums);
@@ -4453,7 +4594,97 @@ var FileSystemResourceConfigLoader = class extends ResourceConfigLoader2 {
4453
4594
  }
4454
4595
  };
4455
4596
 
4456
- // src/lib/crouton-api.module.ts
4597
+ // src/lib/crud/status/status.service.ts
4598
+ var import_node_fs7 = require("fs");
4599
+ var import_node_path10 = require("path");
4600
+ var import_node_url = require("url");
4601
+ var DB_CHECK_TIMEOUT_MS = 3e3;
4602
+ var CONNECTION_STRING_PATTERN = /(?:postgresql|postgres|mysql|mongodb|sqlserver|sqlite):\/\/[^\s"')]+/gi;
4603
+ var stripConnectionStrings = /* @__PURE__ */ __name((message) => message.replace(CONNECTION_STRING_PATTERN, "[REDACTED]"), "stripConnectionStrings");
4604
+ var getCroutonVersion = /* @__PURE__ */ __name(() => {
4605
+ try {
4606
+ const startDir = typeof __dirname !== "undefined" ? __dirname : (0, import_node_path10.dirname)((0, import_node_url.fileURLToPath)(importMetaUrl));
4607
+ let dir = startDir;
4608
+ while (dir !== (0, import_node_path10.dirname)(dir)) {
4609
+ const pkgPath = (0, import_node_path10.join)(dir, "package.json");
4610
+ if ((0, import_node_fs7.existsSync)(pkgPath)) {
4611
+ const pkg = JSON.parse((0, import_node_fs7.readFileSync)(pkgPath, "utf-8"));
4612
+ if (pkg.name === "@ghentcdh/crouton-api") return pkg.version;
4613
+ }
4614
+ dir = (0, import_node_path10.dirname)(dir);
4615
+ }
4616
+ } catch {
4617
+ }
4618
+ return "unknown";
4619
+ }, "getCroutonVersion");
4620
+ var getVersion = /* @__PURE__ */ __name(() => process.env["APP_VERSION"] ?? "unknown", "getVersion");
4621
+ var getEnvironment = /* @__PURE__ */ __name(() => process.env["ENVIRONMENT"] ?? process.env["NODE_ENV"] ?? "unknown", "getEnvironment");
4622
+ var checkDatabases = /* @__PURE__ */ __name(async (registry) => {
4623
+ const entries = registry.entries();
4624
+ const results = [];
4625
+ for (const { name, client } of entries) {
4626
+ try {
4627
+ await Promise.race([
4628
+ client.$queryRaw`SELECT 1`,
4629
+ new Promise((_resolve, reject) => setTimeout(() => reject(new Error("Database health check timed out")), DB_CHECK_TIMEOUT_MS))
4630
+ ]);
4631
+ results.push({
4632
+ name,
4633
+ connected: true
4634
+ });
4635
+ } catch (err) {
4636
+ results.push({
4637
+ name,
4638
+ connected: false,
4639
+ error: stripConnectionStrings(err.message)
4640
+ });
4641
+ }
4642
+ }
4643
+ return results;
4644
+ }, "checkDatabases");
4645
+ var getResourceStatus = /* @__PURE__ */ __name((loadedConfigs) => {
4646
+ const valid = loadedConfigs.map((c) => ({
4647
+ name: c.name,
4648
+ path: c.route,
4649
+ valid: true
4650
+ }));
4651
+ const failed = resourceLoadErrorsRegistry.getAll().map((e) => ({
4652
+ name: e.name,
4653
+ path: e.path,
4654
+ valid: false,
4655
+ error: e.error
4656
+ }));
4657
+ return [
4658
+ ...valid,
4659
+ ...failed
4660
+ ];
4661
+ }, "getResourceStatus");
4662
+ var buildSummary = /* @__PURE__ */ __name((databases, resources) => {
4663
+ const databaseErrors = databases.filter((d) => !d.connected).length;
4664
+ const resourceErrors = resources.filter((r) => !r.valid).length;
4665
+ return {
4666
+ ok: databaseErrors === 0 && resourceErrors === 0,
4667
+ databaseErrors,
4668
+ resourceErrors
4669
+ };
4670
+ }, "buildSummary");
4671
+ var buildStatus = /* @__PURE__ */ __name(async (registry, loadedConfigs) => {
4672
+ const databases = await checkDatabases(registry);
4673
+ const resources = getResourceStatus(loadedConfigs);
4674
+ const summary = buildSummary(databases, resources);
4675
+ return {
4676
+ version: getVersion(),
4677
+ croutonVersion: getCroutonVersion(),
4678
+ environment: getEnvironment(),
4679
+ summary,
4680
+ databases,
4681
+ resources
4682
+ };
4683
+ }, "buildStatus");
4684
+
4685
+ // src/lib/crud/status/status.controller.ts
4686
+ var import_common12 = require("@nestjs/common");
4687
+ var import_swagger7 = require("@nestjs/swagger");
4457
4688
  function _ts_decorate5(decorators, target, key, desc2) {
4458
4689
  var c = arguments.length, r = c < 3 ? target : desc2 === null ? desc2 = Object.getOwnPropertyDescriptor(target, key) : desc2, d;
4459
4690
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc2);
@@ -4461,6 +4692,63 @@ function _ts_decorate5(decorators, target, key, desc2) {
4461
4692
  return c > 3 && r && Object.defineProperty(target, key, r), r;
4462
4693
  }
4463
4694
  __name(_ts_decorate5, "_ts_decorate");
4695
+ function _ts_metadata4(k, v) {
4696
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
4697
+ }
4698
+ __name(_ts_metadata4, "_ts_metadata");
4699
+ var createStatusController = /* @__PURE__ */ __name(() => {
4700
+ let StatusController = class StatusController {
4701
+ static {
4702
+ __name(this, "StatusController");
4703
+ }
4704
+ dataSourceRegistry;
4705
+ configRegistry;
4706
+ constructor(dataSourceRegistry, configRegistry) {
4707
+ this.dataSourceRegistry = dataSourceRegistry;
4708
+ this.configRegistry = configRegistry;
4709
+ }
4710
+ async getStatus() {
4711
+ const configs = await this.configRegistry.getAll();
4712
+ return buildStatus(this.dataSourceRegistry, configs);
4713
+ }
4714
+ };
4715
+ _ts_decorate5([
4716
+ (0, import_common12.Get)("status.json"),
4717
+ (0, import_swagger7.ApiOperation)({
4718
+ summary: "Crouton system status (db, resources, version)"
4719
+ }),
4720
+ (0, import_swagger7.ApiResponse)({
4721
+ status: 200,
4722
+ description: "System status"
4723
+ }),
4724
+ _ts_metadata4("design:type", Function),
4725
+ _ts_metadata4("design:paramtypes", []),
4726
+ _ts_metadata4("design:returntype", Promise)
4727
+ ], StatusController.prototype, "getStatus", null);
4728
+ StatusController = _ts_decorate5([
4729
+ (0, import_common12.Controller)("crouton"),
4730
+ (0, import_swagger7.ApiTags)("Status"),
4731
+ _ts_metadata4("design:type", Function),
4732
+ _ts_metadata4("design:paramtypes", [
4733
+ typeof DataSourceRegistry === "undefined" ? Object : DataSourceRegistry,
4734
+ typeof ResourceConfigRegistry === "undefined" ? Object : ResourceConfigRegistry
4735
+ ])
4736
+ ], StatusController);
4737
+ Reflect.defineMetadata("design:paramtypes", [
4738
+ DataSourceRegistry,
4739
+ ResourceConfigRegistry
4740
+ ], StatusController);
4741
+ return StatusController;
4742
+ }, "createStatusController");
4743
+
4744
+ // src/lib/crouton-api.module.ts
4745
+ function _ts_decorate6(decorators, target, key, desc2) {
4746
+ var c = arguments.length, r = c < 3 ? target : desc2 === null ? desc2 = Object.getOwnPropertyDescriptor(target, key) : desc2, d;
4747
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc2);
4748
+ 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;
4749
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
4750
+ }
4751
+ __name(_ts_decorate6, "_ts_decorate");
4464
4752
  var CroutonApiModule = class _CroutonApiModule {
4465
4753
  static {
4466
4754
  __name(this, "CroutonApiModule");
@@ -4474,10 +4762,33 @@ var CroutonApiModule = class _CroutonApiModule {
4474
4762
  }
4475
4763
  static forResources(configs, dataSources, loader, { baseUrl }, config) {
4476
4764
  const dataSourceRegistry = new DataSourceRegistry(dataSources);
4477
- const configRegistry = new ResourceConfigRegistry(loader, configs);
4765
+ const validConfigs = [];
4766
+ for (const c of configs) {
4767
+ try {
4768
+ const prisma = dataSourceRegistry.resolve(c.database);
4769
+ if (!prisma[c.model]) {
4770
+ resourceLoadErrorsRegistry.record({
4771
+ name: c.name,
4772
+ path: c.route,
4773
+ error: `Model "${c.model}" not found on the provided PrismaClient. Check the resource config for "${c.name}".`
4774
+ });
4775
+ continue;
4776
+ }
4777
+ } catch (e) {
4778
+ resourceLoadErrorsRegistry.record({
4779
+ name: c.name,
4780
+ path: c.route,
4781
+ error: e.message ?? String(e)
4782
+ });
4783
+ continue;
4784
+ }
4785
+ validConfigs.push(c);
4786
+ }
4787
+ const configRegistry = new ResourceConfigRegistry(loader, validConfigs);
4478
4788
  const controllers = [
4479
- ...configs.map((c) => createCrudController(c, baseUrl)),
4480
- createAppLayoutController(configs, config.sidebarGroups, config.title, config.autoSave ?? true)
4789
+ ...validConfigs.map((c) => createCrudController(c, baseUrl)),
4790
+ createAppLayoutController(configs, config.sidebarGroups, config.title, config.autoSave ?? true),
4791
+ createStatusController()
4481
4792
  ];
4482
4793
  return {
4483
4794
  module: _CroutonApiModule,
@@ -4512,8 +4823,8 @@ var CroutonApiModule = class _CroutonApiModule {
4512
4823
  return _CroutonApiModule.forResources(configs, dataSources, loader, appConfig, config);
4513
4824
  }
4514
4825
  };
4515
- CroutonApiModule = _ts_decorate5([
4516
- (0, import_common12.Module)({
4826
+ CroutonApiModule = _ts_decorate6([
4827
+ (0, import_common13.Module)({
4517
4828
  controllers: [],
4518
4829
  providers: [],
4519
4830
  exports: []