@restoai/resto-datacli 0.1.12 → 0.2.1
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/.agents/skills/resto-datacli/SKILL.md +21 -2
- package/README.md +44 -8
- package/dist/cli.js +2 -0
- package/dist/commands/auth.js +366 -99
- package/dist/commands/knowledge.js +32 -13
- package/dist/commands/order.js +109 -0
- package/dist/commands/public-records.js +32 -0
- package/dist/commands/query.js +22 -4
- package/dist/commands/shop.js +66 -14
- package/dist/config/profile.js +1 -0
- package/dist/knowledge/local-store.js +2 -0
- package/dist/knowledge/manifest-client.js +52 -3
- package/dist/knowledge/schema.js +2 -0
- package/dist/knowledge/search-index.js +46 -0
- package/dist/report-api/query-plan.js +20 -4
- package/dist/report-api/report-api-client.js +105 -15
- package/docs/installation.md +44 -14
- package/package.json +1 -1
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import { AuthStore } from "../config/auth-store.js";
|
|
3
3
|
import { LocalKnowledgeStore } from "../knowledge/local-store.js";
|
|
4
|
-
import { ManifestClient } from "../knowledge/manifest-client.js";
|
|
4
|
+
import { KnowledgeManifestError, ManifestClient } from "../knowledge/manifest-client.js";
|
|
5
5
|
import { KnowledgeSyncService } from "../knowledge/sync-service.js";
|
|
6
6
|
import { failJson, printJson } from "./json-command.js";
|
|
7
|
+
const DEFAULT_REPORT_CLI_SERVICE_URL = "http://127.0.0.1:18081";
|
|
7
8
|
function parseSchemaVersion(write, value) {
|
|
8
9
|
const parsed = Number.parseInt(value, 10);
|
|
9
10
|
if (!Number.isInteger(parsed) || parsed <= 0 || String(parsed) !== value) {
|
|
@@ -23,8 +24,14 @@ function createDefaultSyncService(localRoot, profile, baseUrl, factory) {
|
|
|
23
24
|
})
|
|
24
25
|
});
|
|
25
26
|
}
|
|
26
|
-
function resolveManifestBaseUrl(
|
|
27
|
-
return
|
|
27
|
+
function resolveManifestBaseUrl(reportCliUrl) {
|
|
28
|
+
return reportCliUrl || process.env.RESTO_REPORT_CLI_URL || DEFAULT_REPORT_CLI_SERVICE_URL;
|
|
29
|
+
}
|
|
30
|
+
function failKnowledgeError(write, error) {
|
|
31
|
+
if (error instanceof KnowledgeManifestError) {
|
|
32
|
+
failJson(write, error.code, error.message);
|
|
33
|
+
}
|
|
34
|
+
failJson(write, "KNOWLEDGE_ERROR", "knowledge command failed");
|
|
28
35
|
}
|
|
29
36
|
export function createKnowledgeCommand(options = {}) {
|
|
30
37
|
const write = options.write ?? ((message) => process.stdout.write(message));
|
|
@@ -44,6 +51,7 @@ export function createKnowledgeCommand(options = {}) {
|
|
|
44
51
|
.description("Sync local knowledge artifacts")
|
|
45
52
|
.option("--env <env>", "Knowledge environment", "test")
|
|
46
53
|
.option("--schema-version <version>", "Knowledge schema version", "1")
|
|
54
|
+
.option("--report-cli-url <url>", "report-cli-service base URL for knowledge manifest", process.env.RESTO_REPORT_CLI_URL ?? DEFAULT_REPORT_CLI_SERVICE_URL)
|
|
47
55
|
.option("--fixture", "Sync bundled local fixture knowledge without remote services or auth")
|
|
48
56
|
.allowExcessArguments(false)
|
|
49
57
|
.action(async (rawOptions) => {
|
|
@@ -57,18 +65,24 @@ export function createKnowledgeCommand(options = {}) {
|
|
|
57
65
|
if (!profile) {
|
|
58
66
|
failJson(write, "AUTH_NOT_CONFIGURED", "Authentication profile is not configured. Run resto auth login first.");
|
|
59
67
|
}
|
|
60
|
-
return createDefaultSyncService(options.localRoot, profile, resolveManifestBaseUrl(
|
|
68
|
+
return createDefaultSyncService(options.localRoot, profile, resolveManifestBaseUrl(rawOptions.reportCliUrl), options.createKnowledgeSyncService);
|
|
61
69
|
})();
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
70
|
+
try {
|
|
71
|
+
printJson(write, await service.sync({
|
|
72
|
+
env: rawOptions.env,
|
|
73
|
+
schemaVersion
|
|
74
|
+
}));
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
failKnowledgeError(write, error);
|
|
78
|
+
}
|
|
66
79
|
});
|
|
67
80
|
knowledge
|
|
68
81
|
.command("check")
|
|
69
82
|
.description("Check whether local knowledge is stale against the remote manifest")
|
|
70
83
|
.option("--env <env>", "Knowledge environment", "test")
|
|
71
84
|
.option("--schema-version <version>", "Knowledge schema version", "1")
|
|
85
|
+
.option("--report-cli-url <url>", "report-cli-service base URL for knowledge manifest", process.env.RESTO_REPORT_CLI_URL ?? DEFAULT_REPORT_CLI_SERVICE_URL)
|
|
72
86
|
.option("--fixture", "Check bundled local fixture knowledge without remote services or auth")
|
|
73
87
|
.allowExcessArguments(false)
|
|
74
88
|
.action(async (rawOptions) => {
|
|
@@ -82,12 +96,17 @@ export function createKnowledgeCommand(options = {}) {
|
|
|
82
96
|
if (!profile) {
|
|
83
97
|
failJson(write, "AUTH_NOT_CONFIGURED", "Authentication profile is not configured. Run resto auth login first.");
|
|
84
98
|
}
|
|
85
|
-
return createDefaultSyncService(options.localRoot, profile, resolveManifestBaseUrl(
|
|
99
|
+
return createDefaultSyncService(options.localRoot, profile, resolveManifestBaseUrl(rawOptions.reportCliUrl), options.createKnowledgeSyncService);
|
|
86
100
|
})();
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
101
|
+
try {
|
|
102
|
+
printJson(write, await service.check({
|
|
103
|
+
env: rawOptions.env,
|
|
104
|
+
schemaVersion
|
|
105
|
+
}));
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
failKnowledgeError(write, error);
|
|
109
|
+
}
|
|
91
110
|
});
|
|
92
111
|
return knowledge;
|
|
93
112
|
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { AuthStore } from "../config/auth-store.js";
|
|
3
|
+
import { ReportApiError, ReportCliServiceClient } from "../report-api/report-api-client.js";
|
|
4
|
+
import { configureJsonParseErrors, failJson, parsePositiveIntegerArgument, printJson } from "./json-command.js";
|
|
5
|
+
const DEFAULT_REPORT_CLI_SERVICE_URL = "http://127.0.0.1:18081";
|
|
6
|
+
const MAX_POS_ORDER_BUSINESS_DATE_RANGE_DAYS = 31;
|
|
7
|
+
function requiredOption(write, value, optionName) {
|
|
8
|
+
if (!value) {
|
|
9
|
+
failJson(write, "INVALID_ARGUMENTS", `Missing required option: ${optionName}`);
|
|
10
|
+
}
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
function resolveReportCliUrl(options, auth, explicitUrl) {
|
|
14
|
+
return explicitUrl
|
|
15
|
+
?? options.reportCliUrl
|
|
16
|
+
?? process.env.RESTO_REPORT_CLI_URL
|
|
17
|
+
?? auth.baseUrl
|
|
18
|
+
?? DEFAULT_REPORT_CLI_SERVICE_URL;
|
|
19
|
+
}
|
|
20
|
+
function safeErrorMessage(error, token) {
|
|
21
|
+
const message = error instanceof ReportApiError || error instanceof Error
|
|
22
|
+
? error.message
|
|
23
|
+
: "report-cli-service order detail request failed";
|
|
24
|
+
return message.split(token).join("***").replace(/\s+/g, " ").trim().slice(0, 240);
|
|
25
|
+
}
|
|
26
|
+
function parseBusinessDate(write, value, optionName) {
|
|
27
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) {
|
|
28
|
+
failJson(write, "INVALID_ARGUMENTS", `${optionName} must use YYYY-MM-DD.`);
|
|
29
|
+
}
|
|
30
|
+
const [year, month, day] = value.split("-").map(Number);
|
|
31
|
+
const date = new Date(Date.UTC(year, month - 1, day));
|
|
32
|
+
if (date.getUTCFullYear() !== year || date.getUTCMonth() !== month - 1 || date.getUTCDate() !== day) {
|
|
33
|
+
failJson(write, "INVALID_ARGUMENTS", `${optionName} must be a valid calendar date.`);
|
|
34
|
+
}
|
|
35
|
+
return date;
|
|
36
|
+
}
|
|
37
|
+
function resolvePosOrderBusinessDateRange(write, rawOptions) {
|
|
38
|
+
const businessDateStart = requiredOption(write, rawOptions.businessDateStart?.trim(), "--business-date-start");
|
|
39
|
+
const businessDateEnd = requiredOption(write, rawOptions.businessDateEnd?.trim(), "--business-date-end");
|
|
40
|
+
const start = parseBusinessDate(write, businessDateStart, "--business-date-start");
|
|
41
|
+
const end = parseBusinessDate(write, businessDateEnd, "--business-date-end");
|
|
42
|
+
if (start > end) {
|
|
43
|
+
failJson(write, "INVALID_ARGUMENTS", "--business-date-start must not be after --business-date-end.");
|
|
44
|
+
}
|
|
45
|
+
const rangeDays = Math.floor((end.getTime() - start.getTime()) / 86_400_000) + 1;
|
|
46
|
+
if (rangeDays > MAX_POS_ORDER_BUSINESS_DATE_RANGE_DAYS) {
|
|
47
|
+
failJson(write, "INVALID_ARGUMENTS", "POS order business date range must not exceed 31 days.");
|
|
48
|
+
}
|
|
49
|
+
return { businessDateStart, businessDateEnd };
|
|
50
|
+
}
|
|
51
|
+
export function createOrderCommand(options = {}) {
|
|
52
|
+
const write = options.write ?? ((message) => process.stdout.write(message));
|
|
53
|
+
const order = new Command("order").description("Query protected order drilldown details");
|
|
54
|
+
order
|
|
55
|
+
.command("detail")
|
|
56
|
+
.description("Get rendered order detail by POS order id or internal order id")
|
|
57
|
+
.requiredOption("--report-id <id>", "Authorized main report id, for example 888002")
|
|
58
|
+
.option("--pos-order-id <id>", "POS order id")
|
|
59
|
+
.option("--order-id <id>", "Internal order id")
|
|
60
|
+
.option("--business-date-start <date>", "Business date start for POS order id (YYYY-MM-DD)")
|
|
61
|
+
.option("--business-date-end <date>", "Business date end for POS order id (YYYY-MM-DD)")
|
|
62
|
+
.option("--report-cli-url <url>", "report-cli-service gateway base URL")
|
|
63
|
+
.allowExcessArguments(false)
|
|
64
|
+
.action(async (rawOptions) => {
|
|
65
|
+
const auth = new AuthStore(options.localRoot).load();
|
|
66
|
+
if (!auth) {
|
|
67
|
+
failJson(write, "AUTH_NOT_CONFIGURED", "No local auth profile found. Run resto auth login first.");
|
|
68
|
+
}
|
|
69
|
+
const reportId = parsePositiveIntegerArgument(write, "--report-id", requiredOption(write, rawOptions.reportId, "--report-id"));
|
|
70
|
+
const posOrderId = rawOptions.posOrderId?.trim();
|
|
71
|
+
const orderId = rawOptions.orderId?.trim();
|
|
72
|
+
if (Boolean(posOrderId) === Boolean(orderId)) {
|
|
73
|
+
failJson(write, "INVALID_ARGUMENTS", "Pass exactly one of --pos-order-id and --order-id.");
|
|
74
|
+
}
|
|
75
|
+
const businessDateRange = posOrderId
|
|
76
|
+
? resolvePosOrderBusinessDateRange(write, rawOptions)
|
|
77
|
+
: undefined;
|
|
78
|
+
if (!posOrderId && (rawOptions.businessDateStart || rawOptions.businessDateEnd)) {
|
|
79
|
+
failJson(write, "INVALID_ARGUMENTS", "Business date options are only supported with --pos-order-id.");
|
|
80
|
+
}
|
|
81
|
+
const request = {
|
|
82
|
+
reportId,
|
|
83
|
+
...(posOrderId ? { posOrderId } : {}),
|
|
84
|
+
...(orderId ? { orderId } : {}),
|
|
85
|
+
...(businessDateRange ?? {})
|
|
86
|
+
};
|
|
87
|
+
const baseUrl = resolveReportCliUrl(options, auth, rawOptions.reportCliUrl);
|
|
88
|
+
const client = options.orderDetailClient
|
|
89
|
+
?? options.createOrderDetailClient?.(auth, baseUrl)
|
|
90
|
+
?? new ReportCliServiceClient({ auth, baseUrl });
|
|
91
|
+
let response;
|
|
92
|
+
try {
|
|
93
|
+
response = await client.getReportOrderDetail(request);
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
failJson(write, "REPORT_API_ERROR", safeErrorMessage(error, auth.token));
|
|
97
|
+
}
|
|
98
|
+
printJson(write, {
|
|
99
|
+
source: "/api/report-cli/report/order/detail",
|
|
100
|
+
reportId: response.reportId,
|
|
101
|
+
shopId: response.shopId,
|
|
102
|
+
posOrderId: response.posOrderId,
|
|
103
|
+
orderId: response.orderId,
|
|
104
|
+
detail: response.detail
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
configureJsonParseErrors(order, write);
|
|
108
|
+
return order;
|
|
109
|
+
}
|
|
@@ -46,6 +46,36 @@ function toPublicReportCompositionRecord(composition) {
|
|
|
46
46
|
source: composition.source
|
|
47
47
|
};
|
|
48
48
|
}
|
|
49
|
+
function toPublicReportMetricsByDimRecord(record) {
|
|
50
|
+
return {
|
|
51
|
+
reportId: record.reportId,
|
|
52
|
+
sourceReportId: record.sourceReportId,
|
|
53
|
+
modelId: record.modelId,
|
|
54
|
+
dimensionFieldName: record.dimensionFieldName,
|
|
55
|
+
dimensionDisplayText: record.dimensionDisplayText,
|
|
56
|
+
onlyMetricsByDim: record.onlyMetricsByDim,
|
|
57
|
+
disableMetricsByDim: record.disableMetricsByDim,
|
|
58
|
+
metricFieldNames: record.metricFieldNames,
|
|
59
|
+
defaultMetricFieldNames: record.defaultMetricFieldNames,
|
|
60
|
+
source: record.source
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function toPublicReportMergedHeadRecord(record) {
|
|
64
|
+
return {
|
|
65
|
+
reportId: record.reportId,
|
|
66
|
+
sourceReportId: record.sourceReportId,
|
|
67
|
+
modelId: record.modelId,
|
|
68
|
+
mergedHeadId: record.mergedHeadId,
|
|
69
|
+
defaultSortIndex: record.defaultSortIndex,
|
|
70
|
+
primaryHeadMetricsCode: record.primaryHeadMetricsCode,
|
|
71
|
+
subHeadIsDynamic: record.subHeadIsDynamic,
|
|
72
|
+
subHeadMetricsCodes: record.subHeadMetricsCodes,
|
|
73
|
+
subHeadByDimCode: record.subHeadByDimCode,
|
|
74
|
+
displaySubTotal: record.displaySubTotal,
|
|
75
|
+
subTotalMetricsCode: record.subTotalMetricsCode,
|
|
76
|
+
source: record.source
|
|
77
|
+
};
|
|
78
|
+
}
|
|
49
79
|
function toPublicReportQueryPlanRecord(plan) {
|
|
50
80
|
return {
|
|
51
81
|
reportId: plan.reportId,
|
|
@@ -100,6 +130,8 @@ export function toPublicReportRecord(report) {
|
|
|
100
130
|
...(report.routeMap ? { routeMap: report.routeMap.map(toPublicReportRouteMapRecord) } : {}),
|
|
101
131
|
...(report.menuTree ? { menuTree: report.menuTree.map(toPublicReportMenuTreeRecord) } : {}),
|
|
102
132
|
...(report.composition ? { composition: toPublicReportCompositionRecord(report.composition) } : {}),
|
|
133
|
+
...(report.metricsByDim ? { metricsByDim: report.metricsByDim.map(toPublicReportMetricsByDimRecord) } : {}),
|
|
134
|
+
...(report.mergedHeads ? { mergedHeads: report.mergedHeads.map(toPublicReportMergedHeadRecord) } : {}),
|
|
103
135
|
...(report.queryPlans ? { queryPlans: report.queryPlans.map(toPublicReportQueryPlanRecord) } : {}),
|
|
104
136
|
...(report.filterSurfaces ? { filterSurfaces: report.filterSurfaces.map(toPublicReportFilterSurfaceRecord) } : {}),
|
|
105
137
|
...(report.entityBindings ? { entityBindings: report.entityBindings.map(toPublicReportEntityBindingRecord) } : {})
|
package/dist/commands/query.js
CHANGED
|
@@ -2,10 +2,11 @@ import { readFileSync } from "node:fs";
|
|
|
2
2
|
import { Command } from "commander";
|
|
3
3
|
import { AuthStore } from "../config/auth-store.js";
|
|
4
4
|
import { SearchIndex } from "../knowledge/search-index.js";
|
|
5
|
-
import {
|
|
5
|
+
import { ReportApiError, ReportCliServiceClient } from "../report-api/report-api-client.js";
|
|
6
6
|
import { normalizeExecutableQueryPlan, parseQueryPlan, QueryPlanValidator } from "../report-api/query-plan.js";
|
|
7
7
|
import { SnapshotStore } from "../snapshot/snapshot-store.js";
|
|
8
8
|
import { configureJsonParseErrors, failJson, failMissingKnowledge, JsonCommandError, jsonError, printJson } from "./json-command.js";
|
|
9
|
+
const DEFAULT_REPORT_CLI_SERVICE_URL = "http://127.0.0.1:18081";
|
|
9
10
|
function parseSchemaVersion(write, value) {
|
|
10
11
|
const parsed = Number.parseInt(value, 10);
|
|
11
12
|
if (!Number.isInteger(parsed) || parsed <= 0 || String(parsed) !== value) {
|
|
@@ -57,9 +58,23 @@ function throwValidationError(write, validation) {
|
|
|
57
58
|
function failReportApiError(write, error) {
|
|
58
59
|
const message = error instanceof ReportApiError
|
|
59
60
|
? error.message
|
|
60
|
-
: "
|
|
61
|
+
: "report-cli-service request failed";
|
|
61
62
|
failJson(write, "REPORT_API_ERROR", message);
|
|
62
63
|
}
|
|
64
|
+
function parseQueryBackend(write, value) {
|
|
65
|
+
const backend = value || "report-cli-service";
|
|
66
|
+
if (backend === "report-cli-service") {
|
|
67
|
+
return backend;
|
|
68
|
+
}
|
|
69
|
+
if (backend === "report-api") {
|
|
70
|
+
failJson(write, "INVALID_ARGUMENTS", "Direct report-api query backend is not allowed. Use report-cli-service.");
|
|
71
|
+
}
|
|
72
|
+
failJson(write, "INVALID_ARGUMENTS", "Invalid query backend. Expected report-cli-service.");
|
|
73
|
+
}
|
|
74
|
+
function createQueryDataClient(options, auth, reportCliUrl) {
|
|
75
|
+
const baseUrl = reportCliUrl || DEFAULT_REPORT_CLI_SERVICE_URL;
|
|
76
|
+
return options.reportCliServiceClient ?? options.createReportCliServiceClient?.(auth, baseUrl) ?? new ReportCliServiceClient({ auth, baseUrl });
|
|
77
|
+
}
|
|
63
78
|
function schemaFromValidation(validation, response) {
|
|
64
79
|
if (response.schema.length > 0) {
|
|
65
80
|
return response.schema;
|
|
@@ -95,7 +110,7 @@ Examples:
|
|
|
95
110
|
resto query validate --plan ./plan.json
|
|
96
111
|
resto query execute --plan ./plan.json
|
|
97
112
|
|
|
98
|
-
Field names inside plans must use exact report field codes required by report
|
|
113
|
+
Field names inside plans must use exact report field codes required by the report service.
|
|
99
114
|
`);
|
|
100
115
|
query
|
|
101
116
|
.command("validate")
|
|
@@ -115,6 +130,8 @@ Field names inside plans must use exact report field codes required by report-ap
|
|
|
115
130
|
.option("--plan <path>", "Query plan JSON file")
|
|
116
131
|
.option("--env <env>", "Knowledge environment", options.env ?? "test")
|
|
117
132
|
.option("--schema-version <version>", "Knowledge schema version", "1")
|
|
133
|
+
.option("--backend <backend>", "Execution backend: report-cli-service", process.env.RESTO_QUERY_BACKEND ?? "report-cli-service")
|
|
134
|
+
.option("--report-cli-url <url>", "report-cli-service base URL for query execution", process.env.RESTO_REPORT_CLI_URL ?? DEFAULT_REPORT_CLI_SERVICE_URL)
|
|
118
135
|
.allowExcessArguments(false)
|
|
119
136
|
.action(async (rawOptions) => {
|
|
120
137
|
const plan = readPlanFile(requireOption(write, rawOptions.plan, "--plan"), write);
|
|
@@ -128,7 +145,8 @@ Field names inside plans must use exact report field codes required by report-ap
|
|
|
128
145
|
if (!auth) {
|
|
129
146
|
failJson(write, "AUTH_NOT_CONFIGURED", "No local auth profile found. Run resto auth login first.");
|
|
130
147
|
}
|
|
131
|
-
|
|
148
|
+
parseQueryBackend(write, rawOptions.backend);
|
|
149
|
+
const client = createQueryDataClient(options, auth, rawOptions.reportCliUrl);
|
|
132
150
|
let response;
|
|
133
151
|
try {
|
|
134
152
|
response = await client.queryData(parsedPlan, { env: rawOptions.env, schemaVersion });
|
package/dist/commands/shop.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import { AuthStore } from "../config/auth-store.js";
|
|
3
|
-
import {
|
|
3
|
+
import { ReportApiError, ReportCliServiceClient } from "../report-api/report-api-client.js";
|
|
4
4
|
import { configureJsonParseErrors, failJson, parsePositiveIntegerArgument, printJson } from "./json-command.js";
|
|
5
|
+
const DEFAULT_REPORT_CLI_SERVICE_URL = "http://127.0.0.1:18081";
|
|
5
6
|
function requireOption(write, value, optionName) {
|
|
6
7
|
if (!value) {
|
|
7
8
|
failJson(write, "INVALID_ARGUMENTS", `Missing required option: ${optionName}. Pass it explicitly or save report context with auth login.`);
|
|
@@ -47,23 +48,76 @@ function filterShops(response, query, limit) {
|
|
|
47
48
|
.sort((left, right) => left.score - right.score || left.index - right.index);
|
|
48
49
|
return scored.slice(0, limit).map((item) => item.shop);
|
|
49
50
|
}
|
|
50
|
-
function
|
|
51
|
+
function stringValue(value) {
|
|
52
|
+
if (typeof value === "string") {
|
|
53
|
+
return value;
|
|
54
|
+
}
|
|
55
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
56
|
+
return String(value);
|
|
57
|
+
}
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
function redactSensitiveText(value, sensitiveValues) {
|
|
61
|
+
return sensitiveValues.reduce((result, sensitiveValue) => {
|
|
62
|
+
if (!sensitiveValue) {
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
return result.split(sensitiveValue).join("***");
|
|
66
|
+
}, value);
|
|
67
|
+
}
|
|
68
|
+
function safeErrorDetail(error, sensitiveValues) {
|
|
69
|
+
const details = [];
|
|
70
|
+
collectErrorDetails(error, details, new Set());
|
|
71
|
+
const detail = details.join(": ").replace(/\s+/g, " ").trim();
|
|
72
|
+
return detail ? redactSensitiveText(detail, sensitiveValues).slice(0, 240) : undefined;
|
|
73
|
+
}
|
|
74
|
+
function collectErrorDetails(error, details, seen) {
|
|
75
|
+
if (error === null || error === undefined || seen.has(error)) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
seen.add(error);
|
|
79
|
+
if (error instanceof Error && error.message) {
|
|
80
|
+
details.push(error.message);
|
|
81
|
+
}
|
|
82
|
+
if (typeof error !== "object") {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const record = error;
|
|
86
|
+
const causeParts = [record.code, record.syscall, record.address, record.port].flatMap((value) => {
|
|
87
|
+
const text = stringValue(value);
|
|
88
|
+
return text ? [text] : [];
|
|
89
|
+
});
|
|
90
|
+
if (causeParts.length > 0) {
|
|
91
|
+
details.push(causeParts.join(" "));
|
|
92
|
+
}
|
|
93
|
+
collectErrorDetails(record.cause, details, seen);
|
|
94
|
+
if (Array.isArray(record.errors)) {
|
|
95
|
+
for (const nestedError of record.errors.slice(0, 3)) {
|
|
96
|
+
collectErrorDetails(nestedError, details, seen);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function failReportServiceError(write, error, sensitiveValues = []) {
|
|
51
101
|
const status = error instanceof ReportApiError ? error.status : undefined;
|
|
52
|
-
const message = status === undefined ? "
|
|
53
|
-
|
|
102
|
+
const message = status === undefined ? "report-cli-service request failed" : `report-cli-service request failed with status ${status}`;
|
|
103
|
+
const detail = safeErrorDetail(error, sensitiveValues);
|
|
104
|
+
failJson(write, "REPORT_API_ERROR", `${message}${detail ? `: ${detail}` : ""}`);
|
|
105
|
+
}
|
|
106
|
+
function createShopFiltersClient(options, auth, reportCliUrl) {
|
|
107
|
+
const baseUrl = reportCliUrl || options.reportCliUrl || DEFAULT_REPORT_CLI_SERVICE_URL;
|
|
108
|
+
return options.shopFiltersClient ?? options.createShopFiltersClient?.(auth, baseUrl) ?? new ReportCliServiceClient({ auth, baseUrl });
|
|
54
109
|
}
|
|
55
110
|
export function createShopCommand(options = {}) {
|
|
56
111
|
const write = options.write ?? ((message) => process.stdout.write(message));
|
|
57
|
-
const shop = new Command("shop").description("Search authorized report shop filters from report-
|
|
112
|
+
const shop = new Command("shop").description("Search authorized report shop filters from report-cli-service");
|
|
58
113
|
shop
|
|
59
114
|
.command("search")
|
|
60
115
|
.description("Search online authorized shops by name or ID")
|
|
61
116
|
.argument("<query...>", "Shop name or ID")
|
|
62
|
-
.option("--employee-code <code>", "Employee code for permission filtering")
|
|
63
|
-
.option("--organization-id <id>", "Organization ID for permission filtering")
|
|
64
117
|
.option("--org-type-list <list>", "Comma-separated organization type list")
|
|
65
|
-
.option("--organization-type <type>", "Organization
|
|
66
|
-
.option("--report-id <id>", "Report ID used by report-
|
|
118
|
+
.option("--organization-type <type>", "Organization type used by report permission context")
|
|
119
|
+
.option("--report-id <id>", "Report ID used by report-cli-service shop filtering")
|
|
120
|
+
.option("--report-cli-url <url>", "report-cli-service base URL for shop filters", process.env.RESTO_REPORT_CLI_URL ?? options.reportCliUrl ?? DEFAULT_REPORT_CLI_SERVICE_URL)
|
|
67
121
|
.option("--limit <n>", "Maximum number of matches", "20")
|
|
68
122
|
.allowExcessArguments(false)
|
|
69
123
|
.action(async (queryParts, rawOptions) => {
|
|
@@ -75,26 +129,24 @@ export function createShopCommand(options = {}) {
|
|
|
75
129
|
}
|
|
76
130
|
const savedContext = auth.reportContext;
|
|
77
131
|
const request = {
|
|
78
|
-
employeeCode: requireOption(write, rawOptions.employeeCode ?? savedContext?.employeeCode, "--employee-code"),
|
|
79
|
-
organizationId: requireOption(write, rawOptions.organizationId ?? savedContext?.organizationId, "--organization-id"),
|
|
80
132
|
orgTypeList: rawOptions.orgTypeList
|
|
81
133
|
? parseIntegerList(write, "--org-type-list", rawOptions.orgTypeList)
|
|
82
134
|
: requireOption(write, savedContext?.orgTypeList, "--org-type-list"),
|
|
83
135
|
organizationType: requireOption(write, rawOptions.organizationType ?? savedContext?.organizationType, "--organization-type"),
|
|
84
136
|
...(rawOptions.reportId ? { reportId: parsePositiveIntegerArgument(write, "--report-id", rawOptions.reportId) } : {})
|
|
85
137
|
};
|
|
86
|
-
const client = options
|
|
138
|
+
const client = createShopFiltersClient(options, auth, rawOptions.reportCliUrl);
|
|
87
139
|
let response;
|
|
88
140
|
try {
|
|
89
141
|
response = await client.getReportShopFilters(request);
|
|
90
142
|
}
|
|
91
143
|
catch (error) {
|
|
92
|
-
|
|
144
|
+
failReportServiceError(write, error, [auth.token]);
|
|
93
145
|
}
|
|
94
146
|
const matches = filterShops(response, query, limit);
|
|
95
147
|
printJson(write, {
|
|
96
148
|
query,
|
|
97
|
-
source: "/api/report/
|
|
149
|
+
source: "/api/report-cli/shop/filters",
|
|
98
150
|
...(request.reportId === undefined ? {} : { reportId: request.reportId }),
|
|
99
151
|
shopFilterType: response.shopFilterType,
|
|
100
152
|
totalAuthorizedShops: response.shops.length,
|
package/dist/config/profile.js
CHANGED
|
@@ -16,6 +16,8 @@ const artifactFileNames = {
|
|
|
16
16
|
reportMenuTree: "report-menu-tree.jsonl",
|
|
17
17
|
reportCompositions: "report-compositions.jsonl",
|
|
18
18
|
reportMergedFields: "report-merged-fields.jsonl",
|
|
19
|
+
reportMetricsByDim: "report-metrics-by-dim.jsonl",
|
|
20
|
+
reportMergedHeads: "report-merged-heads.jsonl",
|
|
19
21
|
reportQueryPlans: "report-query-plans.jsonl",
|
|
20
22
|
reportFilterSurfaces: "report-filter-surfaces.jsonl",
|
|
21
23
|
reportEntityBindings: "report-entity-bindings.jsonl",
|
|
@@ -1,5 +1,52 @@
|
|
|
1
1
|
import { fetch } from "undici";
|
|
2
2
|
import { parseKnowledgeManifest } from "./schema.js";
|
|
3
|
+
export class KnowledgeManifestError extends Error {
|
|
4
|
+
code;
|
|
5
|
+
constructor(code, message) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = "KnowledgeManifestError";
|
|
8
|
+
this.code = code;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
function stringValue(value) {
|
|
12
|
+
if (typeof value === "string") {
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
15
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
16
|
+
return String(value);
|
|
17
|
+
}
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
function redactSensitiveText(value, sensitiveValues) {
|
|
21
|
+
return sensitiveValues.reduce((result, sensitiveValue) => {
|
|
22
|
+
if (!sensitiveValue) {
|
|
23
|
+
return result;
|
|
24
|
+
}
|
|
25
|
+
return result.split(sensitiveValue).join("***");
|
|
26
|
+
}, value);
|
|
27
|
+
}
|
|
28
|
+
function sanitizeRemoteMessage(value, sensitiveValues) {
|
|
29
|
+
const message = stringValue(value);
|
|
30
|
+
if (!message) {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
return redactSensitiveText(message.replace(/\s+/g, " ").trim(), sensitiveValues).slice(0, 240);
|
|
34
|
+
}
|
|
35
|
+
function throwIfBusinessError(payload, sensitiveValues) {
|
|
36
|
+
if (typeof payload !== "object" || payload === null || Array.isArray(payload)) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const body = payload;
|
|
40
|
+
const success = typeof body.success === "boolean" ? body.success : undefined;
|
|
41
|
+
const code = stringValue(body.errorCode ?? body.code);
|
|
42
|
+
const okCode = !code || code === "0" || code === "000";
|
|
43
|
+
if (okCode && success !== false) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const safeCode = code || "KNOWLEDGE_MANIFEST_ERROR";
|
|
47
|
+
const message = sanitizeRemoteMessage(body.message ?? body.msg ?? body.errorMessage, sensitiveValues);
|
|
48
|
+
throw new KnowledgeManifestError(safeCode, `Knowledge manifest request failed with code ${safeCode}${message ? `: ${message}` : ""}`);
|
|
49
|
+
}
|
|
3
50
|
function unwrapManifestPayload(payload) {
|
|
4
51
|
if (payload && typeof payload === "object" && "data" in payload) {
|
|
5
52
|
return payload.data;
|
|
@@ -16,7 +63,7 @@ export class ManifestClient {
|
|
|
16
63
|
this.fetchImpl = options.fetchImpl ?? fetch;
|
|
17
64
|
}
|
|
18
65
|
async fetchManifest(request) {
|
|
19
|
-
const response = await this.fetchImpl(`${this.baseUrl}/api/knowledge/manifest`, {
|
|
66
|
+
const response = await this.fetchImpl(`${this.baseUrl}/api/report-cli/knowledge/manifest`, {
|
|
20
67
|
method: "POST",
|
|
21
68
|
headers: {
|
|
22
69
|
"corporation-id": this.profile.corporationId,
|
|
@@ -32,8 +79,10 @@ export class ManifestClient {
|
|
|
32
79
|
})
|
|
33
80
|
});
|
|
34
81
|
if (!response.ok) {
|
|
35
|
-
throw new
|
|
82
|
+
throw new KnowledgeManifestError("KNOWLEDGE_MANIFEST_HTTP_ERROR", `Knowledge manifest request failed: ${response.status} ${response.statusText}`.trim());
|
|
36
83
|
}
|
|
37
|
-
|
|
84
|
+
const payload = await response.json();
|
|
85
|
+
throwIfBusinessError(payload, [this.profile.token]);
|
|
86
|
+
return parseKnowledgeManifest(unwrapManifestPayload(payload));
|
|
38
87
|
}
|
|
39
88
|
}
|
package/dist/knowledge/schema.js
CHANGED
|
@@ -82,6 +82,34 @@ const reportMergedFieldRecordSchema = fieldRecordSchema.extend({
|
|
|
82
82
|
aliases: z.array(z.string()),
|
|
83
83
|
tags: z.array(z.string())
|
|
84
84
|
});
|
|
85
|
+
const reportMetricsByDimRecordSchema = z.object({
|
|
86
|
+
reportId: z.number().int().positive(),
|
|
87
|
+
sourceReportId: z.number().int().positive(),
|
|
88
|
+
modelId: z.number().int().positive(),
|
|
89
|
+
dimensionFieldName: z.string().min(1),
|
|
90
|
+
dimensionDisplayText: z.string().min(1),
|
|
91
|
+
onlyMetricsByDim: z.boolean(),
|
|
92
|
+
disableMetricsByDim: z.boolean(),
|
|
93
|
+
metricFieldNames: z.array(z.string().min(1)),
|
|
94
|
+
defaultMetricFieldNames: z.array(z.string().min(1)),
|
|
95
|
+
rawMetricsByDimConfig: z.string().nullable().optional(),
|
|
96
|
+
source: z.string().min(1)
|
|
97
|
+
});
|
|
98
|
+
const reportMergedHeadRecordSchema = z.object({
|
|
99
|
+
reportId: z.number().int().positive(),
|
|
100
|
+
sourceReportId: z.number().int().positive(),
|
|
101
|
+
modelId: z.number().int().positive(),
|
|
102
|
+
mergedHeadId: z.string().min(1),
|
|
103
|
+
defaultSortIndex: z.number().int(),
|
|
104
|
+
primaryHeadMetricsCode: z.string().min(1).nullable().optional(),
|
|
105
|
+
subHeadIsDynamic: z.boolean(),
|
|
106
|
+
subHeadMetricsCodes: z.array(z.string().min(1)),
|
|
107
|
+
subHeadByDimCode: z.string().min(1).nullable().optional(),
|
|
108
|
+
displaySubTotal: z.boolean(),
|
|
109
|
+
subTotalMetricsCode: z.string().min(1).nullable().optional(),
|
|
110
|
+
rawMergedHeadConfig: z.string().min(1),
|
|
111
|
+
source: z.string().min(1)
|
|
112
|
+
});
|
|
85
113
|
const reportQueryPlanRecordSchema = z.object({
|
|
86
114
|
reportId: z.number().int(),
|
|
87
115
|
planId: z.string().min(1),
|
|
@@ -385,6 +413,12 @@ function localizeReportFilterSurfaceRecord(surface, translations) {
|
|
|
385
413
|
...(aliases ? { aliases } : {})
|
|
386
414
|
};
|
|
387
415
|
}
|
|
416
|
+
function localizeReportMetricsByDimRecord(record, translations) {
|
|
417
|
+
return {
|
|
418
|
+
...record,
|
|
419
|
+
dimensionDisplayText: translations.get(record.dimensionFieldName) ?? localizeText(record.dimensionDisplayText, translations)
|
|
420
|
+
};
|
|
421
|
+
}
|
|
388
422
|
function rankByMatch(query, exactIdentifier, exactText, substringFields, overlapFields) {
|
|
389
423
|
const trimmedQuery = query.trim();
|
|
390
424
|
const normalizedQuery = normalizeText(query);
|
|
@@ -455,6 +489,8 @@ export class SearchIndex {
|
|
|
455
489
|
reportMenuTree;
|
|
456
490
|
reportCompositions;
|
|
457
491
|
reportMergedFields;
|
|
492
|
+
reportMetricsByDim;
|
|
493
|
+
reportMergedHeads;
|
|
458
494
|
reportQueryPlans;
|
|
459
495
|
reportFilterSurfaces;
|
|
460
496
|
reportEntityBindings;
|
|
@@ -471,6 +507,8 @@ export class SearchIndex {
|
|
|
471
507
|
this.reportMenuTree = parseRecords(records.reportMenuTree ?? [], "report menu tree", reportMenuTreeRecordSchema).map((item) => localizeReportMenuTreeRecord(item, translations));
|
|
472
508
|
this.reportCompositions = parseRecords(records.reportCompositions ?? [], "report compositions", reportCompositionRecordSchema);
|
|
473
509
|
this.reportMergedFields = parseRecords(records.reportMergedFields ?? [], "report merged fields", reportMergedFieldRecordSchema).map((field) => localizeFieldRecord(field, translations));
|
|
510
|
+
this.reportMetricsByDim = parseRecords(records.reportMetricsByDim ?? [], "report metrics by dim", reportMetricsByDimRecordSchema).map((record) => localizeReportMetricsByDimRecord(record, translations));
|
|
511
|
+
this.reportMergedHeads = parseRecords(records.reportMergedHeads ?? [], "report merged heads", reportMergedHeadRecordSchema);
|
|
474
512
|
this.reportQueryPlans = parseRecords(records.reportQueryPlans ?? [], "report query plans", reportQueryPlanRecordSchema);
|
|
475
513
|
this.reportFilterSurfaces = parseRecords(records.reportFilterSurfaces ?? [], "report filter surfaces", reportFilterSurfaceRecordSchema).map((surface) => localizeReportFilterSurfaceRecord(surface, translations));
|
|
476
514
|
this.reportEntityBindings = parseRecords(records.reportEntityBindings ?? [], "report entity bindings", reportEntityBindingRecordSchema);
|
|
@@ -488,6 +526,8 @@ export class SearchIndex {
|
|
|
488
526
|
reportMenuTree: parseOptionalJsonlFile(join(root, "report-menu-tree.jsonl"), reportMenuTreeRecordSchema),
|
|
489
527
|
reportCompositions: parseOptionalJsonlFile(join(root, "report-compositions.jsonl"), reportCompositionRecordSchema),
|
|
490
528
|
reportMergedFields: parseOptionalJsonlFile(join(root, "report-merged-fields.jsonl"), reportMergedFieldRecordSchema),
|
|
529
|
+
reportMetricsByDim: parseOptionalJsonlFile(join(root, "report-metrics-by-dim.jsonl"), reportMetricsByDimRecordSchema),
|
|
530
|
+
reportMergedHeads: parseOptionalJsonlFile(join(root, "report-merged-heads.jsonl"), reportMergedHeadRecordSchema),
|
|
491
531
|
reportQueryPlans: parseOptionalJsonlFile(join(root, "report-query-plans.jsonl"), reportQueryPlanRecordSchema),
|
|
492
532
|
reportFilterSurfaces: parseOptionalJsonlFile(join(root, "report-filter-surfaces.jsonl"), reportFilterSurfaceRecordSchema),
|
|
493
533
|
reportEntityBindings: parseOptionalJsonlFile(join(root, "report-entity-bindings.jsonl"), reportEntityBindingRecordSchema),
|
|
@@ -517,6 +557,8 @@ export class SearchIndex {
|
|
|
517
557
|
reportMenuTree: parseOptionalJsonlFile(localStore.currentArtifactPath(env, "reportMenuTree"), reportMenuTreeRecordSchema),
|
|
518
558
|
reportCompositions: parseOptionalJsonlFile(localStore.currentArtifactPath(env, "reportCompositions"), reportCompositionRecordSchema),
|
|
519
559
|
reportMergedFields: parseOptionalJsonlFile(localStore.currentArtifactPath(env, "reportMergedFields"), reportMergedFieldRecordSchema),
|
|
560
|
+
reportMetricsByDim: parseOptionalJsonlFile(localStore.currentArtifactPath(env, "reportMetricsByDim"), reportMetricsByDimRecordSchema),
|
|
561
|
+
reportMergedHeads: parseOptionalJsonlFile(localStore.currentArtifactPath(env, "reportMergedHeads"), reportMergedHeadRecordSchema),
|
|
520
562
|
reportQueryPlans: parseOptionalJsonlFile(localStore.currentArtifactPath(env, "reportQueryPlans"), reportQueryPlanRecordSchema),
|
|
521
563
|
reportFilterSurfaces: parseOptionalJsonlFile(localStore.currentArtifactPath(env, "reportFilterSurfaces"), reportFilterSurfaceRecordSchema),
|
|
522
564
|
reportEntityBindings: parseOptionalJsonlFile(localStore.currentArtifactPath(env, "reportEntityBindings"), reportEntityBindingRecordSchema),
|
|
@@ -609,6 +651,8 @@ export class SearchIndex {
|
|
|
609
651
|
const routeMap = this.reportRouteMap.filter((route) => route.reportId === report.reportId);
|
|
610
652
|
const menuTree = this.reportMenuTree.filter((item) => pages.some((page) => page.routeKey === item.key));
|
|
611
653
|
const composition = this.reportCompositions.find((candidate) => candidate.reportId === report.reportId);
|
|
654
|
+
const metricsByDim = this.reportMetricsByDim.filter((record) => record.reportId === report.reportId);
|
|
655
|
+
const mergedHeads = this.reportMergedHeads.filter((record) => record.reportId === report.reportId);
|
|
612
656
|
const queryPlans = this.reportQueryPlans.filter((plan) => plan.reportId === report.reportId);
|
|
613
657
|
const filterSurfaces = this.reportFilterSurfaces.filter((surface) => surface.reportId === report.reportId);
|
|
614
658
|
const entityBindings = this.reportEntityBindings.filter((binding) => binding.reportId === report.reportId);
|
|
@@ -619,6 +663,8 @@ export class SearchIndex {
|
|
|
619
663
|
...(routeMap.length > 0 ? { routeMap } : {}),
|
|
620
664
|
...(menuTree.length > 0 ? { menuTree } : {}),
|
|
621
665
|
...(composition ? { composition } : {}),
|
|
666
|
+
...(metricsByDim.length > 0 ? { metricsByDim } : {}),
|
|
667
|
+
...(mergedHeads.length > 0 ? { mergedHeads } : {}),
|
|
622
668
|
...(queryPlans.length > 0 ? { queryPlans } : {}),
|
|
623
669
|
...(filterSurfaces.length > 0 ? { filterSurfaces } : {}),
|
|
624
670
|
...(entityBindings.length > 0 ? { entityBindings } : {})
|