@olenbetong/appframe-vite 6.1.2 → 6.3.0

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,569 @@
1
+ import { exec } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import { dirname, join, relative, resolve, sep } from "node:path";
4
+ import { promisify } from "node:util";
5
+ import { importJson } from "./importJson.js";
6
+ const execAsync = promisify(exec);
7
+ // ---------------------------------------------------------------------------
8
+ // Biome formatting
9
+ // ---------------------------------------------------------------------------
10
+ /**
11
+ * Format a file with Biome if available. Searches for `biome` in the nearest
12
+ * node_modules/.bin up from cwd, then falls back to the global PATH.
13
+ * Silently skips if Biome is not found or formatting fails.
14
+ */
15
+ export async function formatWithBiome(filePath) {
16
+ let biomeBin = null;
17
+ let current = resolve(process.cwd());
18
+ while (true) {
19
+ let candidate = join(current, "node_modules", ".bin", "biome");
20
+ let candidateCmd = `${candidate}.cmd`;
21
+ if (existsSync(candidateCmd)) {
22
+ biomeBin = candidateCmd;
23
+ break;
24
+ }
25
+ if (existsSync(candidate)) {
26
+ biomeBin = candidate;
27
+ break;
28
+ }
29
+ let parent = dirname(current);
30
+ if (parent === current)
31
+ break;
32
+ current = parent;
33
+ }
34
+ let bin = biomeBin ?? "biome";
35
+ try {
36
+ await execAsync(`"${bin}" format --write "${resolve(filePath)}"`);
37
+ }
38
+ catch {
39
+ // Biome not available or not applicable — silently skip
40
+ }
41
+ }
42
+ // ---------------------------------------------------------------------------
43
+ // YAML config block
44
+ // ---------------------------------------------------------------------------
45
+ /** Options that hold comma-separated lists and should be emitted as YAML lists. */
46
+ const LIST_KEYS = new Set(["fields", "sortOrder", "linkFields", "aggregates", "groupBy", "overrides"]);
47
+ /**
48
+ * Serialize the generation options to a `/* af:config ... *\/` YAML comment block.
49
+ * List-type options (fields, sortOrder, etc.) are emitted as proper YAML lists.
50
+ */
51
+ export function buildYamlConfig(resource, options) {
52
+ let lines = ["/* af:config", `resource: ${resource}`];
53
+ const order = [
54
+ "id",
55
+ "types",
56
+ "global",
57
+ "maxRecords",
58
+ "sortOrder",
59
+ "permissions",
60
+ "master",
61
+ "linkFields",
62
+ "expose",
63
+ "dynamic",
64
+ "unique",
65
+ "overrides",
66
+ "distinct",
67
+ "aggregates",
68
+ "groupBy",
69
+ "where",
70
+ "fields",
71
+ ];
72
+ for (let key of order) {
73
+ let value = options[key];
74
+ if (value === undefined || value === null || value === "" || value === false)
75
+ continue;
76
+ if (key === "fields" && value === true)
77
+ continue;
78
+ if (LIST_KEYS.has(key) && typeof value === "string") {
79
+ let items = value.split(",").filter(Boolean);
80
+ lines.push(`${key}:`);
81
+ for (let item of items) {
82
+ lines.push(` - ${item}`);
83
+ }
84
+ }
85
+ else {
86
+ lines.push(`${key}: ${value}`);
87
+ }
88
+ }
89
+ lines.push("*/");
90
+ return lines.join("\n");
91
+ }
92
+ /**
93
+ * Find and parse the `/* af:config ... *\/` block from generated file content.
94
+ * Returns `{ resource, options }` or `null` if no block is found.
95
+ */
96
+ export function parseYamlConfig(fileContent) {
97
+ let startMarker = "/* af:config\n";
98
+ let endMarker = "\n*/";
99
+ let startIdx = fileContent.indexOf(startMarker);
100
+ if (startIdx === -1)
101
+ return null;
102
+ let blockStart = startIdx + startMarker.length;
103
+ let endIdx = fileContent.indexOf(endMarker, blockStart);
104
+ if (endIdx === -1)
105
+ return null;
106
+ let block = fileContent.slice(blockStart, endIdx);
107
+ let lines = block.split("\n");
108
+ let resource = "";
109
+ let raw = {};
110
+ let currentListKey = null;
111
+ for (let line of lines) {
112
+ if (line.startsWith(" - ") && currentListKey) {
113
+ let arr = raw[currentListKey];
114
+ if (Array.isArray(arr)) {
115
+ arr.push(line.slice(4));
116
+ }
117
+ continue;
118
+ }
119
+ let colonIdx = line.indexOf(": ");
120
+ if (colonIdx === -1 && line.endsWith(":")) {
121
+ let key = line.slice(0, -1);
122
+ raw[key] = [];
123
+ currentListKey = key;
124
+ continue;
125
+ }
126
+ currentListKey = null;
127
+ if (colonIdx !== -1) {
128
+ let key = line.slice(0, colonIdx);
129
+ let value = line.slice(colonIdx + 2);
130
+ raw[key] = value;
131
+ }
132
+ }
133
+ resource = raw["resource"];
134
+ if (!resource)
135
+ return null;
136
+ function str(key) {
137
+ let v = raw[key];
138
+ if (Array.isArray(v))
139
+ return v.join(",");
140
+ return v;
141
+ }
142
+ function bool(key) {
143
+ let v = raw[key];
144
+ if (v === "true")
145
+ return true;
146
+ if (v === "false")
147
+ return false;
148
+ return undefined;
149
+ }
150
+ function list(key) {
151
+ let v = raw[key];
152
+ if (Array.isArray(v))
153
+ return v.join(",");
154
+ if (typeof v === "string")
155
+ return v;
156
+ return undefined;
157
+ }
158
+ let options = {
159
+ server: "dev.obet.no",
160
+ id: (str("id") ?? "dsDataObject"),
161
+ global: bool("global") ?? false,
162
+ dynamic: bool("dynamic") ?? false,
163
+ types: bool("types"),
164
+ maxRecords: str("maxRecords"),
165
+ sortOrder: list("sortOrder"),
166
+ permissions: str("permissions"),
167
+ master: str("master"),
168
+ linkFields: list("linkFields"),
169
+ expose: (() => {
170
+ let v = raw["expose"];
171
+ if (v === "true")
172
+ return true;
173
+ if (v === "false" || v === undefined)
174
+ return undefined;
175
+ return v;
176
+ })(),
177
+ unique: str("unique"),
178
+ overrides: list("overrides"),
179
+ distinct: bool("distinct"),
180
+ aggregates: list("aggregates"),
181
+ groupBy: list("groupBy"),
182
+ where: str("where"),
183
+ fields: list("fields") ?? false,
184
+ };
185
+ return { resource, options };
186
+ }
187
+ // ---------------------------------------------------------------------------
188
+ // Code generation helpers
189
+ // ---------------------------------------------------------------------------
190
+ /**
191
+ * Convert Appframe (SQL) data types to typescript/field definition types
192
+ */
193
+ function afTypeToTsType(type, dateStyle = false) {
194
+ switch (type) {
195
+ case "bigint":
196
+ return "bigint";
197
+ case "int":
198
+ case "decimal":
199
+ case "smallint":
200
+ case "tinyint":
201
+ case "float":
202
+ case "numeric":
203
+ return "number";
204
+ case "bit":
205
+ return "boolean";
206
+ case "datetime2":
207
+ case "datetime":
208
+ case "smalldatetime":
209
+ case "date":
210
+ if (dateStyle === "ts") {
211
+ return "Date";
212
+ }
213
+ else if (dateStyle === "ts-proc") {
214
+ return "string | Date";
215
+ }
216
+ else {
217
+ return type === "date" ? "date" : "datetime";
218
+ }
219
+ default:
220
+ return "string";
221
+ }
222
+ }
223
+ /**
224
+ * Walk from startDir up toward stopDir (inclusive) looking for custom.d.ts or custom.ts.
225
+ * Returns the absolute path without extension, or null if not found.
226
+ */
227
+ function findCustomDts(startDir, stopDir) {
228
+ let current = resolve(startDir);
229
+ let stop = resolve(stopDir);
230
+ while (true) {
231
+ for (let name of ["custom.d.ts", "custom.ts"]) {
232
+ let candidate = resolve(current, name);
233
+ if (existsSync(candidate)) {
234
+ return candidate.replace(/\.(d\.ts|ts)$/, "");
235
+ }
236
+ }
237
+ if (current === stop)
238
+ break;
239
+ let parent = dirname(current);
240
+ if (parent === current)
241
+ break;
242
+ current = parent;
243
+ }
244
+ return null;
245
+ }
246
+ /**
247
+ * Compute the relative import specifier for "custom" from the given output file.
248
+ * Returns a POSIX-style path (e.g. "../custom" or "./custom").
249
+ */
250
+ export function getCustomImportPath(outputPath) {
251
+ let outputDir = dirname(resolve(outputPath));
252
+ let found = findCustomDts(outputDir, process.cwd());
253
+ if (!found) {
254
+ return "./custom";
255
+ }
256
+ let rel = relative(outputDir, found).split(sep).join("/");
257
+ if (!rel.startsWith(".")) {
258
+ rel = `./${rel}`;
259
+ }
260
+ return rel;
261
+ }
262
+ export function getProcedureDefinition(name, procDefinition, options) {
263
+ let parameters = [];
264
+ let typeOverrides = {};
265
+ if (options.typesJsonParamOverrides) {
266
+ Object.assign(typeOverrides, options.typesJsonParamOverrides);
267
+ }
268
+ if (options.overrides) {
269
+ let overrides = options.overrides.split(",");
270
+ for (let override of overrides) {
271
+ let [param, type] = override.split(":");
272
+ typeOverrides[param] = type;
273
+ }
274
+ }
275
+ for (let parameter of procDefinition.Parameters) {
276
+ parameters.push({
277
+ name: parameter.ParamName,
278
+ type: afTypeToTsType(parameter.TypeName, "field"),
279
+ hasDefault: parameter.has_default_value,
280
+ required: !parameter.has_default_value && !parameter.is_nullable,
281
+ });
282
+ }
283
+ let output = [];
284
+ if (!options.global) {
285
+ output.push(`import { ProcedureAPI } from "@olenbetong/appframe-data";`);
286
+ if (options.expose) {
287
+ output.push(`import { expose } from "@olenbetong/appframe-core";`);
288
+ }
289
+ output.push("");
290
+ }
291
+ let paramTypeName = "ProcParams";
292
+ let procName = "proc";
293
+ if (options.id) {
294
+ procName = options.id;
295
+ paramTypeName = `${options.id}Params`;
296
+ if (paramTypeName.startsWith("proc")) {
297
+ paramTypeName = paramTypeName.substring(4);
298
+ }
299
+ }
300
+ if (options.types) {
301
+ if (procDefinition.Parameters.length > 0) {
302
+ let typeOutput = [`export type ${paramTypeName} = {`];
303
+ for (let parameter of procDefinition.Parameters) {
304
+ let type = typeOverrides[parameter.ParamName];
305
+ let name = parameter.ParamName;
306
+ if (!type) {
307
+ type = afTypeToTsType(parameter.TypeName, "ts-proc");
308
+ }
309
+ if (parameter.has_default_value || parameter.is_nullable) {
310
+ type += " | null";
311
+ name += "?";
312
+ }
313
+ typeOutput.push(`\t${name}: ${type}`);
314
+ }
315
+ typeOutput.push("};");
316
+ output.push(typeOutput.join("\n"));
317
+ output.push("");
318
+ }
319
+ else {
320
+ output.push(`export type ${paramTypeName} = null | undefined | Record<string, unknown>;\n`);
321
+ }
322
+ }
323
+ output.push(`export const ${procName} = new ${options.global ? "af." : ""}ProcedureAPI${options.types ? `<${paramTypeName}, ${options.typesJsonReturnType ?? "{ Table?: unknown[] }"}>` : ""}({
324
+ procedureId: "${name}",
325
+ parameters: ${JSON.stringify(parameters, null, 2)},
326
+ timeout: 30000
327
+ });`);
328
+ if (options.expose) {
329
+ output.push("");
330
+ output.push(`${options.global ? "af.common." : ""}expose("af.article.procedures.${procName}", ${procName}, { overwrite: true });`);
331
+ }
332
+ let outputStr = output.join("\n");
333
+ if (outputStr.includes("Custom.")) {
334
+ let customPath = options.output ? getCustomImportPath(options.output) : "./custom";
335
+ if (options.global) {
336
+ outputStr = `import type * as Custom from "${customPath}";\n${outputStr}`;
337
+ }
338
+ else {
339
+ let firstNewline = outputStr.indexOf("\n");
340
+ outputStr =
341
+ `${outputStr.substring(0, firstNewline + 1)}import type * as Custom from "${customPath}";\n` +
342
+ outputStr.substring(firstNewline + 1);
343
+ }
344
+ }
345
+ return outputStr;
346
+ }
347
+ export function getDataObjectDefinition(name, viewDefinition, options) {
348
+ let fields = [];
349
+ let includeFields = typeof options.fields === "string" ? (options.fields?.split(",").filter((f) => !!f) ?? []) : [];
350
+ let aggregates = {};
351
+ if (options.aggregates) {
352
+ for (let aggregateDef of options.aggregates.split(",")) {
353
+ let [field, aggregate] = aggregateDef.split(":");
354
+ aggregates[field] = aggregate;
355
+ }
356
+ }
357
+ for (let field of viewDefinition.Parameters) {
358
+ if (includeFields.length > 0 && !includeFields.includes(field.Name)) {
359
+ continue;
360
+ }
361
+ let fieldDefinition = {
362
+ name: field.Name,
363
+ type: field.DataType,
364
+ nullable: field.Nullable,
365
+ };
366
+ if (field.HasDefault) {
367
+ fieldDefinition.hasDefault = true;
368
+ }
369
+ if (field.Computed) {
370
+ fieldDefinition.computed = true;
371
+ }
372
+ if (field.Identity) {
373
+ fieldDefinition.identity = true;
374
+ }
375
+ if (aggregates[field.Name]) {
376
+ fieldDefinition.aggregate = aggregates[field.Name];
377
+ }
378
+ fields.push(fieldDefinition);
379
+ }
380
+ let api = "generateApiDataObject";
381
+ let types = "";
382
+ let typeName = `${options.id}Record`;
383
+ let typeOverrides = {};
384
+ if (options.typesJsonParamOverrides) {
385
+ Object.assign(typeOverrides, options.typesJsonParamOverrides);
386
+ }
387
+ if (options.overrides) {
388
+ let overrides = options.overrides.split(",");
389
+ for (let override of overrides) {
390
+ let [param, type] = override.split(":");
391
+ typeOverrides[param] = type;
392
+ }
393
+ }
394
+ if (typeName.startsWith("ds")) {
395
+ typeName = typeName.substring(2);
396
+ }
397
+ if (options.types) {
398
+ api = `generateApiDataObject<${typeName}>`;
399
+ let fieldTypes = "";
400
+ for (let field of fields) {
401
+ let type = typeOverrides[field.name];
402
+ if (!type) {
403
+ type = afTypeToTsType(field.type, "ts");
404
+ if (field.nullable) {
405
+ type += " | null";
406
+ }
407
+ }
408
+ fieldTypes += ` ${field.name}: ${type};\n`;
409
+ }
410
+ types = `export type ${typeName} = {
411
+ ${fieldTypes}}`;
412
+ }
413
+ if (options.global) {
414
+ api = `af.data.${api}`;
415
+ }
416
+ let output = "";
417
+ if (!options.global) {
418
+ output += `import { generateApiDataObject${options.sortOrder ? ", SortOrder" : ""} } from "@olenbetong/appframe-data";`;
419
+ if (options.expose) {
420
+ output += `\nimport { expose } from "@olenbetong/appframe-core";`;
421
+ }
422
+ output += "\n\n";
423
+ }
424
+ if (options.master && options.master.indexOf(":") > 0) {
425
+ let [name, path] = options.master.split(":");
426
+ output += `import { ${name} } from "${path}";\n\n`;
427
+ }
428
+ if (options.types) {
429
+ output += `${types}\n\n`;
430
+ }
431
+ let linkFields = "";
432
+ let dsOptions = [];
433
+ dsOptions.push(`resource: "${name}"`);
434
+ if (options.unique) {
435
+ dsOptions.push(`uniqueName: "${options.unique}"`);
436
+ }
437
+ dsOptions.push(`id: "${options.id}"`);
438
+ if (options.master && options.linkFields) {
439
+ dsOptions.push(`masterDataObject: ${options.master.split(":")[0]}`);
440
+ let fields = options.linkFields.split(",");
441
+ linkFields = `linkFields: {\n\t\t${fields
442
+ .map((field) => {
443
+ let [thisField, masterField] = field.split(":");
444
+ return `${thisField}: "${masterField ?? thisField}",`;
445
+ })
446
+ .join("\n\t\t")}\n\t}`;
447
+ dsOptions.push(linkFields);
448
+ }
449
+ dsOptions.push(`allowUpdate: ${options.permissions?.includes("U") ?? false}`);
450
+ dsOptions.push(`allowInsert: ${options.permissions?.includes("I") ?? false}`);
451
+ dsOptions.push(`allowDelete: ${options.permissions?.includes("D") ?? false}`);
452
+ dsOptions.push(`dynamicLoading: ${options.dynamic || false}`);
453
+ let fieldsOption = fields.map((field) => ({
454
+ ...field,
455
+ type: afTypeToTsType(field.type, "field"),
456
+ }));
457
+ dsOptions.push(`fields: ${JSON.stringify(fieldsOption, null, 2).split("\n").join("\n\t")}`);
458
+ let parametersOption = [`maxRecords: ${options.maxRecords}`];
459
+ if (options.sortOrder) {
460
+ let sorts = options.sortOrder.split(",");
461
+ let sortPrefix = options.global ? "af.data.SortOrder." : "SortOrder.";
462
+ let sortOrder = sorts
463
+ .map((sort) => {
464
+ let [field, order = "asc"] = sort.split(":");
465
+ switch (order.toLocaleLowerCase()) {
466
+ case "asc":
467
+ order = "Asc";
468
+ break;
469
+ case "desc":
470
+ order = "Desc";
471
+ break;
472
+ case "ascnullslast":
473
+ order = "AscNullsLast";
474
+ break;
475
+ case "descnullsfirst":
476
+ order = "DescNullsFirst";
477
+ break;
478
+ }
479
+ return `{ ${field}: ${sortPrefix}${order} }`;
480
+ })
481
+ .join(", ");
482
+ parametersOption.push(`sortOrder: [${sortOrder}]`);
483
+ }
484
+ if (options.groupBy) {
485
+ parametersOption.push(`groupBy: ${JSON.stringify(options.groupBy.split(","))}`);
486
+ }
487
+ if (options.where) {
488
+ parametersOption.push(`whereClause: "${options.where}"`);
489
+ }
490
+ if (options.distinct) {
491
+ parametersOption.push(`distinctRows: true`);
492
+ }
493
+ dsOptions.push(`parameters: {\n\t\t${parametersOption.join(",\n\t\t")}\n\t}`);
494
+ output += `export const ${options.id} = ${api}({
495
+ ${dsOptions.join(",\n\t")}
496
+ });
497
+ `;
498
+ if (options.expose) {
499
+ let id = typeof options.expose === "string" ? options.expose : options.id;
500
+ if (options.global) {
501
+ output += `\naf.common.expose("af.article.dataObjects.${id}", ${options.id}, { overwrite: true });`;
502
+ }
503
+ else {
504
+ output += `\nexpose("af.article.dataObjects.${id}", ${options.id}, { overwrite: true });`;
505
+ }
506
+ }
507
+ if (output.includes("Custom.")) {
508
+ let customPath = options.output ? getCustomImportPath(options.output) : "./custom";
509
+ if (options.global) {
510
+ output = `import type * as Custom from "${customPath}";\n${output}`;
511
+ }
512
+ else {
513
+ let firstNewline = output.indexOf("\n");
514
+ output =
515
+ `${output.substring(0, firstNewline + 1)}import type * as Custom from "${customPath}";\n` +
516
+ output.substring(firstNewline + 1);
517
+ }
518
+ }
519
+ return output;
520
+ }
521
+ // ---------------------------------------------------------------------------
522
+ // Server interaction
523
+ // ---------------------------------------------------------------------------
524
+ /**
525
+ * Fetch the resource definition from an authenticated Appframe client.
526
+ */
527
+ export async function fetchResourceDefinition(client, resourceName) {
528
+ let response = await client.fetch("/api/data", {
529
+ method: "POST",
530
+ headers: {
531
+ "Content-Type": "application/json",
532
+ Accept: "application/json",
533
+ },
534
+ body: JSON.stringify({
535
+ operation: "resource-definition",
536
+ resourceName,
537
+ }),
538
+ });
539
+ if (!response.ok) {
540
+ throw new Error(`Failed to fetch resource definition for '${resourceName}': ${response.status} ${response.statusText}`);
541
+ }
542
+ const json = await response.json();
543
+ return json.success ?? json;
544
+ }
545
+ /**
546
+ * Fetch the resource definition from the server, read types.json overrides, and
547
+ * return the generated TypeScript content string (without any header comment).
548
+ *
549
+ * @param resourceName - Pre-resolved resource database object ID (e.g. `aviw_QA_Documents`)
550
+ * @param options - Generation options
551
+ * @param client - Authenticated Appframe client
552
+ */
553
+ export async function fetchAndGenerate(resourceName, options, client) {
554
+ let definition = await fetchResourceDefinition(client, resourceName);
555
+ definition.Parameters = (definition.Parameters ?? []).filter((p) => !["CUT", "CDL"].includes(p.Name));
556
+ let typesJson = {};
557
+ try {
558
+ typesJson = await importJson("./types.json", true);
559
+ }
560
+ catch {
561
+ // no types.json — that's fine
562
+ }
563
+ options.typesJsonParamOverrides = typesJson.parameterTypes?.[resourceName] ?? {};
564
+ // Structured returnType from resources.yaml takes precedence over the raw types.json string
565
+ options.typesJsonReturnType = options.typesJsonReturnType ?? typesJson.procedureReturnTypes?.[resourceName] ?? null;
566
+ return definition.ObjectType === "V"
567
+ ? getDataObjectDefinition(resourceName, definition, options)
568
+ : getProcedureDefinition(resourceName, definition, options);
569
+ }
@@ -0,0 +1,83 @@
1
+ import type { CLIOptions } from "./resourceGenerate.js";
2
+ export type ReturnTypeField = {
3
+ /** TypeScript field name */
4
+ name: string;
5
+ /** TypeScript type, e.g. `string`, `number | null`, `Date` */
6
+ type: string;
7
+ };
8
+ export type ReturnTypeTable = {
9
+ /** Property key when multiple result-sets are returned. Omit for single-table procedures. */
10
+ table?: string;
11
+ fields: ReturnTypeField[];
12
+ };
13
+ /**
14
+ * Build an inline TypeScript type string from a structured returnType definition.
15
+ *
16
+ * The server always returns a DataSet serialized as `{ Table: T[], Table1: U[], ... }`.
17
+ * The first table is always `"Table"`, subsequent ones are `"Table1"`, `"Table2"`, etc.
18
+ * An explicit `table` name on the entry overrides the auto-generated key (for procedures
19
+ * that assign custom DataTable.TableName values).
20
+ */
21
+ export declare function returnTypeToString(returnType: ReturnTypeTable[]): string;
22
+ export type ResourceEntry = {
23
+ /** Data object or procedure identifier used in the generated code (e.g. `dsAccountGroups`) */
24
+ id: string;
25
+ /** Database object ID to fetch the definition from (e.g. `atbv_Accounting_SubsidiaryLedgerGroups`) */
26
+ resource: string;
27
+ /** Output file path relative to the project root (e.g. `src/data/dsAccountGroups.ts`) */
28
+ output: string;
29
+ /** Use `af.data.generateApiDataObject` / `new af.ProcedureAPI` globals instead of imports */
30
+ global?: boolean;
31
+ /** Emit TypeScript type definitions */
32
+ types?: boolean;
33
+ /** Permissions to set: I = insert, U = update, D = delete (e.g. `IUD`) */
34
+ permissions?: string;
35
+ /** Maximum records to fetch (default `50`; use `-1` for all) */
36
+ maxRecords?: number;
37
+ /** Sort order as a list of sort objects */
38
+ sortOrder?: Array<{
39
+ field: string;
40
+ direction?: string;
41
+ }>;
42
+ /** Name of master data object (optionally `name:importPath` to import from another file) */
43
+ master?: string;
44
+ /** Link fields to master data object (comma-separated or array) */
45
+ linkFields?: string | string[];
46
+ /** Expose the object on `af.article.dataObjects` / `af.article.procedures` */
47
+ expose?: boolean | string;
48
+ /** Enable dynamic loading */
49
+ dynamic?: boolean;
50
+ /** Unique table name for update/delete */
51
+ unique?: string;
52
+ /** Type overrides for fields/parameters: comma-separated or array of `field:type` */
53
+ overrides?: string | string[];
54
+ /** Fetch distinct rows */
55
+ distinct?: boolean;
56
+ /** Aggregate bindings for non-grouped fields */
57
+ aggregates?: Array<{
58
+ field: string;
59
+ aggregate: string;
60
+ }>;
61
+ /** Group-by fields */
62
+ groupBy?: string[];
63
+ /** Initial where clause */
64
+ where?: string;
65
+ /** Fields to include: comma-separated or array of field names */
66
+ fields?: string | string[];
67
+ /** Structured return type definition for procedures (generates inline TS type) */
68
+ returnType?: ReturnTypeTable[];
69
+ };
70
+ export type ResourcesConfig = {
71
+ /** Optional server hostname override (defaults to `appframe.proxy.hostname` from package.json) */
72
+ server?: string;
73
+ dataObjects?: ResourceEntry[];
74
+ procedures?: ResourceEntry[];
75
+ };
76
+ export declare const RESOURCES_CONFIG_FILE = "resources.yaml";
77
+ export declare function getResourcesConfigPath(filePath?: string): string;
78
+ export declare function readResourcesConfig(filePath?: string): Promise<ResourcesConfig>;
79
+ export declare function writeResourcesConfig(config: ResourcesConfig, filePath?: string): Promise<void>;
80
+ /** Convert a `ResourceEntry` into a `CLIOptions`-compatible object for code generation. */
81
+ export declare function entryToCLIOptions(entry: ResourceEntry, hostname: string): CLIOptions;
82
+ /** Convert a `CLIOptions` back to a `ResourceEntry` for saving to YAML. */
83
+ export declare function cliOptionsToEntry(resource: string, options: CLIOptions): ResourceEntry;