@olenbetong/appframe-cli 4.4.10 → 4.5.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.
@@ -1,348 +1,27 @@
1
- import type { FieldDefinition } from "@olenbetong/appframe-data";
1
+ import { writeFile } from "node:fs/promises";
2
+ import { Client } from "@olenbetong/appframe-data";
3
+ import {
4
+ buildYamlConfig,
5
+ type CLIOptions,
6
+ fetchAndGenerate,
7
+ formatWithBiome,
8
+ } from "@olenbetong/appframe-vite/resources";
9
+ import { config } from "dotenv";
2
10
  import inquirer from "inquirer";
3
-
4
11
  import { Command } from "./lib/Command.js";
5
12
  import { importJson } from "./lib/importJson.js";
6
13
  import { Server } from "./lib/Server.js";
7
14
 
8
- const appPkg = await importJson("../package.json");
9
-
10
- /**
11
- * Convert Appframe (SQL) data types to typescript/field definition types
12
- * @param {string} type
13
- * @param {"ts" | "ts-proc" | "field"} dateStyle
14
- * @returns
15
- */
16
- function afTypeToTsType(type: string | undefined, dateStyle: "ts" | "ts-proc" | "field" | boolean = false) {
17
- switch (type) {
18
- case "bigint":
19
- return "bigint";
20
- case "int":
21
- case "decimal":
22
- case "smallint":
23
- case "tinyint":
24
- case "float":
25
- case "numeric":
26
- return "number";
27
- case "bit":
28
- return "boolean";
29
- case "datetime2":
30
- case "datetime":
31
- case "smalldatetime":
32
- case "date":
33
- if (dateStyle === "ts") {
34
- return "Date";
35
- } else if (dateStyle === "ts-proc") {
36
- return "string | Date";
37
- } else {
38
- return type === "date" ? "date" : "datetime";
39
- }
40
- default:
41
- return "string";
42
- }
43
- }
44
-
45
- interface ProcedureParameter {
46
- name: string;
47
- type: string;
48
- hasDefault: boolean;
49
- required: boolean;
50
- }
51
-
52
- function getProcedureDefinition(name: string, procDefinition: any, options: CLIOptions) {
53
- let parameters: ProcedureParameter[] = [];
54
- let typeOverrides: Record<string, string> = {};
55
-
56
- if (options.overrides) {
57
- let overrides = options.overrides.split(",");
58
- for (let override of overrides) {
59
- let [param, type] = override.split(":");
60
- typeOverrides[param] = type;
61
- }
62
- }
63
-
64
- for (let parameter of procDefinition.Parameters) {
65
- parameters.push({
66
- name: parameter.ParamName,
67
- type: afTypeToTsType(parameter.TypeName, "field"),
68
- hasDefault: parameter.has_default_value,
69
- required: !parameter.has_default_value && !parameter.is_nullable,
70
- });
71
- }
72
-
73
- let output: string[] = [];
74
- if (!options.global) {
75
- output.push(`import { ProcedureAPI } from "@olenbetong/appframe-data";`);
76
-
77
- if (options.expose) {
78
- output.push(`import { expose } from "@olenbetong/appframe-core";`);
79
- }
80
-
81
- output.push("");
82
- }
83
- let paramTypeName = "ProcParams";
84
- let procName = "proc";
85
- if (options.id) {
86
- procName = options.id;
87
- paramTypeName = `${options.id}Params`;
88
- if (paramTypeName.startsWith("proc")) {
89
- paramTypeName = paramTypeName.substring(4);
90
- }
91
- }
92
-
93
- if (options.types) {
94
- if (procDefinition.Parameters.length > 0) {
95
- let typeOutput = [`export type ${paramTypeName} = {`];
96
- for (let parameter of procDefinition.Parameters) {
97
- let type = typeOverrides[parameter.ParamName];
98
- let name = parameter.ParamName;
99
-
100
- if (!type) {
101
- type = afTypeToTsType(parameter.TypeName, "ts-proc");
102
- }
103
-
104
- if (parameter.has_default_value || parameter.is_nullable) {
105
- type += " | null";
106
- name += "?";
107
- }
108
-
109
- typeOutput.push(`\t${name}: ${type}`);
110
- }
111
- typeOutput.push("};");
112
- output.push(typeOutput.join("\n"));
113
- output.push("");
114
- } else {
115
- output.push(`export type ${paramTypeName} = null | undefined | Record<string, unknown>;\n`);
116
- }
117
- }
118
-
119
- output.push(`export const ${procName} = new ${
120
- options.global ? "af." : ""
121
- }ProcedureAPI${options.types ? `<${paramTypeName}, unknown>` : ""}({
122
- procedureId: "${name}",
123
- parameters: ${JSON.stringify(parameters, null, 2)},
124
- timeout: 30000
125
- });`);
126
-
127
- if (options.expose) {
128
- output.push("");
129
- output.push(
130
- `${
131
- options.global ? "af.common." : ""
132
- }expose("af.article.procedures.${procName}", ${procName}, { overwrite: true });`,
133
- );
134
- }
135
-
136
- return output.join("\n");
137
- }
138
-
139
- function getDataObjectDefinition(name: string, viewDefinition: any, options: CLIOptions) {
140
- let fields: FieldDefinition[] = [];
141
- let includeFields = typeof options.fields === "string" ? (options.fields?.split(",").filter((f) => !!f) ?? []) : [];
142
-
143
- let aggregates: Record<string, string> = {};
144
- if (options.aggregates) {
145
- for (let aggregateDef of options.aggregates.split(",")) {
146
- let [field, aggregate] = aggregateDef.split(":");
147
- aggregates[field] = aggregate;
148
- }
149
- }
150
-
151
- for (let field of viewDefinition.Parameters) {
152
- if (includeFields.length > 0 && !includeFields.includes(field.Name)) {
153
- continue;
154
- }
155
-
156
- let fieldDefinition: FieldDefinition = {
157
- name: field.Name,
158
- type: field.DataType,
159
- nullable: field.Nullable,
160
- };
161
-
162
- if (field.HasDefault) {
163
- fieldDefinition.hasDefault = true;
164
- }
165
-
166
- if (field.Computed) {
167
- fieldDefinition.computed = true;
168
- }
169
-
170
- if (field.Identity) {
171
- fieldDefinition.identity = true;
172
- }
173
-
174
- if (aggregates[field.Name]) {
175
- fieldDefinition.aggregate = aggregates[field.Name];
176
- }
177
-
178
- fields.push(fieldDefinition);
179
- }
180
-
181
- let api = "generateApiDataObject";
182
- let types = "";
183
- let typeName = `${options.id}Record`;
184
- let typeOverrides: Record<string, string> = {};
185
-
186
- if (options.overrides) {
187
- let overrides = options.overrides.split(",");
188
- for (let override of overrides) {
189
- let [param, type] = override.split(":");
190
- typeOverrides[param] = type;
191
- }
192
- }
193
-
194
- // By convention data object name starts with ds, but their type
195
- // definitions should not.
196
- if (typeName.startsWith("ds")) {
197
- typeName = typeName.substring(2);
198
- }
199
-
200
- if (options.types) {
201
- api = `generateApiDataObject<${typeName}>`;
202
-
203
- let fieldTypes = "";
204
- for (let field of fields) {
205
- let type = typeOverrides[field.name];
206
- if (!type) {
207
- type = afTypeToTsType(field.type, "ts");
208
- if (field.nullable) {
209
- type += " | null";
210
- }
211
- }
15
+ config({ path: `${process.cwd()}/.env`, quiet: true });
212
16
 
213
- fieldTypes += ` ${field.name}: ${type};\n`;
214
- }
215
-
216
- types = `export type ${typeName} = {
217
- ${fieldTypes}}`;
218
- }
219
-
220
- if (options.global) {
221
- api = `af.data.${api}`;
222
- }
223
-
224
- let output = "";
225
- if (!options.global) {
226
- output += `import { generateApiDataObject${
227
- options.sortOrder ? ", SortOrder" : ""
228
- } } from "@olenbetong/appframe-data";`;
229
-
230
- if (options.expose) {
231
- output += `\nimport { expose } from "@olenbetong/appframe-core";`;
232
- }
233
- output += "\n\n";
234
- }
235
-
236
- if (options.master && options.master.indexOf(":") > 0) {
237
- let [name, path] = options.master.split(":");
238
- output += `import { ${name} } from "${path}";\n\n`;
239
- }
240
-
241
- if (options.types) {
242
- output += `${types}\n\n`;
243
- }
244
-
245
- let linkFields = "";
246
-
247
- let dsOptions: string[] = [];
248
- dsOptions.push(`resource: "${name}"`);
249
- if (options.unique) {
250
- dsOptions.push(`uniqueName: "${options.unique}"`);
251
- }
252
- dsOptions.push(`id: "${options.id}"`);
253
- if (options.master && options.linkFields) {
254
- dsOptions.push(`masterDataObject: ${options.master.split(":")[0]}`);
255
-
256
- let fields = options.linkFields.split(",");
257
-
258
- linkFields = `linkFields: {\n\t\t${fields
259
- .map((field) => {
260
- let [thisField, masterField] = field.split(":");
261
- return `${thisField}: "${masterField ?? thisField}",`;
262
- })
263
- .join("\n\t\t")}\n\t}`;
264
-
265
- dsOptions.push(linkFields);
266
- }
267
- dsOptions.push(`allowUpdate: ${options.permissions?.includes("U") ?? false}`);
268
- dsOptions.push(`allowInsert: ${options.permissions?.includes("I") ?? false}`);
269
- dsOptions.push(`allowDelete: ${options.permissions?.includes("D") ?? false}`);
270
- dsOptions.push(`dynamicLoading: ${options.dynamic || false}`);
271
-
272
- let fieldsOption = fields.map((field) => ({
273
- ...field,
274
- type: afTypeToTsType(field.type, "field"),
275
- }));
276
- dsOptions.push(`fields: ${JSON.stringify(fieldsOption, null, 2).split("\n").join("\n\t")}`);
277
-
278
- let parametersOption: string[] = [`maxRecords: ${options.maxRecords}`];
279
-
280
- if (options.sortOrder) {
281
- let sorts = options.sortOrder.split(",");
282
- let sortPrefix = options.global ? "af.data.SortOrder." : "SortOrder.";
283
-
284
- let sortOrder = sorts
285
- .map((sort) => {
286
- let [field, order = "asc"] = sort.split(":");
287
- switch (order.toLocaleLowerCase()) {
288
- case "asc":
289
- order = "Asc";
290
- break;
291
- case "desc":
292
- order = "Desc";
293
- break;
294
- case "ascnullslast":
295
- order = "AscNullsLast";
296
- break;
297
- case "descnullsfirst":
298
- order = "DescNullsFirst";
299
- break;
300
- }
301
- return `{ ${field}: ${sortPrefix}${order} }`;
302
- })
303
- .join(", ");
304
-
305
- parametersOption.push(`sortOrder: [${sortOrder}]`);
306
- }
307
-
308
- if (options.groupBy) {
309
- parametersOption.push(`groupBy: ${JSON.stringify(options.groupBy.split(","))}`);
310
- }
311
-
312
- if (options.where) {
313
- parametersOption.push(`whereClause: "${options.where}"`);
314
- }
315
-
316
- if (options.distinct) {
317
- parametersOption.push(`distinctRows: true`);
318
- }
319
-
320
- dsOptions.push(`parameters: {\n\t\t${parametersOption.join(",\n\t\t")}\n\t}`);
321
-
322
- output += `export const ${options.id} = ${api}({
323
- ${dsOptions.join(",\n\t")}
324
- });
325
- `;
326
-
327
- if (options.expose) {
328
- let id = typeof options.expose === "string" ? options.expose : options.id;
329
- if (options.global) {
330
- output += `\naf.common.expose("af.article.dataObjects.${id}", ${options.id}, { overwrite: true });`;
331
- } else {
332
- output += `\nexpose("af.article.dataObjects.${id}", ${options.id}, { overwrite: true });`;
333
- }
334
- }
335
-
336
- return output;
337
- }
338
-
339
- async function getResourceDefinition(resourceName: string, options: CLIOptions) {
340
- let server = new Server("dev.obet.no");
341
- let resource = await server.getResourceArgument(resourceName);
342
- let definition = await server.getResourceDefinition(resource);
343
- definition.Parameters = definition.Parameters.filter((p: any) => !["CUT", "CDL"].includes(p.Name));
17
+ const appPkg = await importJson("../package.json");
344
18
 
19
+ async function getResourceDefinition(resourceName: string, options: CLIOptions & { server: string }) {
345
20
  if (options.fields === true) {
21
+ // Need to fetch the resource to show the interactive field picker
22
+ let server = new Server(options.server);
23
+ let resource = await server.getResourceArgument(resourceName);
24
+ let definition = await server.getResourceDefinition(resource);
346
25
  let response = await inquirer.prompt([
347
26
  {
348
27
  type: "checkbox",
@@ -356,54 +35,44 @@ async function getResourceDefinition(resourceName: string, options: CLIOptions)
356
35
  options.fields = response.fields.join(",");
357
36
  }
358
37
 
359
- let command: string[] = [`af resources generate ${resource}`];
360
- if (options.id) command.push(`--id ${options.id}`);
361
- if (options.unique) command.push(`--unique ${options.unique}`);
362
- if (options.global) command.push("--global");
363
- if (options.types) command.push("--types");
364
- if (options.maxRecords && definition.ObjectType !== "P") command.push(`--max-records ${options.maxRecords}`);
365
- if (options.sortOrder) command.push(`--sort-order ${options.sortOrder}`);
366
- if (options.permissions) command.push(`--permissions ${options.permissions}`);
367
- if (options.master) command.push(`--master ${options.master}`);
368
- if (options.linkFields) command.push(`--link-fields ${options.linkFields}`);
369
- if (options.expose) command.push(`--expose${typeof options.expose === "string" ? ` ${options.expose}` : ""}`);
370
- if (options.dynamic) command.push(`--dynamic`);
371
- if (options.overrides) command.push(`--overrides "${options.overrides}"`);
372
- if (options.distinct) command.push("--distinct");
373
- if (options.aggregates) command.push(`--aggregates ${options.aggregates}`);
374
- if (options.groupBy) command.push(`--group-by ${options.groupBy}`);
375
- if (options.where) command.push(`--where "${options.where}"`);
376
- if (options.fields) command.push(`--fields ${options.fields}`);
38
+ let server = new Server(options.server);
39
+ let resource = await server.getResourceArgument(resourceName);
377
40
 
378
- console.log(`/*\nauto-generated by CLI:\n${command.join(" \\\n ")}\n*/`);
379
- console.log(
380
- definition.ObjectType === "V"
381
- ? getDataObjectDefinition(resource, definition, options)
382
- : getProcedureDefinition(resource, definition, options),
383
- );
41
+ let { APPFRAME_LOGIN: username = "", APPFRAME_PWD: password = "" } = process.env;
42
+ let client = new Client(options.server);
43
+ await client.login(username, password);
44
+
45
+ let content = await fetchAndGenerate(resource, options, client);
46
+
47
+ if (options.output) {
48
+ let header = buildYamlConfig(resource, options);
49
+ await writeFile(options.output, `${header}\n${content}`, "utf-8");
50
+ await formatWithBiome(options.output);
51
+ console.log(`Written to ${options.output}`);
52
+ } else {
53
+ let command: string[] = [`af resources generate ${resource}`];
54
+ if (options.id) command.push(`--id ${options.id}`);
55
+ if (options.unique) command.push(`--unique ${options.unique}`);
56
+ if (options.global) command.push("--global");
57
+ if (options.types) command.push("--types");
58
+ if (options.maxRecords) command.push(`--max-records ${options.maxRecords}`);
59
+ if (options.sortOrder) command.push(`--sort-order ${options.sortOrder}`);
60
+ if (options.permissions) command.push(`--permissions ${options.permissions}`);
61
+ if (options.master) command.push(`--master ${options.master}`);
62
+ if (options.linkFields) command.push(`--link-fields ${options.linkFields}`);
63
+ if (options.expose) command.push(`--expose${typeof options.expose === "string" ? ` ${options.expose}` : ""}`);
64
+ if (options.dynamic) command.push("--dynamic");
65
+ if (options.overrides) command.push(`--overrides "${options.overrides}"`);
66
+ if (options.distinct) command.push("--distinct");
67
+ if (options.aggregates) command.push(`--aggregates ${options.aggregates}`);
68
+ if (options.groupBy) command.push(`--group-by ${options.groupBy}`);
69
+ if (options.where) command.push(`--where "${options.where}"`);
70
+ if (options.fields) command.push(`--fields ${options.fields}`);
71
+ console.log(`/*\nauto-generated by CLI:\n${command.join(" \\\n ")}\n*/`);
72
+ console.log(content);
73
+ }
384
74
  }
385
75
 
386
- type CLIOptions = {
387
- server: string;
388
- types?: boolean;
389
- global: boolean;
390
- id: string;
391
- fields: string | boolean;
392
- permissions?: string;
393
- maxRecords?: string;
394
- sortOrder?: string;
395
- master?: string;
396
- linkFields?: string;
397
- expose?: string | boolean;
398
- dynamic: boolean;
399
- unique?: string;
400
- overrides?: string;
401
- distinct?: boolean;
402
- aggregates?: string;
403
- groupBy?: string;
404
- where?: string;
405
- };
406
-
407
76
  const program = new Command();
408
77
  program
409
78
  .version(appPkg.version)
@@ -443,5 +112,9 @@ program
443
112
  )
444
113
  .option("--distinct", "Data object should fetch distinct data")
445
114
  .option("--where <whereClause>", "Initial where clause to set on the data object")
115
+ .option(
116
+ "-O, --output <path>",
117
+ "Write output to this file instead of stdout; also sets the correct relative import path for custom.d.ts",
118
+ )
446
119
  .action(getResourceDefinition);
447
120
  await program.parseAsync(process.argv);
@@ -1,6 +1,6 @@
1
+ import type { TransactionFilter } from "@olenbetong/appframe-updater";
1
2
  import { Box, Text, useApp } from "ink";
2
3
  import { useCallback, useEffect, useMemo, useState } from "react";
3
-
4
4
  import type { Server } from "../lib/Server.js";
5
5
  import { TransactionsPreviewDialog } from "./TransactionsPreviewDialog.js";
6
6
  import { createStatusLine } from "./tableFormatting.js";
@@ -12,7 +12,6 @@ import { useTransactionsSelection } from "./useTransactionsSelection.js";
12
12
  import { useTransactionsTableLayout } from "./useTransactionsTableLayout.js";
13
13
  import { CURSOR_PREFIX_WIDTH, HEADER_PREFIX, useVirtualScrolling } from "./useVirtualScrolling.js";
14
14
  import { withFullScreen } from "./withFullScreen.js";
15
- import type { TransactionFilter } from "@olenbetong/appframe-updater";
16
15
 
17
16
  const HEADER_LINES = 2;
18
17
  const BASE_FOOTER_LINES = 1;