@lovrabet/cli-framework 1.0.4-beta.2 → 1.0.4
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,4 +1,4 @@
|
|
|
1
|
-
export type { NormalizedServiceTree, NormalizedServiceTreeCommand, NormalizedServiceTreeFlag, NormalizedServiceTreeTarget, ServiceTreeAppBinding, ServiceTreeArg, ServiceTreeCommand, ServiceTreeCompileInput, ServiceTreeDatasetResolver, ServiceTreeDatasetResolverInput, ServiceTreeExecutionPlan, ServiceTreeFlag, ServiceTreeManifest, ServiceTreeMapTo, ServiceTreeMapToObject, ServiceTreeMapToRule, ServiceTreeMapTransform, ServiceTreeMatch, ServiceTreeServiceInfo, ServiceTreeTarget, ServiceTreeTargetCommand, ServiceTreeTargetKind, ServiceTreeValidationIssue, ServiceTreeValidationReport, ServiceTreeValueType, } from "./types.js";
|
|
1
|
+
export type { NormalizedServiceTree, NormalizedServiceTreeCommand, NormalizedServiceTreeFlag, NormalizedServiceTreeTarget, ServiceTreeAppBinding, ServiceTreeAppBindingInput, ServiceTreeArg, ServiceTreeArgInput, ServiceTreeAction, ServiceTreeCommand, ServiceTreeCompileInput, ServiceTreeDatasetResolver, ServiceTreeDatasetResolverInput, ServiceTreeExecutionPlan, ServiceTreeFlag, ServiceTreeFlagsInput, ServiceTreeManifest, ServiceTreeMapTo, ServiceTreeMapToObject, ServiceTreeMapToRule, ServiceTreeMapTransform, ServiceTreeMatch, ServiceTreeResource, ServiceTreeServiceInfo, ServiceTreeSimpleFlag, ServiceTreeTarget, ServiceTreeTargetCommand, ServiceTreeTargetKind, ServiceTreeValidationIssue, ServiceTreeValidationReport, ServiceTreeValueType, } from "./types.js";
|
|
2
2
|
export { compileServiceTreeCommand } from "./compile.js";
|
|
3
3
|
export { matchServiceTreeCommand } from "./match.js";
|
|
4
4
|
export { normalizeServiceTreeManifest } from "./normalize.js";
|
|
@@ -1,35 +1,296 @@
|
|
|
1
1
|
import { normalizeMapTo } from "./map-to.js";
|
|
2
2
|
import { splitServiceTreePath, toKebabCase } from "./path.js";
|
|
3
3
|
const DEFAULT_PROTOCOL = "lovrabet.service-tree/v1";
|
|
4
|
+
const DEFAULT_VERSION = "1.0.0";
|
|
5
|
+
const DEFAULT_APP_REF = "default";
|
|
4
6
|
export function normalizeServiceTreeManifest(manifest) {
|
|
5
|
-
const
|
|
7
|
+
const service = normalizeServiceInfo(manifest);
|
|
8
|
+
const serviceCode = service.code;
|
|
9
|
+
const appBindings = normalizeAppBindings(manifest);
|
|
10
|
+
const defaultAppRef = resolveDefaultAppRef(manifest, appBindings);
|
|
11
|
+
const commands = [
|
|
12
|
+
...normalizeLegacyCommands(manifest, serviceCode, defaultAppRef),
|
|
13
|
+
...normalizeResourceCommands(manifest, serviceCode, defaultAppRef),
|
|
14
|
+
];
|
|
6
15
|
return {
|
|
7
16
|
protocol: manifest.protocol || DEFAULT_PROTOCOL,
|
|
8
|
-
version: manifest.version,
|
|
9
|
-
service
|
|
10
|
-
appBindings
|
|
11
|
-
commands
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
+
version: manifest.version || DEFAULT_VERSION,
|
|
18
|
+
service,
|
|
19
|
+
appBindings,
|
|
20
|
+
commands,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function normalizeServiceInfo(manifest) {
|
|
24
|
+
if (typeof manifest.service === "string") {
|
|
25
|
+
return {
|
|
26
|
+
code: manifest.service,
|
|
27
|
+
name: manifest.name,
|
|
28
|
+
description: manifest.description,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
...manifest.service,
|
|
33
|
+
name: manifest.service.name || manifest.name,
|
|
34
|
+
description: manifest.service.description || manifest.description,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function normalizeAppBindings(manifest) {
|
|
38
|
+
const bindings = {};
|
|
39
|
+
if (manifest.appBindings) {
|
|
40
|
+
for (const [key, value] of Object.entries(manifest.appBindings)) {
|
|
41
|
+
bindings[key] = normalizeAppBinding(value);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (manifest.apps) {
|
|
45
|
+
for (const [key, value] of Object.entries(manifest.apps)) {
|
|
46
|
+
bindings[key] = normalizeAppBinding(value);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (manifest.app !== undefined && !bindings[DEFAULT_APP_REF]) {
|
|
50
|
+
bindings[DEFAULT_APP_REF] = normalizeAppBinding(manifest.app);
|
|
51
|
+
}
|
|
52
|
+
return bindings;
|
|
53
|
+
}
|
|
54
|
+
function normalizeAppBinding(input) {
|
|
55
|
+
if (typeof input !== "string")
|
|
56
|
+
return { ...input };
|
|
57
|
+
return input.startsWith("app-")
|
|
58
|
+
? { appcode: input }
|
|
59
|
+
: { app: input };
|
|
60
|
+
}
|
|
61
|
+
function resolveDefaultAppRef(manifest, appBindings) {
|
|
62
|
+
if (manifest.app !== undefined)
|
|
63
|
+
return DEFAULT_APP_REF;
|
|
64
|
+
const keys = Object.keys(appBindings);
|
|
65
|
+
return keys.length === 1 ? keys[0] : undefined;
|
|
66
|
+
}
|
|
67
|
+
function normalizeLegacyCommands(manifest, serviceCode, defaultAppRef) {
|
|
68
|
+
return (manifest.commands || []).map((command) => normalizeCommand(command, serviceCode, defaultAppRef, manifest.defaults));
|
|
69
|
+
}
|
|
70
|
+
function normalizeResourceCommands(manifest, serviceCode, defaultAppRef) {
|
|
71
|
+
return flattenResources({
|
|
72
|
+
resources: manifest.resources || {},
|
|
73
|
+
serviceCode,
|
|
74
|
+
defaultAppRef,
|
|
75
|
+
path: [],
|
|
76
|
+
inherited: {
|
|
77
|
+
appRef: defaultAppRef,
|
|
78
|
+
defaults: manifest.defaults || {},
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
function flattenResources(input) {
|
|
83
|
+
const commands = [];
|
|
84
|
+
for (const [resourceCode, resource] of Object.entries(input.resources)) {
|
|
85
|
+
const path = [...input.path, resourceCode];
|
|
86
|
+
const inherited = {
|
|
87
|
+
appRef: resource.appRef || resource.app || input.inherited.appRef,
|
|
88
|
+
datasetCode: resource.datasetCode || input.inherited.datasetCode,
|
|
89
|
+
datatable: resource.datatable || resource.table || input.inherited.datatable,
|
|
90
|
+
defaults: {
|
|
91
|
+
...input.inherited.defaults,
|
|
92
|
+
...(resource.defaults || {}),
|
|
93
|
+
...(resource.params || {}),
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
for (const [actionCode, action] of Object.entries(resource.actions || {})) {
|
|
97
|
+
commands.push(normalizeActionCommand({
|
|
98
|
+
serviceCode: input.serviceCode,
|
|
99
|
+
defaultAppRef: input.defaultAppRef,
|
|
100
|
+
path: [...path, actionCode],
|
|
101
|
+
resource,
|
|
102
|
+
action,
|
|
103
|
+
inherited,
|
|
17
104
|
}));
|
|
18
|
-
|
|
105
|
+
}
|
|
106
|
+
if (resource.resources) {
|
|
107
|
+
commands.push(...flattenResources({
|
|
108
|
+
resources: resource.resources,
|
|
109
|
+
serviceCode: input.serviceCode,
|
|
110
|
+
defaultAppRef: input.defaultAppRef,
|
|
19
111
|
path,
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
112
|
+
inherited,
|
|
113
|
+
}));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return commands;
|
|
117
|
+
}
|
|
118
|
+
function normalizeActionCommand(input) {
|
|
119
|
+
const action = input.action;
|
|
120
|
+
const target = normalizeActionTarget(action, input.inherited);
|
|
121
|
+
const args = normalizeArgs(action.args);
|
|
122
|
+
const flags = normalizeFlags(action.flags);
|
|
123
|
+
const mapTo = normalizeMapInput(action.mapTo || action.map, args, flags);
|
|
124
|
+
return buildNormalizedCommand({
|
|
125
|
+
serviceCode: input.serviceCode,
|
|
126
|
+
path: input.path,
|
|
127
|
+
description: action.description,
|
|
128
|
+
risk: action.risk,
|
|
129
|
+
args,
|
|
130
|
+
flags,
|
|
131
|
+
target,
|
|
132
|
+
params: {
|
|
133
|
+
...input.inherited.defaults,
|
|
134
|
+
...(action.defaults || {}),
|
|
135
|
+
...(action.params || {}),
|
|
136
|
+
},
|
|
137
|
+
mapTo,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
function normalizeCommand(command, serviceCode, defaultAppRef, serviceDefaults) {
|
|
141
|
+
const args = normalizeArgs(command.args);
|
|
142
|
+
const flags = normalizeFlags(command.flags);
|
|
143
|
+
const mapTo = normalizeMapInput(command.mapTo || command.map, args, flags);
|
|
144
|
+
const target = {
|
|
145
|
+
...command.target,
|
|
146
|
+
appRef: command.target.appRef || defaultAppRef,
|
|
34
147
|
};
|
|
148
|
+
return buildNormalizedCommand({
|
|
149
|
+
serviceCode,
|
|
150
|
+
path: splitServiceTreePath(command.path),
|
|
151
|
+
description: command.description,
|
|
152
|
+
risk: command.risk,
|
|
153
|
+
args,
|
|
154
|
+
flags,
|
|
155
|
+
target,
|
|
156
|
+
params: {
|
|
157
|
+
...(serviceDefaults || {}),
|
|
158
|
+
...(command.defaults || {}),
|
|
159
|
+
...(command.params || {}),
|
|
160
|
+
},
|
|
161
|
+
mapTo,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
function buildNormalizedCommand(input) {
|
|
165
|
+
const fullPath = input.path[0] === input.serviceCode
|
|
166
|
+
? input.path
|
|
167
|
+
: [input.serviceCode, ...input.path];
|
|
168
|
+
return {
|
|
169
|
+
path: input.path,
|
|
170
|
+
fullPath,
|
|
171
|
+
cliPath: fullPath.join(" "),
|
|
172
|
+
description: input.description,
|
|
173
|
+
risk: input.risk || inferRisk(input.target),
|
|
174
|
+
args: input.args,
|
|
175
|
+
flags: input.flags,
|
|
176
|
+
target: { ...input.target },
|
|
177
|
+
baseParams: {
|
|
178
|
+
...(input.target.params || {}),
|
|
179
|
+
...input.params,
|
|
180
|
+
},
|
|
181
|
+
mapTo: normalizeMapTo(input.mapTo, input.flags),
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
function normalizeActionTarget(action, inherited) {
|
|
185
|
+
if (typeof action.target === "object") {
|
|
186
|
+
return {
|
|
187
|
+
...action.target,
|
|
188
|
+
appRef: action.target.appRef || action.appRef || action.app || inherited.appRef,
|
|
189
|
+
datasetCode: action.target.datasetCode || action.datasetCode || inherited.datasetCode,
|
|
190
|
+
datatable: action.target.datatable || action.datatable || action.table || inherited.datatable,
|
|
191
|
+
sqlCode: action.target.sqlCode || action.sqlCode,
|
|
192
|
+
bffCode: action.target.bffCode || action.bffCode,
|
|
193
|
+
bffId: action.target.bffId || action.bffId,
|
|
194
|
+
scriptName: action.target.scriptName || action.scriptName,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
const parsed = parseTargetShorthand(action.target || action.action, action.kind, action.command);
|
|
198
|
+
return {
|
|
199
|
+
kind: parsed.kind,
|
|
200
|
+
command: parsed.command,
|
|
201
|
+
appRef: action.appRef || action.app || inherited.appRef,
|
|
202
|
+
datasetCode: action.datasetCode || inherited.datasetCode,
|
|
203
|
+
datatable: action.datatable || action.table || inherited.datatable,
|
|
204
|
+
sqlCode: action.sqlCode,
|
|
205
|
+
bffCode: action.bffCode,
|
|
206
|
+
bffId: action.bffId,
|
|
207
|
+
scriptName: action.scriptName,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
function parseTargetShorthand(value, kind, command) {
|
|
211
|
+
if (typeof value === "string" && value.includes(".")) {
|
|
212
|
+
const [targetKind, targetCommand] = value.split(".", 2);
|
|
213
|
+
return {
|
|
214
|
+
kind: targetKind,
|
|
215
|
+
command: targetCommand,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
return {
|
|
219
|
+
kind: kind || "data",
|
|
220
|
+
command: command || (typeof value === "string" ? value : "filter"),
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
function normalizeArgs(args) {
|
|
224
|
+
return (args || []).map((arg) => (typeof arg === "string" ? { name: arg, required: true } : { ...arg }));
|
|
225
|
+
}
|
|
226
|
+
function normalizeFlags(flags) {
|
|
227
|
+
if (!flags)
|
|
228
|
+
return [];
|
|
229
|
+
if (Array.isArray(flags)) {
|
|
230
|
+
return flags.map((flag) => ({
|
|
231
|
+
...flag,
|
|
232
|
+
cliName: flag.cliName ? toKebabCase(flag.cliName) : toKebabCase(flag.name),
|
|
233
|
+
}));
|
|
234
|
+
}
|
|
235
|
+
return Object.entries(flags).map(([name, value]) => {
|
|
236
|
+
if (typeof value === "string") {
|
|
237
|
+
return {
|
|
238
|
+
name,
|
|
239
|
+
cliName: toKebabCase(name),
|
|
240
|
+
type: "string",
|
|
241
|
+
mapTo: value,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
return {
|
|
245
|
+
name,
|
|
246
|
+
cliName: value.cliName ? toKebabCase(value.cliName) : toKebabCase(name),
|
|
247
|
+
type: value.type || "string",
|
|
248
|
+
description: value.description,
|
|
249
|
+
required: value.required,
|
|
250
|
+
default: value.default,
|
|
251
|
+
enum: value.enum,
|
|
252
|
+
mapTo: {
|
|
253
|
+
target: value.target || value.to || "",
|
|
254
|
+
operator: value.operator || value.op,
|
|
255
|
+
transform: value.transform,
|
|
256
|
+
omitEmpty: value.omitEmpty,
|
|
257
|
+
},
|
|
258
|
+
};
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
function normalizeMapInput(mapTo, args, flags) {
|
|
262
|
+
if (!mapTo || Array.isArray(mapTo))
|
|
263
|
+
return mapTo;
|
|
264
|
+
const argNames = new Set(args.map((arg) => arg.name));
|
|
265
|
+
const flagNames = new Set(flags.flatMap((flag) => [flag.name, flag.cliName]));
|
|
266
|
+
const normalized = {};
|
|
267
|
+
for (const [source, target] of Object.entries(mapTo)) {
|
|
268
|
+
normalized[normalizeMapSource(source, argNames, flagNames)] = target;
|
|
269
|
+
}
|
|
270
|
+
return normalized;
|
|
271
|
+
}
|
|
272
|
+
function normalizeMapSource(source, argNames, flagNames) {
|
|
273
|
+
if (source === "const"
|
|
274
|
+
|| source.startsWith("flags.")
|
|
275
|
+
|| source.startsWith("args.")
|
|
276
|
+
|| source.startsWith("const.")
|
|
277
|
+
|| source.startsWith("context.")
|
|
278
|
+
|| source.startsWith("ctx.")) {
|
|
279
|
+
return source;
|
|
280
|
+
}
|
|
281
|
+
if (argNames.has(source))
|
|
282
|
+
return `args.${source}`;
|
|
283
|
+
if (flagNames.has(source))
|
|
284
|
+
return `flags.${source}`;
|
|
285
|
+
return `flags.${source}`;
|
|
286
|
+
}
|
|
287
|
+
function inferRisk(target) {
|
|
288
|
+
if (target.kind !== "data")
|
|
289
|
+
return "read";
|
|
290
|
+
if (target.command === "delete")
|
|
291
|
+
return "high-risk-write";
|
|
292
|
+
if (target.command === "create" || target.command === "batchCreate" || target.command === "update") {
|
|
293
|
+
return "write";
|
|
294
|
+
}
|
|
295
|
+
return "read";
|
|
35
296
|
}
|
|
@@ -14,11 +14,13 @@ export interface ServiceTreeAppBinding {
|
|
|
14
14
|
appCode?: string;
|
|
15
15
|
env?: string;
|
|
16
16
|
}
|
|
17
|
+
export type ServiceTreeAppBindingInput = string | ServiceTreeAppBinding;
|
|
17
18
|
export interface ServiceTreeArg {
|
|
18
19
|
name: string;
|
|
19
20
|
description?: string;
|
|
20
21
|
required?: boolean;
|
|
21
22
|
}
|
|
23
|
+
export type ServiceTreeArgInput = string | ServiceTreeArg;
|
|
22
24
|
export interface ServiceTreeMapToRule {
|
|
23
25
|
source?: string;
|
|
24
26
|
target: string;
|
|
@@ -39,6 +41,21 @@ export interface ServiceTreeFlag {
|
|
|
39
41
|
enum?: string[];
|
|
40
42
|
mapTo?: string | Omit<ServiceTreeMapToRule, "source">;
|
|
41
43
|
}
|
|
44
|
+
export interface ServiceTreeSimpleFlag {
|
|
45
|
+
to?: string;
|
|
46
|
+
target?: string;
|
|
47
|
+
op?: string;
|
|
48
|
+
operator?: string;
|
|
49
|
+
type?: ServiceTreeValueType;
|
|
50
|
+
description?: string;
|
|
51
|
+
required?: boolean;
|
|
52
|
+
default?: unknown;
|
|
53
|
+
enum?: string[];
|
|
54
|
+
cliName?: string;
|
|
55
|
+
transform?: ServiceTreeMapTransform;
|
|
56
|
+
omitEmpty?: boolean;
|
|
57
|
+
}
|
|
58
|
+
export type ServiceTreeFlagsInput = ServiceTreeFlag[] | Record<string, string | ServiceTreeSimpleFlag>;
|
|
42
59
|
export interface ServiceTreeTarget {
|
|
43
60
|
kind: ServiceTreeTargetKind;
|
|
44
61
|
command: ServiceTreeTargetCommand;
|
|
@@ -55,18 +72,62 @@ export interface ServiceTreeCommand {
|
|
|
55
72
|
path: string | string[];
|
|
56
73
|
description?: string;
|
|
57
74
|
risk?: Risk;
|
|
58
|
-
args?:
|
|
59
|
-
flags?:
|
|
75
|
+
args?: ServiceTreeArgInput[];
|
|
76
|
+
flags?: ServiceTreeFlagsInput;
|
|
60
77
|
target: ServiceTreeTarget;
|
|
61
78
|
params?: Record<string, unknown>;
|
|
79
|
+
defaults?: Record<string, unknown>;
|
|
80
|
+
map?: ServiceTreeMapTo;
|
|
81
|
+
mapTo?: ServiceTreeMapTo;
|
|
82
|
+
}
|
|
83
|
+
export interface ServiceTreeAction {
|
|
84
|
+
description?: string;
|
|
85
|
+
risk?: Risk;
|
|
86
|
+
action?: ServiceTreeTargetCommand | `${ServiceTreeTargetKind}.${ServiceTreeTargetCommand}`;
|
|
87
|
+
target?: ServiceTreeTarget | `${ServiceTreeTargetKind}.${ServiceTreeTargetCommand}`;
|
|
88
|
+
kind?: ServiceTreeTargetKind;
|
|
89
|
+
command?: ServiceTreeTargetCommand;
|
|
90
|
+
appRef?: string;
|
|
91
|
+
app?: string;
|
|
92
|
+
datasetCode?: string;
|
|
93
|
+
datatable?: string;
|
|
94
|
+
table?: string;
|
|
95
|
+
sqlCode?: string;
|
|
96
|
+
bffCode?: string;
|
|
97
|
+
bffId?: string | number;
|
|
98
|
+
scriptName?: string;
|
|
99
|
+
args?: ServiceTreeArgInput[];
|
|
100
|
+
flags?: ServiceTreeFlagsInput;
|
|
101
|
+
defaults?: Record<string, unknown>;
|
|
102
|
+
params?: Record<string, unknown>;
|
|
103
|
+
map?: ServiceTreeMapTo;
|
|
62
104
|
mapTo?: ServiceTreeMapTo;
|
|
63
105
|
}
|
|
106
|
+
export interface ServiceTreeResource {
|
|
107
|
+
name?: string;
|
|
108
|
+
description?: string;
|
|
109
|
+
appRef?: string;
|
|
110
|
+
app?: string;
|
|
111
|
+
datasetCode?: string;
|
|
112
|
+
datatable?: string;
|
|
113
|
+
table?: string;
|
|
114
|
+
defaults?: Record<string, unknown>;
|
|
115
|
+
params?: Record<string, unknown>;
|
|
116
|
+
actions?: Record<string, ServiceTreeAction>;
|
|
117
|
+
resources?: Record<string, ServiceTreeResource>;
|
|
118
|
+
}
|
|
64
119
|
export interface ServiceTreeManifest {
|
|
65
120
|
protocol?: string;
|
|
66
|
-
version
|
|
67
|
-
service: ServiceTreeServiceInfo;
|
|
68
|
-
|
|
69
|
-
|
|
121
|
+
version?: string;
|
|
122
|
+
service: string | ServiceTreeServiceInfo;
|
|
123
|
+
name?: string;
|
|
124
|
+
description?: string;
|
|
125
|
+
app?: ServiceTreeAppBindingInput;
|
|
126
|
+
apps?: Record<string, ServiceTreeAppBindingInput>;
|
|
127
|
+
appBindings?: Record<string, ServiceTreeAppBindingInput>;
|
|
128
|
+
defaults?: Record<string, unknown>;
|
|
129
|
+
commands?: ServiceTreeCommand[];
|
|
130
|
+
resources?: Record<string, ServiceTreeResource>;
|
|
70
131
|
}
|
|
71
132
|
export interface NormalizedServiceTreeFlag extends ServiceTreeFlag {
|
|
72
133
|
cliName: string;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { isCommandSegment, isDatatableName, isDatasetCode, isServiceCode,
|
|
1
|
+
import { isCommandSegment, isDatatableName, isDatasetCode, isServiceCode, } from "./path.js";
|
|
2
|
+
import { normalizeServiceTreeManifest } from "./normalize.js";
|
|
2
3
|
const DATA_COMMANDS = new Set(["filter", "getOne", "aggregate", "create", "batchCreate", "update", "delete"]);
|
|
3
4
|
const SQL_COMMANDS = new Set(["exec"]);
|
|
4
5
|
const BFF_COMMANDS = new Set(["exec"]);
|
|
@@ -9,35 +10,54 @@ export function validateServiceTreeManifest(manifest) {
|
|
|
9
10
|
push(issues, "error", "$", "Manifest must be a JSON object.");
|
|
10
11
|
return buildReport(issues);
|
|
11
12
|
}
|
|
12
|
-
if (typeof manifest.
|
|
13
|
-
|
|
13
|
+
if (typeof manifest.service === "string") {
|
|
14
|
+
if (!isServiceCode(manifest.service)) {
|
|
15
|
+
push(issues, "error", "$.service", "service must be lower kebab-case.");
|
|
16
|
+
}
|
|
14
17
|
}
|
|
15
|
-
if (!isRecord(manifest.service)) {
|
|
18
|
+
else if (!isRecord(manifest.service)) {
|
|
16
19
|
push(issues, "error", "$.service", "service is required.");
|
|
17
20
|
}
|
|
18
21
|
else if (typeof manifest.service.code !== "string" || !isServiceCode(manifest.service.code)) {
|
|
19
22
|
push(issues, "error", "$.service.code", "service.code must be lower kebab-case.");
|
|
20
23
|
}
|
|
21
|
-
|
|
22
|
-
|
|
24
|
+
let hasShapeError = false;
|
|
25
|
+
if (manifest.commands !== undefined && !Array.isArray(manifest.commands)) {
|
|
26
|
+
push(issues, "error", "$.commands", "commands must be an array.");
|
|
27
|
+
hasShapeError = true;
|
|
28
|
+
}
|
|
29
|
+
if (manifest.resources !== undefined && !isRecord(manifest.resources)) {
|
|
30
|
+
push(issues, "error", "$.resources", "resources must be an object.");
|
|
31
|
+
hasShapeError = true;
|
|
32
|
+
}
|
|
33
|
+
if (hasShapeError) {
|
|
34
|
+
return buildReport(issues);
|
|
35
|
+
}
|
|
36
|
+
if (!Array.isArray(manifest.commands) && !isRecord(manifest.resources)) {
|
|
37
|
+
push(issues, "error", "$", "commands or resources is required.");
|
|
38
|
+
return buildReport(issues);
|
|
39
|
+
}
|
|
40
|
+
let normalized;
|
|
41
|
+
try {
|
|
42
|
+
normalized = normalizeServiceTreeManifest(manifest);
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
push(issues, "error", "$", error instanceof Error ? error.message : String(error));
|
|
46
|
+
return buildReport(issues);
|
|
47
|
+
}
|
|
48
|
+
if (normalized.commands.length === 0) {
|
|
49
|
+
push(issues, "error", "$", "Service tree must define at least one command.");
|
|
23
50
|
return buildReport(issues);
|
|
24
51
|
}
|
|
25
|
-
const serviceCode = isRecord(manifest.service) && typeof manifest.service.code === "string"
|
|
26
|
-
? manifest.service.code
|
|
27
|
-
: "";
|
|
28
52
|
const seenPaths = new Set();
|
|
29
|
-
|
|
30
|
-
validateCommand(command, index,
|
|
53
|
+
normalized.commands.forEach((command, index) => {
|
|
54
|
+
validateCommand(command, index, seenPaths, issues);
|
|
31
55
|
});
|
|
32
56
|
return buildReport(issues);
|
|
33
57
|
}
|
|
34
|
-
function validateCommand(command, index,
|
|
58
|
+
function validateCommand(command, index, seenPaths, issues) {
|
|
35
59
|
const base = `$.commands[${index}]`;
|
|
36
|
-
|
|
37
|
-
push(issues, "error", base, "Command must be an object.");
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
const path = splitServiceTreePath(command.path || []);
|
|
60
|
+
const path = command.path;
|
|
41
61
|
if (path.length === 0) {
|
|
42
62
|
push(issues, "error", `${base}.path`, "path is required.");
|
|
43
63
|
}
|
|
@@ -46,8 +66,7 @@ function validateCommand(command, index, serviceCode, seenPaths, issues) {
|
|
|
46
66
|
push(issues, "error", `${base}.path[${partIndex}]`, "Command path segments must be lower kebab-case.");
|
|
47
67
|
}
|
|
48
68
|
}
|
|
49
|
-
const
|
|
50
|
-
const fullKey = fullPath.join(" ");
|
|
69
|
+
const fullKey = command.fullPath.join(" ");
|
|
51
70
|
if (seenPaths.has(fullKey)) {
|
|
52
71
|
push(issues, "error", `${base}.path`, `Duplicate command path: ${fullKey}.`);
|
|
53
72
|
}
|
|
@@ -105,6 +124,7 @@ function validateFlags(flags, path, issues) {
|
|
|
105
124
|
return;
|
|
106
125
|
}
|
|
107
126
|
const seen = new Set();
|
|
127
|
+
const seenCliNames = new Set();
|
|
108
128
|
flags.forEach((flag, index) => {
|
|
109
129
|
const flagPath = `${path}[${index}]`;
|
|
110
130
|
if (!isRecord(flag)) {
|
|
@@ -119,9 +139,11 @@ function validateFlags(flags, path, issues) {
|
|
|
119
139
|
push(issues, "error", `${flagPath}.name`, `Duplicate flag name: ${flag.name}.`);
|
|
120
140
|
}
|
|
121
141
|
seen.add(flag.name);
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
142
|
+
if (typeof flag.cliName === "string") {
|
|
143
|
+
if (seenCliNames.has(flag.cliName)) {
|
|
144
|
+
push(issues, "error", `${flagPath}.cliName`, `Duplicate CLI flag name: ${flag.cliName}.`);
|
|
145
|
+
}
|
|
146
|
+
seenCliNames.add(flag.cliName);
|
|
125
147
|
}
|
|
126
148
|
if (typeof flag.type !== "string" || !VALUE_TYPES.has(flag.type)) {
|
|
127
149
|
push(issues, "error", `${flagPath}.type`, "flag.type must be string, number, boolean, or json.");
|