@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.
@@ -0,0 +1,344 @@
1
+ import { OnModuleDestroy, DynamicModule } from '@nestjs/common';
2
+ import { FieldInput, JsonAction, JsonIncludeEntry, CalculatedColumn, JsonActionCondition, SidebarGroupConfig } from '@ghentcdh/crouton-core';
3
+ import { ZodObject, ZodRawShape } from 'zod';
4
+
5
+ type ResourceProcedureAction = {
6
+ type?: 'procedure';
7
+ /** URL segment used in the endpoint: `POST /{route}/procedure/{id}/:recordId` */
8
+ id: string;
9
+ /** Human-readable label shown as a button in the table. */
10
+ label: string;
11
+ /** HTTP method for the endpoint. Defaults to `"post"`. */
12
+ method?: string;
13
+ /** Static data payload merged into the request body by the frontend. */
14
+ data?: Record<string, unknown>;
15
+ /** The procedure function to call with (prisma, recordId). */
16
+ procedure: (prisma: any, recordId: string | number) => Promise<any>;
17
+ /** Optional condition evaluated per row. Button is hidden when false. */
18
+ condition?: JsonActionCondition;
19
+ };
20
+ type ResourceLinkAction = {
21
+ type: 'link';
22
+ /** Unique identifier for the action. */
23
+ id: string;
24
+ /** Human-readable label shown as a button in the table. */
25
+ label: string;
26
+ /**
27
+ * URL pattern to open in a new tab. May contain `{id}` which the frontend
28
+ * replaces with the record id, e.g. `"/preview/{id}"`.
29
+ */
30
+ href: string;
31
+ /** Optional condition evaluated per row. Button is hidden when false. */
32
+ condition?: JsonActionCondition;
33
+ };
34
+ type ResourceAction = ResourceProcedureAction | ResourceLinkAction;
35
+ /** Table-level procedure action — no record id is passed to the procedure. */
36
+ type ResourceTableProcedureAction = {
37
+ type?: 'procedure';
38
+ id: string;
39
+ label?: string;
40
+ icon?: string;
41
+ tooltip?: string;
42
+ method?: string;
43
+ data?: Record<string, unknown>;
44
+ /** The procedure function called with only prisma (no record id). */
45
+ procedure: (prisma: any) => Promise<any>;
46
+ };
47
+ type ResourceTableLinkAction = {
48
+ type: 'link';
49
+ id: string;
50
+ label?: string;
51
+ icon?: string;
52
+ tooltip?: string;
53
+ href: string;
54
+ };
55
+ type ResourceTableAction = ResourceTableProcedureAction | ResourceTableLinkAction;
56
+ type CrudOperation = 'findAll' | 'findOne' | 'create' | 'update' | 'upsert' | 'delete';
57
+ type SchemaInput = ZodObject<ZodRawShape> | JsonSchemaInput;
58
+ interface JsonSchemaInput {
59
+ type: 'object';
60
+ properties: Record<string, unknown>;
61
+ required?: string[];
62
+ additionalProperties?: boolean;
63
+ }
64
+ /** @deprecated Use `JsonSchemaInput` instead — `JsonSchema` clashes with the `@jsonforms/core` type of the same name. */
65
+ type JsonSchema = JsonSchemaInput;
66
+ type WriteOp = 'create' | 'update' | 'upsert' | 'delete';
67
+ type ReadOp = 'findAll' | 'findOne';
68
+ interface WriteHookContext<PRISMACLIENT> {
69
+ prisma: PRISMACLIENT;
70
+ op: WriteOp;
71
+ /** The record id for `update`; `undefined` for `create`/`upsert`. */
72
+ id?: string | number;
73
+ }
74
+ interface ReadHookContext<PRISMACLIENT> {
75
+ prisma: PRISMACLIENT;
76
+ op: ReadOp;
77
+ }
78
+ interface ResourceHooks<PRISMACLIENT = any> {
79
+ /**
80
+ * Runs before the data is passed to Prisma for create/update/upsert.
81
+ * Use this to connect-or-create related entities.
82
+ */
83
+ beforeWrite?: (data: any, ctx: WriteHookContext<PRISMACLIENT>) => Promise<any> | any;
84
+ /**
85
+ * Runs after Prisma has persisted the record for create/update/delete.
86
+ * For upsert the op is resolved to `'create'` or `'update'` depending on
87
+ * whether a matching record existed before the operation.
88
+ * Receives the persisted record; the return value replaces the response.
89
+ */
90
+ afterWrite?: (result: any, ctx: WriteHookContext<PRISMACLIENT>) => Promise<any> | any;
91
+ /**
92
+ * Runs on every row returned by findAll / findOne. Use this to
93
+ * decorate rows with derived fields (e.g. URIs).
94
+ */
95
+ afterRead?: (row: any, ctx: ReadHookContext<PRISMACLIENT>) => Promise<any> | any;
96
+ }
97
+ /**
98
+ * Per-operation descriptor. Use `true` to enable with no schema, or
99
+ * an object to also attach a schema (request body for write ops, row
100
+ * projection for read ops). For read operations the schema also drives
101
+ * the Prisma `select` — nested zod objects/arrays become nested
102
+ * `select` clauses, which transparently load relations.
103
+ */
104
+ type OperationDef = true | {
105
+ schema?: SchemaInput;
106
+ };
107
+ type UpsertOperationDef = {
108
+ schema?: SchemaInput;
109
+ upsertOn: string | string[];
110
+ };
111
+ interface ResourceDefinition {
112
+ findAll?: OperationDef;
113
+ findOne?: OperationDef;
114
+ create?: OperationDef;
115
+ update?: OperationDef;
116
+ upsert?: UpsertOperationDef;
117
+ delete?: OperationDef;
118
+ }
119
+ type DefinitionCallback = () => ResourceDefinition;
120
+ type ViewColumnConfig = {
121
+ id: string;
122
+ label?: string;
123
+ sortable?: boolean;
124
+ searchable?: boolean;
125
+ fieldInput?: FieldInput;
126
+ };
127
+ type ViewConfig = {
128
+ json_schema: Record<string, unknown>;
129
+ ui_schema: Record<string, unknown>;
130
+ columns: ViewColumnConfig[];
131
+ defaultSort?: string;
132
+ };
133
+ type ResourceDisplay = {
134
+ mode: 'page' | 'modal';
135
+ customComponent: string | null;
136
+ };
137
+ /**
138
+ * A column whose stored scalar is serialized as `{ value, label }` on read and
139
+ * normalized back to the scalar on write. Computed at load time from columns
140
+ * flagged `fieldInput.options.emitObject` that carry an `options.values` list.
141
+ */
142
+ type ValueLabelColumn = {
143
+ /** Row field/key to transform. */
144
+ field: string;
145
+ values: {
146
+ value: unknown;
147
+ label: string;
148
+ }[];
149
+ };
150
+ type LookupConfig = {
151
+ /** The primary key field name (used for id-based lookups). */
152
+ key: string;
153
+ /**
154
+ * The field used for text search (`?q=`). When absent, `?q=` is silently ignored.
155
+ * Derived from the first `showInLookup` column, then first `searchable` column.
156
+ */
157
+ label?: string;
158
+ };
159
+ /**
160
+ * Describes a sub-resource relation exposed as `GET /:id/{childRoute}`.
161
+ * The parent `findAll` response includes a count for the relation column.
162
+ */
163
+ type SubResourceConfig = {
164
+ /** Column id in the parent resource that holds the count, e.g. `"text_author"`. */
165
+ column: string;
166
+ /** Prisma relation field name on the parent model, e.g. `"text_author"`. */
167
+ relation: string;
168
+ /** Route segment for the sub-resource endpoint, e.g. `"author"`. */
169
+ childRoute: string;
170
+ /** Prisma model name of the child, e.g. `"text_author"`. */
171
+ childModel: string;
172
+ /** FK field on the child model pointing back to the parent, e.g. `"text_id"`. */
173
+ foreignKey: string;
174
+ /** Human-readable name for the child resource. */
175
+ name?: string;
176
+ /** Display title for the child resource. */
177
+ title?: string;
178
+ /** Primary key field name on the child model. */
179
+ idField?: string;
180
+ /** Primary key type on the child model. */
181
+ idType?: 'string' | 'number';
182
+ /** View schemas (table/form/view) for the child resource. */
183
+ views?: Record<string, ViewConfig>;
184
+ /** Enabled operations on the child resource. */
185
+ operations?: Partial<Record<'findAll' | 'findOne' | 'create' | 'update' | 'delete', boolean>>;
186
+ /** Actions declared in the child resource.json (serializable form, no procedure functions). */
187
+ actions?: JsonAction[];
188
+ /** Modal width when opening a form for this sub-resource. */
189
+ modalSize?: 'xs' | 'sm' | 'lg' | 'xl';
190
+ /** Relations to include when querying this sub-resource. Supports nested includes — see `JsonIncludeEntry`. */
191
+ include?: JsonIncludeEntry[];
192
+ /** Calculated columns to compute and merge for each row of this sub-resource. */
193
+ calculatedColumns?: CalculatedColumn[];
194
+ /** When true, the relation is included in findOne responses (column is visible in form or view). */
195
+ includeInFindOne?: boolean;
196
+ /** Lifecycle hooks for this sub-resource (beforeWrite, afterRead). */
197
+ hooks?: ResourceHooks;
198
+ /** Columns serialized as `{ value, label }` on read / unwrapped on write. */
199
+ valueLabelColumns?: ValueLabelColumn[];
200
+ };
201
+ type ResourceConfig = {
202
+ name: string;
203
+ route: string;
204
+ model: string;
205
+ tag: string;
206
+ title?: string;
207
+ /** Primary key field name on the model. Defaults to `"id"`. */
208
+ idField?: string;
209
+ idType?: 'number' | 'string';
210
+ database?: string;
211
+ sidebar?: {
212
+ hide?: boolean;
213
+ position?: number;
214
+ };
215
+ hooks?: ResourceHooks;
216
+ definition: ResourceDefinition | DefinitionCallback;
217
+ views?: Record<string, ViewConfig>;
218
+ lookup?: LookupConfig;
219
+ subResources?: SubResourceConfig[];
220
+ calculatedColumns?: CalculatedColumn[];
221
+ actions?: ResourceAction[];
222
+ /** Global table-level actions (no record id). Shown as toolbar buttons. */
223
+ tableActions?: ResourceTableAction[];
224
+ /** Relations to eagerly include when querying this resource. Supports nested includes — see `JsonIncludeEntry`. */
225
+ include?: JsonIncludeEntry[];
226
+ /** Modal width when opening a form for this resource. */
227
+ modalSize?: 'xs' | 'sm' | 'lg' | 'xl';
228
+ /** Columns serialized as `{ value, label }` on read / unwrapped on write. */
229
+ valueLabelColumns?: ValueLabelColumn[];
230
+ display: ResourceDisplay;
231
+ };
232
+ declare const resolveDefinition: (config: ResourceConfig) => ResourceDefinition;
233
+ declare const isOperationEnabled: (def: ResourceDefinition, op: CrudOperation) => boolean;
234
+ declare const schemaFor: (def: ResourceDefinition, op: CrudOperation) => SchemaInput | undefined;
235
+ declare const upsertOnFor: (def: ResourceDefinition) => string | string[] | undefined;
236
+
237
+ type DataSourceJsonConfig = {
238
+ type: string;
239
+ name: string;
240
+ default?: boolean;
241
+ };
242
+ type DataSourceEntry = {
243
+ config: DataSourceJsonConfig;
244
+ client: any;
245
+ };
246
+
247
+ declare class DataSourceRegistry implements OnModuleDestroy {
248
+ private readonly clients;
249
+ private defaultName;
250
+ constructor(entries: DataSourceEntry[]);
251
+ get(name: string): any;
252
+ getDefault(): any;
253
+ resolve(database?: string): any;
254
+ onModuleDestroy(): Promise<void>;
255
+ }
256
+
257
+ /**
258
+ * Scan a directory for data-source subdirectories and load their configs + clients.
259
+ *
260
+ * Each subdirectory must contain:
261
+ * - `data-source.json` — config with `name`, `type`, and optional `default`
262
+ * - `index.ts` (or `.js`) — default export of a PrismaClient instance
263
+ */
264
+ declare const loadDataSourcesFromDir: (dirPath: string) => Promise<DataSourceEntry[]>;
265
+
266
+ declare abstract class ResourceConfigLoader {
267
+ abstract loadAll(): Promise<ResourceConfig[]>;
268
+ abstract loadByRoute(route: string): Promise<ResourceConfig | undefined>;
269
+ }
270
+
271
+ type CroutonConfig = {
272
+ baseUrl: string;
273
+ /**
274
+ * Explicit path to the project enum registry (`crouton.enums.json`).
275
+ * When omitted, the loader walks up from the resources dir to find it.
276
+ */
277
+ enumsFile?: string;
278
+ /**
279
+ * Sidebar group definitions, keyed by group slug.
280
+ * Matches `sidebarGroups` in `crouton.json`.
281
+ * Resources reference a group via `sidebar.group` in their `resource.json`.
282
+ */
283
+ sidebarGroups?: Record<string, SidebarGroupConfig>;
284
+ /**
285
+ * Application title served to the frontend via `GET /_app/layout`.
286
+ * Displayed in the admin sidebar header.
287
+ */
288
+ title?: string;
289
+ /**
290
+ * Whether form fields are saved automatically as the user edits them.
291
+ * Served to the frontend via `GET /_app/layout`. Defaults to `true`.
292
+ * Set to `false` to restore explicit Save/Cancel buttons across the app.
293
+ * Matches `autoSave` in `crouton.json`.
294
+ */
295
+ autoSave?: boolean;
296
+ };
297
+ declare class CroutonApiModule {
298
+ static forResources(configs: ResourceConfig[], dataSources: DataSourceEntry[], loader: ResourceConfigLoader, config: CroutonConfig): DynamicModule;
299
+ static forResourceDir(dirPath: string, dataSourcesPath: string, config: CroutonConfig): Promise<DynamicModule>;
300
+ static forLoader(loader: ResourceConfigLoader, configs: ResourceConfig[], dataSources: DataSourceEntry[], config: CroutonConfig): DynamicModule;
301
+ }
302
+
303
+ /**
304
+ * Filesystem-driven resource loader.
305
+ *
306
+ * Two sources feed the resource registry:
307
+ *
308
+ * 1. `<dir>/<name>/resource.json` — declarative configuration
309
+ * (route / operations / columns / …).
310
+ *
311
+ * 2. `<dir>/<name>/resource.ts` — imperative configuration as a
312
+ * plain `ResourceConfig` default export.
313
+ *
314
+ * When both exist for the same name, the JSON wins.
315
+ *
316
+ * The JSON only carries operation toggles; actual Zod schemas come from
317
+ * a sibling `schema.ts` (default export). If the JSON lists `columns`,
318
+ * the loader narrows the Zod schema down to those column ids via
319
+ * `.pick()` before attaching it to every enabled operation.
320
+ *
321
+ * `resource.ts` / `hooks.ts` / `schema.ts` do **not** import from each
322
+ * other — this loader is the only place that stitches them together.
323
+ */
324
+
325
+ declare const loadResourceConfigsFromDir: (dirPath: string, baseUrl?: string, enumsFile?: string) => Promise<ResourceConfig[]>;
326
+
327
+ declare class FileSystemResourceConfigLoader extends ResourceConfigLoader {
328
+ private readonly dirPath;
329
+ private readonly baseUrl?;
330
+ private readonly enumsFile?;
331
+ constructor(dirPath: string, baseUrl?: string | undefined, enumsFile?: string | undefined);
332
+ loadAll(): Promise<ResourceConfig[]>;
333
+ loadByRoute(route: string): Promise<ResourceConfig | undefined>;
334
+ }
335
+
336
+ declare class ResourceConfigRegistry {
337
+ private readonly loader;
338
+ private configs;
339
+ constructor(loader: ResourceConfigLoader, initialConfigs: ResourceConfig[]);
340
+ getAll(): Promise<ResourceConfig[]>;
341
+ getByRoute(route: string): Promise<ResourceConfig | undefined>;
342
+ }
343
+
344
+ export { CroutonApiModule, type CrudOperation, type DataSourceEntry, type DataSourceJsonConfig, DataSourceRegistry, type DefinitionCallback, FileSystemResourceConfigLoader, type JsonSchema, type JsonSchemaInput, type LookupConfig, type OperationDef, type ReadHookContext, type ReadOp, type ResourceAction, type ResourceConfig, ResourceConfigLoader, ResourceConfigRegistry, type ResourceDefinition, type ResourceDisplay, type ResourceHooks, type ResourceLinkAction, type ResourceProcedureAction, type ResourceTableAction, type ResourceTableLinkAction, type ResourceTableProcedureAction, type SchemaInput, type SubResourceConfig, type UpsertOperationDef, type ValueLabelColumn, type ViewColumnConfig, type ViewConfig, type WriteHookContext, type WriteOp, isOperationEnabled, loadDataSourcesFromDir, loadResourceConfigsFromDir, resolveDefinition, schemaFor, upsertOnFor };
@@ -0,0 +1,344 @@
1
+ import { OnModuleDestroy, DynamicModule } from '@nestjs/common';
2
+ import { FieldInput, JsonAction, JsonIncludeEntry, CalculatedColumn, JsonActionCondition, SidebarGroupConfig } from '@ghentcdh/crouton-core';
3
+ import { ZodObject, ZodRawShape } from 'zod';
4
+
5
+ type ResourceProcedureAction = {
6
+ type?: 'procedure';
7
+ /** URL segment used in the endpoint: `POST /{route}/procedure/{id}/:recordId` */
8
+ id: string;
9
+ /** Human-readable label shown as a button in the table. */
10
+ label: string;
11
+ /** HTTP method for the endpoint. Defaults to `"post"`. */
12
+ method?: string;
13
+ /** Static data payload merged into the request body by the frontend. */
14
+ data?: Record<string, unknown>;
15
+ /** The procedure function to call with (prisma, recordId). */
16
+ procedure: (prisma: any, recordId: string | number) => Promise<any>;
17
+ /** Optional condition evaluated per row. Button is hidden when false. */
18
+ condition?: JsonActionCondition;
19
+ };
20
+ type ResourceLinkAction = {
21
+ type: 'link';
22
+ /** Unique identifier for the action. */
23
+ id: string;
24
+ /** Human-readable label shown as a button in the table. */
25
+ label: string;
26
+ /**
27
+ * URL pattern to open in a new tab. May contain `{id}` which the frontend
28
+ * replaces with the record id, e.g. `"/preview/{id}"`.
29
+ */
30
+ href: string;
31
+ /** Optional condition evaluated per row. Button is hidden when false. */
32
+ condition?: JsonActionCondition;
33
+ };
34
+ type ResourceAction = ResourceProcedureAction | ResourceLinkAction;
35
+ /** Table-level procedure action — no record id is passed to the procedure. */
36
+ type ResourceTableProcedureAction = {
37
+ type?: 'procedure';
38
+ id: string;
39
+ label?: string;
40
+ icon?: string;
41
+ tooltip?: string;
42
+ method?: string;
43
+ data?: Record<string, unknown>;
44
+ /** The procedure function called with only prisma (no record id). */
45
+ procedure: (prisma: any) => Promise<any>;
46
+ };
47
+ type ResourceTableLinkAction = {
48
+ type: 'link';
49
+ id: string;
50
+ label?: string;
51
+ icon?: string;
52
+ tooltip?: string;
53
+ href: string;
54
+ };
55
+ type ResourceTableAction = ResourceTableProcedureAction | ResourceTableLinkAction;
56
+ type CrudOperation = 'findAll' | 'findOne' | 'create' | 'update' | 'upsert' | 'delete';
57
+ type SchemaInput = ZodObject<ZodRawShape> | JsonSchemaInput;
58
+ interface JsonSchemaInput {
59
+ type: 'object';
60
+ properties: Record<string, unknown>;
61
+ required?: string[];
62
+ additionalProperties?: boolean;
63
+ }
64
+ /** @deprecated Use `JsonSchemaInput` instead — `JsonSchema` clashes with the `@jsonforms/core` type of the same name. */
65
+ type JsonSchema = JsonSchemaInput;
66
+ type WriteOp = 'create' | 'update' | 'upsert' | 'delete';
67
+ type ReadOp = 'findAll' | 'findOne';
68
+ interface WriteHookContext<PRISMACLIENT> {
69
+ prisma: PRISMACLIENT;
70
+ op: WriteOp;
71
+ /** The record id for `update`; `undefined` for `create`/`upsert`. */
72
+ id?: string | number;
73
+ }
74
+ interface ReadHookContext<PRISMACLIENT> {
75
+ prisma: PRISMACLIENT;
76
+ op: ReadOp;
77
+ }
78
+ interface ResourceHooks<PRISMACLIENT = any> {
79
+ /**
80
+ * Runs before the data is passed to Prisma for create/update/upsert.
81
+ * Use this to connect-or-create related entities.
82
+ */
83
+ beforeWrite?: (data: any, ctx: WriteHookContext<PRISMACLIENT>) => Promise<any> | any;
84
+ /**
85
+ * Runs after Prisma has persisted the record for create/update/delete.
86
+ * For upsert the op is resolved to `'create'` or `'update'` depending on
87
+ * whether a matching record existed before the operation.
88
+ * Receives the persisted record; the return value replaces the response.
89
+ */
90
+ afterWrite?: (result: any, ctx: WriteHookContext<PRISMACLIENT>) => Promise<any> | any;
91
+ /**
92
+ * Runs on every row returned by findAll / findOne. Use this to
93
+ * decorate rows with derived fields (e.g. URIs).
94
+ */
95
+ afterRead?: (row: any, ctx: ReadHookContext<PRISMACLIENT>) => Promise<any> | any;
96
+ }
97
+ /**
98
+ * Per-operation descriptor. Use `true` to enable with no schema, or
99
+ * an object to also attach a schema (request body for write ops, row
100
+ * projection for read ops). For read operations the schema also drives
101
+ * the Prisma `select` — nested zod objects/arrays become nested
102
+ * `select` clauses, which transparently load relations.
103
+ */
104
+ type OperationDef = true | {
105
+ schema?: SchemaInput;
106
+ };
107
+ type UpsertOperationDef = {
108
+ schema?: SchemaInput;
109
+ upsertOn: string | string[];
110
+ };
111
+ interface ResourceDefinition {
112
+ findAll?: OperationDef;
113
+ findOne?: OperationDef;
114
+ create?: OperationDef;
115
+ update?: OperationDef;
116
+ upsert?: UpsertOperationDef;
117
+ delete?: OperationDef;
118
+ }
119
+ type DefinitionCallback = () => ResourceDefinition;
120
+ type ViewColumnConfig = {
121
+ id: string;
122
+ label?: string;
123
+ sortable?: boolean;
124
+ searchable?: boolean;
125
+ fieldInput?: FieldInput;
126
+ };
127
+ type ViewConfig = {
128
+ json_schema: Record<string, unknown>;
129
+ ui_schema: Record<string, unknown>;
130
+ columns: ViewColumnConfig[];
131
+ defaultSort?: string;
132
+ };
133
+ type ResourceDisplay = {
134
+ mode: 'page' | 'modal';
135
+ customComponent: string | null;
136
+ };
137
+ /**
138
+ * A column whose stored scalar is serialized as `{ value, label }` on read and
139
+ * normalized back to the scalar on write. Computed at load time from columns
140
+ * flagged `fieldInput.options.emitObject` that carry an `options.values` list.
141
+ */
142
+ type ValueLabelColumn = {
143
+ /** Row field/key to transform. */
144
+ field: string;
145
+ values: {
146
+ value: unknown;
147
+ label: string;
148
+ }[];
149
+ };
150
+ type LookupConfig = {
151
+ /** The primary key field name (used for id-based lookups). */
152
+ key: string;
153
+ /**
154
+ * The field used for text search (`?q=`). When absent, `?q=` is silently ignored.
155
+ * Derived from the first `showInLookup` column, then first `searchable` column.
156
+ */
157
+ label?: string;
158
+ };
159
+ /**
160
+ * Describes a sub-resource relation exposed as `GET /:id/{childRoute}`.
161
+ * The parent `findAll` response includes a count for the relation column.
162
+ */
163
+ type SubResourceConfig = {
164
+ /** Column id in the parent resource that holds the count, e.g. `"text_author"`. */
165
+ column: string;
166
+ /** Prisma relation field name on the parent model, e.g. `"text_author"`. */
167
+ relation: string;
168
+ /** Route segment for the sub-resource endpoint, e.g. `"author"`. */
169
+ childRoute: string;
170
+ /** Prisma model name of the child, e.g. `"text_author"`. */
171
+ childModel: string;
172
+ /** FK field on the child model pointing back to the parent, e.g. `"text_id"`. */
173
+ foreignKey: string;
174
+ /** Human-readable name for the child resource. */
175
+ name?: string;
176
+ /** Display title for the child resource. */
177
+ title?: string;
178
+ /** Primary key field name on the child model. */
179
+ idField?: string;
180
+ /** Primary key type on the child model. */
181
+ idType?: 'string' | 'number';
182
+ /** View schemas (table/form/view) for the child resource. */
183
+ views?: Record<string, ViewConfig>;
184
+ /** Enabled operations on the child resource. */
185
+ operations?: Partial<Record<'findAll' | 'findOne' | 'create' | 'update' | 'delete', boolean>>;
186
+ /** Actions declared in the child resource.json (serializable form, no procedure functions). */
187
+ actions?: JsonAction[];
188
+ /** Modal width when opening a form for this sub-resource. */
189
+ modalSize?: 'xs' | 'sm' | 'lg' | 'xl';
190
+ /** Relations to include when querying this sub-resource. Supports nested includes — see `JsonIncludeEntry`. */
191
+ include?: JsonIncludeEntry[];
192
+ /** Calculated columns to compute and merge for each row of this sub-resource. */
193
+ calculatedColumns?: CalculatedColumn[];
194
+ /** When true, the relation is included in findOne responses (column is visible in form or view). */
195
+ includeInFindOne?: boolean;
196
+ /** Lifecycle hooks for this sub-resource (beforeWrite, afterRead). */
197
+ hooks?: ResourceHooks;
198
+ /** Columns serialized as `{ value, label }` on read / unwrapped on write. */
199
+ valueLabelColumns?: ValueLabelColumn[];
200
+ };
201
+ type ResourceConfig = {
202
+ name: string;
203
+ route: string;
204
+ model: string;
205
+ tag: string;
206
+ title?: string;
207
+ /** Primary key field name on the model. Defaults to `"id"`. */
208
+ idField?: string;
209
+ idType?: 'number' | 'string';
210
+ database?: string;
211
+ sidebar?: {
212
+ hide?: boolean;
213
+ position?: number;
214
+ };
215
+ hooks?: ResourceHooks;
216
+ definition: ResourceDefinition | DefinitionCallback;
217
+ views?: Record<string, ViewConfig>;
218
+ lookup?: LookupConfig;
219
+ subResources?: SubResourceConfig[];
220
+ calculatedColumns?: CalculatedColumn[];
221
+ actions?: ResourceAction[];
222
+ /** Global table-level actions (no record id). Shown as toolbar buttons. */
223
+ tableActions?: ResourceTableAction[];
224
+ /** Relations to eagerly include when querying this resource. Supports nested includes — see `JsonIncludeEntry`. */
225
+ include?: JsonIncludeEntry[];
226
+ /** Modal width when opening a form for this resource. */
227
+ modalSize?: 'xs' | 'sm' | 'lg' | 'xl';
228
+ /** Columns serialized as `{ value, label }` on read / unwrapped on write. */
229
+ valueLabelColumns?: ValueLabelColumn[];
230
+ display: ResourceDisplay;
231
+ };
232
+ declare const resolveDefinition: (config: ResourceConfig) => ResourceDefinition;
233
+ declare const isOperationEnabled: (def: ResourceDefinition, op: CrudOperation) => boolean;
234
+ declare const schemaFor: (def: ResourceDefinition, op: CrudOperation) => SchemaInput | undefined;
235
+ declare const upsertOnFor: (def: ResourceDefinition) => string | string[] | undefined;
236
+
237
+ type DataSourceJsonConfig = {
238
+ type: string;
239
+ name: string;
240
+ default?: boolean;
241
+ };
242
+ type DataSourceEntry = {
243
+ config: DataSourceJsonConfig;
244
+ client: any;
245
+ };
246
+
247
+ declare class DataSourceRegistry implements OnModuleDestroy {
248
+ private readonly clients;
249
+ private defaultName;
250
+ constructor(entries: DataSourceEntry[]);
251
+ get(name: string): any;
252
+ getDefault(): any;
253
+ resolve(database?: string): any;
254
+ onModuleDestroy(): Promise<void>;
255
+ }
256
+
257
+ /**
258
+ * Scan a directory for data-source subdirectories and load their configs + clients.
259
+ *
260
+ * Each subdirectory must contain:
261
+ * - `data-source.json` — config with `name`, `type`, and optional `default`
262
+ * - `index.ts` (or `.js`) — default export of a PrismaClient instance
263
+ */
264
+ declare const loadDataSourcesFromDir: (dirPath: string) => Promise<DataSourceEntry[]>;
265
+
266
+ declare abstract class ResourceConfigLoader {
267
+ abstract loadAll(): Promise<ResourceConfig[]>;
268
+ abstract loadByRoute(route: string): Promise<ResourceConfig | undefined>;
269
+ }
270
+
271
+ type CroutonConfig = {
272
+ baseUrl: string;
273
+ /**
274
+ * Explicit path to the project enum registry (`crouton.enums.json`).
275
+ * When omitted, the loader walks up from the resources dir to find it.
276
+ */
277
+ enumsFile?: string;
278
+ /**
279
+ * Sidebar group definitions, keyed by group slug.
280
+ * Matches `sidebarGroups` in `crouton.json`.
281
+ * Resources reference a group via `sidebar.group` in their `resource.json`.
282
+ */
283
+ sidebarGroups?: Record<string, SidebarGroupConfig>;
284
+ /**
285
+ * Application title served to the frontend via `GET /_app/layout`.
286
+ * Displayed in the admin sidebar header.
287
+ */
288
+ title?: string;
289
+ /**
290
+ * Whether form fields are saved automatically as the user edits them.
291
+ * Served to the frontend via `GET /_app/layout`. Defaults to `true`.
292
+ * Set to `false` to restore explicit Save/Cancel buttons across the app.
293
+ * Matches `autoSave` in `crouton.json`.
294
+ */
295
+ autoSave?: boolean;
296
+ };
297
+ declare class CroutonApiModule {
298
+ static forResources(configs: ResourceConfig[], dataSources: DataSourceEntry[], loader: ResourceConfigLoader, config: CroutonConfig): DynamicModule;
299
+ static forResourceDir(dirPath: string, dataSourcesPath: string, config: CroutonConfig): Promise<DynamicModule>;
300
+ static forLoader(loader: ResourceConfigLoader, configs: ResourceConfig[], dataSources: DataSourceEntry[], config: CroutonConfig): DynamicModule;
301
+ }
302
+
303
+ /**
304
+ * Filesystem-driven resource loader.
305
+ *
306
+ * Two sources feed the resource registry:
307
+ *
308
+ * 1. `<dir>/<name>/resource.json` — declarative configuration
309
+ * (route / operations / columns / …).
310
+ *
311
+ * 2. `<dir>/<name>/resource.ts` — imperative configuration as a
312
+ * plain `ResourceConfig` default export.
313
+ *
314
+ * When both exist for the same name, the JSON wins.
315
+ *
316
+ * The JSON only carries operation toggles; actual Zod schemas come from
317
+ * a sibling `schema.ts` (default export). If the JSON lists `columns`,
318
+ * the loader narrows the Zod schema down to those column ids via
319
+ * `.pick()` before attaching it to every enabled operation.
320
+ *
321
+ * `resource.ts` / `hooks.ts` / `schema.ts` do **not** import from each
322
+ * other — this loader is the only place that stitches them together.
323
+ */
324
+
325
+ declare const loadResourceConfigsFromDir: (dirPath: string, baseUrl?: string, enumsFile?: string) => Promise<ResourceConfig[]>;
326
+
327
+ declare class FileSystemResourceConfigLoader extends ResourceConfigLoader {
328
+ private readonly dirPath;
329
+ private readonly baseUrl?;
330
+ private readonly enumsFile?;
331
+ constructor(dirPath: string, baseUrl?: string | undefined, enumsFile?: string | undefined);
332
+ loadAll(): Promise<ResourceConfig[]>;
333
+ loadByRoute(route: string): Promise<ResourceConfig | undefined>;
334
+ }
335
+
336
+ declare class ResourceConfigRegistry {
337
+ private readonly loader;
338
+ private configs;
339
+ constructor(loader: ResourceConfigLoader, initialConfigs: ResourceConfig[]);
340
+ getAll(): Promise<ResourceConfig[]>;
341
+ getByRoute(route: string): Promise<ResourceConfig | undefined>;
342
+ }
343
+
344
+ export { CroutonApiModule, type CrudOperation, type DataSourceEntry, type DataSourceJsonConfig, DataSourceRegistry, type DefinitionCallback, FileSystemResourceConfigLoader, type JsonSchema, type JsonSchemaInput, type LookupConfig, type OperationDef, type ReadHookContext, type ReadOp, type ResourceAction, type ResourceConfig, ResourceConfigLoader, ResourceConfigRegistry, type ResourceDefinition, type ResourceDisplay, type ResourceHooks, type ResourceLinkAction, type ResourceProcedureAction, type ResourceTableAction, type ResourceTableLinkAction, type ResourceTableProcedureAction, type SchemaInput, type SubResourceConfig, type UpsertOperationDef, type ValueLabelColumn, type ViewColumnConfig, type ViewConfig, type WriteHookContext, type WriteOp, isOperationEnabled, loadDataSourcesFromDir, loadResourceConfigsFromDir, resolveDefinition, schemaFor, upsertOnFor };