@olenbetong/appframe-vite 6.1.2 → 6.2.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.
- package/README.md +115 -5
- package/lib/cli-resources-add.d.ts +1 -0
- package/lib/cli-resources-add.js +184 -0
- package/lib/cli-resources-edit.d.ts +1 -0
- package/lib/cli-resources-edit.js +177 -0
- package/lib/cli-resources-generate.d.ts +1 -0
- package/lib/cli-resources-generate.js +44 -0
- package/lib/cli.js +40 -16
- package/lib/index.js +27 -0
- package/lib/resourceGenerate.d.ts +63 -0
- package/lib/resourceGenerate.js +567 -0
- package/lib/resourcesConfig.d.ts +55 -0
- package/lib/resourcesConfig.js +111 -0
- package/package.json +12 -3
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { Client } from "@olenbetong/appframe-data";
|
|
2
|
+
/**
|
|
3
|
+
* Format a file with Biome if available. Searches for `biome` in the nearest
|
|
4
|
+
* node_modules/.bin up from cwd, then falls back to the global PATH.
|
|
5
|
+
* Silently skips if Biome is not found or formatting fails.
|
|
6
|
+
*/
|
|
7
|
+
export declare function formatWithBiome(filePath: string): Promise<void>;
|
|
8
|
+
export type CLIOptions = {
|
|
9
|
+
server: string;
|
|
10
|
+
types?: boolean;
|
|
11
|
+
global: boolean;
|
|
12
|
+
id: string;
|
|
13
|
+
fields: string | boolean;
|
|
14
|
+
permissions?: string;
|
|
15
|
+
maxRecords?: string;
|
|
16
|
+
sortOrder?: string;
|
|
17
|
+
master?: string;
|
|
18
|
+
linkFields?: string;
|
|
19
|
+
expose?: string | boolean;
|
|
20
|
+
dynamic: boolean;
|
|
21
|
+
unique?: string;
|
|
22
|
+
overrides?: string;
|
|
23
|
+
distinct?: boolean;
|
|
24
|
+
aggregates?: string;
|
|
25
|
+
groupBy?: string;
|
|
26
|
+
where?: string;
|
|
27
|
+
output?: string;
|
|
28
|
+
typesJsonParamOverrides?: Record<string, string>;
|
|
29
|
+
typesJsonReturnType?: string | null;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Serialize the generation options to a `/* af:config ... *\/` YAML comment block.
|
|
33
|
+
* List-type options (fields, sortOrder, etc.) are emitted as proper YAML lists.
|
|
34
|
+
*/
|
|
35
|
+
export declare function buildYamlConfig(resource: string, options: CLIOptions): string;
|
|
36
|
+
/**
|
|
37
|
+
* Find and parse the `/* af:config ... *\/` block from generated file content.
|
|
38
|
+
* Returns `{ resource, options }` or `null` if no block is found.
|
|
39
|
+
*/
|
|
40
|
+
export declare function parseYamlConfig(fileContent: string): {
|
|
41
|
+
resource: string;
|
|
42
|
+
options: CLIOptions;
|
|
43
|
+
} | null;
|
|
44
|
+
/**
|
|
45
|
+
* Compute the relative import specifier for "custom" from the given output file.
|
|
46
|
+
* Returns a POSIX-style path (e.g. "../custom" or "./custom").
|
|
47
|
+
*/
|
|
48
|
+
export declare function getCustomImportPath(outputPath: string): string;
|
|
49
|
+
export declare function getProcedureDefinition(name: string, procDefinition: any, options: CLIOptions): string;
|
|
50
|
+
export declare function getDataObjectDefinition(name: string, viewDefinition: any, options: CLIOptions): string;
|
|
51
|
+
/**
|
|
52
|
+
* Fetch the resource definition from an authenticated Appframe client.
|
|
53
|
+
*/
|
|
54
|
+
export declare function fetchResourceDefinition(client: Client, resourceName: string): Promise<any>;
|
|
55
|
+
/**
|
|
56
|
+
* Fetch the resource definition from the server, read types.json overrides, and
|
|
57
|
+
* return the generated TypeScript content string (without any header comment).
|
|
58
|
+
*
|
|
59
|
+
* @param resourceName - Pre-resolved resource database object ID (e.g. `aviw_QA_Documents`)
|
|
60
|
+
* @param options - Generation options
|
|
61
|
+
* @param client - Authenticated Appframe client
|
|
62
|
+
*/
|
|
63
|
+
export declare function fetchAndGenerate(resourceName: string, options: CLIOptions, client: Client): Promise<string>;
|
|
@@ -0,0 +1,567 @@
|
|
|
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 ?? "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
|
+
return response.json();
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Fetch the resource definition from the server, read types.json overrides, and
|
|
546
|
+
* return the generated TypeScript content string (without any header comment).
|
|
547
|
+
*
|
|
548
|
+
* @param resourceName - Pre-resolved resource database object ID (e.g. `aviw_QA_Documents`)
|
|
549
|
+
* @param options - Generation options
|
|
550
|
+
* @param client - Authenticated Appframe client
|
|
551
|
+
*/
|
|
552
|
+
export async function fetchAndGenerate(resourceName, options, client) {
|
|
553
|
+
let definition = await fetchResourceDefinition(client, resourceName);
|
|
554
|
+
definition.Parameters = definition.Parameters.filter((p) => !["CUT", "CDL"].includes(p.Name));
|
|
555
|
+
let typesJson = {};
|
|
556
|
+
try {
|
|
557
|
+
typesJson = await importJson("./types.json", true);
|
|
558
|
+
}
|
|
559
|
+
catch {
|
|
560
|
+
// no types.json — that's fine
|
|
561
|
+
}
|
|
562
|
+
options.typesJsonParamOverrides = typesJson.parameterTypes?.[resourceName] ?? {};
|
|
563
|
+
options.typesJsonReturnType = typesJson.procedureReturnTypes?.[resourceName] ?? null;
|
|
564
|
+
return definition.ObjectType === "V"
|
|
565
|
+
? getDataObjectDefinition(resourceName, definition, options)
|
|
566
|
+
: getProcedureDefinition(resourceName, definition, options);
|
|
567
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { CLIOptions } from "./resourceGenerate.js";
|
|
2
|
+
export type ResourceEntry = {
|
|
3
|
+
/** Data object or procedure identifier used in the generated code (e.g. `dsAccountGroups`) */
|
|
4
|
+
id: string;
|
|
5
|
+
/** Database object ID to fetch the definition from (e.g. `atbv_Accounting_SubsidiaryLedgerGroups`) */
|
|
6
|
+
resource: string;
|
|
7
|
+
/** Output file path relative to the project root (e.g. `src/data/dsAccountGroups.ts`) */
|
|
8
|
+
output: string;
|
|
9
|
+
/** Use `af.data.generateApiDataObject` / `new af.ProcedureAPI` globals instead of imports */
|
|
10
|
+
global?: boolean;
|
|
11
|
+
/** Emit TypeScript type definitions */
|
|
12
|
+
types?: boolean;
|
|
13
|
+
/** Permissions to set: I = insert, U = update, D = delete (e.g. `IUD`) */
|
|
14
|
+
permissions?: string;
|
|
15
|
+
/** Maximum records to fetch (default `50`; use `-1` for all) */
|
|
16
|
+
maxRecords?: number;
|
|
17
|
+
/** Sort order: comma-separated list of `field` or `field:Asc|Desc` */
|
|
18
|
+
sortOrder?: string | string[];
|
|
19
|
+
/** Name of master data object (optionally `name:importPath` to import from another file) */
|
|
20
|
+
master?: string;
|
|
21
|
+
/** Link fields to master data object (comma-separated or array) */
|
|
22
|
+
linkFields?: string | string[];
|
|
23
|
+
/** Expose the object on `af.article.dataObjects` / `af.article.procedures` */
|
|
24
|
+
expose?: boolean | string;
|
|
25
|
+
/** Enable dynamic loading */
|
|
26
|
+
dynamic?: boolean;
|
|
27
|
+
/** Unique table name for update/delete */
|
|
28
|
+
unique?: string;
|
|
29
|
+
/** Type overrides for fields/parameters: comma-separated or array of `field:type` */
|
|
30
|
+
overrides?: string | string[];
|
|
31
|
+
/** Fetch distinct rows */
|
|
32
|
+
distinct?: boolean;
|
|
33
|
+
/** Aggregate bindings for non-grouped fields: comma-separated or array of `field:AGG` */
|
|
34
|
+
aggregates?: string | string[];
|
|
35
|
+
/** Group-by fields: comma-separated or array */
|
|
36
|
+
groupBy?: string | string[];
|
|
37
|
+
/** Initial where clause */
|
|
38
|
+
where?: string;
|
|
39
|
+
/** Fields to include: comma-separated or array of field names */
|
|
40
|
+
fields?: string | string[];
|
|
41
|
+
};
|
|
42
|
+
export type ResourcesConfig = {
|
|
43
|
+
/** Optional server hostname override (defaults to `appframe.proxy.hostname` from package.json) */
|
|
44
|
+
server?: string;
|
|
45
|
+
dataObjects?: ResourceEntry[];
|
|
46
|
+
procedures?: ResourceEntry[];
|
|
47
|
+
};
|
|
48
|
+
export declare const RESOURCES_CONFIG_FILE = "resources.yaml";
|
|
49
|
+
export declare function getResourcesConfigPath(filePath?: string): string;
|
|
50
|
+
export declare function readResourcesConfig(filePath?: string): Promise<ResourcesConfig>;
|
|
51
|
+
export declare function writeResourcesConfig(config: ResourcesConfig, filePath?: string): Promise<void>;
|
|
52
|
+
/** Convert a `ResourceEntry` into a `CLIOptions`-compatible object for code generation. */
|
|
53
|
+
export declare function entryToCLIOptions(entry: ResourceEntry, hostname: string): CLIOptions;
|
|
54
|
+
/** Convert a `CLIOptions` back to a `ResourceEntry` for saving to YAML. */
|
|
55
|
+
export declare function cliOptionsToEntry(resource: string, options: CLIOptions): ResourceEntry;
|