@xpert-ai/plugin-analytics-files 0.0.0-stage → 0.1.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/README.md CHANGED
@@ -1,3 +1,12 @@
1
- # Temporary Holding Version
1
+ # File Analytics
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ `@xpert-ai/plugin-analytics-files` provides `file.table`: declarative CSV, JSON and XLSX queries over platform-authorized Workspace files. Standard charts and HTML dashboards consume the same Dataset/Binding results.
4
+
5
+ Administrators configure a `workspace-files` data source with approved file references, explicit format/read options, column types and business descriptions. Agent discovery returns the exact query contract and a bounded real preview; no new Agent middleware is required. File parsing, aggregation and format dependencies remain in this plugin. The platform owns file permissions, data-source authorization and Artifact lifecycle.
6
+
7
+ - [Overview](docs/index.mdx) / [中文概览](docs/zh-Hans/index.mdx)
8
+ - [Configuration and query contract](docs/configuration.mdx) / [中文配置](docs/zh-Hans/configuration.mdx)
9
+ - [Verification](docs/verification.mdx) / [验收](docs/zh-Hans/verification.mdx)
10
+ - [Example catalog](examples/sales.json)
11
+
12
+ Development: `corepack pnpm exec nx run @xpert-ai/plugin-analytics-files:test` from `xpertai/`. Then run the repository plugin lifecycle harness and metadata check. Install at the declared system scope through the platform local deploy command; restart when requested and verify `file.table` discovery before application acceptance.
@@ -0,0 +1,3 @@
1
+ import type { XpertPlugin } from '@xpert-ai/plugin-sdk';
2
+ declare const plugin: XpertPlugin;
3
+ export default plugin;
package/dist/index.js ADDED
@@ -0,0 +1,13 @@
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 metadata = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(__dirname, '../package.json'), 'utf8'));
7
+ const plugin = {
8
+ meta: { name: metadata.name, version: metadata.version, category: 'database', displayName: 'File Analytics',
9
+ description: 'Declarative CSV, JSON and XLSX queries over authorized Workspace files', author: 'XpertAI',
10
+ level: metadata.xpert.plugin.level, artifactNamespace: metadata.xpert.plugin.artifactNamespace },
11
+ register: () => ({ module: plugin_1.AnalyticsFilesPlugin })
12
+ };
13
+ exports.default = plugin;
@@ -0,0 +1,6 @@
1
+ import Decimal from 'decimal.js';
2
+ import type { AnalyticsValue } from '@xpert-ai/contracts';
3
+ import type { Column } from './schema';
4
+ export declare const Exact: typeof Decimal;
5
+ export declare function cell(value: unknown, column: Column, format: 'csv' | 'json' | 'xlsx'): AnalyticsValue;
6
+ export declare function compare(a: AnalyticsValue, b: AnalyticsValue, type: Column['logicalType']): number;
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.Exact = void 0;
7
+ exports.cell = cell;
8
+ exports.compare = compare;
9
+ const decimal_js_1 = __importDefault(require("decimal.js"));
10
+ exports.Exact = decimal_js_1.default.clone({ precision: 160, toExpNeg: -160, toExpPos: 160 });
11
+ const date = (value) => /^\d{4}-\d{2}-\d{2}$/.test(value) && Number.isFinite(Date.parse(value)) && new Date(value).toISOString().slice(0, 10) === value;
12
+ function cell(value, column, format) {
13
+ if ((value === null || (typeof value === 'string' && column.nullValues.includes(value))) && column.nullable)
14
+ return null;
15
+ if (value instanceof Date && format === 'xlsx' && Number.isFinite(value.getTime()))
16
+ value = column.logicalType === 'date' ? value.toISOString().slice(0, 10) : value.toISOString();
17
+ if (typeof value === 'string' && value.length > 4096)
18
+ throw new Error('FILE_DATASET_CELL_LIMIT');
19
+ const text = typeof value === 'string' ? value : undefined;
20
+ switch (column.logicalType) {
21
+ case 'string':
22
+ if (text !== undefined)
23
+ return text;
24
+ break;
25
+ case 'boolean':
26
+ if (typeof value === 'boolean')
27
+ return value;
28
+ if (format === 'csv' && (text === 'true' || text === 'false'))
29
+ return text === 'true';
30
+ break;
31
+ case 'int64': {
32
+ const exact = typeof value === 'number' && Number.isSafeInteger(value) ? String(value) : text;
33
+ if (exact && /^-?\d{1,19}$/.test(exact) && BigInt(exact) >= -(2n ** 63n) && BigInt(exact) < 2n ** 63n)
34
+ return BigInt(exact).toString();
35
+ break;
36
+ }
37
+ case 'decimal': {
38
+ // JSON decimals must be strings. Excel numeric cells carry Excel's stored numeric precision.
39
+ const exact = format === 'xlsx' && typeof value === 'number' && Number.isFinite(value) ? new exports.Exact(value).toFixed() : text;
40
+ if (exact && /^-?\d{1,60}(\.\d{1,30})?$/.test(exact))
41
+ return new exports.Exact(exact).toFixed();
42
+ break;
43
+ }
44
+ case 'float64': {
45
+ const number = format === 'csv' && text && /^-?\d+(\.\d+)?([eE][+-]?\d+)?$/.test(text) ? Number(text) : value;
46
+ if (typeof number === 'number' && Number.isFinite(number))
47
+ return number;
48
+ break;
49
+ }
50
+ case 'date':
51
+ if (text && date(text))
52
+ return text;
53
+ break;
54
+ case 'timestamp':
55
+ if (text && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{1,9})?(Z|[+-]\d{2}:\d{2})$/.test(text) && date(text.slice(0, 10)) && Number.isFinite(Date.parse(text)))
56
+ return text;
57
+ break;
58
+ }
59
+ throw new Error(`FILE_DATASET_FIELD_TYPE_INVALID: ${column.key}`);
60
+ }
61
+ function compare(a, b, type) {
62
+ if (a === null || b === null)
63
+ return a === b ? 0 : a === null ? -1 : 1;
64
+ if (type === 'decimal' || type === 'int64' || type === 'float64')
65
+ return new exports.Exact(String(a)).comparedTo(String(b));
66
+ if (type === 'timestamp')
67
+ return Math.sign(Date.parse(String(a)) - Date.parse(String(b)));
68
+ return a === b ? 0 : a < b ? -1 : 1;
69
+ }
@@ -0,0 +1,2 @@
1
+ export declare class AnalyticsFilesPlugin {
2
+ }
@@ -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.AnalyticsFilesPlugin = void 0;
10
+ const plugin_sdk_1 = require("@xpert-ai/plugin-sdk");
11
+ const provider_1 = require("./provider");
12
+ let AnalyticsFilesPlugin = class AnalyticsFilesPlugin {
13
+ };
14
+ exports.AnalyticsFilesPlugin = AnalyticsFilesPlugin;
15
+ exports.AnalyticsFilesPlugin = AnalyticsFilesPlugin = __decorate([
16
+ (0, plugin_sdk_1.XpertServerPlugin)({ providers: [provider_1.FileAnalyticsProvider] })
17
+ ], AnalyticsFilesPlugin);
@@ -0,0 +1,29 @@
1
+ import type { AnalyticsBinding, AnalyticsDiscoveryResult, AnalyticsProviderDescriptor, AnalyticsValue, JSONValue } from '@xpert-ai/contracts';
2
+ import { type AnalyticsQueryProvider, type AnalyticsProviderContext, type RuntimeCapabilityRegistry } from '@xpert-ai/plugin-sdk';
3
+ import { type FileDataset } from './schema';
4
+ export declare const contractHash: (dataset: FileDataset) => string;
5
+ export declare class FileAnalyticsProvider implements AnalyticsQueryProvider {
6
+ private readonly runtime;
7
+ readonly descriptor: AnalyticsProviderDescriptor;
8
+ constructor(runtime: RuntimeCapabilityRegistry);
9
+ private check;
10
+ private file;
11
+ private read;
12
+ private guarded;
13
+ discover(request: JSONValue, context: AnalyticsProviderContext): Promise<AnalyticsDiscoveryResult>;
14
+ execute(binding: AnalyticsBinding, parameters: Record<string, AnalyticsValue>, context: AnalyticsProviderContext): Promise<{
15
+ sourceFetchedAt: string;
16
+ sourceVersion: string;
17
+ columns: {
18
+ key: string;
19
+ }[];
20
+ rows: Record<string, string | number | boolean | null>[];
21
+ completeness: {
22
+ totalRows: number;
23
+ returnedRows: number;
24
+ totalAccuracy: "exact";
25
+ scope: "page" | "full_query_result";
26
+ truncated: boolean;
27
+ };
28
+ }>;
29
+ }
@@ -0,0 +1,145 @@
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.FileAnalyticsProvider = exports.contractHash = void 0;
16
+ const common_1 = require("@nestjs/common");
17
+ const node_crypto_1 = require("node:crypto");
18
+ const plugin_sdk_1 = require("@xpert-ai/plugin-sdk");
19
+ const schema_1 = require("./schema");
20
+ const query_1 = require("./query");
21
+ const worker_client_1 = require("./worker-client");
22
+ const hash = (value) => (0, node_crypto_1.createHash)('sha256').update(value).digest('hex');
23
+ const contractHash = (dataset) => hash(JSON.stringify(dataset));
24
+ exports.contractHash = contractHash;
25
+ const instructions = `Use an inspected dataset's bindingPayload and columns. Never supply paths, file references, scope, URLs, SQL, scripts or reader options in Binding.payload. select contains output keys. filters are AND conditions; value is {type:"parameter",name:"region"} or {type:"literal",value:"East"}. Every parameter must appear in filters; bind it through Binding.parameters and controls. For an optional all-values filter use skipWhenNull:true, required:false and defaultValue:null; empty string is a literal value. groupBy requires aggregates. Aggregates are {key:"total",operation:"sum",column:"net_sales"} or {key:"count",operation:"count"}; use a NEW key distinct from source columns. With aggregates select contains group keys and aggregate keys only. Sum of int64 outputs decimal; sum/min/max are nullable; count is non-null int64 and has no unit. Other aggregate types/units follow the source. Output sourceField uses output keys, and logicalType/nullable/unit must match the declared columns exactly. Use the authored business descriptions, never sum incompatible units or duplicate grains. decimal/int64 values and controls use exact strings. Preview every query before saving. Input beyond file/row limits fails instead of producing partial totals. Refresh rereads the current file. Artifact versions pin queries, not source data. XLSX formula caches are not recalculated or guaranteed current. Render with Dataset/Binding; never browser fetch.`;
26
+ const queryContract = { operation: 'query', contractVersion: 1,
27
+ payloadSchema: (0, schema_1.jsonSchema)(schema_1.payloadSchema), instructions: instructions + ' metadata.binding is a complete canonical Binding; bindingPayload alone is only its payload, not a Binding. Start from metadata.binding and preserve its envelope. Raw rows do not promise uniqueness; keep grain:[] unless verified, or group first and declare the grouping field IDs as grain.' };
28
+ let FileAnalyticsProvider = class FileAnalyticsProvider {
29
+ constructor(runtime) {
30
+ this.runtime = runtime;
31
+ this.descriptor = {
32
+ id: schema_1.PROVIDER, name: 'Workspace files (CSV / JSON / XLSX)', dataSourceTypes: ['workspace-files'],
33
+ operations: [{ name: 'query', contractVersion: 1, readOnly: true }],
34
+ connectionType: { type: 'workspace-files', name: 'Workspace files', configuration: (0, schema_1.jsonSchema)(schema_1.optionsSchema) }
35
+ };
36
+ }
37
+ check(context) {
38
+ context.signal.throwIfAborted();
39
+ if (Date.now() >= context.deadlineAt)
40
+ throw new Error('FILE_DATASET_DEADLINE_EXCEEDED');
41
+ }
42
+ async file(definition, context) {
43
+ this.check(context);
44
+ const file = definition.file;
45
+ if ('userId' in file && file.userId !== context.actor.userId)
46
+ throw new common_1.ForbiddenException('FILE_DATASET_ACCESS_DENIED');
47
+ if (file.catalog === 'projects') {
48
+ const visible = await this.runtime.require(plugin_sdk_1.ProjectAccessRuntimeCapability).listReadable({ actor: context.actor, projectIds: [file.projectId] });
49
+ if (!visible.some(p => p.projectId === file.projectId && !p.archived))
50
+ throw new common_1.ForbiddenException('FILE_DATASET_ACCESS_DENIED');
51
+ }
52
+ const reference = { ...file, ...context.actor };
53
+ const files = this.runtime.require(plugin_sdk_1.WorkspaceFilesRuntimeCapability);
54
+ const metadata = await files.resolveFile(reference);
55
+ this.check(context);
56
+ if (metadata.catalog !== file.catalog)
57
+ throw new common_1.ForbiddenException('FILE_DATASET_SCOPE_CHANGED');
58
+ if (metadata.size === undefined || metadata.size <= 0 || metadata.size > schema_1.MAX_BYTES)
59
+ throw new Error('FILE_DATASET_SIZE_LIMIT');
60
+ return { files, reference };
61
+ }
62
+ async read(definition, context) {
63
+ const { files, reference } = await this.file(definition, context);
64
+ const { buffer } = await files.readBuffer(reference);
65
+ this.check(context);
66
+ if (!buffer.length || buffer.length > schema_1.MAX_BYTES)
67
+ throw new Error('FILE_DATASET_SIZE_LIMIT');
68
+ return { buffer, fetchedAt: new Date().toISOString() };
69
+ }
70
+ async guarded(run) {
71
+ try {
72
+ return await run();
73
+ }
74
+ catch (error) {
75
+ if (error instanceof common_1.HttpException)
76
+ throw error;
77
+ const code = error instanceof Error && /^FILE_DATASET_[A-Z_]+(?:: [A-Za-z][A-Za-z0-9_]*)?$/.test(error.message) ? error.message : 'FILE_DATASET_INVALID';
78
+ throw new common_1.BadRequestException({ code: code.split(':')[0], message: code });
79
+ }
80
+ }
81
+ async discover(request, context) {
82
+ return this.guarded(async () => {
83
+ const input = schema_1.discoverySchema.parse(request), { datasets } = schema_1.optionsSchema.parse(context.options);
84
+ const selected = input.datasetId ? datasets.filter(d => d.id === input.datasetId) : datasets.slice((input.page - 1) * 20, input.page * 20);
85
+ if (input.datasetId && !selected.length)
86
+ throw new Error('FILE_DATASET_UNAVAILABLE');
87
+ const items = [];
88
+ for (const definition of selected) {
89
+ try {
90
+ await this.file(definition, context);
91
+ }
92
+ catch (e) {
93
+ if (!input.datasetId && e instanceof common_1.ForbiddenException)
94
+ continue;
95
+ throw e;
96
+ }
97
+ const payload = schema_1.payloadSchema.parse({ datasetId: definition.id, revision: definition.revision,
98
+ contractHash: (0, exports.contractHash)(definition), select: definition.columns.map(c => c.key) });
99
+ let metadata = { revision: definition.revision, format: definition.reader.format };
100
+ if (input.datasetId) {
101
+ const read = await this.read(definition, context);
102
+ const preview = await (0, worker_client_1.queryWorker)(read.buffer, definition, payload, {}, 5, context.signal, context.deadlineAt);
103
+ await this.file(definition, context);
104
+ const binding = { schemaVersion: 1, id: definition.id, dataSourceId: context.dataSourceId,
105
+ providerId: schema_1.PROVIDER, operation: 'query', contractVersion: 1, payload: JSON.parse(JSON.stringify(payload)), parameters: [],
106
+ output: { datasetId: definition.id, grain: [], fields: definition.columns.map(({ key, path, description, nullValues, ...field }) => ({ ...field, id: key, sourceField: key })) } };
107
+ const previewFits = Buffer.byteLength(JSON.stringify(preview.rows)) <= 32000;
108
+ metadata = { ...metadata, contractHash: payload.contractHash, bindingPayload: JSON.parse(JSON.stringify(payload)),
109
+ binding: JSON.parse(JSON.stringify(binding)),
110
+ columns: definition.columns.map(({ path, nullValues, ...column }) => column),
111
+ ...(definition.reader.format === 'xlsx' ? { sheet: definition.reader.sheet, formulaPolicy: definition.reader.formulaPolicy } : {}),
112
+ preview: previewFits ? preview.rows : [], previewOmitted: !previewFits,
113
+ ...(previewFits ? { previewCompleteness: preview.completeness } : {}),
114
+ limits: { inputBytes: schema_1.MAX_BYTES, inputRows: schema_1.MAX_ROWS }, sourceVersion: hash(read.buffer) };
115
+ }
116
+ items.push({ id: definition.id, name: definition.name, description: definition.description, kind: 'dataset', inspectRequest: { datasetId: definition.id }, metadata });
117
+ }
118
+ return { schemaVersion: 1, queryContract, items,
119
+ ...(!input.datasetId && input.page * 20 < datasets.length ? { nextRequest: { page: input.page + 1 } } : {}) };
120
+ });
121
+ }
122
+ async execute(binding, parameters, context) {
123
+ return this.guarded(async () => {
124
+ this.check(context);
125
+ const query = schema_1.payloadSchema.parse(binding.payload), { datasets } = schema_1.optionsSchema.parse(context.options);
126
+ const definition = datasets.find(d => d.id === query.datasetId);
127
+ if (!definition)
128
+ throw new Error('FILE_DATASET_UNAVAILABLE');
129
+ if (query.revision !== definition.revision || query.contractHash !== (0, exports.contractHash)(definition))
130
+ throw new Error('FILE_DATASET_CONTRACT_CHANGED');
131
+ (0, query_1.validateOutput)(binding, (0, query_1.outputColumns)(definition, query));
132
+ const read = await this.read(definition, context);
133
+ const result = await (0, worker_client_1.queryWorker)(read.buffer, definition, query, parameters, context.maxRows, context.signal, context.deadlineAt);
134
+ // No successful result is returned after current file/Assistant/Project access has been revoked.
135
+ await this.file(definition, context);
136
+ return { ...result, sourceFetchedAt: read.fetchedAt, sourceVersion: `${query.contractHash}:${hash(read.buffer)}` };
137
+ });
138
+ }
139
+ };
140
+ exports.FileAnalyticsProvider = FileAnalyticsProvider;
141
+ exports.FileAnalyticsProvider = FileAnalyticsProvider = __decorate([
142
+ (0, plugin_sdk_1.AnalyticsProvider)(schema_1.PROVIDER),
143
+ __param(0, (0, common_1.Inject)(plugin_sdk_1.XPERT_RUNTIME_CAPABILITIES_TOKEN)),
144
+ __metadata("design:paramtypes", [Object])
145
+ ], FileAnalyticsProvider);
@@ -0,0 +1,20 @@
1
+ import type { AnalyticsBinding, AnalyticsValue } from '@xpert-ai/contracts';
2
+ import type { Column, FileDataset, FileQuery } from './schema';
3
+ export type Row = Record<string, AnalyticsValue>;
4
+ export declare function outputColumns(definition: FileDataset, query: FileQuery): Column[];
5
+ export declare function validateOutput(binding: AnalyticsBinding, columns: Column[]): void;
6
+ export declare function executeQuery(rows: Row[], definition: FileDataset, query: FileQuery, parameters: Row, maxRows: number): {
7
+ columns: {
8
+ key: string;
9
+ }[];
10
+ rows: {
11
+ [k: string]: AnalyticsValue;
12
+ }[];
13
+ completeness: {
14
+ totalRows: number;
15
+ returnedRows: number;
16
+ totalAccuracy: "exact";
17
+ scope: "page" | "full_query_result";
18
+ truncated: boolean;
19
+ };
20
+ };
@@ -0,0 +1,134 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.outputColumns = outputColumns;
4
+ exports.validateOutput = validateOutput;
5
+ exports.executeQuery = executeQuery;
6
+ const cells_1 = require("./cells");
7
+ function requireColumn(columns, name) {
8
+ const column = columns.get(name);
9
+ if (!column)
10
+ throw new Error('FILE_DATASET_COLUMN_UNAVAILABLE');
11
+ return column;
12
+ }
13
+ function outputColumns(definition, query) {
14
+ const source = new Map(definition.columns.map(c => [c.key, c])), available = new Map();
15
+ if (new Set(query.select).size !== query.select.length || new Set(query.groupBy).size !== query.groupBy.length)
16
+ throw new Error('FILE_DATASET_DUPLICATE_COLUMN');
17
+ if (query.aggregates.length) {
18
+ for (const name of query.groupBy)
19
+ available.set(name, requireColumn(source, name));
20
+ for (const a of query.aggregates) {
21
+ if (source.has(a.key) || available.has(a.key))
22
+ throw new Error('FILE_DATASET_AGGREGATE_ALIAS_INVALID');
23
+ const column = a.column ? requireColumn(source, a.column) : undefined;
24
+ if (!column && a.operation !== 'count')
25
+ throw new Error('FILE_DATASET_AGGREGATE_COLUMN_REQUIRED');
26
+ if (a.operation === 'sum' && column && !['int64', 'decimal', 'float64'].includes(column.logicalType))
27
+ throw new Error('FILE_DATASET_SUM_TYPE_INVALID');
28
+ available.set(a.key, { key: a.key, path: [a.key], label: a.key, description: a.operation,
29
+ logicalType: a.operation === 'count' ? 'int64' : a.operation === 'sum' && column?.logicalType === 'int64' ? 'decimal' : column.logicalType,
30
+ nullable: a.operation !== 'count', role: 'measure', nullValues: [], ...(column?.unit && a.operation !== 'count' ? { unit: column.unit } : {}) });
31
+ }
32
+ }
33
+ else {
34
+ if (query.groupBy.length)
35
+ throw new Error('FILE_DATASET_GROUP_REQUIRES_AGGREGATE');
36
+ for (const [name, column] of source)
37
+ available.set(name, column);
38
+ }
39
+ const columns = query.select.map(name => requireColumn(available, name));
40
+ for (const sort of query.orderBy)
41
+ if (!query.select.includes(sort.column))
42
+ throw new Error('FILE_DATASET_SORT_COLUMN_INVALID');
43
+ for (const filter of query.filters)
44
+ requireColumn(source, filter.column);
45
+ return columns;
46
+ }
47
+ function validateOutput(binding, columns) {
48
+ for (const field of binding.output.fields) {
49
+ const column = columns.find(c => c.key === field.sourceField);
50
+ if (!column || field.semanticRef || field.logicalType !== column.logicalType || field.nullable !== column.nullable || field.unit !== column.unit)
51
+ throw new Error('FILE_DATASET_OUTPUT_CONTRACT_INVALID');
52
+ }
53
+ }
54
+ function executeQuery(rows, definition, query, parameters, maxRows) {
55
+ const columns = outputColumns(definition, query), source = new Map(definition.columns.map(c => [c.key, c]));
56
+ const parameterNames = new Set(query.filters.flatMap(f => f.value.type === 'parameter' ? [f.value.name] : []));
57
+ if (parameterNames.size !== Object.keys(parameters).length || Object.keys(parameters).some(name => !parameterNames.has(name)))
58
+ throw new Error('FILE_DATASET_PARAMETERS_INVALID');
59
+ const filters = query.filters.map(filter => {
60
+ const column = requireColumn(source, filter.column);
61
+ const raw = filter.value.type === 'parameter' ? parameters[filter.value.name] : filter.value.value;
62
+ const value = raw === null ? null : (0, cells_1.cell)(raw, { ...column, nullValues: [] }, 'json');
63
+ if (filter.operator === 'contains' && column.logicalType !== 'string')
64
+ throw new Error('FILE_DATASET_FILTER_TYPE_INVALID');
65
+ if (value === null && !filter.skipWhenNull && !['eq', 'ne'].includes(filter.operator))
66
+ throw new Error('FILE_DATASET_NULL_COMPARISON_INVALID');
67
+ return { ...filter, column, value };
68
+ });
69
+ let result = rows.filter(row => filters.every(f => {
70
+ if (f.value === null && f.skipWhenNull)
71
+ return true;
72
+ const value = row[f.column.key];
73
+ if (f.operator === 'contains')
74
+ return typeof value === 'string' && typeof f.value === 'string' && value.includes(f.value);
75
+ const order = (0, cells_1.compare)(value, f.value, f.column.logicalType);
76
+ if (f.operator === 'eq')
77
+ return order === 0;
78
+ if (f.operator === 'ne')
79
+ return order !== 0;
80
+ if (value === null || f.value === null)
81
+ return false;
82
+ return f.operator === 'gt' ? order > 0 : f.operator === 'gte' ? order >= 0 : f.operator === 'lt' ? order < 0 : order <= 0;
83
+ }));
84
+ if (query.aggregates.length) {
85
+ const groups = new Map();
86
+ if (!query.groupBy.length)
87
+ groups.set('[]', []);
88
+ for (const row of result) {
89
+ const id = JSON.stringify(query.groupBy.map(name => row[name]));
90
+ const group = groups.get(id) ?? [];
91
+ group.push(row);
92
+ groups.set(id, group);
93
+ }
94
+ result = [...groups.values()].map(group => {
95
+ const row = Object.fromEntries(query.groupBy.map(name => [name, group[0][name]]));
96
+ for (const aggregate of query.aggregates) {
97
+ const values = aggregate.column ? group.map(r => r[aggregate.column]).filter(v => v !== null) : [];
98
+ if (aggregate.operation === 'count')
99
+ row[aggregate.key] = String(aggregate.column ? values.length : group.length);
100
+ else if (!values.length)
101
+ row[aggregate.key] = null;
102
+ else if (aggregate.operation === 'sum') {
103
+ const sum = values.reduce((sum, value) => sum.plus(String(value)), new cells_1.Exact(0));
104
+ const float = source.get(aggregate.column).logicalType === 'float64';
105
+ if (float && !Number.isFinite(sum.toNumber()))
106
+ throw new Error('FILE_DATASET_SUM_OVERFLOW');
107
+ row[aggregate.key] = float ? sum.toNumber() : sum.toFixed();
108
+ }
109
+ else
110
+ row[aggregate.key] = values.reduce((a, b) => {
111
+ const order = (0, cells_1.compare)(a, b, source.get(aggregate.column).logicalType);
112
+ return aggregate.operation === 'min' ? order <= 0 ? a : b : order >= 0 ? a : b;
113
+ });
114
+ }
115
+ return row;
116
+ });
117
+ }
118
+ result.sort((a, b) => {
119
+ for (const order of query.orderBy) {
120
+ const diff = (0, cells_1.compare)(a[order.column], b[order.column], columns.find(c => c.key === order.column).logicalType);
121
+ if (diff)
122
+ return order.direction === 'asc' ? diff : -diff;
123
+ }
124
+ return 0;
125
+ });
126
+ const totalRows = result.length, truncated = totalRows > maxRows;
127
+ const projected = result.slice(0, maxRows).map(row => Object.fromEntries(query.select.map(key => [key, row[key]])));
128
+ if (Buffer.byteLength(JSON.stringify(projected)) > 2_000_000)
129
+ throw new Error('FILE_DATASET_RESULT_LIMIT');
130
+ return { columns: columns.map(c => ({ key: c.key })), rows: projected, completeness: {
131
+ totalRows, returnedRows: projected.length, totalAccuracy: 'exact',
132
+ scope: truncated ? 'page' : 'full_query_result', truncated
133
+ } };
134
+ }
@@ -0,0 +1,3 @@
1
+ import type { AnalyticsValue } from '@xpert-ai/contracts';
2
+ import { type FileDataset } from './schema';
3
+ export declare function readRows(bytes: Buffer, definition: FileDataset): Promise<Array<Record<string, AnalyticsValue>>>;
@@ -0,0 +1,97 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.readRows = readRows;
4
+ const sync_1 = require("csv-parse/sync");
5
+ const exceljs_1 = require("exceljs");
6
+ const schema_1 = require("./schema");
7
+ const cells_1 = require("./cells");
8
+ const xlsx_archive_1 = require("./xlsx-archive");
9
+ function matrixRows(matrix, definition) {
10
+ const headers = matrix.shift();
11
+ if (!headers || !headers.length || headers.length > 200 || headers.some(h => typeof h !== 'string' || !h || schema_1.forbidden.has(h)) || new Set(headers).size !== headers.length)
12
+ throw new Error('FILE_DATASET_HEADERS_INVALID');
13
+ for (const column of definition.columns)
14
+ if (!headers.includes(column.path[0]))
15
+ throw new Error('FILE_DATASET_SCHEMA_CHANGED');
16
+ return matrix.map(row => {
17
+ if (row.length !== headers.length)
18
+ throw new Error('FILE_DATASET_ROW_WIDTH_INVALID');
19
+ return Object.fromEntries(headers.map((header, i) => [String(header), row[i]]));
20
+ });
21
+ }
22
+ function excelValue(value, policy) {
23
+ if (value === undefined)
24
+ return null;
25
+ if (!value || typeof value !== 'object' || value instanceof Date)
26
+ return value;
27
+ if ('formula' in value || 'sharedFormula' in value) {
28
+ if (policy !== 'cached')
29
+ throw new Error('FILE_DATASET_FORMULA_DENIED');
30
+ if (value.result === undefined)
31
+ throw new Error('FILE_DATASET_FORMULA_CACHE_MISSING');
32
+ return excelValue(value.result, policy);
33
+ }
34
+ if ('richText' in value)
35
+ return value.richText.map(part => part.text).join('');
36
+ if ('hyperlink' in value)
37
+ return value.text; // Never dereference links.
38
+ throw new Error('FILE_DATASET_EXCEL_CELL_ERROR');
39
+ }
40
+ async function xlsxRows(bytes, definition) {
41
+ const reader = definition.reader;
42
+ if (reader.format !== 'xlsx')
43
+ throw new Error('FILE_DATASET_FORMAT_INVALID');
44
+ (0, xlsx_archive_1.validateXlsxArchive)(bytes);
45
+ const workbook = new exceljs_1.Workbook();
46
+ await workbook.xlsx.load(new Uint8Array(bytes).buffer);
47
+ const sheet = workbook.getWorksheet(reader.sheet);
48
+ if (!sheet || sheet.state !== 'visible')
49
+ throw new Error('FILE_DATASET_SHEET_UNAVAILABLE');
50
+ if (sheet.rowCount > schema_1.MAX_ROWS + reader.headerRow || sheet.columnCount > 200)
51
+ throw new Error('FILE_DATASET_INPUT_LIMIT');
52
+ const header = sheet.getRow(reader.headerRow), width = header.cellCount;
53
+ const matrix = [Array.from({ length: width }, (_, i) => excelValue(header.getCell(i + 1).value, 'reject'))];
54
+ for (let i = reader.headerRow + 1; i <= sheet.rowCount; i++) {
55
+ const row = sheet.getRow(i);
56
+ if (!row.hasValues)
57
+ continue;
58
+ if (row.cellCount > width)
59
+ throw new Error('FILE_DATASET_ROW_WIDTH_INVALID');
60
+ matrix.push(Array.from({ length: width }, (_, c) => {
61
+ const value = row.getCell(c + 1);
62
+ if (value.isMerged)
63
+ throw new Error('FILE_DATASET_MERGED_CELL_DENIED');
64
+ return excelValue(value.value, reader.formulaPolicy);
65
+ }));
66
+ }
67
+ return matrixRows(matrix, definition);
68
+ }
69
+ async function readRows(bytes, definition) {
70
+ if (!bytes.length || bytes.length > schema_1.MAX_BYTES)
71
+ throw new Error('FILE_DATASET_SIZE_LIMIT');
72
+ const reader = definition.reader;
73
+ let records;
74
+ if (reader.format === 'xlsx')
75
+ records = await xlsxRows(bytes, definition);
76
+ else {
77
+ const text = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
78
+ if (reader.format === 'json')
79
+ records = (0, schema_1.readPath)(JSON.parse(text.replace(/^\uFEFF/, '')), reader.rowsPath);
80
+ else {
81
+ const parsed = (0, sync_1.parse)(text, { bom: true, delimiter: reader.delimiter, from_line: reader.headerRow,
82
+ skip_empty_lines: true, max_record_size: 100_000, to: schema_1.MAX_ROWS + 2 });
83
+ if (!Array.isArray(parsed) || !parsed.every(Array.isArray))
84
+ throw new Error('FILE_DATASET_CSV_INVALID');
85
+ records = matrixRows(parsed, definition);
86
+ }
87
+ }
88
+ if (!Array.isArray(records))
89
+ throw new Error('FILE_DATASET_ROWS_INVALID');
90
+ if (records.length > schema_1.MAX_ROWS)
91
+ throw new Error('FILE_DATASET_INPUT_LIMIT');
92
+ return records.map(row => {
93
+ if (!row || typeof row !== 'object' || Array.isArray(row))
94
+ throw new Error('FILE_DATASET_ROW_INVALID');
95
+ return Object.fromEntries(definition.columns.map(column => [column.key, (0, cells_1.cell)((0, schema_1.readPath)(row, column.path), column, reader.format)]));
96
+ });
97
+ }