ai-database 2.1.3 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +35 -1
- package/README.md +880 -669
- package/dist/actions.d.ts +2 -2
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +1 -1
- package/dist/actions.js.map +1 -1
- package/dist/ai-promise-db.d.ts +49 -23
- package/dist/ai-promise-db.d.ts.map +1 -1
- package/dist/ai-promise-db.js +91 -63
- package/dist/ai-promise-db.js.map +1 -1
- package/dist/authorization.d.ts.map +1 -1
- package/dist/authorization.js +38 -30
- package/dist/authorization.js.map +1 -1
- package/dist/cascade-orchestrator.d.ts +404 -0
- package/dist/cascade-orchestrator.d.ts.map +1 -0
- package/dist/cascade-orchestrator.js +828 -0
- package/dist/cascade-orchestrator.js.map +1 -0
- package/dist/cascade-write-strategy.d.ts +584 -0
- package/dist/cascade-write-strategy.d.ts.map +1 -0
- package/dist/cascade-write-strategy.js +590 -0
- package/dist/cascade-write-strategy.js.map +1 -0
- package/dist/ch-adapter.d.ts +358 -0
- package/dist/ch-adapter.d.ts.map +1 -0
- package/dist/ch-adapter.js +929 -0
- package/dist/ch-adapter.js.map +1 -0
- package/dist/client/index.d.ts +42 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +43 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client.d.ts +266 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +81 -0
- package/dist/client.js.map +1 -0
- package/dist/constants.d.ts +64 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +52 -2
- package/dist/constants.js.map +1 -1
- package/dist/dataloader.d.ts +99 -0
- package/dist/dataloader.d.ts.map +1 -0
- package/dist/dataloader.js +225 -0
- package/dist/dataloader.js.map +1 -0
- package/dist/db-provider-port.d.ts +501 -0
- package/dist/db-provider-port.d.ts.map +1 -0
- package/dist/db-provider-port.js +113 -0
- package/dist/db-provider-port.js.map +1 -0
- package/dist/digital-objects-provider.d.ts +49 -0
- package/dist/digital-objects-provider.d.ts.map +1 -0
- package/dist/digital-objects-provider.js +55 -0
- package/dist/digital-objects-provider.js.map +1 -0
- package/dist/do-sqlite-adapter.d.ts +402 -0
- package/dist/do-sqlite-adapter.d.ts.map +1 -0
- package/dist/do-sqlite-adapter.js +745 -0
- package/dist/do-sqlite-adapter.js.map +1 -0
- package/dist/docs-rels/custom-types.d.ts +134 -0
- package/dist/docs-rels/custom-types.d.ts.map +1 -0
- package/dist/docs-rels/custom-types.js +70 -0
- package/dist/docs-rels/custom-types.js.map +1 -0
- package/dist/docs-rels/index.d.ts +16 -0
- package/dist/docs-rels/index.d.ts.map +1 -0
- package/dist/docs-rels/index.js +16 -0
- package/dist/docs-rels/index.js.map +1 -0
- package/dist/docs-rels/migrations/index.d.ts +30 -0
- package/dist/docs-rels/migrations/index.d.ts.map +1 -0
- package/dist/docs-rels/migrations/index.js +128 -0
- package/dist/docs-rels/migrations/index.js.map +1 -0
- package/dist/docs-rels/schema.d.ts +2961 -0
- package/dist/docs-rels/schema.d.ts.map +1 -0
- package/dist/docs-rels/schema.js +244 -0
- package/dist/docs-rels/schema.js.map +1 -0
- package/dist/durable-clickhouse.d.ts.map +1 -1
- package/dist/durable-clickhouse.js +16 -13
- package/dist/durable-clickhouse.js.map +1 -1
- package/dist/durable-promise.d.ts.map +1 -1
- package/dist/durable-promise.js +34 -15
- package/dist/durable-promise.js.map +1 -1
- package/dist/errors.d.ts +127 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +210 -0
- package/dist/errors.js.map +1 -0
- package/dist/eventbridge.d.ts +117 -0
- package/dist/eventbridge.d.ts.map +1 -0
- package/dist/eventbridge.js +238 -0
- package/dist/eventbridge.js.map +1 -0
- package/dist/events.d.ts +2 -2
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +1 -1
- package/dist/events.js.map +1 -1
- package/dist/execution-queue.d.ts.map +1 -1
- package/dist/execution-queue.js +4 -5
- package/dist/execution-queue.js.map +1 -1
- package/dist/index.d.ts +35 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +106 -6
- package/dist/index.js.map +1 -1
- package/dist/linguistic.d.ts +3 -108
- package/dist/linguistic.d.ts.map +1 -1
- package/dist/linguistic.js +3 -372
- package/dist/linguistic.js.map +1 -1
- package/dist/logger.d.ts +132 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +137 -0
- package/dist/logger.js.map +1 -0
- package/dist/memory-provider.d.ts +128 -0
- package/dist/memory-provider.d.ts.map +1 -1
- package/dist/memory-provider.js +592 -257
- package/dist/memory-provider.js.map +1 -1
- package/dist/pg-adapter.d.ts +424 -0
- package/dist/pg-adapter.d.ts.map +1 -0
- package/dist/pg-adapter.js +921 -0
- package/dist/pg-adapter.js.map +1 -0
- package/dist/pipelines-iceberg-emitter.d.ts +327 -0
- package/dist/pipelines-iceberg-emitter.d.ts.map +1 -0
- package/dist/pipelines-iceberg-emitter.js +351 -0
- package/dist/pipelines-iceberg-emitter.js.map +1 -0
- package/dist/provider-capabilities.d.ts +146 -0
- package/dist/provider-capabilities.d.ts.map +1 -0
- package/dist/provider-capabilities.js +214 -0
- package/dist/provider-capabilities.js.map +1 -0
- package/dist/rdb-provider-adapter.d.ts +195 -0
- package/dist/rdb-provider-adapter.d.ts.map +1 -0
- package/dist/rdb-provider-adapter.js +291 -0
- package/dist/rdb-provider-adapter.js.map +1 -0
- package/dist/schema/cascade.d.ts +48 -17
- package/dist/schema/cascade.d.ts.map +1 -1
- package/dist/schema/cascade.js +477 -278
- package/dist/schema/cascade.js.map +1 -1
- package/dist/schema/definition-caches.d.ts +24 -0
- package/dist/schema/definition-caches.d.ts.map +1 -0
- package/dist/schema/definition-caches.js +26 -0
- package/dist/schema/definition-caches.js.map +1 -0
- package/dist/schema/dependency-graph.d.ts +21 -109
- package/dist/schema/dependency-graph.d.ts.map +1 -1
- package/dist/schema/dependency-graph.js +25 -333
- package/dist/schema/dependency-graph.js.map +1 -1
- package/dist/schema/diff.d.ts +103 -0
- package/dist/schema/diff.d.ts.map +1 -0
- package/dist/schema/diff.js +329 -0
- package/dist/schema/diff.js.map +1 -0
- package/dist/schema/entity-operations.d.ts +99 -0
- package/dist/schema/entity-operations.d.ts.map +1 -0
- package/dist/schema/entity-operations.js +818 -0
- package/dist/schema/entity-operations.js.map +1 -0
- package/dist/schema/index.d.ts +28 -34
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/index.js +454 -521
- package/dist/schema/index.js.map +1 -1
- package/dist/schema/migration.d.ts +205 -0
- package/dist/schema/migration.d.ts.map +1 -0
- package/dist/schema/migration.js +327 -0
- package/dist/schema/migration.js.map +1 -0
- package/dist/schema/nl-query-generator.d.ts +68 -0
- package/dist/schema/nl-query-generator.d.ts.map +1 -0
- package/dist/schema/nl-query-generator.js +362 -0
- package/dist/schema/nl-query-generator.js.map +1 -0
- package/dist/schema/nl-query.d.ts +65 -0
- package/dist/schema/nl-query.d.ts.map +1 -0
- package/dist/schema/nl-query.js +178 -0
- package/dist/schema/nl-query.js.map +1 -0
- package/dist/schema/parse.d.ts.map +1 -1
- package/dist/schema/parse.js +144 -89
- package/dist/schema/parse.js.map +1 -1
- package/dist/schema/provider.d.ts +37 -0
- package/dist/schema/provider.d.ts.map +1 -1
- package/dist/schema/provider.js +15 -7
- package/dist/schema/provider.js.map +1 -1
- package/dist/schema/resolve.d.ts +46 -5
- package/dist/schema/resolve.d.ts.map +1 -1
- package/dist/schema/resolve.js +237 -95
- package/dist/schema/resolve.js.map +1 -1
- package/dist/schema/search-utils.d.ts +76 -0
- package/dist/schema/search-utils.d.ts.map +1 -0
- package/dist/schema/search-utils.js +86 -0
- package/dist/schema/search-utils.js.map +1 -0
- package/dist/schema/seed.d.ts +53 -0
- package/dist/schema/seed.d.ts.map +1 -0
- package/dist/schema/seed.js +94 -0
- package/dist/schema/seed.js.map +1 -0
- package/dist/schema/semantic.d.ts +10 -0
- package/dist/schema/semantic.d.ts.map +1 -1
- package/dist/schema/semantic.js +192 -86
- package/dist/schema/semantic.js.map +1 -1
- package/dist/schema/sub-apis.d.ts +52 -0
- package/dist/schema/sub-apis.d.ts.map +1 -0
- package/dist/schema/sub-apis.js +216 -0
- package/dist/schema/sub-apis.js.map +1 -0
- package/dist/schema/system-entities.d.ts +42 -0
- package/dist/schema/system-entities.d.ts.map +1 -0
- package/dist/schema/system-entities.js +101 -0
- package/dist/schema/system-entities.js.map +1 -0
- package/dist/schema/types.d.ts +91 -9
- package/dist/schema/types.d.ts.map +1 -1
- package/dist/schema/union-fallback.d.ts.map +1 -1
- package/dist/schema/union-fallback.js +21 -15
- package/dist/schema/union-fallback.js.map +1 -1
- package/dist/schema/value-generators/ai.d.ts +54 -0
- package/dist/schema/value-generators/ai.d.ts.map +1 -0
- package/dist/schema/value-generators/ai.js +136 -0
- package/dist/schema/value-generators/ai.js.map +1 -0
- package/dist/schema/value-generators/index.d.ts +126 -0
- package/dist/schema/value-generators/index.d.ts.map +1 -0
- package/dist/schema/value-generators/index.js +219 -0
- package/dist/schema/value-generators/index.js.map +1 -0
- package/dist/schema/value-generators/placeholder.d.ts +52 -0
- package/dist/schema/value-generators/placeholder.d.ts.map +1 -0
- package/dist/schema/value-generators/placeholder.js +328 -0
- package/dist/schema/value-generators/placeholder.js.map +1 -0
- package/dist/schema/value-generators/types.d.ts +116 -0
- package/dist/schema/value-generators/types.d.ts.map +1 -0
- package/dist/schema/value-generators/types.js +11 -0
- package/dist/schema/value-generators/types.js.map +1 -0
- package/dist/schema/version.d.ts +111 -0
- package/dist/schema/version.d.ts.map +1 -0
- package/dist/schema/version.js +190 -0
- package/dist/schema/version.js.map +1 -0
- package/dist/schema.d.ts +1095 -24
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +2852 -40
- package/dist/schema.js.map +1 -1
- package/dist/semantic-vectors.d.ts +39 -0
- package/dist/semantic-vectors.d.ts.map +1 -0
- package/dist/semantic-vectors.js +334 -0
- package/dist/semantic-vectors.js.map +1 -0
- package/dist/semantic.d.ts +29 -1
- package/dist/semantic.d.ts.map +1 -1
- package/dist/semantic.js +26 -16
- package/dist/semantic.js.map +1 -1
- package/dist/telemetry.d.ts +128 -0
- package/dist/telemetry.d.ts.map +1 -0
- package/dist/telemetry.js +305 -0
- package/dist/telemetry.js.map +1 -0
- package/dist/tests.d.ts.map +1 -1
- package/dist/tests.js +30 -22
- package/dist/tests.js.map +1 -1
- package/dist/type-guards.d.ts +50 -5
- package/dist/type-guards.d.ts.map +1 -1
- package/dist/type-guards.js +87 -16
- package/dist/type-guards.js.map +1 -1
- package/dist/types.d.ts +33 -245
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +62 -72
- package/dist/types.js.map +1 -1
- package/dist/validation.d.ts +2 -5
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +65 -93
- package/dist/validation.js.map +1 -1
- package/dist/worker/db-provider.d.ts +168 -0
- package/dist/worker/db-provider.d.ts.map +1 -0
- package/dist/worker/db-provider.js +277 -0
- package/dist/worker/db-provider.js.map +1 -0
- package/dist/worker/index.d.ts +35 -0
- package/dist/worker/index.d.ts.map +1 -0
- package/dist/worker/index.js +37 -0
- package/dist/worker/index.js.map +1 -0
- package/dist/worker.d.ts +779 -0
- package/dist/worker.d.ts.map +1 -0
- package/dist/worker.js +2786 -0
- package/dist/worker.js.map +1 -0
- package/package.json +46 -16
- package/src/docs-rels/migrations/0001-init.sql +125 -0
- package/LICENSE +0 -21
|
@@ -0,0 +1,745 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DO SQLite Adapter — Stack B transactional DBProvider
|
|
3
|
+
*
|
|
4
|
+
* First-class adapter for **Cloudflare Durable Objects with SQLite storage**.
|
|
5
|
+
* Stack B's transactional layer per ADR-0003: per-cascade DO isolation gives
|
|
6
|
+
* parallel write paths each at full single-DO throughput, which is what makes
|
|
7
|
+
* the cascade-heavy moat workload viable.
|
|
8
|
+
*
|
|
9
|
+
* ## Sharding model
|
|
10
|
+
*
|
|
11
|
+
* The adapter accepts a {@link DurableObjectNamespaceLike} binding plus a
|
|
12
|
+
* {@link ShardingStrategy} that selects which DO id receives a given
|
|
13
|
+
* operation. Two canonical strategies ship in this module:
|
|
14
|
+
*
|
|
15
|
+
* - **`'per-cascade'`** (default; the enabling pattern per ADR-0003) —
|
|
16
|
+
* each cascade gets its own DO; reads back during traversal stay inside
|
|
17
|
+
* the same DO (no cross-DO hot-path reads). Caller passes the cascade id
|
|
18
|
+
* via the per-operation `context.cascadeId` (or sets a default cascade
|
|
19
|
+
* on the adapter via {@link DOSqliteAdapter.withCascade}).
|
|
20
|
+
*
|
|
21
|
+
* - **`'per-tenant'`** — alternative for multi-tenant deployments where one
|
|
22
|
+
* tenant runs multiple cascades. Caller passes `context.tenantId`.
|
|
23
|
+
*
|
|
24
|
+
* Custom strategies are accepted as a `(ctx) => string` callback for
|
|
25
|
+
* deployments with their own routing logic (e.g., per-day shards, per-type
|
|
26
|
+
* shards, hash-based shards).
|
|
27
|
+
*
|
|
28
|
+
* ## Constraints (from ADR-0003)
|
|
29
|
+
*
|
|
30
|
+
* - **Per-DO 10GB SQLite limit** — declared via
|
|
31
|
+
* {@link DOSqliteAdapter.maxStorageBytes}. Cascade write strategy
|
|
32
|
+
* (`aip-g1i9`) consults this when fanning out.
|
|
33
|
+
* - **No native vector support** — Tier 4 is sidecared via Cloudflare
|
|
34
|
+
* Vectorize. Pass a `vectorize: VectorizeIndexLike` binding to the
|
|
35
|
+
* constructor and the adapter declares the capability with
|
|
36
|
+
* `implementation: 'sidecar'`. Without it, the capability is
|
|
37
|
+
* `undefined` and `vectorSearch()` throws
|
|
38
|
+
* {@link VectorSearchUnavailableError}.
|
|
39
|
+
* - **Hard to query across DOs** — Tier 3 analytics declared as `false`
|
|
40
|
+
* for all sub-fields. Cross-cascade analytics is the dual-write path
|
|
41
|
+
* (DO → Pipelines → Iceberg → ClickHouse), which is `aip-0ypt`'s concern.
|
|
42
|
+
*
|
|
43
|
+
* ## Wire protocol
|
|
44
|
+
*
|
|
45
|
+
* The adapter speaks to {@link DatabaseDO} via its existing fetch routes
|
|
46
|
+
* (`/data`, `/rels`, `/query/*`, `/traverse`). This keeps the DO surface
|
|
47
|
+
* untouched and matches the existing test infrastructure
|
|
48
|
+
* (`@cloudflare/vitest-pool-workers`).
|
|
49
|
+
*
|
|
50
|
+
* @see {@link ../docs/adr/0003-storage-strategy-pg-clickhouse-default.md}
|
|
51
|
+
* @see {@link ../docs/plans/2026-05-05-cascade-storage-execution-implementation.md} Phase 1
|
|
52
|
+
* @packageDocumentation
|
|
53
|
+
*/
|
|
54
|
+
import { VectorSearchUnavailableError } from './errors.js';
|
|
55
|
+
/**
|
|
56
|
+
* Built-in sharding strategies. Callers can also pass any
|
|
57
|
+
* `(ctx) => string` callback.
|
|
58
|
+
*
|
|
59
|
+
* - {@link perCascade} — `ctx.cascadeId`, falling back to a default if
|
|
60
|
+
* provided to the adapter constructor. Throws if neither is set.
|
|
61
|
+
* - {@link perTenant} — `ctx.tenantId`, falling back to default. Throws if
|
|
62
|
+
* neither is set.
|
|
63
|
+
* - {@link perType} — `ctx.type` (every type gets its own DO). Useful for
|
|
64
|
+
* small/dev workloads where cascade isolation isn't yet wired.
|
|
65
|
+
* - {@link unsharded} — always `'__shared__'`. Single DO for everything.
|
|
66
|
+
* Defeats the per-cascade isolation pattern; intended for tests and
|
|
67
|
+
* tiny workloads only.
|
|
68
|
+
*/
|
|
69
|
+
export const ShardingStrategies = {
|
|
70
|
+
perCascade: (defaultCascadeId) => {
|
|
71
|
+
return (ctx) => {
|
|
72
|
+
const id = ctx.cascadeId ?? defaultCascadeId;
|
|
73
|
+
if (!id) {
|
|
74
|
+
throw new Error('DOSqliteAdapter (per-cascade strategy): no cascadeId in context. ' +
|
|
75
|
+
'Set a default via the adapter constructor, or call ' +
|
|
76
|
+
'adapter.withCascade(cascadeId) before issuing operations.');
|
|
77
|
+
}
|
|
78
|
+
return `cascade:${id}`;
|
|
79
|
+
};
|
|
80
|
+
},
|
|
81
|
+
perTenant: (defaultTenantId) => {
|
|
82
|
+
return (ctx) => {
|
|
83
|
+
const id = ctx.tenantId ?? defaultTenantId;
|
|
84
|
+
if (!id) {
|
|
85
|
+
throw new Error('DOSqliteAdapter (per-tenant strategy): no tenantId in context. ' +
|
|
86
|
+
'Set a default via the adapter constructor, or call ' +
|
|
87
|
+
'adapter.withTenant(tenantId) before issuing operations.');
|
|
88
|
+
}
|
|
89
|
+
return `tenant:${id}`;
|
|
90
|
+
};
|
|
91
|
+
},
|
|
92
|
+
perType: () => {
|
|
93
|
+
return (ctx) => {
|
|
94
|
+
if (!ctx.type) {
|
|
95
|
+
throw new Error('DOSqliteAdapter (per-type strategy): no type in context.');
|
|
96
|
+
}
|
|
97
|
+
return `type:${ctx.type}`;
|
|
98
|
+
};
|
|
99
|
+
},
|
|
100
|
+
unsharded: () => {
|
|
101
|
+
return () => '__shared__';
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
const ACTION_TYPE = '__svo_action';
|
|
105
|
+
const VERB_TYPE = '__svo_verb';
|
|
106
|
+
// =============================================================================
|
|
107
|
+
// Adapter
|
|
108
|
+
// =============================================================================
|
|
109
|
+
/**
|
|
110
|
+
* DO SQLite first-class adapter.
|
|
111
|
+
*
|
|
112
|
+
* Implements {@link DBProvider} (Tier 1+2), {@link DBProviderSVO} (SVO
|
|
113
|
+
* Action recording + Verb registry), and exposes
|
|
114
|
+
* {@link ProviderTierCapabilities} via the `capabilities` getter.
|
|
115
|
+
*
|
|
116
|
+
* @example Basic per-cascade usage
|
|
117
|
+
* ```ts
|
|
118
|
+
* import { DOSqliteAdapter } from 'ai-database'
|
|
119
|
+
*
|
|
120
|
+
* const adapter = new DOSqliteAdapter({
|
|
121
|
+
* namespace: env.DATABASE,
|
|
122
|
+
* sharding: 'per-cascade',
|
|
123
|
+
* defaultCascadeId: cascade.id,
|
|
124
|
+
* })
|
|
125
|
+
*
|
|
126
|
+
* // Per-op context overrides
|
|
127
|
+
* const customer = await adapter
|
|
128
|
+
* .withCascade(cascade.id)
|
|
129
|
+
* .create('Customer', undefined, { name: 'Acme' })
|
|
130
|
+
* ```
|
|
131
|
+
*
|
|
132
|
+
* @example Per-tenant strategy
|
|
133
|
+
* ```ts
|
|
134
|
+
* const adapter = new DOSqliteAdapter({
|
|
135
|
+
* namespace: env.DATABASE,
|
|
136
|
+
* sharding: 'per-tenant',
|
|
137
|
+
* defaultTenantId: 'acme',
|
|
138
|
+
* })
|
|
139
|
+
* ```
|
|
140
|
+
*
|
|
141
|
+
* @example Custom strategy
|
|
142
|
+
* ```ts
|
|
143
|
+
* const adapter = new DOSqliteAdapter({
|
|
144
|
+
* namespace: env.DATABASE,
|
|
145
|
+
* sharding: (ctx) => `${ctx.tenantId}:${ctx.cascadeId}`,
|
|
146
|
+
* })
|
|
147
|
+
* ```
|
|
148
|
+
*/
|
|
149
|
+
export class DOSqliteAdapter {
|
|
150
|
+
namespace;
|
|
151
|
+
strategy;
|
|
152
|
+
shardingModel;
|
|
153
|
+
baseContext;
|
|
154
|
+
vectorize;
|
|
155
|
+
vectorizeDimensions;
|
|
156
|
+
vectorizeNamespace;
|
|
157
|
+
/**
|
|
158
|
+
* The per-DO SQLite hard cap from Cloudflare. Declared as `getter` so
|
|
159
|
+
* future Cloudflare changes to the cap can be reflected in one place.
|
|
160
|
+
* Cascade write strategy (`aip-g1i9`) consults this when sizing fan-out.
|
|
161
|
+
*/
|
|
162
|
+
maxStorageBytes = 10 * 1024 * 1024 * 1024; // 10 GB
|
|
163
|
+
constructor(options) {
|
|
164
|
+
if (!options.namespace) {
|
|
165
|
+
throw new Error('DOSqliteAdapter: `namespace` (a DurableObjectNamespace binding) is required.');
|
|
166
|
+
}
|
|
167
|
+
this.namespace = options.namespace;
|
|
168
|
+
// Resolve sharding strategy
|
|
169
|
+
const sharding = options.sharding ?? 'per-cascade';
|
|
170
|
+
if (typeof sharding === 'function') {
|
|
171
|
+
this.strategy = sharding;
|
|
172
|
+
this.shardingModel = 'per-cascade'; // unknown; default declaration
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
switch (sharding) {
|
|
176
|
+
case 'per-cascade':
|
|
177
|
+
this.strategy = ShardingStrategies.perCascade(options.defaultCascadeId);
|
|
178
|
+
this.shardingModel = 'per-cascade';
|
|
179
|
+
break;
|
|
180
|
+
case 'per-tenant':
|
|
181
|
+
this.strategy = ShardingStrategies.perTenant(options.defaultTenantId);
|
|
182
|
+
this.shardingModel = 'partitioned-by-tenant';
|
|
183
|
+
break;
|
|
184
|
+
case 'per-type':
|
|
185
|
+
this.strategy = ShardingStrategies.perType();
|
|
186
|
+
this.shardingModel = 'per-type';
|
|
187
|
+
break;
|
|
188
|
+
case 'unsharded':
|
|
189
|
+
this.strategy = ShardingStrategies.unsharded();
|
|
190
|
+
this.shardingModel = 'unsharded';
|
|
191
|
+
break;
|
|
192
|
+
default:
|
|
193
|
+
throw new Error(`DOSqliteAdapter: unknown sharding strategy "${String(sharding)}"`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
this.baseContext = { ...(options.defaultContext ?? {}) };
|
|
197
|
+
if (options.defaultCascadeId !== undefined) {
|
|
198
|
+
this.baseContext.cascadeId = options.defaultCascadeId;
|
|
199
|
+
}
|
|
200
|
+
if (options.defaultTenantId !== undefined) {
|
|
201
|
+
this.baseContext.tenantId = options.defaultTenantId;
|
|
202
|
+
}
|
|
203
|
+
this.vectorize = options.vectorize;
|
|
204
|
+
this.vectorizeDimensions = options.vectorizeDimensions ?? 1536;
|
|
205
|
+
this.vectorizeNamespace = options.vectorizeNamespace;
|
|
206
|
+
}
|
|
207
|
+
// ===========================================================================
|
|
208
|
+
// Capability declaration (Tier model from ADR-0003)
|
|
209
|
+
// ===========================================================================
|
|
210
|
+
/**
|
|
211
|
+
* Tier capability declaration for this adapter.
|
|
212
|
+
*
|
|
213
|
+
* Per ADR-0003:
|
|
214
|
+
* - **Sharding**: `per-cascade` (default; the enabling pattern). Switches
|
|
215
|
+
* to `partitioned-by-tenant` when constructed with `'per-tenant'` or
|
|
216
|
+
* `unsharded` when constructed with `'unsharded'`.
|
|
217
|
+
* - **Tier 3 analytics**: declared `false` across the board. Cross-DO
|
|
218
|
+
* queries are hard; aggregations are the dual-write/Iceberg path.
|
|
219
|
+
* - **Tier 4 vector search**: `undefined`. DO SQLite has no native
|
|
220
|
+
* vectors; callers wire a Vectorize sidecar (per `aip-kh9l`).
|
|
221
|
+
* - **SVO Action recording / Verb registry**: both `true`. Stored as
|
|
222
|
+
* reserved entity types (`__svo_action`, `__svo_verb`) inside each DO,
|
|
223
|
+
* matching the per-cascade isolation pattern (Action records live in
|
|
224
|
+
* the same DO as the entities they reference).
|
|
225
|
+
*/
|
|
226
|
+
get capabilities() {
|
|
227
|
+
const caps = {
|
|
228
|
+
adapter: 'do-sqlite',
|
|
229
|
+
shardingModel: this.shardingModel,
|
|
230
|
+
analytics: {
|
|
231
|
+
hasAggregations: false,
|
|
232
|
+
hasTimeSeries: false,
|
|
233
|
+
hasLargeScans: false,
|
|
234
|
+
},
|
|
235
|
+
hasActionRecording: true,
|
|
236
|
+
hasVerbRegistry: true,
|
|
237
|
+
};
|
|
238
|
+
// Tier 4 is opt-in via a Vectorize sidecar binding. When present we
|
|
239
|
+
// declare the capability with `implementation: 'sidecar'` so callers
|
|
240
|
+
// can distinguish from native (PG/CH).
|
|
241
|
+
if (this.vectorize) {
|
|
242
|
+
caps.vectorSearch = {
|
|
243
|
+
maxDimensions: this.vectorizeDimensions,
|
|
244
|
+
metrics: ['cosine'],
|
|
245
|
+
implementation: 'sidecar',
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
return caps;
|
|
249
|
+
}
|
|
250
|
+
// ===========================================================================
|
|
251
|
+
// Context derivation — `withCascade`/`withTenant`/`withContext` produce a
|
|
252
|
+
// bound view of the adapter that fills in shard context per op. They
|
|
253
|
+
// return DBProvider+SVO without re-instantiating the underlying namespace.
|
|
254
|
+
// ===========================================================================
|
|
255
|
+
/**
|
|
256
|
+
* Return an adapter bound to `cascadeId` for subsequent operations.
|
|
257
|
+
* Useful when the surrounding code has a stable cascade scope but the
|
|
258
|
+
* adapter was constructed without one.
|
|
259
|
+
*
|
|
260
|
+
* Returns a new bound adapter; the original is unchanged.
|
|
261
|
+
*/
|
|
262
|
+
withCascade(cascadeId) {
|
|
263
|
+
return this.cloneWithContext({ cascadeId });
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Return an adapter bound to `tenantId` for subsequent operations.
|
|
267
|
+
*/
|
|
268
|
+
withTenant(tenantId) {
|
|
269
|
+
return this.cloneWithContext({ tenantId });
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Return an adapter bound to a free-form context. Lets callers route to
|
|
273
|
+
* a custom shard strategy without modifying the strategy itself.
|
|
274
|
+
*/
|
|
275
|
+
withContext(context) {
|
|
276
|
+
return this.cloneWithContext(context);
|
|
277
|
+
}
|
|
278
|
+
cloneWithContext(extra) {
|
|
279
|
+
const clone = Object.create(DOSqliteAdapter.prototype);
|
|
280
|
+
Object.assign(clone, {
|
|
281
|
+
namespace: this.namespace,
|
|
282
|
+
strategy: this.strategy,
|
|
283
|
+
shardingModel: this.shardingModel,
|
|
284
|
+
baseContext: { ...this.baseContext, ...extra },
|
|
285
|
+
maxStorageBytes: this.maxStorageBytes,
|
|
286
|
+
vectorize: this.vectorize,
|
|
287
|
+
vectorizeDimensions: this.vectorizeDimensions,
|
|
288
|
+
vectorizeNamespace: this.vectorizeNamespace,
|
|
289
|
+
});
|
|
290
|
+
return clone;
|
|
291
|
+
}
|
|
292
|
+
// ===========================================================================
|
|
293
|
+
// Internal: shard resolution + fetch
|
|
294
|
+
// ===========================================================================
|
|
295
|
+
resolveStub(ctx) {
|
|
296
|
+
const merged = { ...this.baseContext, ...ctx };
|
|
297
|
+
const idName = this.strategy(merged);
|
|
298
|
+
const id = this.namespace.idFromName(idName);
|
|
299
|
+
return this.namespace.get(id);
|
|
300
|
+
}
|
|
301
|
+
async doFetch(ctx, path, init) {
|
|
302
|
+
const stub = this.resolveStub(ctx);
|
|
303
|
+
const url = `https://do.test${path}`;
|
|
304
|
+
const response = await stub.fetch(url, init);
|
|
305
|
+
return this.parseResponse(response, path);
|
|
306
|
+
}
|
|
307
|
+
async parseResponse(response, path) {
|
|
308
|
+
const text = await response.text();
|
|
309
|
+
let parsed;
|
|
310
|
+
try {
|
|
311
|
+
parsed = text.length > 0 ? JSON.parse(text) : null;
|
|
312
|
+
}
|
|
313
|
+
catch {
|
|
314
|
+
throw new Error(`DOSqliteAdapter: ${path} returned non-JSON (status ${response.status}): ${text.slice(0, 200)}`);
|
|
315
|
+
}
|
|
316
|
+
if (!response.ok) {
|
|
317
|
+
const err = parsed?.error ?? `status ${response.status}`;
|
|
318
|
+
throw new Error(`DOSqliteAdapter: ${path} failed: ${err}`);
|
|
319
|
+
}
|
|
320
|
+
return parsed;
|
|
321
|
+
}
|
|
322
|
+
static normalizeEntity(row) {
|
|
323
|
+
const data = row.data ?? {};
|
|
324
|
+
return {
|
|
325
|
+
...data,
|
|
326
|
+
$id: row.id,
|
|
327
|
+
$type: row.type,
|
|
328
|
+
createdAt: row.created_at,
|
|
329
|
+
updatedAt: row.updated_at,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
static jsonInit(method, body) {
|
|
333
|
+
return {
|
|
334
|
+
method,
|
|
335
|
+
headers: { 'Content-Type': 'application/json' },
|
|
336
|
+
body: JSON.stringify(body),
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
// ===========================================================================
|
|
340
|
+
// Tier 1 — entity CRUD
|
|
341
|
+
// ===========================================================================
|
|
342
|
+
async get(type, id) {
|
|
343
|
+
const stub = this.resolveStub({ type });
|
|
344
|
+
const response = await stub.fetch(`https://do.test/data/${encodeURIComponent(id)}`);
|
|
345
|
+
if (response.status === 404)
|
|
346
|
+
return null;
|
|
347
|
+
const row = await this.parseResponse(response, `/data/${id}`);
|
|
348
|
+
if (row.type !== type)
|
|
349
|
+
return null;
|
|
350
|
+
return DOSqliteAdapter.normalizeEntity(row);
|
|
351
|
+
}
|
|
352
|
+
async list(type, options) {
|
|
353
|
+
const body = { type };
|
|
354
|
+
if (options?.where !== undefined)
|
|
355
|
+
body['where'] = options.where;
|
|
356
|
+
if (options?.orderBy !== undefined)
|
|
357
|
+
body['orderBy'] = options.orderBy;
|
|
358
|
+
if (options?.order !== undefined)
|
|
359
|
+
body['order'] = options.order;
|
|
360
|
+
if (options?.limit !== undefined)
|
|
361
|
+
body['limit'] = options.limit;
|
|
362
|
+
if (options?.offset !== undefined)
|
|
363
|
+
body['offset'] = options.offset;
|
|
364
|
+
const rows = await this.doFetch({ type }, '/query/list', DOSqliteAdapter.jsonInit('POST', body));
|
|
365
|
+
return rows.map(DOSqliteAdapter.normalizeEntity);
|
|
366
|
+
}
|
|
367
|
+
async search(type, query, options) {
|
|
368
|
+
const body = { type, query };
|
|
369
|
+
if (options?.fields !== undefined)
|
|
370
|
+
body['fields'] = options.fields;
|
|
371
|
+
if (options?.limit !== undefined)
|
|
372
|
+
body['limit'] = options.limit;
|
|
373
|
+
if (options?.minScore !== undefined)
|
|
374
|
+
body['minScore'] = options.minScore;
|
|
375
|
+
const rows = await this.doFetch({ type }, '/query/search', DOSqliteAdapter.jsonInit('POST', body));
|
|
376
|
+
let results = rows.map(DOSqliteAdapter.normalizeEntity);
|
|
377
|
+
// Apply where filter client-side (DO /query/search doesn't support where).
|
|
378
|
+
if (options?.where && Object.keys(options.where).length > 0) {
|
|
379
|
+
const where = options.where;
|
|
380
|
+
results = results.filter((entity) => {
|
|
381
|
+
for (const [key, value] of Object.entries(where)) {
|
|
382
|
+
if (entity[key] !== value)
|
|
383
|
+
return false;
|
|
384
|
+
}
|
|
385
|
+
return true;
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
if (options?.offset)
|
|
389
|
+
results = results.slice(options.offset);
|
|
390
|
+
if (options?.limit !== undefined)
|
|
391
|
+
results = results.slice(0, options.limit);
|
|
392
|
+
return results;
|
|
393
|
+
}
|
|
394
|
+
async create(type, id, data) {
|
|
395
|
+
const body = { type, data };
|
|
396
|
+
if (id !== undefined)
|
|
397
|
+
body['id'] = id;
|
|
398
|
+
const row = await this.doFetch({ type }, '/data', DOSqliteAdapter.jsonInit('POST', body));
|
|
399
|
+
return DOSqliteAdapter.normalizeEntity(row);
|
|
400
|
+
}
|
|
401
|
+
async update(type, id, data) {
|
|
402
|
+
const row = await this.doFetch({ type }, `/data/${encodeURIComponent(id)}`, DOSqliteAdapter.jsonInit('PATCH', { data }));
|
|
403
|
+
return DOSqliteAdapter.normalizeEntity(row);
|
|
404
|
+
}
|
|
405
|
+
async delete(type, id) {
|
|
406
|
+
const result = await this.doFetch({ type }, `/data/${encodeURIComponent(id)}`, { method: 'DELETE' });
|
|
407
|
+
return result.deleted === true;
|
|
408
|
+
}
|
|
409
|
+
// ===========================================================================
|
|
410
|
+
// Tier 2 — relationships / graph traversal
|
|
411
|
+
// ===========================================================================
|
|
412
|
+
async related(type, id, relation) {
|
|
413
|
+
const params = new URLSearchParams({ from_id: id, relation });
|
|
414
|
+
const rows = await this.doFetch({ type }, `/traverse?${params.toString()}`);
|
|
415
|
+
return rows.map(DOSqliteAdapter.normalizeEntity);
|
|
416
|
+
}
|
|
417
|
+
async relate(fromType, fromId, relation, toType, toId, metadata) {
|
|
418
|
+
const body = { from_id: fromId, relation, to_id: toId };
|
|
419
|
+
if (metadata !== undefined)
|
|
420
|
+
body['metadata'] = { ...metadata, fromType, toType };
|
|
421
|
+
await this.doFetch({ type: fromType }, '/rels', DOSqliteAdapter.jsonInit('POST', body));
|
|
422
|
+
}
|
|
423
|
+
async unrelate(fromType, fromId, relation, _toType, toId) {
|
|
424
|
+
await this.doFetch({ type: fromType }, '/rels/delete', DOSqliteAdapter.jsonInit('DELETE', { from_id: fromId, relation, to_id: toId }));
|
|
425
|
+
}
|
|
426
|
+
// ===========================================================================
|
|
427
|
+
// SVO surface — Action recording
|
|
428
|
+
// ===========================================================================
|
|
429
|
+
//
|
|
430
|
+
// Actions are stored as reserved-type entities (`__svo_action`) inside the
|
|
431
|
+
// shard the action's subject lives in. This honours per-cascade isolation:
|
|
432
|
+
// an Action recorded as part of a cascade lives in that cascade's DO,
|
|
433
|
+
// alongside the entities it references.
|
|
434
|
+
//
|
|
435
|
+
// Verbs are stored similarly under `__svo_verb`. Because each shard has
|
|
436
|
+
// its own SQLite DB, the Verb registry is **per-shard** — different shards
|
|
437
|
+
// can hold independent Verb sets. For most cascade workloads that's the
|
|
438
|
+
// correct shape (cascades are short-lived). Long-lived workloads that
|
|
439
|
+
// need a global Verb registry should layer it via `digital-objects`
|
|
440
|
+
// upstream of the adapter.
|
|
441
|
+
actionRouteContext(input) {
|
|
442
|
+
// Route Actions to the shard of the subject if available, else object,
|
|
443
|
+
// else the default context. We don't carry a `type` here — the Action
|
|
444
|
+
// type itself is reserved.
|
|
445
|
+
return { type: ACTION_TYPE, subjectId: input.subject, objectId: input.object };
|
|
446
|
+
}
|
|
447
|
+
async recordAction(input) {
|
|
448
|
+
const id = crypto.randomUUID();
|
|
449
|
+
const createdAt = new Date();
|
|
450
|
+
const status = input.status ?? 'completed';
|
|
451
|
+
const data = {
|
|
452
|
+
verb: input.verb,
|
|
453
|
+
status,
|
|
454
|
+
};
|
|
455
|
+
if (input.subject !== undefined)
|
|
456
|
+
data['subject'] = input.subject;
|
|
457
|
+
if (input.object !== undefined)
|
|
458
|
+
data['object'] = input.object;
|
|
459
|
+
if (input.roles !== undefined)
|
|
460
|
+
data['roles'] = input.roles;
|
|
461
|
+
if (input.data !== undefined)
|
|
462
|
+
data['data'] = input.data;
|
|
463
|
+
if (status === 'completed' || status === 'failed' || status === 'cancelled') {
|
|
464
|
+
data['completedAt'] = createdAt.toISOString();
|
|
465
|
+
}
|
|
466
|
+
const ctx = this.actionRouteContext({
|
|
467
|
+
...(input.subject !== undefined && { subject: input.subject }),
|
|
468
|
+
...(input.object !== undefined && { object: input.object }),
|
|
469
|
+
});
|
|
470
|
+
const row = await this.doFetch(ctx, '/data', DOSqliteAdapter.jsonInit('POST', { id, type: ACTION_TYPE, data }));
|
|
471
|
+
const persistedData = (row.data ?? {});
|
|
472
|
+
const action = {
|
|
473
|
+
id: row.id,
|
|
474
|
+
verb: persistedData['verb'],
|
|
475
|
+
status: persistedData['status'] ?? 'completed',
|
|
476
|
+
createdAt: new Date(row.created_at),
|
|
477
|
+
...(persistedData['subject'] !== undefined && {
|
|
478
|
+
subject: persistedData['subject'],
|
|
479
|
+
}),
|
|
480
|
+
...(persistedData['object'] !== undefined && {
|
|
481
|
+
object: persistedData['object'],
|
|
482
|
+
}),
|
|
483
|
+
...(persistedData['roles'] !== undefined && {
|
|
484
|
+
roles: persistedData['roles'],
|
|
485
|
+
}),
|
|
486
|
+
...(persistedData['data'] !== undefined && { data: persistedData['data'] }),
|
|
487
|
+
...(persistedData['completedAt'] !== undefined && {
|
|
488
|
+
completedAt: new Date(persistedData['completedAt']),
|
|
489
|
+
}),
|
|
490
|
+
};
|
|
491
|
+
return action;
|
|
492
|
+
}
|
|
493
|
+
async queryActions(query) {
|
|
494
|
+
// List all action records on the resolved shard. Filtering is applied
|
|
495
|
+
// client-side for simplicity (per-DO SVO records remain small relative
|
|
496
|
+
// to entity volume).
|
|
497
|
+
const where = {};
|
|
498
|
+
if (query?.verb !== undefined)
|
|
499
|
+
where['data.verb'] = query.verb;
|
|
500
|
+
const ctx = this.actionRouteContext({
|
|
501
|
+
...(query?.subject !== undefined && { subject: query.subject }),
|
|
502
|
+
...(query?.object !== undefined && { object: query.object }),
|
|
503
|
+
});
|
|
504
|
+
const rows = await this.doFetch(ctx, '/query/list', DOSqliteAdapter.jsonInit('POST', { type: ACTION_TYPE }));
|
|
505
|
+
let actions = rows.map((row) => {
|
|
506
|
+
const data = (row.data ?? {});
|
|
507
|
+
const action = {
|
|
508
|
+
id: row.id,
|
|
509
|
+
verb: data['verb'],
|
|
510
|
+
status: data['status'] ?? 'completed',
|
|
511
|
+
createdAt: new Date(row.created_at),
|
|
512
|
+
...(data['subject'] !== undefined && { subject: data['subject'] }),
|
|
513
|
+
...(data['object'] !== undefined && { object: data['object'] }),
|
|
514
|
+
...(data['roles'] !== undefined && {
|
|
515
|
+
roles: data['roles'],
|
|
516
|
+
}),
|
|
517
|
+
...(data['data'] !== undefined && { data: data['data'] }),
|
|
518
|
+
...(data['completedAt'] !== undefined && {
|
|
519
|
+
completedAt: new Date(data['completedAt']),
|
|
520
|
+
}),
|
|
521
|
+
};
|
|
522
|
+
return action;
|
|
523
|
+
});
|
|
524
|
+
if (query?.verb !== undefined) {
|
|
525
|
+
actions = actions.filter((a) => a.verb === query.verb);
|
|
526
|
+
}
|
|
527
|
+
if (query?.subject !== undefined) {
|
|
528
|
+
actions = actions.filter((a) => a.subject === query.subject);
|
|
529
|
+
}
|
|
530
|
+
if (query?.object !== undefined) {
|
|
531
|
+
actions = actions.filter((a) => a.object === query.object);
|
|
532
|
+
}
|
|
533
|
+
if (query?.role) {
|
|
534
|
+
const role = query.role;
|
|
535
|
+
actions = actions.filter((a) => {
|
|
536
|
+
for (const [key, value] of Object.entries(role)) {
|
|
537
|
+
if (key === 'subject') {
|
|
538
|
+
if (a.subject !== value)
|
|
539
|
+
return false;
|
|
540
|
+
}
|
|
541
|
+
else if (key === 'object') {
|
|
542
|
+
if (a.object !== value)
|
|
543
|
+
return false;
|
|
544
|
+
}
|
|
545
|
+
else {
|
|
546
|
+
const slot = (a.roles ?? {});
|
|
547
|
+
if (slot[key] !== value)
|
|
548
|
+
return false;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
return true;
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
if (query?.status !== undefined) {
|
|
555
|
+
const want = Array.isArray(query.status) ? new Set(query.status) : new Set([query.status]);
|
|
556
|
+
actions = actions.filter((a) => want.has(a.status));
|
|
557
|
+
}
|
|
558
|
+
if (query?.since !== undefined) {
|
|
559
|
+
actions = actions.filter((a) => a.createdAt >= query.since);
|
|
560
|
+
}
|
|
561
|
+
if (query?.until !== undefined) {
|
|
562
|
+
actions = actions.filter((a) => a.createdAt <= query.until);
|
|
563
|
+
}
|
|
564
|
+
actions.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
|
|
565
|
+
if (query?.offset !== undefined)
|
|
566
|
+
actions = actions.slice(query.offset);
|
|
567
|
+
if (query?.limit !== undefined)
|
|
568
|
+
actions = actions.slice(0, query.limit);
|
|
569
|
+
return actions;
|
|
570
|
+
}
|
|
571
|
+
// ===========================================================================
|
|
572
|
+
// SVO surface — Verb registry
|
|
573
|
+
// ===========================================================================
|
|
574
|
+
async defineVerb(def) {
|
|
575
|
+
const existing = await this.getVerb(def.name);
|
|
576
|
+
if (existing) {
|
|
577
|
+
// Idempotent re-registration is allowed when conjugations match;
|
|
578
|
+
// reject when they conflict.
|
|
579
|
+
const conflict = (def.action !== undefined && def.action !== existing.action) ||
|
|
580
|
+
(def.act !== undefined && def.act !== existing.act) ||
|
|
581
|
+
(def.activity !== undefined && def.activity !== existing.activity);
|
|
582
|
+
if (conflict) {
|
|
583
|
+
throw new Error(`DOSqliteAdapter.defineVerb: verb "${def.name}" already registered with conflicting conjugations`);
|
|
584
|
+
}
|
|
585
|
+
return existing;
|
|
586
|
+
}
|
|
587
|
+
const action = def.action ?? def.name;
|
|
588
|
+
const act = def.act ?? `${action}s`;
|
|
589
|
+
const activity = def.activity ?? `${action}ing`;
|
|
590
|
+
const event = def.event ?? action;
|
|
591
|
+
const data = {
|
|
592
|
+
name: def.name,
|
|
593
|
+
action,
|
|
594
|
+
act,
|
|
595
|
+
activity,
|
|
596
|
+
event,
|
|
597
|
+
...(def.reverseBy !== undefined && { reverseBy: def.reverseBy }),
|
|
598
|
+
...(def.reverseAt !== undefined && { reverseAt: def.reverseAt }),
|
|
599
|
+
...(def.reverseIn !== undefined && { reverseIn: def.reverseIn }),
|
|
600
|
+
...(def.inverse !== undefined && { inverse: def.inverse }),
|
|
601
|
+
...(def.description !== undefined && { description: def.description }),
|
|
602
|
+
...(def.frame !== undefined && { frame: def.frame }),
|
|
603
|
+
...(def.source !== undefined && { source: def.source }),
|
|
604
|
+
...(def.canonical !== undefined && { canonical: def.canonical }),
|
|
605
|
+
};
|
|
606
|
+
const ctx = { type: VERB_TYPE };
|
|
607
|
+
const row = await this.doFetch(ctx, '/data', DOSqliteAdapter.jsonInit('POST', { id: def.name, type: VERB_TYPE, data }));
|
|
608
|
+
return DOSqliteAdapter.toVerbRecord(row);
|
|
609
|
+
}
|
|
610
|
+
async getVerb(name) {
|
|
611
|
+
const ctx = { type: VERB_TYPE };
|
|
612
|
+
const stub = this.resolveStub(ctx);
|
|
613
|
+
const response = await stub.fetch(`https://do.test/data/${encodeURIComponent(name)}`);
|
|
614
|
+
if (response.status === 404)
|
|
615
|
+
return null;
|
|
616
|
+
const row = await this.parseResponse(response, `/data/${name}`);
|
|
617
|
+
if (row.type !== VERB_TYPE)
|
|
618
|
+
return null;
|
|
619
|
+
return DOSqliteAdapter.toVerbRecord(row);
|
|
620
|
+
}
|
|
621
|
+
async listVerbs() {
|
|
622
|
+
const ctx = { type: VERB_TYPE };
|
|
623
|
+
const rows = await this.doFetch(ctx, '/query/list', DOSqliteAdapter.jsonInit('POST', { type: VERB_TYPE }));
|
|
624
|
+
return rows.map(DOSqliteAdapter.toVerbRecord);
|
|
625
|
+
}
|
|
626
|
+
// ===========================================================================
|
|
627
|
+
// Tier 4 — vector search (Vectorize sidecar)
|
|
628
|
+
// ===========================================================================
|
|
629
|
+
/**
|
|
630
|
+
* Vector search via the Cloudflare Vectorize sidecar binding.
|
|
631
|
+
*
|
|
632
|
+
* - When the adapter was constructed **without** a `vectorize` binding,
|
|
633
|
+
* this throws {@link VectorSearchUnavailableError}.
|
|
634
|
+
* - When **with** a binding, the binding's `query()` is invoked with
|
|
635
|
+
* `topK = options.limit ?? 10` and `returnMetadata: true` so each hit
|
|
636
|
+
* carries enough context to reconstruct an entity. The vector id
|
|
637
|
+
* returned by Vectorize is treated as the Thing id; if the index
|
|
638
|
+
* stores the Thing's `data` in `metadata`, the result entity is
|
|
639
|
+
* composed from that — otherwise a fallback `get()` against the
|
|
640
|
+
* resolved DO shard fills in the data. Dimensions and metrics are
|
|
641
|
+
* determined by how the index was provisioned upstream; we pass the
|
|
642
|
+
* caller's `metric` through unchanged for documentation purposes
|
|
643
|
+
* (Vectorize doesn't accept a per-query metric).
|
|
644
|
+
*
|
|
645
|
+
* Frame-aware role filtering is **deferred** to a refinement bead;
|
|
646
|
+
* Vectorize's `filter` parameter could carry role-shaped metadata
|
|
647
|
+
* filters, but the wire shape needs design with how callers seed the
|
|
648
|
+
* index, which is out of scope here.
|
|
649
|
+
*/
|
|
650
|
+
async vectorSearch(type, queryEmbedding, options) {
|
|
651
|
+
if (!this.vectorize) {
|
|
652
|
+
throw new VectorSearchUnavailableError('do-sqlite', 'no Vectorize binding configured (pass `vectorize` to DOSqliteAdapter constructor)');
|
|
653
|
+
}
|
|
654
|
+
if (!Array.isArray(queryEmbedding) || queryEmbedding.length === 0) {
|
|
655
|
+
throw new Error('vectorSearch: queryEmbedding must be a non-empty array of numbers');
|
|
656
|
+
}
|
|
657
|
+
for (const v of queryEmbedding) {
|
|
658
|
+
if (typeof v !== 'number' || !Number.isFinite(v)) {
|
|
659
|
+
throw new Error('vectorSearch: queryEmbedding values must be finite numbers');
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
// The metric option is a hint only — Vectorize indexes pin a metric at
|
|
663
|
+
// creation time. We accept it for API parity with PG/CH but don't
|
|
664
|
+
// forward it to the binding.
|
|
665
|
+
void options?.metric;
|
|
666
|
+
const limit = Math.max(1, options?.limit ?? 10);
|
|
667
|
+
const queryOptions = { topK: limit, returnMetadata: true };
|
|
668
|
+
if (this.vectorizeNamespace !== undefined) {
|
|
669
|
+
queryOptions.namespace = this.vectorizeNamespace;
|
|
670
|
+
}
|
|
671
|
+
const result = await this.vectorize.query(queryEmbedding, queryOptions);
|
|
672
|
+
const matches = result?.matches ?? [];
|
|
673
|
+
let hits = [];
|
|
674
|
+
for (const m of matches) {
|
|
675
|
+
const meta = (m.metadata ?? {});
|
|
676
|
+
// Prefer entity payload from index metadata when callers stored it;
|
|
677
|
+
// otherwise fall back to fetching the row from the resolved shard.
|
|
678
|
+
let entity = null;
|
|
679
|
+
const metaType = typeof meta['type'] === 'string' ? meta['type'] : undefined;
|
|
680
|
+
const metaData = typeof meta['data'] === 'object' && meta['data'] !== null
|
|
681
|
+
? meta['data']
|
|
682
|
+
: undefined;
|
|
683
|
+
if (metaType === type && metaData) {
|
|
684
|
+
entity = { ...metaData, $id: m.id, $type: type };
|
|
685
|
+
}
|
|
686
|
+
else if (metaType === undefined && metaData === undefined) {
|
|
687
|
+
// Index didn't store metadata — read-back from the DO shard.
|
|
688
|
+
const fetched = (await this.get(type, m.id));
|
|
689
|
+
if (fetched) {
|
|
690
|
+
entity = fetched;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
// Skip matches that aren't of the requested type.
|
|
694
|
+
if (!entity)
|
|
695
|
+
continue;
|
|
696
|
+
hits.push({ entity, score: m.score });
|
|
697
|
+
}
|
|
698
|
+
if (options?.minScore !== undefined) {
|
|
699
|
+
const min = options.minScore;
|
|
700
|
+
hits = hits.filter((h) => h.score >= min);
|
|
701
|
+
}
|
|
702
|
+
return hits;
|
|
703
|
+
}
|
|
704
|
+
static toVerbRecord(row) {
|
|
705
|
+
const data = (row.data ?? {});
|
|
706
|
+
return {
|
|
707
|
+
name: data['name'] ?? row.id,
|
|
708
|
+
action: data['action'],
|
|
709
|
+
act: data['act'],
|
|
710
|
+
activity: data['activity'],
|
|
711
|
+
event: data['event'],
|
|
712
|
+
...(data['reverseBy'] !== undefined && { reverseBy: data['reverseBy'] }),
|
|
713
|
+
...(data['reverseAt'] !== undefined && { reverseAt: data['reverseAt'] }),
|
|
714
|
+
...(data['reverseIn'] !== undefined && { reverseIn: data['reverseIn'] }),
|
|
715
|
+
...(data['inverse'] !== undefined && { inverse: data['inverse'] }),
|
|
716
|
+
...(data['description'] !== undefined && {
|
|
717
|
+
description: data['description'],
|
|
718
|
+
}),
|
|
719
|
+
...(data['frame'] !== undefined && {
|
|
720
|
+
frame: data['frame'],
|
|
721
|
+
}),
|
|
722
|
+
...(data['source'] !== undefined && { source: data['source'] }),
|
|
723
|
+
...(data['canonical'] !== undefined && { canonical: data['canonical'] }),
|
|
724
|
+
createdAt: new Date(row.created_at),
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
/**
|
|
729
|
+
* Convenience factory: create a {@link DOSqliteAdapter}.
|
|
730
|
+
*
|
|
731
|
+
* @example
|
|
732
|
+
* ```ts
|
|
733
|
+
* import { createDOSqliteAdapter } from 'ai-database'
|
|
734
|
+
*
|
|
735
|
+
* const adapter = createDOSqliteAdapter({
|
|
736
|
+
* namespace: env.DATABASE,
|
|
737
|
+
* sharding: 'per-cascade',
|
|
738
|
+
* defaultCascadeId: cascade.id,
|
|
739
|
+
* })
|
|
740
|
+
* ```
|
|
741
|
+
*/
|
|
742
|
+
export function createDOSqliteAdapter(options) {
|
|
743
|
+
return new DOSqliteAdapter(options);
|
|
744
|
+
}
|
|
745
|
+
//# sourceMappingURL=do-sqlite-adapter.js.map
|