@vendoai/store 0.1.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +81 -0
- package/dist/blobs.d.ts +5 -0
- package/dist/blobs.d.ts.map +1 -0
- package/dist/blobs.js +62 -0
- package/dist/blobs.js.map +1 -0
- package/dist/crypto.d.ts +10 -0
- package/dist/crypto.d.ts.map +1 -0
- package/dist/crypto.js +44 -0
- package/dist/crypto.js.map +1 -0
- package/dist/db.d.ts +26 -26
- package/dist/db.d.ts.map +1 -0
- package/dist/db.js +335 -86
- package/dist/db.js.map +1 -0
- package/dist/erase.d.ts +33 -0
- package/dist/erase.d.ts.map +1 -0
- package/dist/erase.js +107 -0
- package/dist/erase.js.map +1 -0
- package/dist/helpers/approvals.d.ts +12 -0
- package/dist/helpers/approvals.d.ts.map +1 -0
- package/dist/helpers/approvals.js +37 -0
- package/dist/helpers/approvals.js.map +1 -0
- package/dist/helpers/apps.d.ts +15 -0
- package/dist/helpers/apps.d.ts.map +1 -0
- package/dist/helpers/apps.js +42 -0
- package/dist/helpers/apps.js.map +1 -0
- package/dist/helpers/audit.d.ts +24 -0
- package/dist/helpers/audit.d.ts.map +1 -0
- package/dist/helpers/audit.js +79 -0
- package/dist/helpers/audit.js.map +1 -0
- package/dist/helpers/grants.d.ts +14 -0
- package/dist/helpers/grants.d.ts.map +1 -0
- package/dist/helpers/grants.js +46 -0
- package/dist/helpers/grants.js.map +1 -0
- package/dist/helpers/rows.d.ts +30 -0
- package/dist/helpers/rows.d.ts.map +1 -0
- package/dist/helpers/rows.js +191 -0
- package/dist/helpers/rows.js.map +1 -0
- package/dist/helpers/runs.d.ts +19 -0
- package/dist/helpers/runs.d.ts.map +1 -0
- package/dist/helpers/runs.js +47 -0
- package/dist/helpers/runs.js.map +1 -0
- package/dist/helpers/state.d.ts +9 -0
- package/dist/helpers/state.d.ts.map +1 -0
- package/dist/helpers/state.js +22 -0
- package/dist/helpers/state.js.map +1 -0
- package/dist/helpers/subjects.d.ts +32 -0
- package/dist/helpers/subjects.d.ts.map +1 -0
- package/dist/helpers/subjects.js +78 -0
- package/dist/helpers/subjects.js.map +1 -0
- package/dist/helpers/threads.d.ts +19 -0
- package/dist/helpers/threads.d.ts.map +1 -0
- package/dist/helpers/threads.js +38 -0
- package/dist/helpers/threads.js.map +1 -0
- package/dist/helpers/types.d.ts +60 -0
- package/dist/helpers/types.d.ts.map +1 -0
- package/dist/helpers/types.js +2 -0
- package/dist/helpers/types.js.map +1 -0
- package/dist/helpers/utils.d.ts +37 -0
- package/dist/helpers/utils.d.ts.map +1 -0
- package/dist/helpers/utils.js +94 -0
- package/dist/helpers/utils.js.map +1 -0
- package/dist/index.d.ts +13 -11
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -15
- package/dist/index.js.map +1 -0
- package/dist/records.d.ts +7 -0
- package/dist/records.d.ts.map +1 -0
- package/dist/records.js +208 -0
- package/dist/records.js.map +1 -0
- package/dist/routing.d.ts +7 -0
- package/dist/routing.d.ts.map +1 -0
- package/dist/routing.js +393 -0
- package/dist/routing.js.map +1 -0
- package/dist/schema.d.ts +18 -1420
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +206 -114
- package/dist/schema.js.map +1 -0
- package/dist/secrets.d.ts +13 -0
- package/dist/secrets.d.ts.map +1 -0
- package/dist/secrets.js +86 -0
- package/dist/secrets.js.map +1 -0
- package/dist/sessions.d.ts +53 -0
- package/dist/sessions.d.ts.map +1 -0
- package/dist/sessions.js +85 -0
- package/dist/sessions.js.map +1 -0
- package/dist/store.d.ts +24 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +49 -0
- package/dist/store.js.map +1 -0
- package/dist/validate.d.ts +25 -0
- package/dist/validate.d.ts.map +1 -0
- package/dist/validate.js +144 -0
- package/dist/validate.js.map +1 -0
- package/package.json +15 -14
- package/dist/automation-store.d.ts +0 -62
- package/dist/automation-store.js +0 -675
- package/dist/connections-store.d.ts +0 -25
- package/dist/connections-store.js +0 -102
- package/dist/decision-store.d.ts +0 -8
- package/dist/decision-store.js +0 -39
- package/dist/meta.d.ts +0 -5
- package/dist/meta.js +0 -20
- package/dist/thread-store.d.ts +0 -5
- package/dist/thread-store.js +0 -209
- package/dist/vendo-registry.d.ts +0 -5
- package/dist/vendo-registry.js +0 -52
- package/migrations/.gitkeep +0 -0
- package/migrations/0000_living_kate_bishop.sql +0 -110
- package/migrations/0001_brainy_tinkerer.sql +0 -13
- package/migrations/meta/0000_snapshot.json +0 -757
- package/migrations/meta/0001_snapshot.json +0 -859
- package/migrations/meta/_journal.json +0 -20
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import type { Principal } from "@vendoai/core";
|
|
2
|
-
import type { VendoDb } from "./db.js";
|
|
3
|
-
/** Structural stand-in for `vendo/server`'s `IntegrationCatalogEntry`. */
|
|
4
|
-
export interface IntegrationCatalogEntry {
|
|
5
|
-
id: string;
|
|
6
|
-
name: string;
|
|
7
|
-
}
|
|
8
|
-
export interface DurableConnectionsStore {
|
|
9
|
-
list(): Promise<Array<IntegrationCatalogEntry & {
|
|
10
|
-
connected: boolean;
|
|
11
|
-
}>>;
|
|
12
|
-
connect(id: string): Promise<void>;
|
|
13
|
-
disconnect(id: string): Promise<void>;
|
|
14
|
-
connectedToolkits(): Promise<string[]>;
|
|
15
|
-
/** Record the Composio connected-account id once the OAuth flow lands. */
|
|
16
|
-
setConnectedAccount(toolkit: string, connectedAccountId: string): Promise<void>;
|
|
17
|
-
/** Webhook routing: which principal owns this connected account? */
|
|
18
|
-
findByConnectedAccount(connectedAccountId: string): Promise<{
|
|
19
|
-
toolkit: string;
|
|
20
|
-
principal: Principal;
|
|
21
|
-
} | undefined>;
|
|
22
|
-
}
|
|
23
|
-
export declare function createDrizzleConnectionsStore(handle: VendoDb, scope: Principal, catalog: IntegrationCatalogEntry[], opts?: {
|
|
24
|
-
now?: () => string;
|
|
25
|
-
}): DurableConnectionsStore;
|
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* DrizzleConnectionsStore — durable implementation of the STRUCTURAL shape of
|
|
3
|
-
* `vendo/server`'s `ConnectionsStore` (packages/vendo-server/src/connections.ts):
|
|
4
|
-
* which toolkits are connected, i.e. what the agent ingests. Duck-typed
|
|
5
|
-
* locally (not imported) to avoid a server -> store -> server dependency cycle.
|
|
6
|
-
* Both the upstream interface and this durable port are fully async — every
|
|
7
|
-
* operation here is a DB round-trip, and the upstream in-memory store matches
|
|
8
|
-
* that shape (even though it never actually awaits) so the two are drop-in
|
|
9
|
-
* compatible: `vendo/server`'s handler wires this in whenever durable
|
|
10
|
-
* storage is configured, the in-memory one otherwise.
|
|
11
|
-
*
|
|
12
|
-
* Two additions beyond the upstream shape (webhook + integrations flow):
|
|
13
|
-
* - `setConnectedAccount` records the Composio connected-account id once the
|
|
14
|
-
* OAuth flow lands.
|
|
15
|
-
* - `findByConnectedAccount` is a CROSS-PRINCIPAL lookup (unlike every other
|
|
16
|
-
* method here, which is scoped to the store's own Principal) — webhook
|
|
17
|
-
* routing needs to answer "which principal owns this connected account?"
|
|
18
|
-
* before it has a scope to work with.
|
|
19
|
-
*/
|
|
20
|
-
import { and, eq } from "drizzle-orm";
|
|
21
|
-
import { connections } from "./schema.js";
|
|
22
|
-
const CONNECTED = "connected";
|
|
23
|
-
const DISCONNECTED = "disconnected";
|
|
24
|
-
export function createDrizzleConnectionsStore(handle, scope, catalog, opts = {}) {
|
|
25
|
-
const db = handle.db;
|
|
26
|
-
const now = opts.now ?? (() => new Date().toISOString());
|
|
27
|
-
const validIds = new Set(catalog.map((c) => c.id));
|
|
28
|
-
async function connectedSet() {
|
|
29
|
-
const rows = await db
|
|
30
|
-
.select({ toolkit: connections.toolkit })
|
|
31
|
-
.from(connections)
|
|
32
|
-
.where(and(eq(connections.tenantId, scope.tenantId), eq(connections.subject, scope.subject), eq(connections.status, CONNECTED)));
|
|
33
|
-
return new Set(rows.map((r) => r.toolkit));
|
|
34
|
-
}
|
|
35
|
-
return {
|
|
36
|
-
async list() {
|
|
37
|
-
const connected = await connectedSet();
|
|
38
|
-
return catalog.map((c) => ({ ...c, connected: connected.has(c.id) }));
|
|
39
|
-
},
|
|
40
|
-
async connect(id) {
|
|
41
|
-
if (!validIds.has(id))
|
|
42
|
-
return;
|
|
43
|
-
await db
|
|
44
|
-
.insert(connections)
|
|
45
|
-
.values({
|
|
46
|
-
toolkit: id,
|
|
47
|
-
tenantId: scope.tenantId,
|
|
48
|
-
subject: scope.subject,
|
|
49
|
-
connectedAccountId: null,
|
|
50
|
-
status: CONNECTED,
|
|
51
|
-
createdAt: now(),
|
|
52
|
-
})
|
|
53
|
-
.onConflictDoUpdate({
|
|
54
|
-
target: [connections.tenantId, connections.subject, connections.toolkit],
|
|
55
|
-
set: { status: CONNECTED },
|
|
56
|
-
});
|
|
57
|
-
},
|
|
58
|
-
async disconnect(id) {
|
|
59
|
-
await db
|
|
60
|
-
.update(connections)
|
|
61
|
-
.set({ status: DISCONNECTED })
|
|
62
|
-
.where(and(eq(connections.tenantId, scope.tenantId), eq(connections.subject, scope.subject), eq(connections.toolkit, id)));
|
|
63
|
-
},
|
|
64
|
-
async connectedToolkits() {
|
|
65
|
-
const connected = await connectedSet();
|
|
66
|
-
return catalog.filter((c) => connected.has(c.id)).map((c) => c.id);
|
|
67
|
-
},
|
|
68
|
-
async setConnectedAccount(toolkit, connectedAccountId) {
|
|
69
|
-
if (!validIds.has(toolkit))
|
|
70
|
-
return;
|
|
71
|
-
await db
|
|
72
|
-
.insert(connections)
|
|
73
|
-
.values({
|
|
74
|
-
toolkit,
|
|
75
|
-
tenantId: scope.tenantId,
|
|
76
|
-
subject: scope.subject,
|
|
77
|
-
connectedAccountId,
|
|
78
|
-
status: CONNECTED,
|
|
79
|
-
createdAt: now(),
|
|
80
|
-
})
|
|
81
|
-
.onConflictDoUpdate({
|
|
82
|
-
target: [connections.tenantId, connections.subject, connections.toolkit],
|
|
83
|
-
set: { connectedAccountId, status: CONNECTED },
|
|
84
|
-
});
|
|
85
|
-
},
|
|
86
|
-
async findByConnectedAccount(connectedAccountId) {
|
|
87
|
-
// Status-filtered: disconnect() flips the row to DISCONNECTED without
|
|
88
|
-
// clearing connectedAccountId (so reconnecting can leave routing
|
|
89
|
-
// history intact) — a redelivered/live webhook for a disconnected
|
|
90
|
-
// account must NOT resolve a principal, or a revoked toolkit would
|
|
91
|
-
// keep firing automations after the user disconnected it.
|
|
92
|
-
const rows = await db
|
|
93
|
-
.select()
|
|
94
|
-
.from(connections)
|
|
95
|
-
.where(and(eq(connections.connectedAccountId, connectedAccountId), eq(connections.status, CONNECTED)));
|
|
96
|
-
const row = rows[0];
|
|
97
|
-
if (!row)
|
|
98
|
-
return undefined;
|
|
99
|
-
return { toolkit: row.toolkit, principal: { tenantId: row.tenantId, subject: row.subject } };
|
|
100
|
-
},
|
|
101
|
-
};
|
|
102
|
-
}
|
package/dist/decision-store.d.ts
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import type { Principal } from "@vendoai/core";
|
|
2
|
-
import type { DecisionStore } from "@vendoai/runtime";
|
|
3
|
-
import type { VendoDb } from "./db.js";
|
|
4
|
-
/** `createDrizzleDecisionStore(handle, scope)` — one store per Principal,
|
|
5
|
-
* mirroring how `rememberDecisions` is wired per-request. */
|
|
6
|
-
export declare function createDrizzleDecisionStore(handle: VendoDb, scope: Principal, opts?: {
|
|
7
|
-
now?: () => string;
|
|
8
|
-
}): DecisionStore;
|
package/dist/decision-store.js
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* DrizzleDecisionStore — durable port of the runtime `DecisionStore` seam
|
|
3
|
-
* (packages/vendo-runtime/src/policy/remember.ts): memoised ask-once
|
|
4
|
-
* approval decisions, keyed by an opaque canonical key and scoped per
|
|
5
|
-
* Principal. Not a junk drawer — see the `decisions` table doc in schema.ts.
|
|
6
|
-
*/
|
|
7
|
-
import { and, eq } from "drizzle-orm";
|
|
8
|
-
import { decisions } from "./schema.js";
|
|
9
|
-
/** `createDrizzleDecisionStore(handle, scope)` — one store per Principal,
|
|
10
|
-
* mirroring how `rememberDecisions` is wired per-request. */
|
|
11
|
-
export function createDrizzleDecisionStore(handle, scope, opts = {}) {
|
|
12
|
-
const db = handle.db;
|
|
13
|
-
const now = opts.now ?? (() => new Date().toISOString());
|
|
14
|
-
return {
|
|
15
|
-
async get(canonicalKey) {
|
|
16
|
-
const rows = await db
|
|
17
|
-
.select()
|
|
18
|
-
.from(decisions)
|
|
19
|
-
.where(and(eq(decisions.tenantId, scope.tenantId), eq(decisions.subject, scope.subject), eq(decisions.canonicalKey, canonicalKey)));
|
|
20
|
-
const row = rows[0];
|
|
21
|
-
return row ? row.decision : undefined;
|
|
22
|
-
},
|
|
23
|
-
async set(canonicalKey, decision) {
|
|
24
|
-
await db
|
|
25
|
-
.insert(decisions)
|
|
26
|
-
.values({
|
|
27
|
-
tenantId: scope.tenantId,
|
|
28
|
-
subject: scope.subject,
|
|
29
|
-
canonicalKey,
|
|
30
|
-
decision,
|
|
31
|
-
createdAt: now(),
|
|
32
|
-
})
|
|
33
|
-
.onConflictDoUpdate({
|
|
34
|
-
target: [decisions.tenantId, decisions.subject, decisions.canonicalKey],
|
|
35
|
-
set: { decision },
|
|
36
|
-
});
|
|
37
|
-
},
|
|
38
|
-
};
|
|
39
|
-
}
|
package/dist/meta.d.ts
DELETED
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
import type { VendoDb } from "./db.js";
|
|
2
|
-
/** Read one meta value; `undefined` on a miss. */
|
|
3
|
-
export declare function getMeta(handle: VendoDb, key: string): Promise<unknown>;
|
|
4
|
-
/** Upsert one meta value (last write wins). */
|
|
5
|
-
export declare function setMeta(handle: VendoDb, key: string, value: unknown): Promise<void>;
|
package/dist/meta.js
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `meta` helpers — the tiny operational KV over the `vendo.meta` table
|
|
3
|
-
* (scheduler heartbeat, future flags). NOT for domain data; see the table
|
|
4
|
-
* doc in schema.ts.
|
|
5
|
-
*/
|
|
6
|
-
import { eq } from "drizzle-orm";
|
|
7
|
-
import { meta } from "./schema.js";
|
|
8
|
-
/** Read one meta value; `undefined` on a miss. */
|
|
9
|
-
export async function getMeta(handle, key) {
|
|
10
|
-
const rows = await handle.db.select().from(meta).where(eq(meta.key, key));
|
|
11
|
-
return rows[0]?.value;
|
|
12
|
-
}
|
|
13
|
-
/** Upsert one meta value (last write wins). */
|
|
14
|
-
export async function setMeta(handle, key, value) {
|
|
15
|
-
const updatedAt = new Date().toISOString();
|
|
16
|
-
await handle.db
|
|
17
|
-
.insert(meta)
|
|
18
|
-
.values({ key, value, updatedAt })
|
|
19
|
-
.onConflictDoUpdate({ target: meta.key, set: { value, updatedAt } });
|
|
20
|
-
}
|
package/dist/thread-store.d.ts
DELETED
package/dist/thread-store.js
DELETED
|
@@ -1,209 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* DrizzleThreadStore — durable port of the core `ThreadStore` seam
|
|
3
|
-
* (packages/vendo-core/src/seams/store.ts). Behavioral spec:
|
|
4
|
-
* InMemoryThreadStore (packages/vendo-runtime/src/embedded/in-memory-store.ts).
|
|
5
|
-
*
|
|
6
|
-
* Seq allocation is race-safe: `threads.nextSeq` is reserved with an atomic
|
|
7
|
-
* `UPDATE ... SET next_seq = next_seq + n RETURNING next_seq` inside a
|
|
8
|
-
* transaction, never `MAX(seq)+1` (which double-allocates under concurrency —
|
|
9
|
-
* see the Task 9 plan note). `upsertMessages` reserves seqs ONLY for messages
|
|
10
|
-
* that are genuinely new; pre-existing message ids are updated in place
|
|
11
|
-
* (message column only — their seq, and therefore position, never moves).
|
|
12
|
-
* Existing-vs-new is resolved inside the same transaction as the reservation,
|
|
13
|
-
* so two concurrent upserts of disjoint message ids always get disjoint seq
|
|
14
|
-
* ranges without ever touching the `thread_messages_seq_uq` index twice for
|
|
15
|
-
* the same value (a single INSERT ... ON CONFLICT covering both new and
|
|
16
|
-
* pre-existing rows would risk exactly that: passing an existing row's own
|
|
17
|
-
* seq back through the VALUES list can collide with the seq-uniqueness index,
|
|
18
|
-
* which isn't the ON CONFLICT arbiter and so isn't suppressed — Postgres still
|
|
19
|
-
* raises 23505 for a violation on a non-arbiter unique index. Splitting the
|
|
20
|
-
* write into "plain UPDATE for existing ids" + "plain INSERT of freshly
|
|
21
|
-
* reserved seqs for new ids" avoids that path entirely.)
|
|
22
|
-
*
|
|
23
|
-
* Remaining race: two CONCURRENT txns can both run their existing-ids SELECT
|
|
24
|
-
* before either commits and both classify the SAME message id as "new" —
|
|
25
|
-
* each reserves its own seq block and both attempt to INSERT that message
|
|
26
|
-
* id. The new-row INSERT below carries an `ON CONFLICT (tenant_id, subject,
|
|
27
|
-
* thread_id, message_id) DO UPDATE` naming that exact unique index as its
|
|
28
|
-
* arbiter, so the loser's insert becomes an UPDATE of the winner's row
|
|
29
|
-
* (message content only) instead of a 23505 unique-violation. The loser's
|
|
30
|
-
* reserved seq is simply never used — a benign gap in the sequence, not a
|
|
31
|
-
* correctness issue (seq only needs to be strictly ordered, not contiguous).
|
|
32
|
-
*/
|
|
33
|
-
import { randomUUID } from "node:crypto";
|
|
34
|
-
import { and, eq, inArray, sql } from "drizzle-orm";
|
|
35
|
-
import { threadMessages, threads } from "./schema.js";
|
|
36
|
-
import { toIso } from "./automation-store.js";
|
|
37
|
-
function rowToThread(row) {
|
|
38
|
-
const record = {
|
|
39
|
-
id: row.id,
|
|
40
|
-
tenantId: row.tenantId,
|
|
41
|
-
subject: row.subject,
|
|
42
|
-
createdAt: toIso(row.createdAt),
|
|
43
|
-
updatedAt: toIso(row.updatedAt),
|
|
44
|
-
};
|
|
45
|
-
if (row.title != null)
|
|
46
|
-
record.title = row.title;
|
|
47
|
-
return record;
|
|
48
|
-
}
|
|
49
|
-
export function createDrizzleThreadStore(handle, opts = {}) {
|
|
50
|
-
const db = handle.db;
|
|
51
|
-
const now = opts.now ?? (() => new Date().toISOString());
|
|
52
|
-
function withTransaction(fn) {
|
|
53
|
-
return handle.db.transaction((tx) => fn(tx));
|
|
54
|
-
}
|
|
55
|
-
async function selectThreadRow(tx, scope, threadId) {
|
|
56
|
-
const rows = await tx
|
|
57
|
-
.select()
|
|
58
|
-
.from(threads)
|
|
59
|
-
.where(and(eq(threads.tenantId, scope.tenantId), eq(threads.subject, scope.subject), eq(threads.id, threadId)));
|
|
60
|
-
return rows[0];
|
|
61
|
-
}
|
|
62
|
-
/** Atomically reserves `n` seqs on the thread row and returns the first one
|
|
63
|
-
* in the reserved block. Raw SQL (like `claimPendingApproval` in
|
|
64
|
-
* automation-store.ts) because drizzle's `.returning()` doesn't type-check
|
|
65
|
-
* cleanly against the pglite/node-postgres union `Db` bridge type. */
|
|
66
|
-
async function reserveSeqs(tx, scope, threadId, n, at) {
|
|
67
|
-
const result = await tx.execute(sql `
|
|
68
|
-
UPDATE vendo.threads
|
|
69
|
-
SET next_seq = next_seq + ${n}, updated_at = ${at}
|
|
70
|
-
WHERE tenant_id = ${scope.tenantId} AND subject = ${scope.subject} AND id = ${threadId}
|
|
71
|
-
RETURNING next_seq
|
|
72
|
-
`);
|
|
73
|
-
const rows = result.rows;
|
|
74
|
-
const reserved = rows[0]?.next_seq ?? n;
|
|
75
|
-
return reserved - n;
|
|
76
|
-
}
|
|
77
|
-
return {
|
|
78
|
-
async create(scope, init = {}) {
|
|
79
|
-
const id = randomUUID();
|
|
80
|
-
const createdAt = now();
|
|
81
|
-
await db.insert(threads).values({
|
|
82
|
-
id,
|
|
83
|
-
tenantId: scope.tenantId,
|
|
84
|
-
subject: scope.subject,
|
|
85
|
-
title: init.title ?? null,
|
|
86
|
-
nextSeq: 0,
|
|
87
|
-
createdAt,
|
|
88
|
-
updatedAt: createdAt,
|
|
89
|
-
});
|
|
90
|
-
const record = {
|
|
91
|
-
id,
|
|
92
|
-
tenantId: scope.tenantId,
|
|
93
|
-
subject: scope.subject,
|
|
94
|
-
createdAt,
|
|
95
|
-
updatedAt: createdAt,
|
|
96
|
-
};
|
|
97
|
-
if (init.title !== undefined)
|
|
98
|
-
record.title = init.title;
|
|
99
|
-
return record;
|
|
100
|
-
},
|
|
101
|
-
async get(scope, threadId) {
|
|
102
|
-
const row = await selectThreadRow(db, scope, threadId);
|
|
103
|
-
return row ? rowToThread(row) : undefined;
|
|
104
|
-
},
|
|
105
|
-
async list(scope) {
|
|
106
|
-
const rows = await db
|
|
107
|
-
.select()
|
|
108
|
-
.from(threads)
|
|
109
|
-
.where(and(eq(threads.tenantId, scope.tenantId), eq(threads.subject, scope.subject)));
|
|
110
|
-
return rows.map(rowToThread);
|
|
111
|
-
},
|
|
112
|
-
async appendMessages(scope, threadId, messages) {
|
|
113
|
-
const owned = await selectThreadRow(db, scope, threadId);
|
|
114
|
-
if (!owned) {
|
|
115
|
-
throw new Error(`unknown thread "${threadId}" for scope ${scope.tenantId}/${scope.subject}`);
|
|
116
|
-
}
|
|
117
|
-
if (messages.length === 0)
|
|
118
|
-
return;
|
|
119
|
-
await withTransaction(async (tx) => {
|
|
120
|
-
const at = now();
|
|
121
|
-
const startSeq = await reserveSeqs(tx, scope, threadId, messages.length, at);
|
|
122
|
-
await tx.insert(threadMessages).values(messages.map((m, i) => ({
|
|
123
|
-
tenantId: scope.tenantId,
|
|
124
|
-
subject: scope.subject,
|
|
125
|
-
threadId,
|
|
126
|
-
messageId: m.id,
|
|
127
|
-
seq: startSeq + i,
|
|
128
|
-
message: m,
|
|
129
|
-
})));
|
|
130
|
-
});
|
|
131
|
-
},
|
|
132
|
-
async getMessages(scope, threadId) {
|
|
133
|
-
const rows = await db
|
|
134
|
-
.select()
|
|
135
|
-
.from(threadMessages)
|
|
136
|
-
.where(and(eq(threadMessages.tenantId, scope.tenantId), eq(threadMessages.subject, scope.subject), eq(threadMessages.threadId, threadId)))
|
|
137
|
-
.orderBy(threadMessages.seq);
|
|
138
|
-
return rows.map((row) => row.message);
|
|
139
|
-
},
|
|
140
|
-
async upsertMessages(scope, threadId, messages) {
|
|
141
|
-
if (messages.length === 0)
|
|
142
|
-
return;
|
|
143
|
-
await withTransaction(async (tx) => {
|
|
144
|
-
const at = now();
|
|
145
|
-
const existingThread = await selectThreadRow(tx, scope, threadId);
|
|
146
|
-
if (!existingThread) {
|
|
147
|
-
// The client owns thread ids for upsert (ai-SDK resume writes before
|
|
148
|
-
// any explicit create() call) — an unrecognized threadId is a
|
|
149
|
-
// first-write, not an error. `onConflictDoNothing` guards the (rare)
|
|
150
|
-
// race of two concurrent first-writes to the same unknown id.
|
|
151
|
-
await tx
|
|
152
|
-
.insert(threads)
|
|
153
|
-
.values({
|
|
154
|
-
id: threadId,
|
|
155
|
-
tenantId: scope.tenantId,
|
|
156
|
-
subject: scope.subject,
|
|
157
|
-
nextSeq: 0,
|
|
158
|
-
createdAt: at,
|
|
159
|
-
updatedAt: at,
|
|
160
|
-
})
|
|
161
|
-
.onConflictDoNothing();
|
|
162
|
-
}
|
|
163
|
-
const ids = messages.map((m) => m.id);
|
|
164
|
-
const existingRows = await tx
|
|
165
|
-
.select({ messageId: threadMessages.messageId })
|
|
166
|
-
.from(threadMessages)
|
|
167
|
-
.where(and(eq(threadMessages.tenantId, scope.tenantId), eq(threadMessages.subject, scope.subject), eq(threadMessages.threadId, threadId), inArray(threadMessages.messageId, ids)));
|
|
168
|
-
const existingIds = new Set(existingRows.map((r) => r.messageId));
|
|
169
|
-
const existingMessages = messages.filter((m) => existingIds.has(m.id));
|
|
170
|
-
const newMessages = messages.filter((m) => !existingIds.has(m.id));
|
|
171
|
-
// Update pre-existing rows in place: message column only, seq (and
|
|
172
|
-
// therefore position) is never touched.
|
|
173
|
-
for (const m of existingMessages) {
|
|
174
|
-
await tx
|
|
175
|
-
.update(threadMessages)
|
|
176
|
-
.set({ message: m })
|
|
177
|
-
.where(and(eq(threadMessages.tenantId, scope.tenantId), eq(threadMessages.subject, scope.subject), eq(threadMessages.threadId, threadId), eq(threadMessages.messageId, m.id)));
|
|
178
|
-
}
|
|
179
|
-
if (newMessages.length > 0) {
|
|
180
|
-
const startSeq = await reserveSeqs(tx, scope, threadId, newMessages.length, at);
|
|
181
|
-
await tx
|
|
182
|
-
.insert(threadMessages)
|
|
183
|
-
.values(newMessages.map((m, i) => ({
|
|
184
|
-
tenantId: scope.tenantId,
|
|
185
|
-
subject: scope.subject,
|
|
186
|
-
threadId,
|
|
187
|
-
messageId: m.id,
|
|
188
|
-
seq: startSeq + i,
|
|
189
|
-
message: m,
|
|
190
|
-
})))
|
|
191
|
-
// Concurrent-same-new-message race: see the module doc. The
|
|
192
|
-
// arbiter is the message-id unique index, not the seq one, so
|
|
193
|
-
// this can never suppress a genuine seq collision.
|
|
194
|
-
.onConflictDoUpdate({
|
|
195
|
-
target: [threadMessages.tenantId, threadMessages.subject, threadMessages.threadId, threadMessages.messageId],
|
|
196
|
-
set: { message: sql `excluded.message` },
|
|
197
|
-
});
|
|
198
|
-
}
|
|
199
|
-
else {
|
|
200
|
-
// Still bump updatedAt even when nothing new was reserved.
|
|
201
|
-
await tx
|
|
202
|
-
.update(threads)
|
|
203
|
-
.set({ updatedAt: at })
|
|
204
|
-
.where(and(eq(threads.tenantId, scope.tenantId), eq(threads.subject, scope.subject), eq(threads.id, threadId)));
|
|
205
|
-
}
|
|
206
|
-
});
|
|
207
|
-
},
|
|
208
|
-
};
|
|
209
|
-
}
|
package/dist/vendo-registry.d.ts
DELETED
package/dist/vendo-registry.js
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* DrizzleSavedVendoStore — durable port of the core `SavedVendoStore`
|
|
3
|
-
* seam (packages/vendo-core/src/seams/store.ts). Behavioral spec:
|
|
4
|
-
* InMemorySavedVendoStore (packages/vendo-runtime/src/embedded/in-memory-store.ts).
|
|
5
|
-
*
|
|
6
|
-
* The whole `SavedVendo` record (uiTree, query, originatingPrompt, etc.)
|
|
7
|
-
* lives verbatim in the `record` jsonb column; `updatedAt` is denormalized
|
|
8
|
-
* onto its own column purely so `list()` can order by it without unpacking
|
|
9
|
-
* jsonb per row.
|
|
10
|
-
*/
|
|
11
|
-
import { randomUUID } from "node:crypto";
|
|
12
|
-
import { and, desc, eq } from "drizzle-orm";
|
|
13
|
-
import { savedVendos } from "./schema.js";
|
|
14
|
-
export function createDrizzleSavedVendoStore(handle, opts = {}) {
|
|
15
|
-
const db = handle.db;
|
|
16
|
-
const now = opts.now ?? (() => new Date().toISOString());
|
|
17
|
-
return {
|
|
18
|
-
async save(scope, vendo) {
|
|
19
|
-
const id = randomUUID();
|
|
20
|
-
const createdAt = now();
|
|
21
|
-
const record = { ...vendo, id, createdAt, updatedAt: createdAt };
|
|
22
|
-
await db.insert(savedVendos).values({
|
|
23
|
-
id,
|
|
24
|
-
tenantId: scope.tenantId,
|
|
25
|
-
subject: scope.subject,
|
|
26
|
-
record,
|
|
27
|
-
updatedAt: createdAt,
|
|
28
|
-
});
|
|
29
|
-
return record;
|
|
30
|
-
},
|
|
31
|
-
async get(scope, id) {
|
|
32
|
-
const rows = await db
|
|
33
|
-
.select()
|
|
34
|
-
.from(savedVendos)
|
|
35
|
-
.where(and(eq(savedVendos.tenantId, scope.tenantId), eq(savedVendos.subject, scope.subject), eq(savedVendos.id, id)));
|
|
36
|
-
return rows[0] ? rows[0].record : undefined;
|
|
37
|
-
},
|
|
38
|
-
async list(scope) {
|
|
39
|
-
const rows = await db
|
|
40
|
-
.select()
|
|
41
|
-
.from(savedVendos)
|
|
42
|
-
.where(and(eq(savedVendos.tenantId, scope.tenantId), eq(savedVendos.subject, scope.subject)))
|
|
43
|
-
.orderBy(desc(savedVendos.updatedAt));
|
|
44
|
-
return rows.map((row) => row.record);
|
|
45
|
-
},
|
|
46
|
-
async delete(scope, id) {
|
|
47
|
-
await db
|
|
48
|
-
.delete(savedVendos)
|
|
49
|
-
.where(and(eq(savedVendos.tenantId, scope.tenantId), eq(savedVendos.subject, scope.subject), eq(savedVendos.id, id)));
|
|
50
|
-
},
|
|
51
|
-
};
|
|
52
|
-
}
|
package/migrations/.gitkeep
DELETED
|
File without changes
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
CREATE SCHEMA IF NOT EXISTS "vendo";
|
|
2
|
-
--> statement-breakpoint
|
|
3
|
-
CREATE TABLE IF NOT EXISTS "vendo"."automation_runs" (
|
|
4
|
-
"id" text PRIMARY KEY NOT NULL,
|
|
5
|
-
"automation_id" text NOT NULL,
|
|
6
|
-
"tenant_id" text NOT NULL,
|
|
7
|
-
"subject" text NOT NULL,
|
|
8
|
-
"version" integer NOT NULL,
|
|
9
|
-
"manifest_hash" text,
|
|
10
|
-
"status" text NOT NULL,
|
|
11
|
-
"outcome" text,
|
|
12
|
-
"trigger" jsonb NOT NULL,
|
|
13
|
-
"steps" jsonb NOT NULL,
|
|
14
|
-
"pending_approval" jsonb,
|
|
15
|
-
"error" text,
|
|
16
|
-
"is_test" boolean NOT NULL,
|
|
17
|
-
"started_at" timestamp with time zone NOT NULL,
|
|
18
|
-
"finished_at" timestamp with time zone
|
|
19
|
-
);
|
|
20
|
-
--> statement-breakpoint
|
|
21
|
-
CREATE TABLE IF NOT EXISTS "vendo"."automation_versions" (
|
|
22
|
-
"automation_id" text NOT NULL,
|
|
23
|
-
"version" integer NOT NULL,
|
|
24
|
-
"spec" jsonb NOT NULL,
|
|
25
|
-
"dsl_version" integer NOT NULL,
|
|
26
|
-
"manifest_hash" text,
|
|
27
|
-
"grants" jsonb NOT NULL,
|
|
28
|
-
"created_by" text NOT NULL,
|
|
29
|
-
"created_at" timestamp with time zone NOT NULL,
|
|
30
|
-
CONSTRAINT "automation_versions_automation_id_version_pk" PRIMARY KEY("automation_id","version")
|
|
31
|
-
);
|
|
32
|
-
--> statement-breakpoint
|
|
33
|
-
CREATE TABLE IF NOT EXISTS "vendo"."automations" (
|
|
34
|
-
"id" text PRIMARY KEY NOT NULL,
|
|
35
|
-
"tenant_id" text NOT NULL,
|
|
36
|
-
"subject" text NOT NULL,
|
|
37
|
-
"name" text NOT NULL,
|
|
38
|
-
"status" text NOT NULL,
|
|
39
|
-
"disabled_reason" text,
|
|
40
|
-
"spec" jsonb NOT NULL,
|
|
41
|
-
"current_version" integer NOT NULL,
|
|
42
|
-
"trigger_kind" text NOT NULL,
|
|
43
|
-
"trigger_key" text,
|
|
44
|
-
"counters" jsonb NOT NULL,
|
|
45
|
-
"created_from_thread_id" text,
|
|
46
|
-
"created_at" timestamp with time zone NOT NULL,
|
|
47
|
-
"updated_at" timestamp with time zone NOT NULL
|
|
48
|
-
);
|
|
49
|
-
--> statement-breakpoint
|
|
50
|
-
CREATE TABLE IF NOT EXISTS "vendo"."connections" (
|
|
51
|
-
"toolkit" text NOT NULL,
|
|
52
|
-
"tenant_id" text NOT NULL,
|
|
53
|
-
"subject" text NOT NULL,
|
|
54
|
-
"connected_account_id" text,
|
|
55
|
-
"status" text NOT NULL,
|
|
56
|
-
"created_at" timestamp with time zone NOT NULL,
|
|
57
|
-
CONSTRAINT "connections_tenant_id_subject_toolkit_pk" PRIMARY KEY("tenant_id","subject","toolkit")
|
|
58
|
-
);
|
|
59
|
-
--> statement-breakpoint
|
|
60
|
-
CREATE TABLE IF NOT EXISTS "vendo"."decisions" (
|
|
61
|
-
"tenant_id" text NOT NULL,
|
|
62
|
-
"subject" text NOT NULL,
|
|
63
|
-
"canonical_key" text NOT NULL,
|
|
64
|
-
"decision" jsonb NOT NULL,
|
|
65
|
-
"created_at" timestamp with time zone NOT NULL,
|
|
66
|
-
CONSTRAINT "decisions_tenant_id_subject_canonical_key_pk" PRIMARY KEY("tenant_id","subject","canonical_key")
|
|
67
|
-
);
|
|
68
|
-
--> statement-breakpoint
|
|
69
|
-
CREATE TABLE IF NOT EXISTS "vendo"."meta" (
|
|
70
|
-
"key" text PRIMARY KEY NOT NULL,
|
|
71
|
-
"value" jsonb NOT NULL,
|
|
72
|
-
"updated_at" timestamp with time zone NOT NULL
|
|
73
|
-
);
|
|
74
|
-
--> statement-breakpoint
|
|
75
|
-
CREATE TABLE IF NOT EXISTS "vendo"."saved_vendos" (
|
|
76
|
-
"id" text NOT NULL,
|
|
77
|
-
"tenant_id" text NOT NULL,
|
|
78
|
-
"subject" text NOT NULL,
|
|
79
|
-
"record" jsonb NOT NULL,
|
|
80
|
-
"updated_at" timestamp with time zone NOT NULL,
|
|
81
|
-
CONSTRAINT "saved_vendos_tenant_id_subject_id_pk" PRIMARY KEY("tenant_id","subject","id")
|
|
82
|
-
);
|
|
83
|
-
--> statement-breakpoint
|
|
84
|
-
CREATE TABLE IF NOT EXISTS "vendo"."thread_messages" (
|
|
85
|
-
"row_id" bigserial PRIMARY KEY NOT NULL,
|
|
86
|
-
"tenant_id" text NOT NULL,
|
|
87
|
-
"subject" text NOT NULL,
|
|
88
|
-
"thread_id" text NOT NULL,
|
|
89
|
-
"message_id" text NOT NULL,
|
|
90
|
-
"seq" integer NOT NULL,
|
|
91
|
-
"message" jsonb NOT NULL
|
|
92
|
-
);
|
|
93
|
-
--> statement-breakpoint
|
|
94
|
-
CREATE TABLE IF NOT EXISTS "vendo"."threads" (
|
|
95
|
-
"id" text NOT NULL,
|
|
96
|
-
"tenant_id" text NOT NULL,
|
|
97
|
-
"subject" text NOT NULL,
|
|
98
|
-
"title" text,
|
|
99
|
-
"next_seq" integer DEFAULT 0 NOT NULL,
|
|
100
|
-
"created_at" timestamp with time zone NOT NULL,
|
|
101
|
-
"updated_at" timestamp with time zone NOT NULL,
|
|
102
|
-
CONSTRAINT "threads_tenant_id_subject_id_pk" PRIMARY KEY("tenant_id","subject","id")
|
|
103
|
-
);
|
|
104
|
-
--> statement-breakpoint
|
|
105
|
-
CREATE INDEX IF NOT EXISTS "runs_automation_idx" ON "vendo"."automation_runs" USING btree ("automation_id","tenant_id","subject");--> statement-breakpoint
|
|
106
|
-
CREATE INDEX IF NOT EXISTS "automations_scope_idx" ON "vendo"."automations" USING btree ("tenant_id","subject");--> statement-breakpoint
|
|
107
|
-
CREATE INDEX IF NOT EXISTS "automations_trigger_idx" ON "vendo"."automations" USING btree ("trigger_kind","trigger_key");--> statement-breakpoint
|
|
108
|
-
CREATE INDEX IF NOT EXISTS "connections_account_idx" ON "vendo"."connections" USING btree ("connected_account_id");--> statement-breakpoint
|
|
109
|
-
CREATE UNIQUE INDEX IF NOT EXISTS "thread_messages_id_uq" ON "vendo"."thread_messages" USING btree ("tenant_id","subject","thread_id","message_id");--> statement-breakpoint
|
|
110
|
-
CREATE UNIQUE INDEX IF NOT EXISTS "thread_messages_seq_uq" ON "vendo"."thread_messages" USING btree ("tenant_id","subject","thread_id","seq");
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
CREATE TABLE IF NOT EXISTS "vendo"."parked_actions" (
|
|
2
|
-
"id" text PRIMARY KEY NOT NULL,
|
|
3
|
-
"tenant_id" text NOT NULL,
|
|
4
|
-
"subject" text NOT NULL,
|
|
5
|
-
"automation_id" text NOT NULL,
|
|
6
|
-
"run_id" text NOT NULL,
|
|
7
|
-
"resolution" text,
|
|
8
|
-
"requested_at" timestamp with time zone NOT NULL,
|
|
9
|
-
"record" jsonb NOT NULL
|
|
10
|
-
);
|
|
11
|
-
--> statement-breakpoint
|
|
12
|
-
ALTER TABLE "vendo"."automation_runs" ADD COLUMN "parked_count" integer DEFAULT 0 NOT NULL;--> statement-breakpoint
|
|
13
|
-
CREATE INDEX IF NOT EXISTS "parked_actions_scope_idx" ON "vendo"."parked_actions" USING btree ("tenant_id","subject","automation_id","run_id");
|