@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
package/README.md
CHANGED
|
@@ -80,12 +80,122 @@ This package reads `package.json.appframe` to know what to proxy and how to buil
|
|
|
80
80
|
- Resolve
|
|
81
81
|
- Adds the alias `~/` to `/src/` so imports like `~/components/Button` resolve to `src/components/Button`.
|
|
82
82
|
|
|
83
|
-
##
|
|
83
|
+
## CLI — `appframe-vite`
|
|
84
84
|
|
|
85
|
-
|
|
85
|
+
The package ships a CLI (`appframe-vite`) alongside the Vite plugin.
|
|
86
86
|
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
### `generate-types`
|
|
88
|
+
|
|
89
|
+
Generates TypeScript type definitions from the article's data objects and procedures:
|
|
90
|
+
|
|
91
|
+
```sh
|
|
92
|
+
appframe-vite generate-types
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### `resources generate`
|
|
96
|
+
|
|
97
|
+
Reads `resources.yaml` and regenerates all data object / procedure files:
|
|
98
|
+
|
|
99
|
+
```sh
|
|
100
|
+
appframe-vite resources generate
|
|
101
|
+
# custom config file path:
|
|
102
|
+
appframe-vite resources generate --config path/to/resources.yaml
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### `resources add`
|
|
106
|
+
|
|
107
|
+
Interactive wizard — prompts for resource, ID, output path, permissions, fields, and more, then appends an entry to `resources.yaml` and generates the output file:
|
|
108
|
+
|
|
109
|
+
```sh
|
|
110
|
+
appframe-vite resources add
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### `resources edit [id]`
|
|
114
|
+
|
|
115
|
+
Interactive editor — pre-fills the wizard with the current values from `resources.yaml` and regenerates the file on save:
|
|
116
|
+
|
|
117
|
+
```sh
|
|
118
|
+
appframe-vite resources edit dsAccountGroups
|
|
119
|
+
# or without an id (shows a searchable list):
|
|
120
|
+
appframe-vite resources edit
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## `resources.yaml` config file
|
|
124
|
+
|
|
125
|
+
Place `resources.yaml` at the project root to declare all data objects and procedures for an app. The Vite dev server watches this file and auto-regenerates all output files when it changes.
|
|
126
|
+
|
|
127
|
+
**Example:**
|
|
128
|
+
|
|
129
|
+
```yaml
|
|
130
|
+
dataObjects:
|
|
131
|
+
- id: dsAccountGroups
|
|
132
|
+
resource: atbv_Accounting_SubsidiaryLedgerGroups
|
|
133
|
+
global: true
|
|
134
|
+
types: true
|
|
135
|
+
maxRecords: -1
|
|
136
|
+
expose: true
|
|
137
|
+
fields:
|
|
138
|
+
- Domain
|
|
139
|
+
- SubsidiaryLedgerGroup
|
|
140
|
+
- PrimKey
|
|
141
|
+
- Description
|
|
142
|
+
output: src/data/dsAccountGroups.ts
|
|
143
|
+
|
|
144
|
+
- id: dsSubsidiaryLedger
|
|
145
|
+
resource: atbv_Accounting_SubsidiaryLedger
|
|
146
|
+
global: true
|
|
147
|
+
types: true
|
|
148
|
+
permissions: IUD
|
|
149
|
+
output: src/data/dsSubsidiaryLedger.ts
|
|
150
|
+
|
|
151
|
+
procedures:
|
|
152
|
+
- id: procCreateCustomer
|
|
153
|
+
resource: astp_Accounting_SubsidiaryLedger_Create
|
|
154
|
+
global: true
|
|
155
|
+
types: true
|
|
156
|
+
expose: true
|
|
157
|
+
output: src/data/procCreateCustomer.ts
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
**Supported fields per entry:**
|
|
161
|
+
|
|
162
|
+
| Field | Type | Description |
|
|
163
|
+
|---|---|---|
|
|
164
|
+
| `id` | `string` | Variable name used in generated code (e.g. `dsAccountGroups`) |
|
|
165
|
+
| `resource` | `string` | Database object ID (e.g. `atbv_Accounting_SubsidiaryLedgerGroups`) |
|
|
166
|
+
| `output` | `string` | Output file path relative to project root |
|
|
167
|
+
| `global` | `boolean` | Use `af.data.generateApiDataObject` / `new af.ProcedureAPI` globals |
|
|
168
|
+
| `types` | `boolean` | Emit TypeScript type definitions |
|
|
169
|
+
| `permissions` | `string` | Permissions: I = insert, U = update, D = delete (e.g. `IUD`) |
|
|
170
|
+
| `maxRecords` | `number` | Max records to fetch (default `50`; `-1` for all) |
|
|
171
|
+
| `sortOrder` | `string \| string[]` | Sort order, e.g. `Created:Desc` or `[Created:Desc, Name]` |
|
|
172
|
+
| `master` | `string` | Master data object name (or `name:importPath`) |
|
|
173
|
+
| `linkFields` | `string \| string[]` | Fields linking child to master |
|
|
174
|
+
| `expose` | `boolean \| string` | Expose on `af.article.dataObjects` / `af.article.procedures` |
|
|
175
|
+
| `dynamic` | `boolean` | Enable dynamic loading |
|
|
176
|
+
| `unique` | `string` | Unique table name for update/delete |
|
|
177
|
+
| `overrides` | `string \| string[]` | Type overrides, e.g. `MyField:string[]` |
|
|
178
|
+
| `distinct` | `boolean` | Fetch distinct rows |
|
|
179
|
+
| `aggregates` | `string \| string[]` | Aggregate bindings, e.g. `Qty:SUM` |
|
|
180
|
+
| `groupBy` | `string \| string[]` | Group-by fields |
|
|
181
|
+
| `where` | `string` | Initial where clause |
|
|
182
|
+
| `fields` | `string \| string[]` | Fields to include (all if omitted) |
|
|
183
|
+
|
|
184
|
+
A top-level `server` key can override the hostname (defaults to `appframe.proxy.hostname` from `package.json`).
|
|
185
|
+
|
|
186
|
+
## `@olenbetong/appframe-vite/resources` export
|
|
187
|
+
|
|
188
|
+
Shared code generation utilities for Node.js consumers:
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
import {
|
|
192
|
+
fetchAndGenerate,
|
|
193
|
+
buildYamlConfig,
|
|
194
|
+
parseYamlConfig,
|
|
195
|
+
formatWithBiome,
|
|
196
|
+
getCustomImportPath,
|
|
197
|
+
type CLIOptions,
|
|
198
|
+
} from "@olenbetong/appframe-vite/resources";
|
|
199
|
+
```
|
|
89
200
|
|
|
90
|
-
See `docs/reference/appframe-vite/` for a deeper dive into internals and configuration.
|
|
91
201
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function addResource(): Promise<void>;
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, relative, resolve } from "node:path";
|
|
3
|
+
import { Client, generateApiDataHandler } from "@olenbetong/appframe-data";
|
|
4
|
+
import { config } from "dotenv";
|
|
5
|
+
import fuzzy from "fuzzy";
|
|
6
|
+
import inquirer from "inquirer";
|
|
7
|
+
import autocomplete from "inquirer-autocomplete-standalone";
|
|
8
|
+
import { getLoginInfo } from "./devServer.js";
|
|
9
|
+
import { fetchAndGenerate, fetchResourceDefinition, formatWithBiome } from "./resourceGenerate.js";
|
|
10
|
+
import { entryToCLIOptions, readResourcesConfig, writeResourcesConfig } from "./resourcesConfig.js";
|
|
11
|
+
config({ quiet: true });
|
|
12
|
+
async function getResources(client) {
|
|
13
|
+
let ds = generateApiDataHandler({
|
|
14
|
+
client,
|
|
15
|
+
resource: "API_Resources",
|
|
16
|
+
fields: [
|
|
17
|
+
{ name: "DBObjectID", type: "string", nullable: false },
|
|
18
|
+
{ name: "Name", type: "string", nullable: true },
|
|
19
|
+
{ name: "ObjectType", type: "string", nullable: true },
|
|
20
|
+
],
|
|
21
|
+
});
|
|
22
|
+
return ds.retrieve({ maxRecords: -1 });
|
|
23
|
+
}
|
|
24
|
+
export async function addResource() {
|
|
25
|
+
let { hostname, username, password } = await getLoginInfo();
|
|
26
|
+
let client = new Client(hostname);
|
|
27
|
+
await client.login(username, password);
|
|
28
|
+
// Step 1: pick a resource
|
|
29
|
+
let resources = await getResources(client);
|
|
30
|
+
let resourceChoices = resources.map((r) => ({
|
|
31
|
+
name: r.Name && r.Name !== r.DBObjectID ? `${r.DBObjectID} / ${r.Name}` : r.DBObjectID,
|
|
32
|
+
value: r.DBObjectID,
|
|
33
|
+
objectType: r.ObjectType,
|
|
34
|
+
}));
|
|
35
|
+
let chosenResource = await autocomplete({
|
|
36
|
+
message: "Select a resource",
|
|
37
|
+
source: async (input) => {
|
|
38
|
+
return fuzzy
|
|
39
|
+
.filter(input ?? "", resourceChoices, { extract: (el) => el.name })
|
|
40
|
+
.map((el) => ({ name: el.original.name, value: el.original.value }));
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
let chosenMeta = resourceChoices.find((r) => r.value === chosenResource);
|
|
44
|
+
let isView = chosenMeta?.objectType === "V";
|
|
45
|
+
// Fetch definition for field picker
|
|
46
|
+
let definition = await fetchResourceDefinition(client, chosenResource);
|
|
47
|
+
let allFields = definition.Parameters?.filter((p) => !["CUT", "CDL"].includes(p.Name)).map((p) => isView ? p.Name : p.ParamName) ?? [];
|
|
48
|
+
// Step 2: choose ID
|
|
49
|
+
let defaultId = isView
|
|
50
|
+
? `ds${chosenResource.replace(/^a[a-z]+_[A-Za-z]+_/, "")}`
|
|
51
|
+
: `proc${chosenResource.replace(/^astp_[A-Za-z]+_/, "")}`;
|
|
52
|
+
let { id } = await inquirer.prompt([
|
|
53
|
+
{
|
|
54
|
+
type: "input",
|
|
55
|
+
name: "id",
|
|
56
|
+
message: "Data object / procedure ID",
|
|
57
|
+
default: defaultId,
|
|
58
|
+
},
|
|
59
|
+
]);
|
|
60
|
+
// Step 3: output path
|
|
61
|
+
let defaultOutput = `src/data/${id}.ts`;
|
|
62
|
+
let { output } = await inquirer.prompt([
|
|
63
|
+
{
|
|
64
|
+
type: "input",
|
|
65
|
+
name: "output",
|
|
66
|
+
message: "Output file path (relative to project root)",
|
|
67
|
+
default: defaultOutput,
|
|
68
|
+
},
|
|
69
|
+
]);
|
|
70
|
+
// Step 4: global vs module imports
|
|
71
|
+
let { useGlobal } = await inquirer.prompt([
|
|
72
|
+
{
|
|
73
|
+
type: "confirm",
|
|
74
|
+
name: "useGlobal",
|
|
75
|
+
message: "Use global af.data / af.ProcedureAPI (instead of ES imports)?",
|
|
76
|
+
default: true,
|
|
77
|
+
},
|
|
78
|
+
]);
|
|
79
|
+
// Step 5: TypeScript types
|
|
80
|
+
let { withTypes } = await inquirer.prompt([
|
|
81
|
+
{
|
|
82
|
+
type: "confirm",
|
|
83
|
+
name: "withTypes",
|
|
84
|
+
message: "Generate TypeScript type definitions?",
|
|
85
|
+
default: true,
|
|
86
|
+
},
|
|
87
|
+
]);
|
|
88
|
+
// Step 6: expose
|
|
89
|
+
let { exposeIt } = await inquirer.prompt([
|
|
90
|
+
{
|
|
91
|
+
type: "confirm",
|
|
92
|
+
name: "exposeIt",
|
|
93
|
+
message: `Expose on af.article.${isView ? "dataObjects" : "procedures"}?`,
|
|
94
|
+
default: false,
|
|
95
|
+
},
|
|
96
|
+
]);
|
|
97
|
+
// View-specific options
|
|
98
|
+
let permissions;
|
|
99
|
+
let maxRecords = "50";
|
|
100
|
+
let selectedFields = [];
|
|
101
|
+
let sortOrder;
|
|
102
|
+
if (isView) {
|
|
103
|
+
// Permissions
|
|
104
|
+
let { permInput } = await inquirer.prompt([
|
|
105
|
+
{
|
|
106
|
+
type: "checkbox",
|
|
107
|
+
name: "permInput",
|
|
108
|
+
message: "Permissions",
|
|
109
|
+
choices: [
|
|
110
|
+
{ name: "Insert (I)", value: "I" },
|
|
111
|
+
{ name: "Update (U)", value: "U" },
|
|
112
|
+
{ name: "Delete (D)", value: "D" },
|
|
113
|
+
],
|
|
114
|
+
},
|
|
115
|
+
]);
|
|
116
|
+
permissions = permInput.length > 0 ? permInput.join("") : undefined;
|
|
117
|
+
// Max records
|
|
118
|
+
let { maxRec } = await inquirer.prompt([
|
|
119
|
+
{
|
|
120
|
+
type: "input",
|
|
121
|
+
name: "maxRec",
|
|
122
|
+
message: "Max records (-1 for all)",
|
|
123
|
+
default: "50",
|
|
124
|
+
validate: (v) => (!Number.isNaN(Number(v)) ? true : "Must be a number"),
|
|
125
|
+
},
|
|
126
|
+
]);
|
|
127
|
+
maxRecords = maxRec;
|
|
128
|
+
// Field picker
|
|
129
|
+
if (allFields.length > 0) {
|
|
130
|
+
let { fields } = await inquirer.prompt([
|
|
131
|
+
{
|
|
132
|
+
type: "checkbox",
|
|
133
|
+
name: "fields",
|
|
134
|
+
message: "Select fields to include (leave all unchecked to include all)",
|
|
135
|
+
choices: allFields,
|
|
136
|
+
},
|
|
137
|
+
]);
|
|
138
|
+
selectedFields = fields;
|
|
139
|
+
}
|
|
140
|
+
// Sort order
|
|
141
|
+
let { sortInput } = await inquirer.prompt([
|
|
142
|
+
{
|
|
143
|
+
type: "input",
|
|
144
|
+
name: "sortInput",
|
|
145
|
+
message: "Sort order (e.g. Created:Desc,Name — leave blank for none)",
|
|
146
|
+
default: "",
|
|
147
|
+
},
|
|
148
|
+
]);
|
|
149
|
+
sortOrder = sortInput || undefined;
|
|
150
|
+
}
|
|
151
|
+
// Build entry
|
|
152
|
+
let relativeOutput = relative(process.cwd(), resolve(process.cwd(), output)).replace(/\\/g, "/");
|
|
153
|
+
let entry = {
|
|
154
|
+
id,
|
|
155
|
+
resource: chosenResource,
|
|
156
|
+
output: relativeOutput,
|
|
157
|
+
global: useGlobal || undefined,
|
|
158
|
+
types: withTypes || undefined,
|
|
159
|
+
expose: exposeIt || undefined,
|
|
160
|
+
permissions: permissions,
|
|
161
|
+
maxRecords: maxRecords !== "50" ? Number(maxRecords) : undefined,
|
|
162
|
+
sortOrder: sortOrder ? sortOrder.split(",") : undefined,
|
|
163
|
+
fields: selectedFields.length > 0 ? selectedFields : undefined,
|
|
164
|
+
};
|
|
165
|
+
// Write to resources.yaml
|
|
166
|
+
let config2 = await readResourcesConfig();
|
|
167
|
+
if (isView) {
|
|
168
|
+
config2.dataObjects = [...(config2.dataObjects ?? []), entry];
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
config2.procedures = [...(config2.procedures ?? []), entry];
|
|
172
|
+
}
|
|
173
|
+
await writeResourcesConfig(config2);
|
|
174
|
+
console.log(`Added '${id}' to resources.yaml`);
|
|
175
|
+
// Generate the file
|
|
176
|
+
let options = entryToCLIOptions(entry, hostname);
|
|
177
|
+
options.output = resolve(process.cwd(), output);
|
|
178
|
+
let content = await fetchAndGenerate(chosenResource, options, client);
|
|
179
|
+
let outputPath = resolve(process.cwd(), output);
|
|
180
|
+
await mkdir(dirname(outputPath), { recursive: true });
|
|
181
|
+
await writeFile(outputPath, content, "utf-8");
|
|
182
|
+
await formatWithBiome(outputPath);
|
|
183
|
+
console.log(`Generated ${output}`);
|
|
184
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function editResource(id?: string): Promise<void>;
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, relative, resolve } from "node:path";
|
|
3
|
+
import { Client } from "@olenbetong/appframe-data";
|
|
4
|
+
import { config } from "dotenv";
|
|
5
|
+
import fuzzy from "fuzzy";
|
|
6
|
+
import inquirer from "inquirer";
|
|
7
|
+
import autocomplete from "inquirer-autocomplete-standalone";
|
|
8
|
+
import { getLoginInfo } from "./devServer.js";
|
|
9
|
+
import { fetchAndGenerate, fetchResourceDefinition, formatWithBiome } from "./resourceGenerate.js";
|
|
10
|
+
import { entryToCLIOptions, readResourcesConfig, writeResourcesConfig } from "./resourcesConfig.js";
|
|
11
|
+
config({ quiet: true });
|
|
12
|
+
export async function editResource(id) {
|
|
13
|
+
let resourcesConfig = await readResourcesConfig();
|
|
14
|
+
let allEntries = [
|
|
15
|
+
...(resourcesConfig.dataObjects ?? []).map((e) => ({ isView: true, entry: e })),
|
|
16
|
+
...(resourcesConfig.procedures ?? []).map((e) => ({ isView: false, entry: e })),
|
|
17
|
+
];
|
|
18
|
+
if (allEntries.length === 0) {
|
|
19
|
+
console.error("No resources found in resources.yaml. Run 'appframe-vite resources add' to add one.");
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
// Select entry to edit
|
|
23
|
+
let selected;
|
|
24
|
+
if (id) {
|
|
25
|
+
let found = allEntries.find((e) => e.entry.id === id);
|
|
26
|
+
if (!found) {
|
|
27
|
+
console.error(`No resource with id '${id}' found in resources.yaml.`);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
selected = found;
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
let entryChoices = allEntries.map((e) => ({
|
|
34
|
+
name: `${e.isView ? "[view]" : "[proc]"} ${e.entry.id} (${e.entry.resource})`,
|
|
35
|
+
value: e.entry.id,
|
|
36
|
+
}));
|
|
37
|
+
let chosenId = await autocomplete({
|
|
38
|
+
message: "Select a resource to edit",
|
|
39
|
+
source: async (input) => {
|
|
40
|
+
return fuzzy
|
|
41
|
+
.filter(input ?? "", entryChoices, { extract: (el) => el.name })
|
|
42
|
+
.map((el) => ({ name: el.original.name, value: el.original.value }));
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
selected = allEntries.find((e) => e.entry.id === chosenId);
|
|
46
|
+
}
|
|
47
|
+
let { entry, isView } = selected;
|
|
48
|
+
let { hostname, username, password } = await getLoginInfo();
|
|
49
|
+
let client = new Client(hostname);
|
|
50
|
+
await client.login(username, password);
|
|
51
|
+
// Fetch definition for field picker
|
|
52
|
+
let definition = await fetchResourceDefinition(client, entry.resource);
|
|
53
|
+
let allFields = definition.Parameters?.filter((p) => !["CUT", "CDL"].includes(p.Name)).map((p) => isView ? p.Name : p.ParamName) ?? [];
|
|
54
|
+
let currentFields = Array.isArray(entry.fields) ? entry.fields : (entry.fields?.split(",").filter(Boolean) ?? []);
|
|
55
|
+
// Re-run wizard pre-filled with current values
|
|
56
|
+
let { id: newId } = await inquirer.prompt([
|
|
57
|
+
{
|
|
58
|
+
type: "input",
|
|
59
|
+
name: "id",
|
|
60
|
+
message: "Data object / procedure ID",
|
|
61
|
+
default: entry.id,
|
|
62
|
+
},
|
|
63
|
+
]);
|
|
64
|
+
let { output } = await inquirer.prompt([
|
|
65
|
+
{
|
|
66
|
+
type: "input",
|
|
67
|
+
name: "output",
|
|
68
|
+
message: "Output file path (relative to project root)",
|
|
69
|
+
default: entry.output,
|
|
70
|
+
},
|
|
71
|
+
]);
|
|
72
|
+
let { useGlobal } = await inquirer.prompt([
|
|
73
|
+
{
|
|
74
|
+
type: "confirm",
|
|
75
|
+
name: "useGlobal",
|
|
76
|
+
message: "Use global af.data / af.ProcedureAPI?",
|
|
77
|
+
default: entry.global ?? false,
|
|
78
|
+
},
|
|
79
|
+
]);
|
|
80
|
+
let { withTypes } = await inquirer.prompt([
|
|
81
|
+
{
|
|
82
|
+
type: "confirm",
|
|
83
|
+
name: "withTypes",
|
|
84
|
+
message: "Generate TypeScript type definitions?",
|
|
85
|
+
default: entry.types ?? false,
|
|
86
|
+
},
|
|
87
|
+
]);
|
|
88
|
+
let { exposeIt } = await inquirer.prompt([
|
|
89
|
+
{
|
|
90
|
+
type: "confirm",
|
|
91
|
+
name: "exposeIt",
|
|
92
|
+
message: `Expose on af.article.${isView ? "dataObjects" : "procedures"}?`,
|
|
93
|
+
default: !!entry.expose,
|
|
94
|
+
},
|
|
95
|
+
]);
|
|
96
|
+
let permissions = entry.permissions;
|
|
97
|
+
let maxRecords = String(entry.maxRecords ?? 50);
|
|
98
|
+
let selectedFields = currentFields;
|
|
99
|
+
let sortOrder = Array.isArray(entry.sortOrder) ? entry.sortOrder.join(",") : entry.sortOrder;
|
|
100
|
+
if (isView) {
|
|
101
|
+
let currentPerms = entry.permissions ?? "";
|
|
102
|
+
let { permInput } = await inquirer.prompt([
|
|
103
|
+
{
|
|
104
|
+
type: "checkbox",
|
|
105
|
+
name: "permInput",
|
|
106
|
+
message: "Permissions",
|
|
107
|
+
choices: [
|
|
108
|
+
{ name: "Insert (I)", value: "I", checked: currentPerms.includes("I") },
|
|
109
|
+
{ name: "Update (U)", value: "U", checked: currentPerms.includes("U") },
|
|
110
|
+
{ name: "Delete (D)", value: "D", checked: currentPerms.includes("D") },
|
|
111
|
+
],
|
|
112
|
+
},
|
|
113
|
+
]);
|
|
114
|
+
permissions = permInput.length > 0 ? permInput.join("") : undefined;
|
|
115
|
+
let { maxRec } = await inquirer.prompt([
|
|
116
|
+
{
|
|
117
|
+
type: "input",
|
|
118
|
+
name: "maxRec",
|
|
119
|
+
message: "Max records (-1 for all)",
|
|
120
|
+
default: String(entry.maxRecords ?? 50),
|
|
121
|
+
validate: (v) => (!Number.isNaN(Number(v)) ? true : "Must be a number"),
|
|
122
|
+
},
|
|
123
|
+
]);
|
|
124
|
+
maxRecords = maxRec;
|
|
125
|
+
if (allFields.length > 0) {
|
|
126
|
+
let { fields } = await inquirer.prompt([
|
|
127
|
+
{
|
|
128
|
+
type: "checkbox",
|
|
129
|
+
name: "fields",
|
|
130
|
+
message: "Select fields to include (leave all unchecked to include all)",
|
|
131
|
+
choices: allFields.map((f) => ({ name: f, checked: currentFields.includes(f) })),
|
|
132
|
+
},
|
|
133
|
+
]);
|
|
134
|
+
selectedFields = fields;
|
|
135
|
+
}
|
|
136
|
+
let { sortInput } = await inquirer.prompt([
|
|
137
|
+
{
|
|
138
|
+
type: "input",
|
|
139
|
+
name: "sortInput",
|
|
140
|
+
message: "Sort order (e.g. Created:Desc,Name — leave blank for none)",
|
|
141
|
+
default: sortOrder ?? "",
|
|
142
|
+
},
|
|
143
|
+
]);
|
|
144
|
+
sortOrder = sortInput || undefined;
|
|
145
|
+
}
|
|
146
|
+
let relativeOutput = relative(process.cwd(), resolve(process.cwd(), output)).replace(/\\/g, "/");
|
|
147
|
+
let updatedEntry = {
|
|
148
|
+
id: newId,
|
|
149
|
+
resource: entry.resource,
|
|
150
|
+
output: relativeOutput,
|
|
151
|
+
global: useGlobal || undefined,
|
|
152
|
+
types: withTypes || undefined,
|
|
153
|
+
expose: exposeIt || undefined,
|
|
154
|
+
permissions,
|
|
155
|
+
maxRecords: maxRecords !== "50" ? Number(maxRecords) : undefined,
|
|
156
|
+
sortOrder: sortOrder ? sortOrder.split(",") : undefined,
|
|
157
|
+
fields: selectedFields.length > 0 ? selectedFields : undefined,
|
|
158
|
+
};
|
|
159
|
+
// Update resources.yaml
|
|
160
|
+
if (isView) {
|
|
161
|
+
resourcesConfig.dataObjects = (resourcesConfig.dataObjects ?? []).map((e) => e.id === entry.id ? updatedEntry : e);
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
resourcesConfig.procedures = (resourcesConfig.procedures ?? []).map((e) => (e.id === entry.id ? updatedEntry : e));
|
|
165
|
+
}
|
|
166
|
+
await writeResourcesConfig(resourcesConfig);
|
|
167
|
+
console.log(`Updated '${newId}' in resources.yaml`);
|
|
168
|
+
// Regenerate
|
|
169
|
+
let options = entryToCLIOptions(updatedEntry, hostname);
|
|
170
|
+
options.output = resolve(process.cwd(), output);
|
|
171
|
+
let content = await fetchAndGenerate(entry.resource, options, client);
|
|
172
|
+
let outputPath = resolve(process.cwd(), output);
|
|
173
|
+
await mkdir(dirname(outputPath), { recursive: true });
|
|
174
|
+
await writeFile(outputPath, content, "utf-8");
|
|
175
|
+
await formatWithBiome(outputPath);
|
|
176
|
+
console.log(`Regenerated ${output}`);
|
|
177
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function generateFromConfig(configPath?: string): Promise<void>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
import { Client } from "@olenbetong/appframe-data";
|
|
4
|
+
import { config } from "dotenv";
|
|
5
|
+
import { getLoginInfo } from "./devServer.js";
|
|
6
|
+
import { fetchAndGenerate, formatWithBiome } from "./resourceGenerate.js";
|
|
7
|
+
import { entryToCLIOptions, readResourcesConfig } from "./resourcesConfig.js";
|
|
8
|
+
config({ quiet: true });
|
|
9
|
+
export async function generateFromConfig(configPath) {
|
|
10
|
+
let { hostname, username, password } = await getLoginInfo();
|
|
11
|
+
let resourcesConfig = await readResourcesConfig(configPath);
|
|
12
|
+
let allEntries = [
|
|
13
|
+
...(resourcesConfig.dataObjects ?? []).map((e) => ({ type: "dataObject", entry: e })),
|
|
14
|
+
...(resourcesConfig.procedures ?? []).map((e) => ({ type: "procedure", entry: e })),
|
|
15
|
+
];
|
|
16
|
+
if (allEntries.length === 0) {
|
|
17
|
+
console.log("No resources found in resources.yaml");
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
let client = new Client(hostname);
|
|
21
|
+
await client.login(username, password);
|
|
22
|
+
let errors = [];
|
|
23
|
+
for (let { entry } of allEntries) {
|
|
24
|
+
let options = entryToCLIOptions(entry, resourcesConfig.server ?? hostname);
|
|
25
|
+
options.output = resolve(process.cwd(), entry.output);
|
|
26
|
+
try {
|
|
27
|
+
let content = await fetchAndGenerate(entry.resource, options, client);
|
|
28
|
+
let outputPath = resolve(process.cwd(), entry.output);
|
|
29
|
+
await mkdir(dirname(outputPath), { recursive: true });
|
|
30
|
+
await writeFile(outputPath, content, "utf-8");
|
|
31
|
+
await formatWithBiome(outputPath);
|
|
32
|
+
console.log(` ✓ ${entry.id} → ${entry.output}`);
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
let message = error?.message ?? String(error);
|
|
36
|
+
console.error(` ✗ ${entry.id}: ${message}`);
|
|
37
|
+
errors.push({ id: entry.id, error: message });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (errors.length > 0) {
|
|
41
|
+
console.error(`\n${errors.length} resource(s) failed to generate.`);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
}
|
package/lib/cli.js
CHANGED
|
@@ -1,29 +1,53 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
2
3
|
import { config } from "dotenv";
|
|
3
4
|
import { runGenerateTypes } from "./generateTypes.js";
|
|
4
5
|
import { importJson } from "./importJson.js";
|
|
5
6
|
import { createLogMessage } from "./utils.js";
|
|
7
|
+
config({ path: `${process.cwd()}/.env`, quiet: true });
|
|
8
|
+
const pkg = await importJson("../package.json");
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// generate-types
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
6
12
|
async function generateTypes() {
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
const hostname = pkg.appframe.proxy?.hostname ?? "dev.obet.no";
|
|
13
|
+
const appPkg = await importJson("./package.json", true);
|
|
14
|
+
const hostname = appPkg.appframe.proxy?.hostname ?? "dev.obet.no";
|
|
10
15
|
const { APPFRAME_LOGIN: username = "", APPFRAME_PWD: password = "" } = process.env;
|
|
11
16
|
const logger = {
|
|
12
17
|
info: (msg) => console.log(msg),
|
|
13
18
|
error: (msg) => console.error(msg),
|
|
14
19
|
};
|
|
15
20
|
logger.info(createLogMessage("Generating types…", { source: hostname }));
|
|
16
|
-
await runGenerateTypes(hostname, username, password,
|
|
17
|
-
}
|
|
18
|
-
const [, , command] = process.argv;
|
|
19
|
-
switch (command) {
|
|
20
|
-
case "generate-types":
|
|
21
|
-
await generateTypes();
|
|
22
|
-
break;
|
|
23
|
-
default:
|
|
24
|
-
console.error(createLogMessage(`Unknown command: ${command ?? "(none)"}`, { type: "error" }));
|
|
25
|
-
console.error("Usage: appframe-vite <command>");
|
|
26
|
-
console.error("Commands:");
|
|
27
|
-
console.error(" generate-types Generate TypeScript type definitions");
|
|
28
|
-
process.exit(1);
|
|
21
|
+
await runGenerateTypes(hostname, username, password, appPkg.appframe, logger);
|
|
29
22
|
}
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Program
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
const program = new Command();
|
|
27
|
+
program.name("appframe-vite").version(pkg.version).description("Appframe Vite tooling CLI");
|
|
28
|
+
program.command("generate-types").description("Generate TypeScript type definitions").action(generateTypes);
|
|
29
|
+
// resources sub-command
|
|
30
|
+
const resources = program.command("resources").description("Manage data objects and procedures via resources.yaml");
|
|
31
|
+
resources
|
|
32
|
+
.command("generate")
|
|
33
|
+
.description("Generate all output files from resources.yaml")
|
|
34
|
+
.option("-c, --config <path>", "Path to resources config file (default: resources.yaml)")
|
|
35
|
+
.action(async (opts) => {
|
|
36
|
+
let { generateFromConfig } = await import("./cli-resources-generate.js");
|
|
37
|
+
await generateFromConfig(opts.config);
|
|
38
|
+
});
|
|
39
|
+
resources
|
|
40
|
+
.command("add")
|
|
41
|
+
.description("Interactively add a data object or procedure to resources.yaml")
|
|
42
|
+
.action(async () => {
|
|
43
|
+
let { addResource } = await import("./cli-resources-add.js");
|
|
44
|
+
await addResource();
|
|
45
|
+
});
|
|
46
|
+
resources
|
|
47
|
+
.command("edit [id]")
|
|
48
|
+
.description("Interactively edit an existing resource in resources.yaml")
|
|
49
|
+
.action(async (id) => {
|
|
50
|
+
let { editResource } = await import("./cli-resources-edit.js");
|
|
51
|
+
await editResource(id);
|
|
52
|
+
});
|
|
53
|
+
await program.parseAsync(process.argv);
|
package/lib/index.js
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
1
2
|
import bodyParser from "body-parser";
|
|
2
3
|
import { watch } from "chokidar";
|
|
3
4
|
import { addAppframeBuildConfig } from "./build.js";
|
|
5
|
+
import { generateFromConfig } from "./cli-resources-generate.js";
|
|
4
6
|
import { createDevMiddleware, getLoginInfo, getProxyRoutes } from "./devServer.js";
|
|
5
7
|
import { runGenerateTypes } from "./generateTypes.js";
|
|
6
8
|
import { localizeMiddleware } from "./localization.js";
|
|
7
9
|
import { checkSession, getLastSession, login } from "./proxy.js";
|
|
10
|
+
import { RESOURCES_CONFIG_FILE } from "./resourcesConfig.js";
|
|
8
11
|
import { createLogMessage, getServerName } from "./utils.js";
|
|
9
12
|
let command = "build";
|
|
10
13
|
let interval;
|
|
11
14
|
let server;
|
|
12
15
|
let lastHostname;
|
|
13
16
|
let watcher;
|
|
17
|
+
let resourcesWatcher = null;
|
|
18
|
+
let resourcesDebounce = null;
|
|
14
19
|
try {
|
|
15
20
|
let appPkgUrl = `file://${process.cwd()}/package.json`;
|
|
16
21
|
watcher = watch(appPkgUrl).on("all", async () => {
|
|
@@ -139,6 +144,28 @@ export default function appframe() {
|
|
|
139
144
|
server = _server;
|
|
140
145
|
// Run type generation in the background — doesn't block the dev server from starting.
|
|
141
146
|
runGenerateTypes(hostname, username, password, appframe, _server.config.logger);
|
|
147
|
+
// Watch resources.yaml and regenerate on change
|
|
148
|
+
let resourcesConfigPath = resolve(process.cwd(), RESOURCES_CONFIG_FILE);
|
|
149
|
+
if (resourcesWatcher) {
|
|
150
|
+
await resourcesWatcher.close();
|
|
151
|
+
}
|
|
152
|
+
resourcesWatcher = watch(resourcesConfigPath, { ignoreInitial: true }).on("all", () => {
|
|
153
|
+
if (resourcesDebounce)
|
|
154
|
+
clearTimeout(resourcesDebounce);
|
|
155
|
+
resourcesDebounce = setTimeout(async () => {
|
|
156
|
+
_server.config.logger.info(createLogMessage("resources.yaml changed — regenerating…", { source: hostname }));
|
|
157
|
+
try {
|
|
158
|
+
await generateFromConfig();
|
|
159
|
+
_server.config.logger.info(createLogMessage("resources regenerated", { source: hostname }));
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
_server.config.logger.error(createLogMessage(`resources regeneration failed: ${error?.message ?? error}`, {
|
|
163
|
+
source: hostname,
|
|
164
|
+
type: "error",
|
|
165
|
+
}));
|
|
166
|
+
}
|
|
167
|
+
}, 300);
|
|
168
|
+
});
|
|
142
169
|
_server.middlewares.use(`/api/user/localize/new/${appframe.article.id}`, jsonParser);
|
|
143
170
|
_server.middlewares.use(`/api/user/localize/new/${appframe.article.id}`, localizeMiddleware);
|
|
144
171
|
_server.middlewares.use("/data/Logger/LogError", jsonParser);
|