@microsoft/rayfin-connector-fabric-semanticmodel 1.34.0 → 1.35.0-alpha.1276

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.
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Apache Arrow IPC decoding for the `fabric-semanticmodel` connector.
3
+ *
4
+ * The semantic-model worker returns DAX query results as a binary Apache
5
+ * Arrow stream (`Content-Type: application/vnd.apache.arrow.stream`). This
6
+ * module decodes that stream into the connector's existing JSON wire shape
7
+ * ({@link FabricSemanticModelTabularResponse}) so callers and
8
+ * {@link toQueryResult} keep working unchanged.
9
+ *
10
+ * The value-coercion logic (Int64 overflow guards, decimal scaling,
11
+ * timezone-unaware DateTime formatting, dictionary unwrapping, LZ4 frame
12
+ * decompression) mirrors the `@microsoft/fabric-app-data` reference decoder so
13
+ * values match the legacy JSON path exactly.
14
+ */
15
+ import type { FabricSemanticModelTabularResponse } from './types.js';
16
+ /**
17
+ * Thrown when an Arrow value cannot be represented as a JavaScript `number`
18
+ * without losing precision (Int64 or scaled Decimal out of safe-integer
19
+ * range). Surfaced to callers as an `'overflow'`-category query result.
20
+ */
21
+ export declare class ArrowOverflowError extends Error {
22
+ constructor(message: string);
23
+ }
24
+ /**
25
+ * Decode an Apache Arrow IPC stream into a
26
+ * {@link FabricSemanticModelTabularResponse}.
27
+ *
28
+ * Success rows are objects keyed by the fully-qualified column name
29
+ * (e.g. `"Sales[Region]"`), matching the JSON wire shape so existing callers
30
+ * and {@link toQueryResult} are unaffected. DAX error tables map to
31
+ * `output.queryError` (categorised `'query'`), and value-overflow conditions
32
+ * map to a per-table error (categorised `'overflow'`).
33
+ *
34
+ * @param bytes - The raw Arrow IPC stream bytes.
35
+ * @param requestId - The Power BI request id, when known. Defaults to `''`.
36
+ * @returns The decoded tabular response envelope.
37
+ */
38
+ export declare function parseArrowStream(bytes: ArrayBuffer | Uint8Array, requestId?: string): FabricSemanticModelTabularResponse;
39
+ //# sourceMappingURL=arrow.d.ts.map
package/dist/arrow.js ADDED
@@ -0,0 +1,252 @@
1
+ /**
2
+ * Apache Arrow IPC decoding for the `fabric-semanticmodel` connector.
3
+ *
4
+ * The semantic-model worker returns DAX query results as a binary Apache
5
+ * Arrow stream (`Content-Type: application/vnd.apache.arrow.stream`). This
6
+ * module decodes that stream into the connector's existing JSON wire shape
7
+ * ({@link FabricSemanticModelTabularResponse}) so callers and
8
+ * {@link toQueryResult} keep working unchanged.
9
+ *
10
+ * The value-coercion logic (Int64 overflow guards, decimal scaling,
11
+ * timezone-unaware DateTime formatting, dictionary unwrapping, LZ4 frame
12
+ * decompression) mirrors the `@microsoft/fabric-app-data` reference decoder so
13
+ * values match the legacy JSON path exactly.
14
+ */
15
+ import { tableFromIPC, compressionRegistry, CompressionType, Type, } from 'apache-arrow';
16
+ import { compress, decompress } from 'lz4js';
17
+ // Power BI Arrow frames may be LZ4-compressed. apache-arrow ships the registry
18
+ // but not the codec, so register it once on module load.
19
+ compressionRegistry.set(CompressionType.LZ4_FRAME, {
20
+ decode: (data) => decompress(data),
21
+ encode: (data) => compress(data),
22
+ });
23
+ /**
24
+ * Columns whose combined presence marks an Arrow table as a DAX error table.
25
+ * Power BI surfaces query errors in-band (HTTP 200) as a single-row table
26
+ * with these columns rather than as a transport error.
27
+ */
28
+ const REQUIRED_ERROR_COLUMNS = [
29
+ 'ErrorCode',
30
+ 'ErrorMessage',
31
+ 'ErrorDescription',
32
+ ];
33
+ const MAX_SAFE = BigInt(Number.MAX_SAFE_INTEGER);
34
+ const MIN_SAFE = BigInt(-Number.MAX_SAFE_INTEGER);
35
+ /**
36
+ * Thrown when an Arrow value cannot be represented as a JavaScript `number`
37
+ * without losing precision (Int64 or scaled Decimal out of safe-integer
38
+ * range). Surfaced to callers as an `'overflow'`-category query result.
39
+ */
40
+ export class ArrowOverflowError extends Error {
41
+ constructor(message) {
42
+ super(message);
43
+ this.name = 'ArrowOverflowError';
44
+ }
45
+ }
46
+ /**
47
+ * Coerce an Arrow Int64 (`bigint`) into a `number`, throwing when the value
48
+ * cannot be represented without precision loss.
49
+ */
50
+ function coerceBigInt(raw, columnName) {
51
+ if (raw === null || raw === undefined)
52
+ return null;
53
+ const v = raw;
54
+ if (v > MAX_SAFE || v < MIN_SAFE) {
55
+ throw new ArrowOverflowError(`Integer value ${v} in column "${columnName}" exceeds Number.MAX_SAFE_INTEGER and cannot be safely represented.`);
56
+ }
57
+ return Number(v);
58
+ }
59
+ /**
60
+ * Build a coercer for a fixed-scale Arrow Decimal: divide the integer mantissa
61
+ * by `10^scale`, guarding against non-finite and unsafe results.
62
+ */
63
+ function makeDecimalCoercer(scale) {
64
+ const divisor = Math.pow(10, scale);
65
+ return function coerceDecimal(raw, columnName) {
66
+ if (raw === null || raw === undefined)
67
+ return null;
68
+ // apache-arrow returns Decimal cells as a `DecimalBigNum` (a Uint32Array
69
+ // subclass). `Number()` yields the unscaled mantissa for in-range values,
70
+ // but throws for magnitudes beyond Number.MAX_SAFE_INTEGER; treat that
71
+ // throw as an overflow so it surfaces as a per-table error instead of
72
+ // escaping the decode.
73
+ let mantissa;
74
+ try {
75
+ mantissa = Number(raw);
76
+ }
77
+ catch {
78
+ throw new ArrowOverflowError(`Decimal value in column "${columnName}" exceeds Number.MAX_SAFE_INTEGER and cannot be safely represented.`);
79
+ }
80
+ const num = mantissa / divisor;
81
+ if (!Number.isFinite(num)) {
82
+ throw new ArrowOverflowError(`Decimal value in column "${columnName}" overflows to ${num} and cannot be represented as a finite Number.`);
83
+ }
84
+ if (Math.abs(num) > Number.MAX_SAFE_INTEGER) {
85
+ throw new ArrowOverflowError(`Decimal value ${num} in column "${columnName}" exceeds Number.MAX_SAFE_INTEGER and cannot be safely represented.`);
86
+ }
87
+ return num;
88
+ };
89
+ }
90
+ /**
91
+ * Coerce an Arrow Date/Timestamp into an ISO string *without* a trailing `Z`,
92
+ * matching Analysis Services' timezone-unaware DateTime semantics.
93
+ */
94
+ function coerceDateTime(raw) {
95
+ if (raw === null || raw === undefined)
96
+ return null;
97
+ // apache-arrow normalizes Date/Timestamp cells (all units) to epoch
98
+ // milliseconds; coerce a defensive `bigint` to a number so `new Date` never
99
+ // throws a raw `TypeError` on it.
100
+ const ms = typeof raw === 'bigint' ? Number(raw) : raw;
101
+ return new Date(ms).toISOString().slice(0, -1);
102
+ }
103
+ /**
104
+ * Resolve the underlying data type for a field, unwrapping dictionary-encoded
105
+ * columns to their value type first.
106
+ */
107
+ function unwrapType(field) {
108
+ let type = field.type;
109
+ if (type.typeId === Type.Dictionary) {
110
+ type = type.dictionary;
111
+ }
112
+ return { typeId: type.typeId, type };
113
+ }
114
+ /** Select a value coercer for a field, or `null` to pass the value through. */
115
+ function buildCoercer(field) {
116
+ const { typeId, type } = unwrapType(field);
117
+ switch (typeId) {
118
+ case Type.Int:
119
+ // Only 64-bit integers arrive as `bigint` and need narrowing.
120
+ return type.bitWidth === 64 ? coerceBigInt : null;
121
+ case Type.Decimal:
122
+ return makeDecimalCoercer(type.scale);
123
+ case Type.Date:
124
+ case Type.Timestamp:
125
+ return coerceDateTime;
126
+ default:
127
+ return null;
128
+ }
129
+ }
130
+ /** Map an Arrow field to a Power-BI-style dataType string. */
131
+ function resolveDataType(field) {
132
+ const { typeId, type } = unwrapType(field);
133
+ switch (typeId) {
134
+ case Type.Int:
135
+ return 'Int64';
136
+ case Type.Float:
137
+ return 'Double';
138
+ case Type.Decimal:
139
+ return 'Decimal';
140
+ case Type.Bool:
141
+ return 'Boolean';
142
+ case Type.Utf8:
143
+ case Type.LargeUtf8:
144
+ return 'String';
145
+ case Type.Date:
146
+ case Type.Timestamp:
147
+ return 'DateTime';
148
+ default:
149
+ return String(type);
150
+ }
151
+ }
152
+ /** A table is a DAX error table when it carries all error-marker columns. */
153
+ function isArrowErrorTable(columnNames) {
154
+ const nameSet = new Set(columnNames);
155
+ return REQUIRED_ERROR_COLUMNS.every((col) => nameSet.has(col));
156
+ }
157
+ /**
158
+ * Decode an Apache Arrow IPC stream into a
159
+ * {@link FabricSemanticModelTabularResponse}.
160
+ *
161
+ * Success rows are objects keyed by the fully-qualified column name
162
+ * (e.g. `"Sales[Region]"`), matching the JSON wire shape so existing callers
163
+ * and {@link toQueryResult} are unaffected. DAX error tables map to
164
+ * `output.queryError` (categorised `'query'`), and value-overflow conditions
165
+ * map to a per-table error (categorised `'overflow'`).
166
+ *
167
+ * @param bytes - The raw Arrow IPC stream bytes.
168
+ * @param requestId - The Power BI request id, when known. Defaults to `''`.
169
+ * @returns The decoded tabular response envelope.
170
+ */
171
+ export function parseArrowStream(bytes, requestId = '') {
172
+ const data = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
173
+ // An empty body carries no tables; decode it to an empty (successful)
174
+ // response rather than letting `tableFromIPC` throw on truncated input.
175
+ if (data.byteLength === 0) {
176
+ return {
177
+ status: 'Succeeded',
178
+ output: { tables: [], requestId },
179
+ errors: [],
180
+ };
181
+ }
182
+ const arrowTable = tableFromIPC(data);
183
+ const fields = arrowTable.schema.fields;
184
+ const columnNames = fields.map((f) => f.name);
185
+ // DAX errors are returned in-band as a single-row error table.
186
+ if (isArrowErrorTable(columnNames)) {
187
+ const firstRow = arrowTable.get(0);
188
+ const rowJson = (firstRow?.toJSON?.() ?? {});
189
+ const message = rowJson['ErrorMessage'] ?? 'Unknown DAX error';
190
+ return {
191
+ status: 'Succeeded',
192
+ output: {
193
+ tables: [],
194
+ requestId,
195
+ queryError: { message: String(message) },
196
+ },
197
+ errors: [],
198
+ };
199
+ }
200
+ const coercers = fields.map((f) => buildCoercer(f));
201
+ const numRows = arrowTable.numRows;
202
+ const rows = new Array(numRows);
203
+ for (let r = 0; r < numRows; r++) {
204
+ rows[r] = {};
205
+ }
206
+ try {
207
+ for (let c = 0; c < fields.length; c++) {
208
+ const col = arrowTable.getChildAt(c);
209
+ if (!col)
210
+ continue;
211
+ const coercer = coercers[c];
212
+ const colName = columnNames[c];
213
+ if (coercer) {
214
+ for (let r = 0; r < numRows; r++) {
215
+ rows[r][colName] = coercer(col.get(r), colName);
216
+ }
217
+ }
218
+ else {
219
+ for (let r = 0; r < numRows; r++) {
220
+ rows[r][colName] = col.get(r) ?? null;
221
+ }
222
+ }
223
+ }
224
+ }
225
+ catch (err) {
226
+ if (err instanceof ArrowOverflowError) {
227
+ // Surface as a per-table error so toQueryResult categorises it 'overflow'.
228
+ return {
229
+ status: 'Succeeded',
230
+ output: {
231
+ tables: [{ rows: [], error: { message: err.message } }],
232
+ requestId,
233
+ },
234
+ errors: [],
235
+ };
236
+ }
237
+ throw err;
238
+ }
239
+ const columns = fields.map((f) => ({
240
+ name: f.name,
241
+ dataType: resolveDataType(f),
242
+ }));
243
+ return {
244
+ status: 'Succeeded',
245
+ output: {
246
+ tables: [{ rows, columns }],
247
+ requestId,
248
+ },
249
+ errors: [],
250
+ };
251
+ }
252
+ //# sourceMappingURL=arrow.js.map
package/dist/index.d.ts CHANGED
@@ -2,4 +2,6 @@ export type { ExecuteQueryInput, FabricSemanticModelColumn, FabricSemanticModelE
2
2
  export type { FabricSemanticModel, FabricSemanticModelOperation, FabricSemanticModelOperationCatalog, } from './marker.js';
3
3
  export { toQueryResult } from './queryResult.js';
4
4
  export type { QueryColumn, QueryError, QueryErrorCategory, QueryTable, SemanticModelQueryResult, } from './queryResult.js';
5
+ export { ArrowOverflowError, parseArrowStream } from './arrow.js';
6
+ export { fabricSemanticModel } from './runtime.js';
5
7
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -1,2 +1,4 @@
1
1
  export { toQueryResult } from './queryResult.js';
2
+ export { ArrowOverflowError, parseArrowStream } from './arrow.js';
3
+ export { fabricSemanticModel } from './runtime.js';
2
4
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Ambient module declaration for `lz4js`, which ships without bundled types.
3
+ *
4
+ * This is a global (non-module) script: it intentionally has no top-level
5
+ * `import`/`export`, so the `declare module 'lz4js'` block provides ambient
6
+ * typings for the otherwise-untyped package. A standalone `.d.ts` file cannot
7
+ * be used here because `packages/**\/*.d.ts` is git-ignored as a build
8
+ * artifact, so this `.ts` shim is committed instead.
9
+ */
10
+ declare module 'lz4js' {
11
+ /** Decompress an LZ4 block/frame buffer. */
12
+ function decompress(data: Uint8Array): Uint8Array;
13
+ /** Compress a buffer using LZ4. */
14
+ function compress(data: Uint8Array): Uint8Array;
15
+ }
16
+ //# sourceMappingURL=lz4js-shim.d.ts.map
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ /**
3
+ * Ambient module declaration for `lz4js`, which ships without bundled types.
4
+ *
5
+ * This is a global (non-module) script: it intentionally has no top-level
6
+ * `import`/`export`, so the `declare module 'lz4js'` block provides ambient
7
+ * typings for the otherwise-untyped package. A standalone `.d.ts` file cannot
8
+ * be used here because `packages/**\/*.d.ts` is git-ignored as a build
9
+ * artifact, so this `.ts` shim is committed instead.
10
+ */
11
+ //# sourceMappingURL=lz4js-shim.js.map
package/dist/marker.d.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  * category. Contributes the operation catalog that the typed
4
4
  * `client.connectors.<name>` proxy uses to type its operations.
5
5
  */
6
- import type { ConnectorMarker, OperationDef } from '@microsoft/rayfin-connectors';
6
+ import type { ConnectorMarker, OperationDef, TypedConnectorClient } from '@microsoft/rayfin-connectors';
7
7
  import type { ExecuteQueryInput, FabricSemanticModelTabularResponse } from './types.js';
8
8
  /**
9
9
  * Union of all operation names this connector category supports.
@@ -28,5 +28,13 @@ export interface FabricSemanticModelOperationCatalog {
28
28
  * };
29
29
  * ```
30
30
  */
31
- export type FabricSemanticModel<TOps extends FabricSemanticModelOperation = FabricSemanticModelOperation> = ConnectorMarker<Pick<FabricSemanticModelOperationCatalog, TOps>>;
31
+ export type FabricSemanticModel<TOps extends FabricSemanticModelOperation = FabricSemanticModelOperation> = ConnectorMarker<TypedConnectorClient<Pick<FabricSemanticModelOperationCatalog, TOps>>> & {
32
+ /**
33
+ * Diagnostic phantom — carries the operation catalog at the type level
34
+ * so tools (e.g. typedoc, IDE hovers) can surface the connector's
35
+ * operation surface without inferring through the resolved client type.
36
+ * Not used by {@link TypedConnectorsApi}, which reads `__client` only.
37
+ */
38
+ readonly __operations?: Pick<FabricSemanticModelOperationCatalog, TOps>;
39
+ };
32
40
  //# sourceMappingURL=marker.d.ts.map
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Runtime companion for the `fabric-semanticmodel` connector marker.
3
+ *
4
+ * Connector markers are type-only, so the binary decode for the `executeQuery`
5
+ * operation is supplied here as a {@link ConnectorRuntime}. Pass the result of
6
+ * {@link fabricSemanticModel} to `ConnectorsRayfinClient` (or
7
+ * `createConnectorsApi`) under the same connector name used in the typed
8
+ * schema so the connectors proxy decodes the Apache Arrow response into a
9
+ * {@link FabricSemanticModelTabularResponse}.
10
+ */
11
+ import type { ConnectorRuntime } from '@microsoft/rayfin-connectors';
12
+ /**
13
+ * Build the runtime hooks for a `fabric-semanticmodel` connector instance.
14
+ *
15
+ * Registers the Apache Arrow decoder for the `executeQuery` operation. JSON
16
+ * responses bypass the decoder, so a worker that still returns JSON keeps
17
+ * working unchanged.
18
+ *
19
+ * @example
20
+ * ```ts
21
+ * import { ConnectorsRayfinClient } from '@microsoft/rayfin-client/experimental';
22
+ * import {
23
+ * fabricSemanticModel,
24
+ * type FabricSemanticModel,
25
+ * } from '@microsoft/rayfin-connector-fabric-semanticmodel';
26
+ *
27
+ * type AppConnectorsSchema = {
28
+ * salesModel: FabricSemanticModel<'executeQuery'>;
29
+ * };
30
+ *
31
+ * const client = new ConnectorsRayfinClient<
32
+ * DataSchema,
33
+ * FunctionsSchema,
34
+ * AppConnectorsSchema
35
+ * >(config, { salesModel: fabricSemanticModel() });
36
+ * ```
37
+ *
38
+ * @returns The connector runtime hooks for the semantic-model connector.
39
+ */
40
+ export declare function fabricSemanticModel(): ConnectorRuntime;
41
+ //# sourceMappingURL=runtime.d.ts.map
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Runtime companion for the `fabric-semanticmodel` connector marker.
3
+ *
4
+ * Connector markers are type-only, so the binary decode for the `executeQuery`
5
+ * operation is supplied here as a {@link ConnectorRuntime}. Pass the result of
6
+ * {@link fabricSemanticModel} to `ConnectorsRayfinClient` (or
7
+ * `createConnectorsApi`) under the same connector name used in the typed
8
+ * schema so the connectors proxy decodes the Apache Arrow response into a
9
+ * {@link FabricSemanticModelTabularResponse}.
10
+ */
11
+ import { parseArrowStream } from './arrow.js';
12
+ /**
13
+ * Build the runtime hooks for a `fabric-semanticmodel` connector instance.
14
+ *
15
+ * Registers the Apache Arrow decoder for the `executeQuery` operation. JSON
16
+ * responses bypass the decoder, so a worker that still returns JSON keeps
17
+ * working unchanged.
18
+ *
19
+ * @example
20
+ * ```ts
21
+ * import { ConnectorsRayfinClient } from '@microsoft/rayfin-client/experimental';
22
+ * import {
23
+ * fabricSemanticModel,
24
+ * type FabricSemanticModel,
25
+ * } from '@microsoft/rayfin-connector-fabric-semanticmodel';
26
+ *
27
+ * type AppConnectorsSchema = {
28
+ * salesModel: FabricSemanticModel<'executeQuery'>;
29
+ * };
30
+ *
31
+ * const client = new ConnectorsRayfinClient<
32
+ * DataSchema,
33
+ * FunctionsSchema,
34
+ * AppConnectorsSchema
35
+ * >(config, { salesModel: fabricSemanticModel() });
36
+ * ```
37
+ *
38
+ * @returns The connector runtime hooks for the semantic-model connector.
39
+ */
40
+ export function fabricSemanticModel() {
41
+ return {
42
+ operations: {
43
+ executeQuery: {
44
+ decodeBinary: (data) => parseArrowStream(data),
45
+ },
46
+ },
47
+ };
48
+ }
49
+ //# sourceMappingURL=runtime.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@microsoft/rayfin-connector-fabric-semanticmodel",
3
- "version": "1.34.0",
3
+ "version": "1.35.0-alpha.1276",
4
4
  "description": "Typed connector marker for the Fabric semantic-model (Power BI dataset) connector category. Pair with @microsoft/rayfin-client to type `client.connectors.<name>.executeQuery(...)`.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -19,7 +19,9 @@
19
19
  "rimraf": "~6.0.1"
20
20
  },
21
21
  "dependencies": {
22
- "@microsoft/rayfin-connectors": "1.34.0"
22
+ "apache-arrow": "^21.1.0",
23
+ "lz4js": "^0.2.0",
24
+ "@microsoft/rayfin-connectors": "1.35.0-alpha.1276"
23
25
  },
24
26
  "publishConfig": {
25
27
  "registry": "https://npm.pkg.github.com",