@cyanheads/mcp-ts-core 0.8.7 → 0.8.9
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/CLAUDE.md +2 -0
- package/README.md +1 -1
- package/biome.json +1 -1
- package/changelog/0.8.x/0.8.8.md +11 -0
- package/changelog/0.8.x/0.8.9.md +34 -0
- package/dist/canvas/core/CanvasInstance.d.ts +43 -0
- package/dist/canvas/core/CanvasInstance.d.ts.map +1 -0
- package/dist/canvas/core/CanvasInstance.js +63 -0
- package/dist/canvas/core/CanvasInstance.js.map +1 -0
- package/dist/canvas/core/CanvasRegistry.d.ts +96 -0
- package/dist/canvas/core/CanvasRegistry.d.ts.map +1 -0
- package/dist/canvas/core/CanvasRegistry.js +250 -0
- package/dist/canvas/core/CanvasRegistry.js.map +1 -0
- package/dist/canvas/core/DataCanvas.d.ts +49 -0
- package/dist/canvas/core/DataCanvas.d.ts.map +1 -0
- package/dist/canvas/core/DataCanvas.js +85 -0
- package/dist/canvas/core/DataCanvas.js.map +1 -0
- package/dist/canvas/core/IDataCanvasProvider.d.ts +47 -0
- package/dist/canvas/core/IDataCanvasProvider.d.ts.map +1 -0
- package/dist/canvas/core/IDataCanvasProvider.js +10 -0
- package/dist/canvas/core/IDataCanvasProvider.js.map +1 -0
- package/dist/canvas/core/canvasFactory.d.ts +26 -0
- package/dist/canvas/core/canvasFactory.d.ts.map +1 -0
- package/dist/canvas/core/canvasFactory.js +63 -0
- package/dist/canvas/core/canvasFactory.js.map +1 -0
- package/dist/canvas/core/sqlGate.d.ts +107 -0
- package/dist/canvas/core/sqlGate.d.ts.map +1 -0
- package/dist/canvas/core/sqlGate.js +267 -0
- package/dist/canvas/core/sqlGate.js.map +1 -0
- package/dist/canvas/index.d.ts +21 -0
- package/dist/canvas/index.d.ts.map +1 -0
- package/dist/canvas/index.js +19 -0
- package/dist/canvas/index.js.map +1 -0
- package/dist/canvas/providers/duckdb/DuckdbProvider.d.ts +56 -0
- package/dist/canvas/providers/duckdb/DuckdbProvider.d.ts.map +1 -0
- package/dist/canvas/providers/duckdb/DuckdbProvider.js +600 -0
- package/dist/canvas/providers/duckdb/DuckdbProvider.js.map +1 -0
- package/dist/canvas/providers/duckdb/exportWriter.d.ts +48 -0
- package/dist/canvas/providers/duckdb/exportWriter.d.ts.map +1 -0
- package/dist/canvas/providers/duckdb/exportWriter.js +119 -0
- package/dist/canvas/providers/duckdb/exportWriter.js.map +1 -0
- package/dist/canvas/providers/duckdb/schemaSniffer.d.ts +44 -0
- package/dist/canvas/providers/duckdb/schemaSniffer.d.ts.map +1 -0
- package/dist/canvas/providers/duckdb/schemaSniffer.js +134 -0
- package/dist/canvas/providers/duckdb/schemaSniffer.js.map +1 -0
- package/dist/canvas/types.d.ts +134 -0
- package/dist/canvas/types.d.ts.map +1 -0
- package/dist/canvas/types.js +9 -0
- package/dist/canvas/types.js.map +1 -0
- package/dist/config/index.d.ts +51 -15
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +44 -0
- package/dist/config/index.js.map +1 -1
- package/dist/core/app.d.ts +8 -0
- package/dist/core/app.d.ts.map +1 -1
- package/dist/core/app.js +11 -0
- package/dist/core/app.js.map +1 -1
- package/dist/core/worker.d.ts +7 -0
- package/dist/core/worker.d.ts.map +1 -1
- package/dist/core/worker.js +1 -0
- package/dist/core/worker.js.map +1 -1
- package/dist/logs/combined.log +4 -4
- package/dist/logs/error.log +4 -4
- package/dist/storage/core/storageValidation.d.ts.map +1 -1
- package/dist/storage/core/storageValidation.js +7 -1
- package/dist/storage/core/storageValidation.js.map +1 -1
- package/dist/utils/internal/error-handler/errorHandler.d.ts.map +1 -1
- package/dist/utils/internal/error-handler/errorHandler.js +2 -1
- package/dist/utils/internal/error-handler/errorHandler.js.map +1 -1
- package/package.json +18 -8
- package/skills/api-canvas/SKILL.md +260 -0
- package/skills/api-config/SKILL.md +18 -0
- package/skills/api-workers/SKILL.md +6 -0
- package/skills/report-issue-framework/SKILL.md +1 -0
- package/skills/report-issue-local/SKILL.md +2 -0
- package/templates/.github/ISSUE_TEMPLATE/bug_report.yml +1 -0
- package/templates/.github/ISSUE_TEMPLATE/feature_request.yml +1 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview User-facing DataCanvas service. Wraps the provider and registry
|
|
3
|
+
* with debug logging and tenant-id resolution; the registry handles TTL,
|
|
4
|
+
* caps, and provider-keying. Mirrors the StorageService surface pattern but
|
|
5
|
+
* stays OTel-free in v1 (per issue #97 scope).
|
|
6
|
+
* @module src/canvas/core/DataCanvas
|
|
7
|
+
*/
|
|
8
|
+
import { JsonRpcErrorCode, McpError } from '../../types-global/errors.js';
|
|
9
|
+
import { logger } from '../../utils/internal/logger.js';
|
|
10
|
+
import { CanvasInstance } from './CanvasInstance.js';
|
|
11
|
+
/**
|
|
12
|
+
* Resolve the effective tenant ID from the context, throwing when absent.
|
|
13
|
+
* Mirrors `requireTenantId` in StorageService — canvas operations are
|
|
14
|
+
* tenant-scoped by construction.
|
|
15
|
+
*/
|
|
16
|
+
function requireTenantId(context) {
|
|
17
|
+
const tenantId = context.tenantId;
|
|
18
|
+
if (tenantId === undefined || tenantId === null || tenantId === '') {
|
|
19
|
+
throw new McpError(JsonRpcErrorCode.InternalError, 'Tenant ID is required for canvas operations but was not found in the request context.', { operation: context.operation || 'DataCanvas.acquire', requestId: context.requestId });
|
|
20
|
+
}
|
|
21
|
+
return tenantId;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Service entry point for the DataCanvas primitive. Returned from
|
|
25
|
+
* `core.canvas` on `CoreServices` when a canvas provider is configured.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```ts
|
|
29
|
+
* const canvas = await core.canvas!.acquire(input.canvas_id, ctx);
|
|
30
|
+
* await canvas.registerTable('germplasm', rows);
|
|
31
|
+
* const result = await canvas.query('SELECT name FROM germplasm LIMIT 10');
|
|
32
|
+
* return { canvas_id: canvas.canvasId, expires_at: canvas.expiresAt, ... };
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export class DataCanvas {
|
|
36
|
+
provider;
|
|
37
|
+
registry;
|
|
38
|
+
constructor(provider, registry) {
|
|
39
|
+
this.provider = provider;
|
|
40
|
+
this.registry = registry;
|
|
41
|
+
logger.info(`DataCanvas initialized with provider: ${provider.name}`);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Resolve an existing canvas or create a new one. The returned
|
|
45
|
+
* {@link CanvasInstance} captures `(canvasId, tenantId)` so subsequent
|
|
46
|
+
* operations don't need to repeat them.
|
|
47
|
+
*/
|
|
48
|
+
async acquire(maybeId, context, _options) {
|
|
49
|
+
const tenantId = requireTenantId(context);
|
|
50
|
+
const result = await this.registry.acquire(maybeId, tenantId, context);
|
|
51
|
+
logger.debug('Canvas acquired.', {
|
|
52
|
+
...context,
|
|
53
|
+
canvasId: result.canvasId,
|
|
54
|
+
tenantId,
|
|
55
|
+
isNew: result.isNew,
|
|
56
|
+
});
|
|
57
|
+
return new CanvasInstance(result.canvasId, result.tenantId, result.isNew, result.expiresAt, this.registry, this.provider, context);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Drop a canvas explicitly. Returns true when the canvas existed and was
|
|
61
|
+
* destroyed.
|
|
62
|
+
*/
|
|
63
|
+
async drop(canvasId, context) {
|
|
64
|
+
const tenantId = requireTenantId(context);
|
|
65
|
+
return await this.registry.drop(canvasId, tenantId, context);
|
|
66
|
+
}
|
|
67
|
+
/** Active canvas count for the calling tenant. */
|
|
68
|
+
countForTenant(context) {
|
|
69
|
+
const tenantId = requireTenantId(context);
|
|
70
|
+
return this.registry.countForTenant(tenantId);
|
|
71
|
+
}
|
|
72
|
+
/** Liveness check on the underlying provider. */
|
|
73
|
+
healthCheck() {
|
|
74
|
+
return this.provider.healthCheck();
|
|
75
|
+
}
|
|
76
|
+
/** Tear down the registry and provider. Called from `ServerHandle.shutdown()`. */
|
|
77
|
+
async shutdown(context) {
|
|
78
|
+
await this.registry.shutdown(context);
|
|
79
|
+
}
|
|
80
|
+
/** @internal Surface the underlying provider for advanced use cases. */
|
|
81
|
+
getProvider() {
|
|
82
|
+
return this.provider;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=DataCanvas.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DataCanvas.js","sourceRoot":"","sources":["../../../src/canvas/core/DataCanvas.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AACtE,OAAO,EAAE,MAAM,EAAE,MAAM,4BAA4B,CAAC;AAGpD,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAIrD;;;;GAIG;AACH,SAAS,eAAe,CAAC,OAAuB;IAC9C,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC;IAClC,IAAI,QAAQ,KAAK,SAAS,IAAI,QAAQ,KAAK,IAAI,IAAI,QAAQ,KAAK,EAAE,EAAE,CAAC;QACnE,MAAM,IAAI,QAAQ,CAChB,gBAAgB,CAAC,aAAa,EAC9B,uFAAuF,EACvF,EAAE,SAAS,EAAE,OAAO,CAAC,SAAS,IAAI,oBAAoB,EAAE,SAAS,EAAE,OAAO,CAAC,SAAS,EAAE,CACvF,CAAC;IACJ,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,OAAO,UAAU;IAEF;IACA;IAFnB,YACmB,QAA6B,EAC7B,QAAwB;QADxB,aAAQ,GAAR,QAAQ,CAAqB;QAC7B,aAAQ,GAAR,QAAQ,CAAgB;QAEzC,MAAM,CAAC,IAAI,CAAC,yCAAyC,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC;IACxE,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,OAAO,CACX,OAA2B,EAC3B,OAAuB,EACvB,QAAyB;QAEzB,MAAM,QAAQ,GAAG,eAAe,CAAC,OAAO,CAAC,CAAC;QAC1C,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,OAAO,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAC;QACvE,MAAM,CAAC,KAAK,CAAC,kBAAkB,EAAE;YAC/B,GAAG,OAAO;YACV,QAAQ,EAAE,MAAM,CAAC,QAAQ;YACzB,QAAQ;YACR,KAAK,EAAE,MAAM,CAAC,KAAK;SACpB,CAAC,CAAC;QACH,OAAO,IAAI,cAAc,CACvB,MAAM,CAAC,QAAQ,EACf,MAAM,CAAC,QAAQ,EACf,MAAM,CAAC,KAAK,EACZ,MAAM,CAAC,SAAS,EAChB,IAAI,CAAC,QAAQ,EACb,IAAI,CAAC,QAAQ,EACb,OAAO,CACR,CAAC;IACJ,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,IAAI,CAAC,QAAgB,EAAE,OAAuB;QAClD,MAAM,QAAQ,GAAG,eAAe,CAAC,OAAO,CAAC,CAAC;QAC1C,OAAO,MAAM,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,QAAQ,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAC;IAC/D,CAAC;IAED,kDAAkD;IAClD,cAAc,CAAC,OAAuB;QACpC,MAAM,QAAQ,GAAG,eAAe,CAAC,OAAO,CAAC,CAAC;QAC1C,OAAO,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,QAAQ,CAAC,CAAC;IAChD,CAAC;IAED,iDAAiD;IACjD,WAAW;QACT,OAAO,IAAI,CAAC,QAAQ,CAAC,WAAW,EAAE,CAAC;IACrC,CAAC;IAED,kFAAkF;IAClF,KAAK,CAAC,QAAQ,CAAC,OAAuB;QACpC,MAAM,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;IACxC,CAAC;IAED,wEAAwE;IACxE,WAAW;QACT,OAAO,IAAI,CAAC,QAAQ,CAAC;IACvB,CAAC;CACF"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Engine-level provider interface for the DataCanvas primitive.
|
|
3
|
+
* Implementations own the physical resources backing each canvas (e.g. one
|
|
4
|
+
* DuckDB instance per canvasId) and expose register/query/export/describe
|
|
5
|
+
* operations keyed by canvasId. The lifecycle wrapper ({@link CanvasRegistry})
|
|
6
|
+
* keys these calls by `(tenantId, canvasId)` and enforces TTL/caps.
|
|
7
|
+
* @module src/canvas/core/IDataCanvasProvider
|
|
8
|
+
*/
|
|
9
|
+
import type { RequestContext } from '../../utils/internal/requestContext.js';
|
|
10
|
+
import type { DescribeOptions, ExportOptions, ExportResult, ExportTarget, QueryOptions, QueryResult, RegisterRows, RegisterTableOptions, RegisterTableResult, TableInfo } from '../types.js';
|
|
11
|
+
/**
|
|
12
|
+
* Engine-level contract. The lifecycle wrapper guarantees `canvasId` is
|
|
13
|
+
* already validated and authorized for the caller's tenant before any of
|
|
14
|
+
* these methods are invoked, so providers may treat `canvasId` as opaque
|
|
15
|
+
* and trusted.
|
|
16
|
+
*/
|
|
17
|
+
export interface IDataCanvasProvider {
|
|
18
|
+
/** Drop every table on the canvas. Returns the number dropped. */
|
|
19
|
+
clear(canvasId: string, context: RequestContext): Promise<number>;
|
|
20
|
+
/** Describe one or all tables on the canvas. */
|
|
21
|
+
describe(canvasId: string, context: RequestContext, options?: DescribeOptions): Promise<TableInfo[]>;
|
|
22
|
+
/**
|
|
23
|
+
* Release engine resources for a canvas. After this call, further ops on
|
|
24
|
+
* the canvas throw `NotFound`.
|
|
25
|
+
*/
|
|
26
|
+
destroyCanvas(canvasId: string, context: RequestContext): Promise<void>;
|
|
27
|
+
/** Drop a single canvas table. Returns `true` when found and removed. */
|
|
28
|
+
drop(canvasId: string, name: string, context: RequestContext): Promise<boolean>;
|
|
29
|
+
/** Export a canvas table to a file or stream target. */
|
|
30
|
+
export(canvasId: string, tableName: string, target: ExportTarget, context: RequestContext, options?: ExportOptions): Promise<ExportResult>;
|
|
31
|
+
/** Liveness check on the underlying engine. */
|
|
32
|
+
healthCheck(): Promise<boolean>;
|
|
33
|
+
/**
|
|
34
|
+
* Allocate engine resources for a new canvas. Idempotent — calling twice
|
|
35
|
+
* with the same id is a no-op.
|
|
36
|
+
*/
|
|
37
|
+
initCanvas(canvasId: string, context: RequestContext): Promise<void>;
|
|
38
|
+
/** Provider name (e.g. `'duckdb'`). Used in logs and health output. */
|
|
39
|
+
readonly name: string;
|
|
40
|
+
/** Run a SQL query against the canvas. Read-only enforcement is the gate's job. */
|
|
41
|
+
query(canvasId: string, sql: string, context: RequestContext, options?: QueryOptions): Promise<QueryResult>;
|
|
42
|
+
/** Register a table on the canvas from in-memory or async iterator rows. */
|
|
43
|
+
registerTable(canvasId: string, name: string, rows: RegisterRows, context: RequestContext, options?: RegisterTableOptions): Promise<RegisterTableResult>;
|
|
44
|
+
/** Tear down all engine resources. Called from `ServerHandle.shutdown()`. */
|
|
45
|
+
shutdown(): Promise<void>;
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=IDataCanvasProvider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"IDataCanvasProvider.d.ts","sourceRoot":"","sources":["../../../src/canvas/core/IDataCanvasProvider.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAC;AACzE,OAAO,KAAK,EACV,eAAe,EACf,aAAa,EACb,YAAY,EACZ,YAAY,EACZ,YAAY,EACZ,WAAW,EACX,YAAY,EACZ,oBAAoB,EACpB,mBAAmB,EACnB,SAAS,EACV,MAAM,aAAa,CAAC;AAErB;;;;;GAKG;AACH,MAAM,WAAW,mBAAmB;IAClC,kEAAkE;IAClE,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAElE,gDAAgD;IAChD,QAAQ,CACN,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,cAAc,EACvB,OAAO,CAAC,EAAE,eAAe,GACxB,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC;IAExB;;;OAGG;IACH,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAExE,yEAAyE;IACzE,IAAI,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAEhF,wDAAwD;IACxD,MAAM,CACJ,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,YAAY,EACpB,OAAO,EAAE,cAAc,EACvB,OAAO,CAAC,EAAE,aAAa,GACtB,OAAO,CAAC,YAAY,CAAC,CAAC;IAEzB,+CAA+C;IAC/C,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;IAEhC;;;OAGG;IACH,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrE,uEAAuE;IACvE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAEtB,mFAAmF;IACnF,KAAK,CACH,QAAQ,EAAE,MAAM,EAChB,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,cAAc,EACvB,OAAO,CAAC,EAAE,YAAY,GACrB,OAAO,CAAC,WAAW,CAAC,CAAC;IAExB,4EAA4E;IAC5E,aAAa,CACX,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,YAAY,EAClB,OAAO,EAAE,cAAc,EACvB,OAAO,CAAC,EAAE,oBAAoB,GAC7B,OAAO,CAAC,mBAAmB,CAAC,CAAC;IAEhC,6EAA6E;IAC7E,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3B"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Engine-level provider interface for the DataCanvas primitive.
|
|
3
|
+
* Implementations own the physical resources backing each canvas (e.g. one
|
|
4
|
+
* DuckDB instance per canvasId) and expose register/query/export/describe
|
|
5
|
+
* operations keyed by canvasId. The lifecycle wrapper ({@link CanvasRegistry})
|
|
6
|
+
* keys these calls by `(tenantId, canvasId)` and enforces TTL/caps.
|
|
7
|
+
* @module src/canvas/core/IDataCanvasProvider
|
|
8
|
+
*/
|
|
9
|
+
export {};
|
|
10
|
+
//# sourceMappingURL=IDataCanvasProvider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"IDataCanvasProvider.js","sourceRoot":"","sources":["../../../src/canvas/core/IDataCanvasProvider.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Factory for the optional DataCanvas service. Mirrors the
|
|
3
|
+
* pattern in `createStorageProvider` — switches on `config.canvas.providerType`,
|
|
4
|
+
* fails closed in serverless environments for `duckdb`, and returns
|
|
5
|
+
* `undefined` when canvas is disabled (`'none'`).
|
|
6
|
+
*
|
|
7
|
+
* The factory does NOT eager-load `@duckdb/node-api`; the provider lazy-loads
|
|
8
|
+
* on first use via `lazyImport`. Servers that set `CANVAS_PROVIDER_TYPE=none`
|
|
9
|
+
* (the default) pay zero install cost.
|
|
10
|
+
*
|
|
11
|
+
* @module src/canvas/core/canvasFactory
|
|
12
|
+
*/
|
|
13
|
+
import type { AppConfig } from '../../config/index.js';
|
|
14
|
+
import { DataCanvas } from './DataCanvas.js';
|
|
15
|
+
/**
|
|
16
|
+
* Construct the canvas service from configuration. Returns `undefined` when
|
|
17
|
+
* canvas is disabled (`CANVAS_PROVIDER_TYPE=none`), keeping `core.canvas`
|
|
18
|
+
* `undefined` on `CoreServices` for that case.
|
|
19
|
+
*
|
|
20
|
+
* In serverless environments, an explicit `CANVAS_PROVIDER_TYPE=duckdb`
|
|
21
|
+
* fails closed with a clear ConfigurationError — DuckDB has no V8-isolate
|
|
22
|
+
* build, so canvas is unavailable on Workers. (Refinement #1 in issue #97 is
|
|
23
|
+
* about export-path sandboxing, separate from this Worker fail-closed.)
|
|
24
|
+
*/
|
|
25
|
+
export declare function createCanvasService(config: AppConfig): DataCanvas | undefined;
|
|
26
|
+
//# sourceMappingURL=canvasFactory.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"canvasFactory.d.ts","sourceRoot":"","sources":["../../../src/canvas/core/canvasFactory.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAOnD,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAO7C;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,SAAS,GAAG,UAAU,GAAG,SAAS,CAkC7E"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Factory for the optional DataCanvas service. Mirrors the
|
|
3
|
+
* pattern in `createStorageProvider` — switches on `config.canvas.providerType`,
|
|
4
|
+
* fails closed in serverless environments for `duckdb`, and returns
|
|
5
|
+
* `undefined` when canvas is disabled (`'none'`).
|
|
6
|
+
*
|
|
7
|
+
* The factory does NOT eager-load `@duckdb/node-api`; the provider lazy-loads
|
|
8
|
+
* on first use via `lazyImport`. Servers that set `CANVAS_PROVIDER_TYPE=none`
|
|
9
|
+
* (the default) pay zero install cost.
|
|
10
|
+
*
|
|
11
|
+
* @module src/canvas/core/canvasFactory
|
|
12
|
+
*/
|
|
13
|
+
import { configurationError } from '../../types-global/errors.js';
|
|
14
|
+
import { logger } from '../../utils/internal/logger.js';
|
|
15
|
+
import { requestContextService } from '../../utils/internal/requestContext.js';
|
|
16
|
+
import { DuckdbProvider } from '../providers/duckdb/DuckdbProvider.js';
|
|
17
|
+
import { CanvasRegistry } from './CanvasRegistry.js';
|
|
18
|
+
import { DataCanvas } from './DataCanvas.js';
|
|
19
|
+
/** Evaluated at call time so worker.ts can set IS_SERVERLESS before first use. */
|
|
20
|
+
function isServerless() {
|
|
21
|
+
return typeof process === 'undefined' || process.env.IS_SERVERLESS === 'true';
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Construct the canvas service from configuration. Returns `undefined` when
|
|
25
|
+
* canvas is disabled (`CANVAS_PROVIDER_TYPE=none`), keeping `core.canvas`
|
|
26
|
+
* `undefined` on `CoreServices` for that case.
|
|
27
|
+
*
|
|
28
|
+
* In serverless environments, an explicit `CANVAS_PROVIDER_TYPE=duckdb`
|
|
29
|
+
* fails closed with a clear ConfigurationError — DuckDB has no V8-isolate
|
|
30
|
+
* build, so canvas is unavailable on Workers. (Refinement #1 in issue #97 is
|
|
31
|
+
* about export-path sandboxing, separate from this Worker fail-closed.)
|
|
32
|
+
*/
|
|
33
|
+
export function createCanvasService(config) {
|
|
34
|
+
const providerType = config.canvas.providerType;
|
|
35
|
+
if (providerType === 'none')
|
|
36
|
+
return;
|
|
37
|
+
const context = requestContextService.createRequestContext({
|
|
38
|
+
operation: 'createCanvasService',
|
|
39
|
+
});
|
|
40
|
+
if (providerType === 'duckdb') {
|
|
41
|
+
if (isServerless()) {
|
|
42
|
+
throw configurationError('DuckDB canvas requires Node.js or Bun. Set CANVAS_PROVIDER_TYPE=none or omit it for Cloudflare Workers deployment.', context);
|
|
43
|
+
}
|
|
44
|
+
logger.info('Creating DuckDB canvas provider', context);
|
|
45
|
+
const provider = new DuckdbProvider({
|
|
46
|
+
memoryLimitMb: config.canvas.defaultMemoryLimitMb,
|
|
47
|
+
exportRootPath: config.canvas.exportRootPath,
|
|
48
|
+
defaultRowLimit: config.canvas.defaultRowLimit,
|
|
49
|
+
schemaSniffRows: config.canvas.schemaSniffRows,
|
|
50
|
+
});
|
|
51
|
+
const registry = new CanvasRegistry(provider, {
|
|
52
|
+
ttlMs: config.canvas.ttlMs,
|
|
53
|
+
absoluteCapMs: config.canvas.absoluteCapMs,
|
|
54
|
+
maxCanvasesPerTenant: config.canvas.maxCanvasesPerTenant,
|
|
55
|
+
sweeperIntervalMs: config.canvas.sweeperIntervalMs,
|
|
56
|
+
});
|
|
57
|
+
return new DataCanvas(provider, registry);
|
|
58
|
+
}
|
|
59
|
+
// Exhaustive check for the providerType union.
|
|
60
|
+
const exhaustive = providerType;
|
|
61
|
+
throw configurationError(`Unhandled canvas provider type: ${String(exhaustive)}`, context);
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=canvasFactory.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"canvasFactory.js","sourceRoot":"","sources":["../../../src/canvas/core/canvasFactory.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAC9D,OAAO,EAAE,MAAM,EAAE,MAAM,4BAA4B,CAAC;AACpD,OAAO,EAAE,qBAAqB,EAAE,MAAM,oCAAoC,CAAC;AAE3E,OAAO,EAAE,cAAc,EAAE,MAAM,uCAAuC,CAAC;AACvE,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAE7C,kFAAkF;AAClF,SAAS,YAAY;IACnB,OAAO,OAAO,OAAO,KAAK,WAAW,IAAI,OAAO,CAAC,GAAG,CAAC,aAAa,KAAK,MAAM,CAAC;AAChF,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,mBAAmB,CAAC,MAAiB;IACnD,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,CAAC,YAAY,CAAC;IAChD,IAAI,YAAY,KAAK,MAAM;QAAE,OAAO;IAEpC,MAAM,OAAO,GAAG,qBAAqB,CAAC,oBAAoB,CAAC;QACzD,SAAS,EAAE,qBAAqB;KACjC,CAAC,CAAC;IAEH,IAAI,YAAY,KAAK,QAAQ,EAAE,CAAC;QAC9B,IAAI,YAAY,EAAE,EAAE,CAAC;YACnB,MAAM,kBAAkB,CACtB,oHAAoH,EACpH,OAAO,CACR,CAAC;QACJ,CAAC;QACD,MAAM,CAAC,IAAI,CAAC,iCAAiC,EAAE,OAAO,CAAC,CAAC;QACxD,MAAM,QAAQ,GAAG,IAAI,cAAc,CAAC;YAClC,aAAa,EAAE,MAAM,CAAC,MAAM,CAAC,oBAAoB;YACjD,cAAc,EAAE,MAAM,CAAC,MAAM,CAAC,cAAc;YAC5C,eAAe,EAAE,MAAM,CAAC,MAAM,CAAC,eAAe;YAC9C,eAAe,EAAE,MAAM,CAAC,MAAM,CAAC,eAAe;SAC/C,CAAC,CAAC;QACH,MAAM,QAAQ,GAAG,IAAI,cAAc,CAAC,QAAQ,EAAE;YAC5C,KAAK,EAAE,MAAM,CAAC,MAAM,CAAC,KAAK;YAC1B,aAAa,EAAE,MAAM,CAAC,MAAM,CAAC,aAAa;YAC1C,oBAAoB,EAAE,MAAM,CAAC,MAAM,CAAC,oBAAoB;YACxD,iBAAiB,EAAE,MAAM,CAAC,MAAM,CAAC,iBAAiB;SACnD,CAAC,CAAC;QACH,OAAO,IAAI,UAAU,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;IAC5C,CAAC;IAED,+CAA+C;IAC/C,MAAM,UAAU,GAAU,YAAY,CAAC;IACvC,MAAM,kBAAkB,CAAC,mCAAmC,MAAM,CAAC,UAAU,CAAC,EAAE,EAAE,OAAO,CAAC,CAAC;AAC7F,CAAC"}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Read-only SQL gate for the DataCanvas primitive. Engine-agnostic
|
|
3
|
+
* pure validators that the provider invokes after pulling DuckDB-specific
|
|
4
|
+
* metadata (statement extraction, prepared-statement type, EXPLAIN plan JSON).
|
|
5
|
+
*
|
|
6
|
+
* Three layers of enforcement, each authoritative on its own:
|
|
7
|
+
*
|
|
8
|
+
* 1. **Single-statement check.** The provider parses the input via DuckDB's
|
|
9
|
+
* `extractStatements` and passes the count here. Anything other than 1 is
|
|
10
|
+
* rejected — comment-hidden second statements, multi-statement smuggling,
|
|
11
|
+
* and Unicode tricks all collapse here because DuckDB's parser is the
|
|
12
|
+
* arbiter, not a regex.
|
|
13
|
+
* 2. **Statement-type check.** The provider prepares the single statement and
|
|
14
|
+
* passes the resulting `statementType`. We require `SELECT`. Any DDL, DML,
|
|
15
|
+
* or utility (PRAGMA/ATTACH/COPY/INSTALL/LOAD/SET/EXECUTE) fails to type as
|
|
16
|
+
* SELECT and is rejected here.
|
|
17
|
+
* 3. **Plan-walk allowlist.** The provider runs `EXPLAIN (FORMAT JSON)` and
|
|
18
|
+
* passes the plan JSON. We walk every node and reject if any operator
|
|
19
|
+
* name is outside the curated allowlist — defense-in-depth against future
|
|
20
|
+
* DuckDB additions that might smuggle work into a SELECT envelope.
|
|
21
|
+
*
|
|
22
|
+
* Rejection paths throw `ValidationError` with a structured `data.reason`
|
|
23
|
+
* suitable for surfacing to the agent.
|
|
24
|
+
*
|
|
25
|
+
* @module src/canvas/core/sqlGate
|
|
26
|
+
*/
|
|
27
|
+
/**
|
|
28
|
+
* DuckDB statement-type strings emitted by the Neo client. Only `SELECT` is
|
|
29
|
+
* accepted by the canvas. Other values (`INSERT`, `UPDATE`, `DELETE`,
|
|
30
|
+
* `CREATE`, `DROP`, `ALTER`, `COPY`, `PRAGMA`, `ATTACH`, `DETACH`, `LOAD`,
|
|
31
|
+
* `INSTALL`, `SET`, `RESET`, `EXECUTE`, `EXPLAIN`, `TRANSACTION`, `VACUUM`,
|
|
32
|
+
* `CHECKPOINT`, `CALL`, …) are rejected outright. This is a surface — not
|
|
33
|
+
* an enum we own — so the gate matches on the literal string emitted.
|
|
34
|
+
*/
|
|
35
|
+
export type DuckdbStatementType = string;
|
|
36
|
+
/** Subset of statement types the gate permits. */
|
|
37
|
+
export declare const ALLOWED_STATEMENT_TYPES: ReadonlySet<DuckdbStatementType>;
|
|
38
|
+
/**
|
|
39
|
+
* Curated allowlist of operator names that can appear in an EXPLAIN plan.
|
|
40
|
+
* Sourced from DuckDB's logical/physical-plan node families (1.5.x). Not
|
|
41
|
+
* every member is reachable from a SELECT — but every member is read-only.
|
|
42
|
+
*
|
|
43
|
+
* Pinned by `tests/canvas/sqlGate.fixtures.test.ts` against live DuckDB
|
|
44
|
+
* EXPLAIN output so version bumps that add operators are caught in CI rather
|
|
45
|
+
* than silently widening the gate.
|
|
46
|
+
*
|
|
47
|
+
* Operators **not** in this list cause rejection. Notable exclusions:
|
|
48
|
+
*
|
|
49
|
+
* - `READ_CSV`, `READ_PARQUET`, `READ_JSON` — bypass canvas, read external files.
|
|
50
|
+
* - `INSERT`, `UPDATE`, `DELETE`, `MERGE`, `CREATE_*`, `DROP_*`, `ALTER_*` — writes.
|
|
51
|
+
* - `COPY_TO_FILE`, `BATCH_COPY_TO_FILE` — exports a SELECT to a file.
|
|
52
|
+
* - `ATTACH`, `DETACH`, `LOAD`, `INSTALL`, `PRAGMA`, `SET`, `RESET` — utility.
|
|
53
|
+
*/
|
|
54
|
+
export declare const ALLOWED_PLAN_OPERATORS: ReadonlySet<string>;
|
|
55
|
+
/**
|
|
56
|
+
* Public entry point — validates the trio of `(statementCount, statementType,
|
|
57
|
+
* planJson)`. Throws on the first violation, leaving the provider to pass
|
|
58
|
+
* results back to the caller untouched on success.
|
|
59
|
+
*/
|
|
60
|
+
export declare function assertReadOnlyQuery(input: {
|
|
61
|
+
/** Number of statements DuckDB extracted from the user-supplied SQL. */
|
|
62
|
+
statementCount: number;
|
|
63
|
+
/** DuckDB statement type for the (single) prepared statement. */
|
|
64
|
+
statementType: DuckdbStatementType;
|
|
65
|
+
/** Parsed `EXPLAIN (FORMAT JSON)` payload. */
|
|
66
|
+
planJson: unknown;
|
|
67
|
+
}): void;
|
|
68
|
+
/**
|
|
69
|
+
* Pre-EXPLAIN gate: validate statement count and type. Run before the EXPLAIN
|
|
70
|
+
* call so non-SELECT statements (which DuckDB's EXPLAIN can't always wrap —
|
|
71
|
+
* e.g. ATTACH/PRAGMA/COPY/INSTALL) fail with a structured ValidationError
|
|
72
|
+
* here rather than a confusing parser error from EXPLAIN itself.
|
|
73
|
+
*/
|
|
74
|
+
export declare function assertSelectOnly(input: {
|
|
75
|
+
statementCount: number;
|
|
76
|
+
statementType: DuckdbStatementType;
|
|
77
|
+
}): void;
|
|
78
|
+
/**
|
|
79
|
+
* Post-EXPLAIN gate: walk the plan tree and reject any operator outside the
|
|
80
|
+
* curated allowlist. Defense-in-depth against future DuckDB additions that
|
|
81
|
+
* smuggle work into a SELECT envelope.
|
|
82
|
+
*/
|
|
83
|
+
export declare function assertPlanReadOnly(planJson: unknown): void;
|
|
84
|
+
/**
|
|
85
|
+
* Walks the EXPLAIN plan and returns the set of operator names not in
|
|
86
|
+
* `ALLOWED_PLAN_OPERATORS`. Exported for fixture-driven tests that want
|
|
87
|
+
* to inspect the gate's view of a plan without throwing.
|
|
88
|
+
*
|
|
89
|
+
* Tolerant of structural variation — DuckDB emits operator identity under
|
|
90
|
+
* either `name` (logical plan) or `operator_type` (physical/profile plan)
|
|
91
|
+
* depending on the EXPLAIN flavor. We honor both. Children traversal
|
|
92
|
+
* supports `children`, `child`, and `inputs` arrays.
|
|
93
|
+
*/
|
|
94
|
+
export declare function collectDisallowedOperators(planJson: unknown): Set<string>;
|
|
95
|
+
/**
|
|
96
|
+
* Validate an identifier for use as a canvas-local table or column name.
|
|
97
|
+
* Throws `ValidationError` on rejection.
|
|
98
|
+
*/
|
|
99
|
+
export declare function assertValidIdentifier(value: string, kind: 'table' | 'column'): void;
|
|
100
|
+
/**
|
|
101
|
+
* Wrap an identifier in double quotes for safe inclusion in SQL. Internal
|
|
102
|
+
* double quotes are doubled per the SQL standard. Callers should still
|
|
103
|
+
* validate via {@link assertValidIdentifier} before quoting — this helper
|
|
104
|
+
* only escapes; it does not validate shape.
|
|
105
|
+
*/
|
|
106
|
+
export declare function quoteIdentifier(value: string): string;
|
|
107
|
+
//# sourceMappingURL=sqlGate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sqlGate.d.ts","sourceRoot":"","sources":["../../../src/canvas/core/sqlGate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAIH;;;;;;;GAOG;AACH,MAAM,MAAM,mBAAmB,GAAG,MAAM,CAAC;AAEzC,kDAAkD;AAClD,eAAO,MAAM,uBAAuB,EAAE,WAAW,CAAC,mBAAmB,CAAuB,CAAC;AAE7F;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,sBAAsB,EAAE,WAAW,CAAC,MAAM,CAwDrD,CAAC;AAEH;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE;IACzC,wEAAwE;IACxE,cAAc,EAAE,MAAM,CAAC;IACvB,iEAAiE;IACjE,aAAa,EAAE,mBAAmB,CAAC;IACnC,8CAA8C;IAC9C,QAAQ,EAAE,OAAO,CAAC;CACnB,GAAG,IAAI,CAGP;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE;IACtC,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,mBAAmB,CAAC;CACpC,GAAG,IAAI,CAaP;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,OAAO,GAAG,IAAI,CAW1D;AAED;;;;;;;;;GASG;AACH,wBAAgB,0BAA0B,CAAC,QAAQ,EAAE,OAAO,GAAG,GAAG,CAAC,MAAM,CAAC,CAIzE;AAsFD;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,QAAQ,GAAG,IAAI,CAmBnF;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAErD"}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Read-only SQL gate for the DataCanvas primitive. Engine-agnostic
|
|
3
|
+
* pure validators that the provider invokes after pulling DuckDB-specific
|
|
4
|
+
* metadata (statement extraction, prepared-statement type, EXPLAIN plan JSON).
|
|
5
|
+
*
|
|
6
|
+
* Three layers of enforcement, each authoritative on its own:
|
|
7
|
+
*
|
|
8
|
+
* 1. **Single-statement check.** The provider parses the input via DuckDB's
|
|
9
|
+
* `extractStatements` and passes the count here. Anything other than 1 is
|
|
10
|
+
* rejected — comment-hidden second statements, multi-statement smuggling,
|
|
11
|
+
* and Unicode tricks all collapse here because DuckDB's parser is the
|
|
12
|
+
* arbiter, not a regex.
|
|
13
|
+
* 2. **Statement-type check.** The provider prepares the single statement and
|
|
14
|
+
* passes the resulting `statementType`. We require `SELECT`. Any DDL, DML,
|
|
15
|
+
* or utility (PRAGMA/ATTACH/COPY/INSTALL/LOAD/SET/EXECUTE) fails to type as
|
|
16
|
+
* SELECT and is rejected here.
|
|
17
|
+
* 3. **Plan-walk allowlist.** The provider runs `EXPLAIN (FORMAT JSON)` and
|
|
18
|
+
* passes the plan JSON. We walk every node and reject if any operator
|
|
19
|
+
* name is outside the curated allowlist — defense-in-depth against future
|
|
20
|
+
* DuckDB additions that might smuggle work into a SELECT envelope.
|
|
21
|
+
*
|
|
22
|
+
* Rejection paths throw `ValidationError` with a structured `data.reason`
|
|
23
|
+
* suitable for surfacing to the agent.
|
|
24
|
+
*
|
|
25
|
+
* @module src/canvas/core/sqlGate
|
|
26
|
+
*/
|
|
27
|
+
import { validationError } from '../../types-global/errors.js';
|
|
28
|
+
/** Subset of statement types the gate permits. */
|
|
29
|
+
export const ALLOWED_STATEMENT_TYPES = new Set(['SELECT']);
|
|
30
|
+
/**
|
|
31
|
+
* Curated allowlist of operator names that can appear in an EXPLAIN plan.
|
|
32
|
+
* Sourced from DuckDB's logical/physical-plan node families (1.5.x). Not
|
|
33
|
+
* every member is reachable from a SELECT — but every member is read-only.
|
|
34
|
+
*
|
|
35
|
+
* Pinned by `tests/canvas/sqlGate.fixtures.test.ts` against live DuckDB
|
|
36
|
+
* EXPLAIN output so version bumps that add operators are caught in CI rather
|
|
37
|
+
* than silently widening the gate.
|
|
38
|
+
*
|
|
39
|
+
* Operators **not** in this list cause rejection. Notable exclusions:
|
|
40
|
+
*
|
|
41
|
+
* - `READ_CSV`, `READ_PARQUET`, `READ_JSON` — bypass canvas, read external files.
|
|
42
|
+
* - `INSERT`, `UPDATE`, `DELETE`, `MERGE`, `CREATE_*`, `DROP_*`, `ALTER_*` — writes.
|
|
43
|
+
* - `COPY_TO_FILE`, `BATCH_COPY_TO_FILE` — exports a SELECT to a file.
|
|
44
|
+
* - `ATTACH`, `DETACH`, `LOAD`, `INSTALL`, `PRAGMA`, `SET`, `RESET` — utility.
|
|
45
|
+
*/
|
|
46
|
+
export const ALLOWED_PLAN_OPERATORS = new Set([
|
|
47
|
+
// Scans (registered tables only — file scans like READ_CSV are excluded)
|
|
48
|
+
'SEQ_SCAN',
|
|
49
|
+
'COLUMN_DATA_SCAN',
|
|
50
|
+
'CHUNK_SCAN',
|
|
51
|
+
'EXPRESSION_SCAN',
|
|
52
|
+
'DUMMY_SCAN',
|
|
53
|
+
'EMPTY_RESULT',
|
|
54
|
+
'IN_MEMORY_TABLE_SCAN',
|
|
55
|
+
// Projection / filter
|
|
56
|
+
'PROJECTION',
|
|
57
|
+
'FILTER',
|
|
58
|
+
// Joins
|
|
59
|
+
'HASH_JOIN',
|
|
60
|
+
'NESTED_LOOP_JOIN',
|
|
61
|
+
'BLOCKWISE_NL_JOIN',
|
|
62
|
+
'IE_JOIN',
|
|
63
|
+
'PIECEWISE_MERGE_JOIN',
|
|
64
|
+
'CROSS_PRODUCT',
|
|
65
|
+
'POSITIONAL_JOIN',
|
|
66
|
+
'ASOF_JOIN',
|
|
67
|
+
'DELIM_JOIN',
|
|
68
|
+
// Aggregates
|
|
69
|
+
'HASH_GROUP_BY',
|
|
70
|
+
'PERFECT_HASH_GROUP_BY',
|
|
71
|
+
'UNGROUPED_AGGREGATE',
|
|
72
|
+
'SIMPLE_AGGREGATE',
|
|
73
|
+
'PARTITIONED_AGGREGATE',
|
|
74
|
+
// Distinct / set ops
|
|
75
|
+
'HASH_DISTINCT',
|
|
76
|
+
'DISTINCT',
|
|
77
|
+
'UNION',
|
|
78
|
+
// Sorting / limits
|
|
79
|
+
'ORDER_BY',
|
|
80
|
+
'TOP_N',
|
|
81
|
+
'LIMIT',
|
|
82
|
+
'LIMIT_PERCENT',
|
|
83
|
+
'STREAMING_LIMIT',
|
|
84
|
+
// Window
|
|
85
|
+
'WINDOW',
|
|
86
|
+
// Nested
|
|
87
|
+
'UNNEST',
|
|
88
|
+
// CTEs
|
|
89
|
+
'CTE',
|
|
90
|
+
'CTE_REF',
|
|
91
|
+
'RECURSIVE_CTE',
|
|
92
|
+
'MATERIALIZED_CTE',
|
|
93
|
+
// Sampling
|
|
94
|
+
'RESERVOIR_SAMPLE',
|
|
95
|
+
'SAMPLE',
|
|
96
|
+
'STREAMING_SAMPLE',
|
|
97
|
+
// Result framing
|
|
98
|
+
'RESULT_COLLECTOR',
|
|
99
|
+
'EXPLAIN',
|
|
100
|
+
// Pivot/unpivot collapsed planner forms
|
|
101
|
+
'PIVOT',
|
|
102
|
+
]);
|
|
103
|
+
/**
|
|
104
|
+
* Public entry point — validates the trio of `(statementCount, statementType,
|
|
105
|
+
* planJson)`. Throws on the first violation, leaving the provider to pass
|
|
106
|
+
* results back to the caller untouched on success.
|
|
107
|
+
*/
|
|
108
|
+
export function assertReadOnlyQuery(input) {
|
|
109
|
+
assertSelectOnly(input);
|
|
110
|
+
assertPlanReadOnly(input.planJson);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Pre-EXPLAIN gate: validate statement count and type. Run before the EXPLAIN
|
|
114
|
+
* call so non-SELECT statements (which DuckDB's EXPLAIN can't always wrap —
|
|
115
|
+
* e.g. ATTACH/PRAGMA/COPY/INSTALL) fail with a structured ValidationError
|
|
116
|
+
* here rather than a confusing parser error from EXPLAIN itself.
|
|
117
|
+
*/
|
|
118
|
+
export function assertSelectOnly(input) {
|
|
119
|
+
if (input.statementCount !== 1) {
|
|
120
|
+
throw validationError('Canvas query must contain exactly one SQL statement.', {
|
|
121
|
+
reason: 'multi_statement',
|
|
122
|
+
statementCount: input.statementCount,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
if (!ALLOWED_STATEMENT_TYPES.has(input.statementType)) {
|
|
126
|
+
throw validationError(`Canvas query must be SELECT; got ${input.statementType}. Mutations must use registerTable, drop, or clear.`, { reason: 'non_select_statement', statementType: input.statementType });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Post-EXPLAIN gate: walk the plan tree and reject any operator outside the
|
|
131
|
+
* curated allowlist. Defense-in-depth against future DuckDB additions that
|
|
132
|
+
* smuggle work into a SELECT envelope.
|
|
133
|
+
*/
|
|
134
|
+
export function assertPlanReadOnly(planJson) {
|
|
135
|
+
const offending = collectDisallowedOperators(planJson);
|
|
136
|
+
if (offending.size > 0) {
|
|
137
|
+
throw validationError(`Canvas query contains disallowed operators: ${[...offending].sort().join(', ')}.`, {
|
|
138
|
+
reason: 'plan_operator_not_allowed',
|
|
139
|
+
operators: [...offending].sort(),
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Walks the EXPLAIN plan and returns the set of operator names not in
|
|
145
|
+
* `ALLOWED_PLAN_OPERATORS`. Exported for fixture-driven tests that want
|
|
146
|
+
* to inspect the gate's view of a plan without throwing.
|
|
147
|
+
*
|
|
148
|
+
* Tolerant of structural variation — DuckDB emits operator identity under
|
|
149
|
+
* either `name` (logical plan) or `operator_type` (physical/profile plan)
|
|
150
|
+
* depending on the EXPLAIN flavor. We honor both. Children traversal
|
|
151
|
+
* supports `children`, `child`, and `inputs` arrays.
|
|
152
|
+
*/
|
|
153
|
+
export function collectDisallowedOperators(planJson) {
|
|
154
|
+
const offending = new Set();
|
|
155
|
+
walk(planJson, offending);
|
|
156
|
+
return offending;
|
|
157
|
+
}
|
|
158
|
+
function walk(node, offending) {
|
|
159
|
+
if (node === null || typeof node !== 'object')
|
|
160
|
+
return;
|
|
161
|
+
if (Array.isArray(node)) {
|
|
162
|
+
for (const child of node)
|
|
163
|
+
walk(child, offending);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
const obj = node;
|
|
167
|
+
const operator = readOperatorName(obj);
|
|
168
|
+
if (operator !== undefined && !ALLOWED_PLAN_OPERATORS.has(operator)) {
|
|
169
|
+
offending.add(operator);
|
|
170
|
+
}
|
|
171
|
+
// Traverse known child slots; ignore string/number leaves.
|
|
172
|
+
for (const key of ['children', 'child', 'inputs', 'plan', 'root']) {
|
|
173
|
+
if (key in obj)
|
|
174
|
+
walk(obj[key], offending);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
function readOperatorName(obj) {
|
|
178
|
+
const candidates = ['name', 'operator_type', 'operator', 'type'];
|
|
179
|
+
for (const key of candidates) {
|
|
180
|
+
const value = obj[key];
|
|
181
|
+
if (typeof value === 'string' && value !== '') {
|
|
182
|
+
return value.toUpperCase();
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
// ---------------------------------------------------------------------------
|
|
188
|
+
// Identifier validation and quoting
|
|
189
|
+
// ---------------------------------------------------------------------------
|
|
190
|
+
/**
|
|
191
|
+
* Allowed shape for canvas-local table and column names. Matches the
|
|
192
|
+
* conservative SQL identifier convention: starts with letter/underscore,
|
|
193
|
+
* followed by letters/digits/underscores, max 63 chars (PostgreSQL/DuckDB cap).
|
|
194
|
+
*/
|
|
195
|
+
const IDENTIFIER_REGEX = /^[A-Za-z_][A-Za-z0-9_]{0,62}$/;
|
|
196
|
+
/**
|
|
197
|
+
* DuckDB reserved words that must not be used as bare identifiers. Not
|
|
198
|
+
* exhaustive — this is a courtesy guard so misnamed tables fail at register
|
|
199
|
+
* time rather than confusing-error time. The `IDENTIFIER_REGEX` is the
|
|
200
|
+
* authoritative shape gate.
|
|
201
|
+
*/
|
|
202
|
+
const RESERVED_IDENTIFIERS = new Set([
|
|
203
|
+
'select',
|
|
204
|
+
'from',
|
|
205
|
+
'where',
|
|
206
|
+
'order',
|
|
207
|
+
'group',
|
|
208
|
+
'having',
|
|
209
|
+
'limit',
|
|
210
|
+
'offset',
|
|
211
|
+
'union',
|
|
212
|
+
'intersect',
|
|
213
|
+
'except',
|
|
214
|
+
'all',
|
|
215
|
+
'distinct',
|
|
216
|
+
'as',
|
|
217
|
+
'and',
|
|
218
|
+
'or',
|
|
219
|
+
'not',
|
|
220
|
+
'null',
|
|
221
|
+
'true',
|
|
222
|
+
'false',
|
|
223
|
+
'case',
|
|
224
|
+
'when',
|
|
225
|
+
'then',
|
|
226
|
+
'else',
|
|
227
|
+
'end',
|
|
228
|
+
'join',
|
|
229
|
+
'inner',
|
|
230
|
+
'outer',
|
|
231
|
+
'left',
|
|
232
|
+
'right',
|
|
233
|
+
'full',
|
|
234
|
+
'cross',
|
|
235
|
+
'on',
|
|
236
|
+
'using',
|
|
237
|
+
'with',
|
|
238
|
+
'recursive',
|
|
239
|
+
]);
|
|
240
|
+
/**
|
|
241
|
+
* Validate an identifier for use as a canvas-local table or column name.
|
|
242
|
+
* Throws `ValidationError` on rejection.
|
|
243
|
+
*/
|
|
244
|
+
export function assertValidIdentifier(value, kind) {
|
|
245
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
246
|
+
throw validationError(`Canvas ${kind} name must be a non-empty string.`, {
|
|
247
|
+
reason: 'identifier_empty',
|
|
248
|
+
kind,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
if (!IDENTIFIER_REGEX.test(value)) {
|
|
252
|
+
throw validationError(`Canvas ${kind} name "${value}" is invalid. Use letters, digits, and underscores; must start with a letter or underscore; max 63 chars.`, { reason: 'identifier_shape', kind, value });
|
|
253
|
+
}
|
|
254
|
+
if (RESERVED_IDENTIFIERS.has(value.toLowerCase())) {
|
|
255
|
+
throw validationError(`Canvas ${kind} name "${value}" is a reserved SQL keyword. Choose another name.`, { reason: 'identifier_reserved', kind, value });
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Wrap an identifier in double quotes for safe inclusion in SQL. Internal
|
|
260
|
+
* double quotes are doubled per the SQL standard. Callers should still
|
|
261
|
+
* validate via {@link assertValidIdentifier} before quoting — this helper
|
|
262
|
+
* only escapes; it does not validate shape.
|
|
263
|
+
*/
|
|
264
|
+
export function quoteIdentifier(value) {
|
|
265
|
+
return `"${value.replace(/"/g, '""')}"`;
|
|
266
|
+
}
|
|
267
|
+
//# sourceMappingURL=sqlGate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sqlGate.js","sourceRoot":"","sources":["../../../src/canvas/core/sqlGate.ts"],"names":[],"mappings":"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"}
|