@lovrabet/cli-framework 1.0.3 → 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.
package/lib/index.d.ts CHANGED
@@ -205,6 +205,7 @@ Paging,
205
205
  * @description List response wrapper with records and optional paging.
206
206
  */
207
207
  ListResponse, } from "./framework/response.js";
208
+ export * from "./service-tree/index.js";
208
209
  export { createFormatOutput, formatOutput } from "./framework/output.js";
209
210
  export type {
210
211
  /**
package/lib/index.js CHANGED
@@ -46,6 +46,8 @@ export {
46
46
  extractList,
47
47
  /** @see ./framework/response.ts */
48
48
  extractPaging, } from "./framework/response.js";
49
+ // ─── Service Tree Manifest Utilities ─────────────────────────────────────────
50
+ export * from "./service-tree/index.js";
49
51
  // ─── Output Formatting ─────────────────────────────────────────────────────────
50
52
  export { createFormatOutput, formatOutput } from "./framework/output.js";
51
53
  // ─── Bundled jq Sidecar ────────────────────────────────────────────────────────
@@ -0,0 +1,2 @@
1
+ import type { NormalizedServiceTreeCommand, ServiceTreeCompileInput, ServiceTreeExecutionPlan } from "./types.js";
2
+ export declare function compileServiceTreeCommand(command: NormalizedServiceTreeCommand, input?: ServiceTreeCompileInput): ServiceTreeExecutionPlan;
@@ -0,0 +1,128 @@
1
+ import { stripParamsPrefix } from "./path.js";
2
+ export function compileServiceTreeCommand(command, input = {}) {
3
+ const params = cloneRecord(command.baseParams);
4
+ for (const rule of command.mapTo) {
5
+ const value = resolveRuleValue(rule, command, input);
6
+ if (value === undefined)
7
+ continue;
8
+ if (rule.omitEmpty !== false && value === "")
9
+ continue;
10
+ const transformed = transformValue(value, rule.transform);
11
+ const targetPath = rule.operator
12
+ ? `${stripParamsPrefix(rule.target)}.${rule.operator}`
13
+ : stripParamsPrefix(rule.target);
14
+ setDeep(params, targetPath, transformed);
15
+ }
16
+ return {
17
+ kind: command.target.kind,
18
+ command: command.target.command,
19
+ appRef: command.target.appRef,
20
+ datasetCode: command.target.datasetCode,
21
+ datatable: command.target.datatable,
22
+ sqlCode: command.target.sqlCode,
23
+ bffCode: command.target.bffCode,
24
+ bffId: command.target.bffId,
25
+ scriptName: command.target.scriptName,
26
+ params,
27
+ };
28
+ }
29
+ function resolveRuleValue(rule, command, input) {
30
+ if (rule.source === undefined || rule.source === "const" || rule.source.startsWith("const.")) {
31
+ return rule.value;
32
+ }
33
+ if (rule.source.startsWith("flags.")) {
34
+ const name = rule.source.slice("flags.".length);
35
+ return readFlagValue(name, command, input.flags || {});
36
+ }
37
+ if (rule.source.startsWith("args.")) {
38
+ const name = rule.source.slice("args.".length);
39
+ return readArgValue(name, command, input.args || {});
40
+ }
41
+ if (rule.source.startsWith("context.")) {
42
+ const name = rule.source.slice("context.".length);
43
+ return readPath(input.context || {}, name);
44
+ }
45
+ if (rule.source.startsWith("ctx.")) {
46
+ const name = rule.source.slice("ctx.".length);
47
+ return readPath(input.context || {}, name);
48
+ }
49
+ return undefined;
50
+ }
51
+ function readFlagValue(name, command, flags) {
52
+ if (Object.prototype.hasOwnProperty.call(flags, name))
53
+ return flags[name];
54
+ const flag = command.flags.find((item) => item.name === name || item.cliName === name);
55
+ if (flag) {
56
+ if (Object.prototype.hasOwnProperty.call(flags, flag.cliName))
57
+ return flags[flag.cliName];
58
+ if (Object.prototype.hasOwnProperty.call(flags, flag.name))
59
+ return flags[flag.name];
60
+ if (flag.default !== undefined)
61
+ return flag.default;
62
+ }
63
+ return undefined;
64
+ }
65
+ function readArgValue(name, command, args) {
66
+ if (Array.isArray(args)) {
67
+ const index = /^\d+$/.test(name)
68
+ ? Number(name)
69
+ : command.args.findIndex((arg) => arg.name === name);
70
+ return index >= 0 ? args[index] : undefined;
71
+ }
72
+ return args[name];
73
+ }
74
+ function readPath(value, path) {
75
+ if (!path)
76
+ return value;
77
+ const parts = path.split(".").filter(Boolean);
78
+ let cursor = value;
79
+ for (const part of parts) {
80
+ if (!isRecord(cursor) || !Object.prototype.hasOwnProperty.call(cursor, part)) {
81
+ return undefined;
82
+ }
83
+ cursor = cursor[part];
84
+ }
85
+ return cursor;
86
+ }
87
+ function transformValue(value, transform) {
88
+ if (!transform)
89
+ return value;
90
+ if (transform === "string")
91
+ return String(value);
92
+ if (transform === "number")
93
+ return typeof value === "number" ? value : Number(value);
94
+ if (transform === "boolean") {
95
+ if (typeof value === "boolean")
96
+ return value;
97
+ if (typeof value === "string")
98
+ return value === "true" || value === "1";
99
+ return Boolean(value);
100
+ }
101
+ if (transform === "json") {
102
+ if (typeof value !== "string")
103
+ return value;
104
+ return JSON.parse(value);
105
+ }
106
+ return value;
107
+ }
108
+ function setDeep(target, path, value) {
109
+ const parts = path.split(".").filter(Boolean);
110
+ let cursor = target;
111
+ parts.forEach((part, index) => {
112
+ if (index === parts.length - 1) {
113
+ cursor[part] = value;
114
+ return;
115
+ }
116
+ const existing = cursor[part];
117
+ if (!isRecord(existing)) {
118
+ cursor[part] = {};
119
+ }
120
+ cursor = cursor[part];
121
+ });
122
+ }
123
+ function cloneRecord(value) {
124
+ return JSON.parse(JSON.stringify(value));
125
+ }
126
+ function isRecord(value) {
127
+ return typeof value === "object" && value !== null && !Array.isArray(value);
128
+ }
@@ -0,0 +1,6 @@
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
+ export { compileServiceTreeCommand } from "./compile.js";
3
+ export { matchServiceTreeCommand } from "./match.js";
4
+ export { normalizeServiceTreeManifest } from "./normalize.js";
5
+ export { isCommandSegment, isDatatableName, isDatasetCode, isServiceCode, pathToString, splitServiceTreePath, stripParamsPrefix, toKebabCase, } from "./path.js";
6
+ export { validateServiceTreeManifest } from "./validate.js";
@@ -0,0 +1,5 @@
1
+ export { compileServiceTreeCommand } from "./compile.js";
2
+ export { matchServiceTreeCommand } from "./match.js";
3
+ export { normalizeServiceTreeManifest } from "./normalize.js";
4
+ export { isCommandSegment, isDatatableName, isDatasetCode, isServiceCode, pathToString, splitServiceTreePath, stripParamsPrefix, toKebabCase, } from "./path.js";
5
+ export { validateServiceTreeManifest } from "./validate.js";
@@ -0,0 +1,2 @@
1
+ import type { ServiceTreeFlag, ServiceTreeMapTo, ServiceTreeMapToRule } from "./types.js";
2
+ export declare function normalizeMapTo(mapTo: ServiceTreeMapTo | undefined, flags?: ServiceTreeFlag[]): ServiceTreeMapToRule[];
@@ -0,0 +1,27 @@
1
+ export function normalizeMapTo(mapTo, flags = []) {
2
+ const rules = [];
3
+ if (Array.isArray(mapTo)) {
4
+ rules.push(...mapTo.map((rule) => ({ ...rule })));
5
+ }
6
+ else if (mapTo && typeof mapTo === "object") {
7
+ for (const [source, target] of Object.entries(mapTo)) {
8
+ if (typeof target === "string") {
9
+ rules.push({ source, target });
10
+ }
11
+ else {
12
+ rules.push({ source, ...target });
13
+ }
14
+ }
15
+ }
16
+ for (const flag of flags) {
17
+ if (!flag.mapTo)
18
+ continue;
19
+ if (typeof flag.mapTo === "string") {
20
+ rules.push({ source: `flags.${flag.name}`, target: flag.mapTo });
21
+ }
22
+ else {
23
+ rules.push({ source: `flags.${flag.name}`, ...flag.mapTo });
24
+ }
25
+ }
26
+ return rules;
27
+ }
@@ -0,0 +1,2 @@
1
+ import type { NormalizedServiceTree, ServiceTreeMatch } from "./types.js";
2
+ export declare function matchServiceTreeCommand(tree: NormalizedServiceTree, input: string | string[]): ServiceTreeMatch | undefined;
@@ -0,0 +1,19 @@
1
+ import { splitServiceTreePath } from "./path.js";
2
+ export function matchServiceTreeCommand(tree, input) {
3
+ const path = splitServiceTreePath(input);
4
+ let best;
5
+ for (const command of tree.commands) {
6
+ if (!startsWith(path, command.fullPath))
7
+ continue;
8
+ const remainingArgs = path.slice(command.fullPath.length);
9
+ if (!best || command.fullPath.length > best.command.fullPath.length) {
10
+ best = { command, remainingArgs };
11
+ }
12
+ }
13
+ return best;
14
+ }
15
+ function startsWith(path, prefix) {
16
+ if (prefix.length > path.length)
17
+ return false;
18
+ return prefix.every((segment, index) => path[index] === segment);
19
+ }
@@ -0,0 +1,2 @@
1
+ import type { NormalizedServiceTree, ServiceTreeManifest } from "./types.js";
2
+ export declare function normalizeServiceTreeManifest(manifest: ServiceTreeManifest): NormalizedServiceTree;
@@ -0,0 +1,296 @@
1
+ import { normalizeMapTo } from "./map-to.js";
2
+ import { splitServiceTreePath, toKebabCase } from "./path.js";
3
+ const DEFAULT_PROTOCOL = "lovrabet.service-tree/v1";
4
+ const DEFAULT_VERSION = "1.0.0";
5
+ const DEFAULT_APP_REF = "default";
6
+ export function normalizeServiceTreeManifest(manifest) {
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
+ ];
15
+ return {
16
+ protocol: manifest.protocol || DEFAULT_PROTOCOL,
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,
104
+ }));
105
+ }
106
+ if (resource.resources) {
107
+ commands.push(...flattenResources({
108
+ resources: resource.resources,
109
+ serviceCode: input.serviceCode,
110
+ defaultAppRef: input.defaultAppRef,
111
+ path,
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,
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";
296
+ }
@@ -0,0 +1,8 @@
1
+ export declare function splitServiceTreePath(path: string | string[]): string[];
2
+ export declare function toKebabCase(value: string): string;
3
+ export declare function isCommandSegment(value: string): boolean;
4
+ export declare function isServiceCode(value: string): boolean;
5
+ export declare function isDatatableName(value: string): boolean;
6
+ export declare function isDatasetCode(value: string): boolean;
7
+ export declare function stripParamsPrefix(path: string): string;
8
+ export declare function pathToString(path: string[]): string;
@@ -0,0 +1,30 @@
1
+ export function splitServiceTreePath(path) {
2
+ const parts = Array.isArray(path) ? path : path.trim().split(/\s+/);
3
+ return parts.map((part) => part.trim()).filter(Boolean);
4
+ }
5
+ export function toKebabCase(value) {
6
+ return value
7
+ .trim()
8
+ .replace(/([a-z0-9])([A-Z])/g, "$1-$2")
9
+ .replace(/[_\s]+/g, "-")
10
+ .replace(/-+/g, "-")
11
+ .toLowerCase();
12
+ }
13
+ export function isCommandSegment(value) {
14
+ return /^[a-z][a-z0-9-]*$/.test(value);
15
+ }
16
+ export function isServiceCode(value) {
17
+ return /^[a-z][a-z0-9-]*$/.test(value);
18
+ }
19
+ export function isDatatableName(value) {
20
+ return /^[A-Za-z][A-Za-z0-9_]*$/.test(value);
21
+ }
22
+ export function isDatasetCode(value) {
23
+ return /^[a-f0-9]{32}$/i.test(value);
24
+ }
25
+ export function stripParamsPrefix(path) {
26
+ return path.startsWith("params.") ? path.slice("params.".length) : path;
27
+ }
28
+ export function pathToString(path) {
29
+ return path.join(" ");
30
+ }
@@ -0,0 +1,193 @@
1
+ import type { Risk } from "../framework/types.js";
2
+ export type ServiceTreeTargetKind = "data" | "sql" | "bff";
3
+ export type ServiceTreeTargetCommand = "filter" | "getOne" | "aggregate" | "create" | "batchCreate" | "update" | "delete" | "exec";
4
+ export type ServiceTreeValueType = "string" | "number" | "boolean" | "json";
5
+ export type ServiceTreeMapTransform = ServiceTreeValueType;
6
+ export interface ServiceTreeServiceInfo {
7
+ code: string;
8
+ name?: string;
9
+ description?: string;
10
+ }
11
+ export interface ServiceTreeAppBinding {
12
+ app?: string;
13
+ appcode?: string;
14
+ appCode?: string;
15
+ env?: string;
16
+ }
17
+ export type ServiceTreeAppBindingInput = string | ServiceTreeAppBinding;
18
+ export interface ServiceTreeArg {
19
+ name: string;
20
+ description?: string;
21
+ required?: boolean;
22
+ }
23
+ export type ServiceTreeArgInput = string | ServiceTreeArg;
24
+ export interface ServiceTreeMapToRule {
25
+ source?: string;
26
+ target: string;
27
+ value?: unknown;
28
+ transform?: ServiceTreeMapTransform;
29
+ operator?: string;
30
+ omitEmpty?: boolean;
31
+ }
32
+ export type ServiceTreeMapToObject = Record<string, string | Omit<ServiceTreeMapToRule, "source">>;
33
+ export type ServiceTreeMapTo = ServiceTreeMapToRule[] | ServiceTreeMapToObject;
34
+ export interface ServiceTreeFlag {
35
+ name: string;
36
+ cliName?: string;
37
+ type: ServiceTreeValueType;
38
+ description?: string;
39
+ required?: boolean;
40
+ default?: unknown;
41
+ enum?: string[];
42
+ mapTo?: string | Omit<ServiceTreeMapToRule, "source">;
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>;
59
+ export interface ServiceTreeTarget {
60
+ kind: ServiceTreeTargetKind;
61
+ command: ServiceTreeTargetCommand;
62
+ appRef?: string;
63
+ datasetCode?: string;
64
+ datatable?: string;
65
+ sqlCode?: string;
66
+ bffCode?: string;
67
+ bffId?: string | number;
68
+ scriptName?: string;
69
+ params?: Record<string, unknown>;
70
+ }
71
+ export interface ServiceTreeCommand {
72
+ path: string | string[];
73
+ description?: string;
74
+ risk?: Risk;
75
+ args?: ServiceTreeArgInput[];
76
+ flags?: ServiceTreeFlagsInput;
77
+ target: ServiceTreeTarget;
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;
104
+ mapTo?: ServiceTreeMapTo;
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
+ }
119
+ export interface ServiceTreeManifest {
120
+ protocol?: string;
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>;
131
+ }
132
+ export interface NormalizedServiceTreeFlag extends ServiceTreeFlag {
133
+ cliName: string;
134
+ }
135
+ export interface NormalizedServiceTreeTarget extends ServiceTreeTarget {
136
+ command: ServiceTreeTargetCommand;
137
+ }
138
+ export interface NormalizedServiceTreeCommand {
139
+ path: string[];
140
+ fullPath: string[];
141
+ cliPath: string;
142
+ description?: string;
143
+ risk: Risk;
144
+ args: ServiceTreeArg[];
145
+ flags: NormalizedServiceTreeFlag[];
146
+ target: NormalizedServiceTreeTarget;
147
+ baseParams: Record<string, unknown>;
148
+ mapTo: ServiceTreeMapToRule[];
149
+ }
150
+ export interface NormalizedServiceTree {
151
+ protocol: string;
152
+ version: string;
153
+ service: ServiceTreeServiceInfo;
154
+ appBindings: Record<string, ServiceTreeAppBinding>;
155
+ commands: NormalizedServiceTreeCommand[];
156
+ }
157
+ export interface ServiceTreeValidationIssue {
158
+ level: "error" | "warning";
159
+ path: string;
160
+ message: string;
161
+ }
162
+ export interface ServiceTreeValidationReport {
163
+ ok: boolean;
164
+ errors: ServiceTreeValidationIssue[];
165
+ warnings: ServiceTreeValidationIssue[];
166
+ }
167
+ export interface ServiceTreeMatch {
168
+ command: NormalizedServiceTreeCommand;
169
+ remainingArgs: string[];
170
+ }
171
+ export interface ServiceTreeCompileInput {
172
+ flags?: Record<string, unknown>;
173
+ args?: Record<string, unknown> | string[];
174
+ context?: Record<string, unknown>;
175
+ }
176
+ export interface ServiceTreeExecutionPlan {
177
+ kind: ServiceTreeTargetKind;
178
+ command: ServiceTreeTargetCommand;
179
+ appRef?: string;
180
+ datasetCode?: string;
181
+ datatable?: string;
182
+ sqlCode?: string;
183
+ bffCode?: string;
184
+ bffId?: string | number;
185
+ scriptName?: string;
186
+ params: Record<string, unknown>;
187
+ }
188
+ export interface ServiceTreeDatasetResolverInput {
189
+ appRef?: string;
190
+ datatable: string;
191
+ commandPath: string[];
192
+ }
193
+ export type ServiceTreeDatasetResolver = (input: ServiceTreeDatasetResolverInput) => string | undefined | Promise<string | undefined>;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ import type { ServiceTreeValidationReport } from "./types.js";
2
+ export declare function validateServiceTreeManifest(manifest: unknown): ServiceTreeValidationReport;
@@ -0,0 +1,210 @@
1
+ import { isCommandSegment, isDatatableName, isDatasetCode, isServiceCode, } from "./path.js";
2
+ import { normalizeServiceTreeManifest } from "./normalize.js";
3
+ const DATA_COMMANDS = new Set(["filter", "getOne", "aggregate", "create", "batchCreate", "update", "delete"]);
4
+ const SQL_COMMANDS = new Set(["exec"]);
5
+ const BFF_COMMANDS = new Set(["exec"]);
6
+ const VALUE_TYPES = new Set(["string", "number", "boolean", "json"]);
7
+ export function validateServiceTreeManifest(manifest) {
8
+ const issues = [];
9
+ if (!isRecord(manifest)) {
10
+ push(issues, "error", "$", "Manifest must be a JSON object.");
11
+ return buildReport(issues);
12
+ }
13
+ if (typeof manifest.service === "string") {
14
+ if (!isServiceCode(manifest.service)) {
15
+ push(issues, "error", "$.service", "service must be lower kebab-case.");
16
+ }
17
+ }
18
+ else if (!isRecord(manifest.service)) {
19
+ push(issues, "error", "$.service", "service is required.");
20
+ }
21
+ else if (typeof manifest.service.code !== "string" || !isServiceCode(manifest.service.code)) {
22
+ push(issues, "error", "$.service.code", "service.code must be lower kebab-case.");
23
+ }
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.");
50
+ return buildReport(issues);
51
+ }
52
+ const seenPaths = new Set();
53
+ normalized.commands.forEach((command, index) => {
54
+ validateCommand(command, index, seenPaths, issues);
55
+ });
56
+ return buildReport(issues);
57
+ }
58
+ function validateCommand(command, index, seenPaths, issues) {
59
+ const base = `$.commands[${index}]`;
60
+ const path = command.path;
61
+ if (path.length === 0) {
62
+ push(issues, "error", `${base}.path`, "path is required.");
63
+ }
64
+ for (const [partIndex, segment] of path.entries()) {
65
+ if (!isCommandSegment(segment)) {
66
+ push(issues, "error", `${base}.path[${partIndex}]`, "Command path segments must be lower kebab-case.");
67
+ }
68
+ }
69
+ const fullKey = command.fullPath.join(" ");
70
+ if (seenPaths.has(fullKey)) {
71
+ push(issues, "error", `${base}.path`, `Duplicate command path: ${fullKey}.`);
72
+ }
73
+ seenPaths.add(fullKey);
74
+ validateTarget(command.target, `${base}.target`, issues);
75
+ validateFlags(command.flags, `${base}.flags`, issues);
76
+ validateMapTo(command.mapTo, `${base}.mapTo`, issues);
77
+ }
78
+ function validateTarget(target, path, issues) {
79
+ if (!isRecord(target)) {
80
+ push(issues, "error", path, "target is required.");
81
+ return;
82
+ }
83
+ const typed = target;
84
+ if (typed.kind === "data") {
85
+ if (!DATA_COMMANDS.has(typed.command)) {
86
+ push(issues, "error", `${path}.command`, "data target command is not supported.");
87
+ }
88
+ if (!typed.datasetCode && !typed.datatable) {
89
+ push(issues, "error", path, "data target requires datasetCode or datatable.");
90
+ }
91
+ if (typed.datasetCode && !isDatasetCode(typed.datasetCode)) {
92
+ push(issues, "error", `${path}.datasetCode`, "datasetCode must be a 32-character code.");
93
+ }
94
+ if (typed.datatable && !isDatatableName(typed.datatable)) {
95
+ push(issues, "error", `${path}.datatable`, "datatable must be a valid physical table name.");
96
+ }
97
+ return;
98
+ }
99
+ if (typed.kind === "sql") {
100
+ if (!SQL_COMMANDS.has(typed.command)) {
101
+ push(issues, "error", `${path}.command`, "sql target command is not supported.");
102
+ }
103
+ if (!typed.sqlCode) {
104
+ push(issues, "error", `${path}.sqlCode`, "sql target requires sqlCode.");
105
+ }
106
+ return;
107
+ }
108
+ if (typed.kind === "bff") {
109
+ if (!BFF_COMMANDS.has(typed.command)) {
110
+ push(issues, "error", `${path}.command`, "bff target command is not supported.");
111
+ }
112
+ if (!typed.bffCode && !typed.bffId && !typed.scriptName) {
113
+ push(issues, "error", path, "bff target requires bffCode, bffId, or scriptName.");
114
+ }
115
+ return;
116
+ }
117
+ push(issues, "error", `${path}.kind`, "target.kind must be data, sql, or bff.");
118
+ }
119
+ function validateFlags(flags, path, issues) {
120
+ if (flags === undefined)
121
+ return;
122
+ if (!Array.isArray(flags)) {
123
+ push(issues, "error", path, "flags must be an array.");
124
+ return;
125
+ }
126
+ const seen = new Set();
127
+ const seenCliNames = new Set();
128
+ flags.forEach((flag, index) => {
129
+ const flagPath = `${path}[${index}]`;
130
+ if (!isRecord(flag)) {
131
+ push(issues, "error", flagPath, "flag must be an object.");
132
+ return;
133
+ }
134
+ if (typeof flag.name !== "string" || !flag.name.trim()) {
135
+ push(issues, "error", `${flagPath}.name`, "flag.name is required.");
136
+ return;
137
+ }
138
+ if (seen.has(flag.name)) {
139
+ push(issues, "error", `${flagPath}.name`, `Duplicate flag name: ${flag.name}.`);
140
+ }
141
+ seen.add(flag.name);
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);
147
+ }
148
+ if (typeof flag.type !== "string" || !VALUE_TYPES.has(flag.type)) {
149
+ push(issues, "error", `${flagPath}.type`, "flag.type must be string, number, boolean, or json.");
150
+ }
151
+ });
152
+ }
153
+ function validateMapTo(mapTo, path, issues) {
154
+ if (mapTo === undefined)
155
+ return;
156
+ if (Array.isArray(mapTo)) {
157
+ mapTo.forEach((rule, index) => validateMapRule(rule, `${path}[${index}]`, issues));
158
+ return;
159
+ }
160
+ if (isRecord(mapTo)) {
161
+ for (const [source, target] of Object.entries(mapTo)) {
162
+ if (!isValidSource(source)) {
163
+ push(issues, "error", `${path}.${source}`, "mapTo source must start with flags., args., const., context., or ctx.");
164
+ }
165
+ if (typeof target === "string") {
166
+ if (!target.trim())
167
+ push(issues, "error", `${path}.${source}`, "mapTo target is required.");
168
+ }
169
+ else {
170
+ validateMapRule({ source, ...target }, `${path}.${source}`, issues);
171
+ }
172
+ }
173
+ return;
174
+ }
175
+ push(issues, "error", path, "mapTo must be an object or an array.");
176
+ }
177
+ function validateMapRule(rule, path, issues) {
178
+ if (!isRecord(rule)) {
179
+ push(issues, "error", path, "mapTo rule must be an object.");
180
+ return;
181
+ }
182
+ if (rule.source !== undefined && (typeof rule.source !== "string" || !isValidSource(rule.source))) {
183
+ push(issues, "error", `${path}.source`, "mapTo source must start with flags., args., const., context., or ctx.");
184
+ }
185
+ if (typeof rule.target !== "string" || !rule.target.trim()) {
186
+ push(issues, "error", `${path}.target`, "mapTo target is required.");
187
+ }
188
+ if (rule.transform !== undefined && (typeof rule.transform !== "string" || !VALUE_TYPES.has(rule.transform))) {
189
+ push(issues, "error", `${path}.transform`, "mapTo transform must be string, number, boolean, or json.");
190
+ }
191
+ }
192
+ function isValidSource(source) {
193
+ return source === "const"
194
+ || source.startsWith("flags.")
195
+ || source.startsWith("args.")
196
+ || source.startsWith("const.")
197
+ || source.startsWith("context.")
198
+ || source.startsWith("ctx.");
199
+ }
200
+ function buildReport(issues) {
201
+ const errors = issues.filter((issue) => issue.level === "error");
202
+ const warnings = issues.filter((issue) => issue.level === "warning");
203
+ return { ok: errors.length === 0, errors, warnings };
204
+ }
205
+ function push(issues, level, path, message) {
206
+ issues.push({ level, path, message });
207
+ }
208
+ function isRecord(value) {
209
+ return typeof value === "object" && value !== null && !Array.isArray(value);
210
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lovrabet/cli-framework",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
4
4
  "type": "module",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",