@xpert-ai/plugin-analytics-data-xpert 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -2
- package/dist/index.d.ts +3 -0
- package/dist/index.js +11 -0
- package/dist/lib/discovery.d.ts +61 -0
- package/dist/lib/discovery.js +52 -0
- package/dist/lib/plugin.d.ts +2 -0
- package/dist/lib/plugin.js +17 -0
- package/dist/lib/provider.d.ts +61 -0
- package/dist/lib/provider.js +164 -0
- package/docs/AGENTS.md +11 -0
- package/docs/docs.json +26 -0
- package/docs/index.mdx +100 -0
- package/docs/verification.mdx +58 -0
- package/index.cjs +1 -0
- package/package.json +56 -3
package/README.md
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
|
-
#
|
|
1
|
+
# data-xpert analytics provider
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
System plugin `@xpert-ai/plugin-analytics-data-xpert`; provider `data-xpert.ontology`, operation `action`, version 1.
|
|
4
|
+
|
|
5
|
+
Contributes the data-xpert DataSource type through DataSourceType sync. Server configuration: baseUrl (including /api), audience, assistantCode, assistantXpertId, workspaceId. No database runner is required. Platform code has no ontology dependency.
|
|
6
|
+
|
|
7
|
+
```json
|
|
8
|
+
{"resourceId":"datax-semantic-model-<published-model-id>","actionTypeCode":"sql.query_cube_slice","target":{"entityTypeCode":"semantic_cube","entityRef":"<published-model-id>::Sales"},"params":{"dimensions":["Region"],"measures":["Amount"],"filters":[{"field":"Region","op":"contains","value":"west"}],"limit":100},"parameterPaths":{"region":["filters","0","value"]}}
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
This is a SQL semantic-model Action example; it is not the contract for other ontology adapters. Replace placeholders and use exact targets/fields returned by published-resource discovery. Each registered Action defines its own params shape. Array path segments use canonical zero-based indices. Bindings cannot supply actor, credentials or URL. Paths replace entire existing scalar values, never SQL/MDX/OData fragments. Unknown/prototype paths fail.
|
|
12
|
+
|
|
13
|
+
The provider obtains the current actor's token via ActorTokenRuntimeFactoryCapability.createScopedApi(context.actor), then POSTs to /agent-workbench/analytics/query. data-xpert validates OIDC, Assistant/resource binding, explicit viewer policy and the existing Live Artifact read-only allowlist, bypassing Action result caches. Typed rows/columns and audit receipts are projected into the canonical Dataset.
|
|
14
|
+
|
|
15
|
+
Run `corepack pnpm exec nx run @xpert-ai/plugin-analytics-data-xpert:test` with the current platform SDK. Real integration requires the corresponding data-xpert endpoint, published resources, Assistant bindings, explicit viewer policy and working xpert-pro OIDC. Mocked identity tests do not establish SSO acceptance.
|
|
16
|
+
|
|
17
|
+
Discovery preserves each adapter's `definition` (identity, version, native metadata and instructions) and each Action's input schema. Semantic Model, OData and future registered read-only adapters share this transport without sharing query syntax.
|
|
18
|
+
|
|
19
|
+
See [architecture and Binding usage](docs/index.mdx) and [compatibility and verification](docs/verification.mdx).
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const node_fs_1 = require("node:fs");
|
|
4
|
+
const node_path_1 = require("node:path");
|
|
5
|
+
const plugin_1 = require("./lib/plugin");
|
|
6
|
+
const packageJson = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(__dirname, '../package.json'), 'utf8'));
|
|
7
|
+
const plugin = {
|
|
8
|
+
meta: { name: packageJson.name, version: packageJson.version, category: 'database', displayName: 'data-xpert Analytics', description: 'data-xpert Analytics', author: 'XpertAI', level: packageJson.xpert.plugin.level, artifactNamespace: packageJson.xpert.plugin.artifactNamespace },
|
|
9
|
+
register: () => ({ module: plugin_1.AnalyticsDataXpertPlugin })
|
|
10
|
+
};
|
|
11
|
+
exports.default = plugin;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { z } from 'zod/v3';
|
|
2
|
+
import type { AnalyticsDiscoveryResult } from '@xpert-ai/contracts';
|
|
3
|
+
/** Private wire contract for data-xpert discovery; actor and Assistant scope are server-owned. */
|
|
4
|
+
export declare const dataXpertDiscoveryRequestSchema: z.ZodDiscriminatedUnion<"kind", [z.ZodObject<{
|
|
5
|
+
kind: z.ZodLiteral<"resources">;
|
|
6
|
+
after: z.ZodOptional<z.ZodString>;
|
|
7
|
+
}, "strict", z.ZodTypeAny, {
|
|
8
|
+
kind: "resources";
|
|
9
|
+
after?: string | undefined;
|
|
10
|
+
}, {
|
|
11
|
+
kind: "resources";
|
|
12
|
+
after?: string | undefined;
|
|
13
|
+
}>, z.ZodObject<{
|
|
14
|
+
kind: z.ZodLiteral<"entities">;
|
|
15
|
+
resourceId: z.ZodString;
|
|
16
|
+
entityTypeCode: z.ZodOptional<z.ZodString>;
|
|
17
|
+
query: z.ZodOptional<z.ZodString>;
|
|
18
|
+
}, "strict", z.ZodTypeAny, {
|
|
19
|
+
kind: "entities";
|
|
20
|
+
resourceId: string;
|
|
21
|
+
entityTypeCode?: string | undefined;
|
|
22
|
+
query?: string | undefined;
|
|
23
|
+
}, {
|
|
24
|
+
kind: "entities";
|
|
25
|
+
resourceId: string;
|
|
26
|
+
entityTypeCode?: string | undefined;
|
|
27
|
+
query?: string | undefined;
|
|
28
|
+
}>, z.ZodObject<{
|
|
29
|
+
kind: z.ZodLiteral<"actions">;
|
|
30
|
+
resourceId: z.ZodString;
|
|
31
|
+
target: z.ZodObject<{
|
|
32
|
+
entityTypeCode: z.ZodString;
|
|
33
|
+
entityRef: z.ZodString;
|
|
34
|
+
}, "strict", z.ZodTypeAny, {
|
|
35
|
+
entityTypeCode: string;
|
|
36
|
+
entityRef: string;
|
|
37
|
+
}, {
|
|
38
|
+
entityTypeCode: string;
|
|
39
|
+
entityRef: string;
|
|
40
|
+
}>;
|
|
41
|
+
}, "strict", z.ZodTypeAny, {
|
|
42
|
+
kind: "actions";
|
|
43
|
+
resourceId: string;
|
|
44
|
+
target: {
|
|
45
|
+
entityTypeCode: string;
|
|
46
|
+
entityRef: string;
|
|
47
|
+
};
|
|
48
|
+
}, {
|
|
49
|
+
kind: "actions";
|
|
50
|
+
resourceId: string;
|
|
51
|
+
target: {
|
|
52
|
+
entityTypeCode: string;
|
|
53
|
+
entityRef: string;
|
|
54
|
+
};
|
|
55
|
+
}>]>;
|
|
56
|
+
/**
|
|
57
|
+
* All ontology adapters share this envelope, not a native query language.
|
|
58
|
+
* Preserve definition.adapterId/contractVersion/metadata/instructions and Action inputSchema
|
|
59
|
+
* from data-xpert. New adapters must not require branches in this provider or the renderer.
|
|
60
|
+
*/
|
|
61
|
+
export declare const dataXpertQueryContract: AnalyticsDiscoveryResult['queryContract'];
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.dataXpertQueryContract = exports.dataXpertDiscoveryRequestSchema = void 0;
|
|
4
|
+
const v3_1 = require("zod/v3");
|
|
5
|
+
/** Private wire contract for data-xpert discovery; actor and Assistant scope are server-owned. */
|
|
6
|
+
exports.dataXpertDiscoveryRequestSchema = v3_1.z.discriminatedUnion('kind', [
|
|
7
|
+
v3_1.z.object({ kind: v3_1.z.literal('resources'), after: v3_1.z.string().min(1).max(160).optional() }).strict(),
|
|
8
|
+
v3_1.z.object({
|
|
9
|
+
kind: v3_1.z.literal('entities'), resourceId: v3_1.z.string().min(1).max(160),
|
|
10
|
+
entityTypeCode: v3_1.z.string().min(1).max(160).optional(), query: v3_1.z.string().max(256).optional()
|
|
11
|
+
}).strict(),
|
|
12
|
+
v3_1.z.object({
|
|
13
|
+
kind: v3_1.z.literal('actions'), resourceId: v3_1.z.string().min(1).max(160),
|
|
14
|
+
target: v3_1.z.object({ entityTypeCode: v3_1.z.string().min(1).max(160), entityRef: v3_1.z.string().min(1).max(512) }).strict()
|
|
15
|
+
}).strict()
|
|
16
|
+
]);
|
|
17
|
+
/**
|
|
18
|
+
* All ontology adapters share this envelope, not a native query language.
|
|
19
|
+
* Preserve definition.adapterId/contractVersion/metadata/instructions and Action inputSchema
|
|
20
|
+
* from data-xpert. New adapters must not require branches in this provider or the renderer.
|
|
21
|
+
*/
|
|
22
|
+
exports.dataXpertQueryContract = {
|
|
23
|
+
operation: 'action', contractVersion: 1,
|
|
24
|
+
payloadSchema: {
|
|
25
|
+
type: 'object', additionalProperties: false,
|
|
26
|
+
required: ['resourceId', 'actionTypeCode', 'target', 'params', 'parameterPaths'],
|
|
27
|
+
properties: {
|
|
28
|
+
resourceId: { type: 'string', minLength: 1, maxLength: 160 },
|
|
29
|
+
actionTypeCode: { type: 'string', minLength: 1, maxLength: 160 },
|
|
30
|
+
target: {
|
|
31
|
+
type: 'object', additionalProperties: false,
|
|
32
|
+
properties: {
|
|
33
|
+
entityId: { type: 'string', minLength: 1, maxLength: 256 },
|
|
34
|
+
entityTypeCode: { type: 'string', minLength: 1, maxLength: 256 },
|
|
35
|
+
entityRef: { type: 'string', minLength: 1, maxLength: 512 }
|
|
36
|
+
},
|
|
37
|
+
anyOf: [{ required: ['entityId'] }, { required: ['entityRef'] }]
|
|
38
|
+
},
|
|
39
|
+
params: { type: 'object' },
|
|
40
|
+
parameterPaths: { type: 'object', additionalProperties: {
|
|
41
|
+
type: 'array', minItems: 1, maxItems: 8, items: { type: 'string', minLength: 1 }
|
|
42
|
+
} }
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
instructions: [
|
|
46
|
+
'Follow inspectRequest: resources -> entities -> actions. Continue nextRequest even when the visible resource page is empty. Entity discovery returns top 20 matches; narrow query/entityTypeCode if needed.',
|
|
47
|
+
'Entity metadata.definition contains adapterId, contractVersion, metadata and instructions. Follow that adapter-specific definition and the discovered Action inputSchema/inputHint. Preserve canonical targets, resource IDs and exact field names. Do not infer adapter types from names or apply one adapter\'s query, filter or time rules to another.',
|
|
48
|
+
'Use only published metadata and authorized previews to establish business metrics, time axes, supported filters and complete aggregation. Missing metadata does not authorize guessing; obtain sufficient evidence before generating a time filter.',
|
|
49
|
+
'Use parameterPaths relative to params to replace an existing scalar value, for example {region:["filters","0","value"]}. Paths must be unique. Never interpolate values into query text. Controls supply scalar values; arrays remain static unless the native Action explicitly supports another declared scalar mapping. Null handling and filter operators are defined by the Action schema and adapter instructions.',
|
|
50
|
+
'Only read-only Actions allowed by the current viewer policy are returned. Map output sourceField to returned column keys (or an explicit semanticRef). Numeric JSON cells require float64; decimal/int64 require exact strings. Preview to verify types, grain and completeness before binding charts. A partial page is not a complete business aggregate.'
|
|
51
|
+
].join(' ')
|
|
52
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.AnalyticsDataXpertPlugin = void 0;
|
|
10
|
+
const plugin_sdk_1 = require("@xpert-ai/plugin-sdk");
|
|
11
|
+
const provider_1 = require("./provider");
|
|
12
|
+
let AnalyticsDataXpertPlugin = class AnalyticsDataXpertPlugin {
|
|
13
|
+
};
|
|
14
|
+
exports.AnalyticsDataXpertPlugin = AnalyticsDataXpertPlugin;
|
|
15
|
+
exports.AnalyticsDataXpertPlugin = AnalyticsDataXpertPlugin = __decorate([
|
|
16
|
+
(0, plugin_sdk_1.XpertServerPlugin)({ providers: [provider_1.DataXpertAnalyticsProvider] })
|
|
17
|
+
], AnalyticsDataXpertPlugin);
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { z } from 'zod/v3';
|
|
2
|
+
import type { AnalyticsBinding, AnalyticsProviderDescriptor, AnalyticsValue, JSONValue, AnalyticsDiscoveryResult } from '@xpert-ai/contracts';
|
|
3
|
+
import { type AnalyticsQueryProvider, type AnalyticsProviderContext, type AnalyticsProviderResult, type RuntimeCapabilityRegistry } from '@xpert-ai/plugin-sdk';
|
|
4
|
+
/** Native Action arguments stay plugin-owned; the host only interprets Dataset/Binding. */
|
|
5
|
+
export declare const dataXpertPayloadSchema: z.ZodObject<{
|
|
6
|
+
resourceId: z.ZodString;
|
|
7
|
+
actionTypeCode: z.ZodString;
|
|
8
|
+
target: z.ZodEffects<z.ZodObject<{
|
|
9
|
+
entityId: z.ZodOptional<z.ZodString>;
|
|
10
|
+
entityTypeCode: z.ZodOptional<z.ZodString>;
|
|
11
|
+
entityRef: z.ZodOptional<z.ZodString>;
|
|
12
|
+
}, "strict", z.ZodTypeAny, {
|
|
13
|
+
entityTypeCode?: string | undefined;
|
|
14
|
+
entityRef?: string | undefined;
|
|
15
|
+
entityId?: string | undefined;
|
|
16
|
+
}, {
|
|
17
|
+
entityTypeCode?: string | undefined;
|
|
18
|
+
entityRef?: string | undefined;
|
|
19
|
+
entityId?: string | undefined;
|
|
20
|
+
}>, {
|
|
21
|
+
entityTypeCode?: string | undefined;
|
|
22
|
+
entityRef?: string | undefined;
|
|
23
|
+
entityId?: string | undefined;
|
|
24
|
+
}, {
|
|
25
|
+
entityTypeCode?: string | undefined;
|
|
26
|
+
entityRef?: string | undefined;
|
|
27
|
+
entityId?: string | undefined;
|
|
28
|
+
}>;
|
|
29
|
+
params: z.ZodRecord<z.ZodString, z.ZodType<JSONValue, z.ZodTypeDef, JSONValue>>;
|
|
30
|
+
parameterPaths: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString, "many">>>;
|
|
31
|
+
}, "strict", z.ZodTypeAny, {
|
|
32
|
+
params: Record<string, JSONValue>;
|
|
33
|
+
resourceId: string;
|
|
34
|
+
target: {
|
|
35
|
+
entityTypeCode?: string | undefined;
|
|
36
|
+
entityRef?: string | undefined;
|
|
37
|
+
entityId?: string | undefined;
|
|
38
|
+
};
|
|
39
|
+
actionTypeCode: string;
|
|
40
|
+
parameterPaths: Record<string, string[]>;
|
|
41
|
+
}, {
|
|
42
|
+
params: Record<string, JSONValue>;
|
|
43
|
+
resourceId: string;
|
|
44
|
+
target: {
|
|
45
|
+
entityTypeCode?: string | undefined;
|
|
46
|
+
entityRef?: string | undefined;
|
|
47
|
+
entityId?: string | undefined;
|
|
48
|
+
};
|
|
49
|
+
actionTypeCode: string;
|
|
50
|
+
parameterPaths?: Record<string, string[]> | undefined;
|
|
51
|
+
}>;
|
|
52
|
+
export declare class DataXpertAnalyticsProvider implements AnalyticsQueryProvider {
|
|
53
|
+
private readonly runtime;
|
|
54
|
+
readonly descriptor: AnalyticsProviderDescriptor;
|
|
55
|
+
constructor(runtime: RuntimeCapabilityRegistry);
|
|
56
|
+
discover(request: JSONValue, context: AnalyticsProviderContext): Promise<AnalyticsDiscoveryResult>;
|
|
57
|
+
private request;
|
|
58
|
+
execute(binding: AnalyticsBinding, parameters: Record<string, AnalyticsValue>, context: AnalyticsProviderContext): Promise<AnalyticsProviderResult>;
|
|
59
|
+
}
|
|
60
|
+
/** Typed paths replace whole parameter values, never splice text into SQL/MDX/OData. */
|
|
61
|
+
export declare function bindParameters(params: Record<string, JSONValue>, paths: Record<string, string[]>, parameters: Record<string, AnalyticsValue>): Record<string, JSONValue>;
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
12
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.DataXpertAnalyticsProvider = exports.dataXpertPayloadSchema = void 0;
|
|
16
|
+
exports.bindParameters = bindParameters;
|
|
17
|
+
const common_1 = require("@nestjs/common");
|
|
18
|
+
const v3_1 = require("zod/v3");
|
|
19
|
+
const discovery_1 = require("./discovery");
|
|
20
|
+
const plugin_sdk_1 = require("@xpert-ai/plugin-sdk");
|
|
21
|
+
const scalar = v3_1.z.union([v3_1.z.string(), v3_1.z.number().finite(), v3_1.z.boolean(), v3_1.z.null()]);
|
|
22
|
+
const json = v3_1.z.lazy(() => v3_1.z.union([scalar, v3_1.z.array(json), v3_1.z.record(json)]));
|
|
23
|
+
/** Native Action arguments stay plugin-owned; the host only interprets Dataset/Binding. */
|
|
24
|
+
exports.dataXpertPayloadSchema = v3_1.z.object({
|
|
25
|
+
resourceId: v3_1.z.string().min(1).max(160), actionTypeCode: v3_1.z.string().min(1).max(160),
|
|
26
|
+
target: v3_1.z.object({
|
|
27
|
+
entityId: v3_1.z.string().trim().min(1).max(256).optional(),
|
|
28
|
+
entityTypeCode: v3_1.z.string().trim().min(1).max(256).optional(),
|
|
29
|
+
entityRef: v3_1.z.string().trim().min(1).max(512).optional()
|
|
30
|
+
}).strict().refine(target => Boolean(target.entityId || target.entityRef), 'Target reference is required'),
|
|
31
|
+
params: v3_1.z.record(json),
|
|
32
|
+
parameterPaths: v3_1.z.record(v3_1.z.array(v3_1.z.string().min(1)).min(1).max(8)).default({})
|
|
33
|
+
}).strict();
|
|
34
|
+
/** Trusted DataSource configuration; never merge it with payloads, controls or discovery requests. */
|
|
35
|
+
const optionsSchema = v3_1.z.object({
|
|
36
|
+
baseUrl: v3_1.z.string().url(), audience: v3_1.z.string().min(1),
|
|
37
|
+
assistantCode: v3_1.z.string().min(1), assistantXpertId: v3_1.z.string().uuid(), workspaceId: v3_1.z.string().uuid()
|
|
38
|
+
}).strict();
|
|
39
|
+
const responseSchema = v3_1.z.object({
|
|
40
|
+
effect: v3_1.z.object({ version: v3_1.z.literal(2), kind: v3_1.z.enum(['rows', 'series', 'entity']),
|
|
41
|
+
columns: v3_1.z.array(v3_1.z.object({ key: v3_1.z.string(), semanticRef: v3_1.z.string().optional() })),
|
|
42
|
+
rows: v3_1.z.array(v3_1.z.record(scalar)),
|
|
43
|
+
summary: v3_1.z.object({ rowCount: v3_1.z.number().int().nonnegative(), returnedRowCount: v3_1.z.number().int().nonnegative(), truncated: v3_1.z.boolean() }) }),
|
|
44
|
+
fetchedAt: v3_1.z.string().datetime(), executionId: v3_1.z.string(), auditRef: v3_1.z.string(), fromCache: v3_1.z.literal(false)
|
|
45
|
+
});
|
|
46
|
+
let DataXpertAnalyticsProvider = class DataXpertAnalyticsProvider {
|
|
47
|
+
constructor(runtime) {
|
|
48
|
+
this.runtime = runtime;
|
|
49
|
+
this.descriptor = {
|
|
50
|
+
id: 'data-xpert.ontology', name: 'data-xpert Ontology', dataSourceTypes: ['data-xpert'],
|
|
51
|
+
operations: [{ name: 'action', contractVersion: 1, readOnly: true }],
|
|
52
|
+
connectionType: { type: 'data-xpert', name: 'data-xpert', configuration: {
|
|
53
|
+
type: 'object', properties: { baseUrl: { type: 'string', title: 'API URL' }, audience: { type: 'string', title: 'OIDC audience' },
|
|
54
|
+
assistantCode: { type: 'string', title: 'Assistant code' }, assistantXpertId: { type: 'string', title: 'Assistant Xpert ID' },
|
|
55
|
+
workspaceId: { type: 'string', title: 'Workspace ID' } },
|
|
56
|
+
required: ['baseUrl', 'audience', 'assistantCode', 'assistantXpertId', 'workspaceId'], additionalProperties: false
|
|
57
|
+
} }
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
async discover(request, context) {
|
|
61
|
+
const empty = request && typeof request === 'object' && !Array.isArray(request) && Object.keys(request).length === 0;
|
|
62
|
+
const parsed = discovery_1.dataXpertDiscoveryRequestSchema.parse(empty ? { kind: 'resources' } : request);
|
|
63
|
+
const response = await this.request('discover', context, { request: parsed });
|
|
64
|
+
const result = v3_1.z.object({ items: v3_1.z.array(v3_1.z.unknown()),
|
|
65
|
+
nextRequest: json.optional() }).strict().parse(await boundedJson(response));
|
|
66
|
+
return plugin_sdk_1.analyticsDiscoveryResultSchema.parse({ ...result, schemaVersion: 1, queryContract: discovery_1.dataXpertQueryContract });
|
|
67
|
+
}
|
|
68
|
+
async request(endpoint, context, body) {
|
|
69
|
+
context.signal.throwIfAborted();
|
|
70
|
+
const options = optionsSchema.parse(context.options);
|
|
71
|
+
const base = new URL(options.baseUrl);
|
|
72
|
+
if (!['http:', 'https:'].includes(base.protocol) || base.username || base.password || base.search || base.hash)
|
|
73
|
+
throw new Error('DATAX_URL_INVALID');
|
|
74
|
+
const token = await this.runtime.require(plugin_sdk_1.ActorTokenRuntimeFactoryCapability).createScopedApi(context.actor).getToken({ audience: options.audience });
|
|
75
|
+
context.signal.throwIfAborted();
|
|
76
|
+
const response = await fetch(`${base.href.replace(/\/$/, '')}/agent-workbench/analytics/${endpoint}`, {
|
|
77
|
+
method: 'POST', redirect: 'error', signal: context.signal,
|
|
78
|
+
headers: { Authorization: `Bearer ${token.token}`, 'Content-Type': 'application/json', 'organization-id': context.actor.organizationId },
|
|
79
|
+
body: JSON.stringify({ assistant: { code: options.assistantCode, xpertId: options.assistantXpertId, workspaceId: options.workspaceId }, ...body })
|
|
80
|
+
});
|
|
81
|
+
if (!response.ok) {
|
|
82
|
+
// Do not retain an unconsumed upstream error stream or expose its potentially sensitive body.
|
|
83
|
+
await response.body?.cancel().catch(() => undefined);
|
|
84
|
+
throw new Error(`DATAX_QUERY_HTTP_${response.status}`);
|
|
85
|
+
}
|
|
86
|
+
return response;
|
|
87
|
+
}
|
|
88
|
+
async execute(binding, parameters, context) {
|
|
89
|
+
const payload = exports.dataXpertPayloadSchema.parse(binding.payload);
|
|
90
|
+
const response = await this.request('query', context, {
|
|
91
|
+
binding: { id: binding.id, resourceId: payload.resourceId, actionTypeCode: payload.actionTypeCode, target: payload.target,
|
|
92
|
+
params: bindParameters(payload.params, payload.parameterPaths, parameters) }, maxRows: context.maxRows
|
|
93
|
+
});
|
|
94
|
+
const result = responseSchema.parse(await boundedJson(response));
|
|
95
|
+
return { columns: result.effect.columns, rows: result.effect.rows,
|
|
96
|
+
completeness: { returnedRows: result.effect.summary.returnedRowCount, totalRows: result.effect.summary.rowCount,
|
|
97
|
+
totalAccuracy: result.effect.summary.truncated ? 'unknown' : 'exact', scope: result.effect.summary.truncated ? 'page' : 'full_query_result',
|
|
98
|
+
truncated: result.effect.summary.truncated }, sourceFetchedAt: result.fetchedAt,
|
|
99
|
+
upstreamExecutionId: result.executionId, auditRef: result.auditRef };
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
exports.DataXpertAnalyticsProvider = DataXpertAnalyticsProvider;
|
|
103
|
+
exports.DataXpertAnalyticsProvider = DataXpertAnalyticsProvider = __decorate([
|
|
104
|
+
(0, plugin_sdk_1.AnalyticsProvider)('data-xpert.ontology'),
|
|
105
|
+
__param(0, (0, common_1.Inject)(plugin_sdk_1.XPERT_RUNTIME_CAPABILITIES_TOKEN)),
|
|
106
|
+
__metadata("design:paramtypes", [Object])
|
|
107
|
+
], DataXpertAnalyticsProvider);
|
|
108
|
+
/** Typed paths replace whole parameter values, never splice text into SQL/MDX/OData. */
|
|
109
|
+
function bindParameters(params, paths, parameters) {
|
|
110
|
+
const result = structuredClone(params);
|
|
111
|
+
if (Object.keys(parameters).some((name) => !Object.hasOwn(paths, name)) || Object.keys(paths).some((name) => !Object.hasOwn(parameters, name)))
|
|
112
|
+
throw new Error('DATAX_PARAMETER_MAPPING_INVALID');
|
|
113
|
+
if (new Set(Object.values(paths).map((path) => JSON.stringify(path))).size !== Object.keys(paths).length)
|
|
114
|
+
throw new Error('DATAX_PARAMETER_MAPPING_INVALID');
|
|
115
|
+
for (const [name, path] of Object.entries(paths)) {
|
|
116
|
+
let current = result;
|
|
117
|
+
for (const [index, key] of path.entries()) {
|
|
118
|
+
if (['__proto__', 'prototype', 'constructor'].includes(key) || !current || typeof current !== 'object' || !Object.hasOwn(current, key))
|
|
119
|
+
throw new common_1.BadRequestException('DATAX_PARAMETER_PATH_INVALID: parameterPaths are relative to params. Omit the leading params segment and target an existing scalar, for example ["slicers","1","value"].');
|
|
120
|
+
if (Array.isArray(current)) {
|
|
121
|
+
if (!/^(0|[1-9]\d*)$/.test(key) || Number(key) >= current.length)
|
|
122
|
+
throw new Error('DATAX_PARAMETER_PATH_INVALID');
|
|
123
|
+
const position = Number(key);
|
|
124
|
+
if (index === path.length - 1) {
|
|
125
|
+
if (current[position] !== null && typeof current[position] === 'object')
|
|
126
|
+
throw new Error('DATAX_PARAMETER_PATH_NOT_SCALAR');
|
|
127
|
+
current[position] = parameters[name];
|
|
128
|
+
}
|
|
129
|
+
else
|
|
130
|
+
current = current[position];
|
|
131
|
+
}
|
|
132
|
+
else if (index === path.length - 1) {
|
|
133
|
+
if (current[key] !== null && typeof current[key] === 'object')
|
|
134
|
+
throw new Error('DATAX_PARAMETER_PATH_NOT_SCALAR');
|
|
135
|
+
current[key] = parameters[name];
|
|
136
|
+
}
|
|
137
|
+
else
|
|
138
|
+
current = current[key];
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return result;
|
|
142
|
+
}
|
|
143
|
+
async function boundedJson(response) {
|
|
144
|
+
if (!response.body)
|
|
145
|
+
throw new Error('DATAX_EMPTY_RESPONSE');
|
|
146
|
+
const reader = response.body.getReader();
|
|
147
|
+
const chunks = [];
|
|
148
|
+
let size = 0;
|
|
149
|
+
try {
|
|
150
|
+
for (;;) {
|
|
151
|
+
const chunk = await reader.read();
|
|
152
|
+
if (chunk.done)
|
|
153
|
+
break;
|
|
154
|
+
size += chunk.value.byteLength;
|
|
155
|
+
if (size > 2_500_000)
|
|
156
|
+
throw new Error('DATAX_RESPONSE_TOO_LARGE');
|
|
157
|
+
chunks.push(chunk.value);
|
|
158
|
+
}
|
|
159
|
+
return JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
160
|
+
}
|
|
161
|
+
finally {
|
|
162
|
+
await reader.cancel();
|
|
163
|
+
}
|
|
164
|
+
}
|
package/docs/AGENTS.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# data-xpert analytics documentation
|
|
2
|
+
|
|
3
|
+
This Mintlify site documents the analytics provider, not ontology authoring or deployment automation.
|
|
4
|
+
Use MDX with YAML frontmatter and keep `docs.json` navigation synchronized.
|
|
5
|
+
|
|
6
|
+
- Distinguish Xpert Dataset/Binding, this provider's transport, and data-xpert's ontology adapters.
|
|
7
|
+
- Keep native query semantics in adapter-owned `definition` and Action `inputSchema`.
|
|
8
|
+
- Explain current-viewer Actor Token and Assistant/resource authorization without exposing credentials.
|
|
9
|
+
- Do not claim that a registered ontology adapter automatically permits a read-only Action.
|
|
10
|
+
- Distinguish HTTP fixtures and lifecycle mocks from real SSO and dashboard acceptance.
|
|
11
|
+
- Keep machine paths, private receipts and tokens out of the package.
|
package/docs/docs.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://mintlify.com/docs.json",
|
|
3
|
+
"theme": "maple",
|
|
4
|
+
"name": "data-xpert Analytics",
|
|
5
|
+
"colors": {
|
|
6
|
+
"primary": "#0f766e",
|
|
7
|
+
"light": "#14b8a6",
|
|
8
|
+
"dark": "#115e59"
|
|
9
|
+
},
|
|
10
|
+
"navigation": {
|
|
11
|
+
"tabs": [
|
|
12
|
+
{
|
|
13
|
+
"tab": "Documentation",
|
|
14
|
+
"groups": [
|
|
15
|
+
{
|
|
16
|
+
"group": "Provider",
|
|
17
|
+
"pages": [
|
|
18
|
+
"index",
|
|
19
|
+
"verification"
|
|
20
|
+
]
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
}
|
package/docs/index.mdx
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "data-xpert analytics provider"
|
|
3
|
+
description: "Adapt governed ontology Actions to Xpert Dataset/Binding without coupling the platform to native query languages."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## Responsibilities
|
|
7
|
+
|
|
8
|
+
Xpert owns the shared Dataset/Binding protocol, current actor, source configuration,
|
|
9
|
+
parameter validation, execution budget and Artifact persistence. This system plugin registers
|
|
10
|
+
`data-xpert.ontology`, operation `action`, contract version `1`, and the `data-xpert`
|
|
11
|
+
DataSource type. It converts declared scalar parameters into native Action arguments and
|
|
12
|
+
maps canonical rows, completeness and audit receipts back to the shared Dataset path.
|
|
13
|
+
|
|
14
|
+
Data-xpert owns published resource discovery, ontology adapter metadata, Action input
|
|
15
|
+
schemas, Assistant/resource access, explicit viewer policy and registered read-only execution.
|
|
16
|
+
No database runner or ontology-specific branch is added to the Xpert platform.
|
|
17
|
+
|
|
18
|
+
The provider ID is a stable Binding reference; package ownership uses the
|
|
19
|
+
`analytics_data_xpert` artifact namespace. Changing the ID requires saved-Binding migration.
|
|
20
|
+
|
|
21
|
+
## Server configuration and identity
|
|
22
|
+
|
|
23
|
+
Configure `baseUrl` (including `/api`), `audience`, `assistantCode`, `assistantXpertId`
|
|
24
|
+
and `workspaceId` on the platform DataSource. The Assistant and workspace must already
|
|
25
|
+
have the intended published resources and viewer policy in data-xpert.
|
|
26
|
+
|
|
27
|
+
The provider gets a short-lived token through
|
|
28
|
+
`ActorTokenRuntimeFactoryCapability.createScopedApi(context.actor)`. Every request sends
|
|
29
|
+
that token and the current actor's `organization-id` header. Neither the Agent's Binding nor
|
|
30
|
+
the HTML iframe supplies a token, actor, API URL or Assistant scope.
|
|
31
|
+
|
|
32
|
+
Only HTTP(S) base URLs without embedded credentials, query or fragment are accepted.
|
|
33
|
+
Redirects are rejected. The host abort signal propagates through fetch and response streaming;
|
|
34
|
+
a cancellation before or during token acquisition prevents a subsequent HTTP call.
|
|
35
|
+
|
|
36
|
+
## Discovery across ontology adapters
|
|
37
|
+
|
|
38
|
+
Start with `{}` or `{ "kind": "resources" }`. Follow each `inspectRequest` from resources
|
|
39
|
+
to entities and then Actions. Follow `nextRequest` even if authorization filtered the entire
|
|
40
|
+
page. Entity results are limited to 20 matches; narrow `query` or `entityTypeCode` as needed.
|
|
41
|
+
|
|
42
|
+
Entity metadata carries this adapter-owned envelope:
|
|
43
|
+
|
|
44
|
+
```json
|
|
45
|
+
{
|
|
46
|
+
"definition": {
|
|
47
|
+
"adapterId": "sap_odata_api",
|
|
48
|
+
"contractVersion": 1,
|
|
49
|
+
"metadata": { "properties": [], "keys": [], "queryCapabilities": {} },
|
|
50
|
+
"instructions": ["Use the published property definitions and Action inputSchema."]
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The provider preserves this envelope, canonical targets, snapshot/version/partition fields,
|
|
56
|
+
and each Action's `inputSchema` and `inputHint`. Semantic Model metadata may contain an
|
|
57
|
+
`analysisContract`; OData metadata has different fields. The plugin does not translate either
|
|
58
|
+
into the other's language or infer an adapter from labels.
|
|
59
|
+
|
|
60
|
+
A new ontology adapter can use this path when data-xpert registers its discovery metadata
|
|
61
|
+
and permits its read-only Actions with canonical effects. Registration, published resource
|
|
62
|
+
access and explicit viewer policy are all required. A resource type alone is insufficient.
|
|
63
|
+
|
|
64
|
+
## Binding and filters
|
|
65
|
+
|
|
66
|
+
The payload selects exact discovered `resourceId`, `actionTypeCode`, canonical `target`,
|
|
67
|
+
native `params` and `parameterPaths`. The target must contain `entityId` or `entityRef`.
|
|
68
|
+
Use discovered `entityTypeCode` with external references when supplied.
|
|
69
|
+
|
|
70
|
+
For an Action whose input schema defines the following filter, a mapping looks like this:
|
|
71
|
+
|
|
72
|
+
```json
|
|
73
|
+
{
|
|
74
|
+
"params": { "filters": [{ "field": "Region", "op": "eq", "value": null }] },
|
|
75
|
+
"parameterPaths": { "region": ["filters", "0", "value"] }
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
This is a parameter-mapping example, not a universal ontology filter format. Use the selected
|
|
80
|
+
Action's actual input schema. Paths are relative to `params`, unique, and must identify an
|
|
81
|
+
existing scalar. Array segments use canonical zero-based indices. Missing/extra mappings,
|
|
82
|
+
prototype keys and replacing an entire object/array are rejected. Parameters are cloned and
|
|
83
|
+
replaced as whole values; the provider never interpolates query text.
|
|
84
|
+
|
|
85
|
+
Each adapter defines its own null semantics, operators, supported time fields and aggregation
|
|
86
|
+
limits. Verify business meaning and time axes with published metadata and authorized previews.
|
|
87
|
+
Map returned column keys or explicit semantic references to Dataset field IDs.
|
|
88
|
+
|
|
89
|
+
## Rendering and freshness
|
|
90
|
+
|
|
91
|
+
Data-xpert's query endpoint supplies canonical rows for `rows`, `series` and `entity` effects.
|
|
92
|
+
The provider carries those rows, source-fetch time, upstream execution ID and audit reference
|
|
93
|
+
into the platform normalizer. Truncated results use page scope and unknown total accuracy.
|
|
94
|
+
The reported count must not be interpreted as a verified full business aggregate.
|
|
95
|
+
|
|
96
|
+
Structured Data Analytics views and free HTML/JS Artifacts consume the same Dataset through
|
|
97
|
+
the host's declared Binding bridge. They do not call data-xpert directly. Filtering resolves
|
|
98
|
+
new declared parameters; refreshing performs another request. Responses must declare
|
|
99
|
+
`fromCache: false`, meaning the data-xpert Action result cache was bypassed. Upstream source
|
|
100
|
+
caches and business-data freshness are separate concerns.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Build and verification"
|
|
3
|
+
description: "Separate transport-contract tests from real SSO and end-to-end dashboard acceptance."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## Compatibility
|
|
7
|
+
|
|
8
|
+
Development dependencies pin published SDK/contracts 3.25.0, which export AnalyticsProvider,
|
|
9
|
+
AnalyticsQueryProvider, ActorTokenRuntimeFactoryCapability and the shared Dataset/Binding
|
|
10
|
+
contracts. The host must provide the corresponding runtime capabilities.
|
|
11
|
+
|
|
12
|
+
Data-xpert must expose `POST /agent-workbench/analytics/discover` and
|
|
13
|
+
`POST /agent-workbench/analytics/query` under the configured API base. Discovery returns
|
|
14
|
+
adapter-owned `definition` metadata. Query returns canonical rows, execution/audit references,
|
|
15
|
+
fetch time and `fromCache: false` after the normal authorization and read-only Action pipeline.
|
|
16
|
+
|
|
17
|
+
## Local verification
|
|
18
|
+
|
|
19
|
+
From the xpertai workspace:
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
corepack pnpm exec nx run @xpert-ai/plugin-analytics-data-xpert:test
|
|
23
|
+
corepack pnpm exec nx run @xpert-ai/plugin-analytics-data-xpert:typecheck
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Tests start a disposable local HTTP fixture and use synthetic Actor Tokens. They verify:
|
|
27
|
+
|
|
28
|
+
- current actor, organization, trusted Assistant scope and token audience;
|
|
29
|
+
- Semantic Model/OData metadata, canonical targets, pagination and Action schemas;
|
|
30
|
+
- scalar filter mapping and rejection of unknown/prototype/duplicate paths;
|
|
31
|
+
- rows/series/entity projection, partial totals, audit receipts and fresh requests;
|
|
32
|
+
- unknown inputs, missing targets, cached responses and invalid configured URLs;
|
|
33
|
+
- refused redirects, bounded response streaming and cancellation.
|
|
34
|
+
|
|
35
|
+
Build and package before running `plugin-dev-harness`. Its lifecycle mocks verify package
|
|
36
|
+
loading and Nest injection, not a real identity provider or data-xpert authorization decision.
|
|
37
|
+
Validate `docs.json` navigation and run `mint validate` when updating documentation.
|
|
38
|
+
|
|
39
|
+
## Separate live acceptance
|
|
40
|
+
|
|
41
|
+
Install through the authenticated system-plugin workflow with tenant scope in the Default
|
|
42
|
+
tenant. Configure a DataSource pointing to the intended data-xpert API and its OIDC audience.
|
|
43
|
+
Use xpert-pro SSO, a published Assistant/resource binding and explicit viewer policy.
|
|
44
|
+
|
|
45
|
+
Verify an allowed viewer can discover and query, while a denied viewer cannot. Preview both
|
|
46
|
+
Semantic Model and OData (or another registered adapter) using their actual Action schemas.
|
|
47
|
+
Then verify one-sentence generation, filters, changed-source refresh, save/reopen and rejection
|
|
48
|
+
of undeclared HTML frontend queries. Local HTTP fixtures do not establish those outcomes.
|
|
49
|
+
|
|
50
|
+
## Errors
|
|
51
|
+
|
|
52
|
+
- `DATAX_PARAMETER_MAPPING_INVALID`: align parameter names with unique declared paths.
|
|
53
|
+
- `DATAX_PARAMETER_PATH_INVALID`: use an existing path relative to params with canonical indices.
|
|
54
|
+
- `DATAX_PARAMETER_PATH_NOT_SCALAR`: target a scalar value, not an object or array.
|
|
55
|
+
- `DATAX_URL_INVALID`: correct trusted source configuration.
|
|
56
|
+
- `DATAX_QUERY_HTTP_<status>`: investigate authorization or request validation in data-xpert;
|
|
57
|
+
upstream error bodies and secrets are not forwarded.
|
|
58
|
+
- `DATAX_RESPONSE_TOO_LARGE`: reduce result size; response buffering is bounded.
|
package/index.cjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = require('./dist/index.js').default
|
package/package.json
CHANGED
|
@@ -1,6 +1,59 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xpert-ai/plugin-analytics-data-xpert",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Governed data-xpert ontology analytics provider",
|
|
5
|
+
"main": "./index.cjs",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"require": "./index.cjs",
|
|
11
|
+
"default": "./index.cjs"
|
|
12
|
+
},
|
|
13
|
+
"./package.json": "./package.json"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"index.cjs",
|
|
18
|
+
"README.md",
|
|
19
|
+
"docs"
|
|
20
|
+
],
|
|
21
|
+
"license": "AGPL-3.0",
|
|
22
|
+
"private": false,
|
|
23
|
+
"xpert": {
|
|
24
|
+
"plugin": {
|
|
25
|
+
"level": "system",
|
|
26
|
+
"artifactNamespace": "analytics_data_xpert"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"peerDependencies": {
|
|
30
|
+
"@xpert-ai/plugin-sdk": "^3.25.0",
|
|
31
|
+
"@xpert-ai/contracts": "^3.25.0",
|
|
32
|
+
"@nestjs/common": "^11.1.6",
|
|
33
|
+
"@nestjs/core": "^11.1.6",
|
|
34
|
+
"zod": "3.25.67"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@xpert-ai/plugin-sdk": "3.25.0",
|
|
38
|
+
"@xpert-ai/contracts": "3.25.0",
|
|
39
|
+
"@nestjs/common": "^11.1.6",
|
|
40
|
+
"@nestjs/core": "^11.1.6",
|
|
41
|
+
"zod": "3.25.67",
|
|
42
|
+
"typescript": "^5.9.2",
|
|
43
|
+
"@types/node": "^20.19.9",
|
|
44
|
+
"reflect-metadata": "^0.2.2"
|
|
45
|
+
},
|
|
46
|
+
"repository": {
|
|
47
|
+
"type": "git",
|
|
48
|
+
"url": "https://github.com/xpert-ai/xpert-plugins.git",
|
|
49
|
+
"directory": "xpertai/tools/analytics-data-xpert"
|
|
50
|
+
},
|
|
51
|
+
"publishConfig": {
|
|
52
|
+
"access": "public"
|
|
53
|
+
},
|
|
54
|
+
"scripts": {
|
|
55
|
+
"build": "tsc -p tsconfig.json",
|
|
56
|
+
"test": "pnpm run build && node --test tests/*.test.cjs",
|
|
57
|
+
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
58
|
+
}
|
|
6
59
|
}
|