@vendoai/store 0.1.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/LICENSE +202 -0
- package/dist/automation-store.d.ts +62 -0
- package/dist/automation-store.js +675 -0
- package/dist/connections-store.d.ts +25 -0
- package/dist/connections-store.js +102 -0
- package/dist/db.d.ts +28 -0
- package/dist/db.js +105 -0
- package/dist/decision-store.d.ts +8 -0
- package/dist/decision-store.js +39 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +15 -0
- package/dist/meta.d.ts +5 -0
- package/dist/meta.js +20 -0
- package/dist/schema.d.ts +1420 -0
- package/dist/schema.js +114 -0
- package/dist/thread-store.d.ts +5 -0
- package/dist/thread-store.js +209 -0
- package/dist/vendo-registry.d.ts +5 -0
- package/dist/vendo-registry.js +52 -0
- package/migrations/.gitkeep +0 -0
- package/migrations/0000_living_kate_bishop.sql +110 -0
- package/migrations/0001_brainy_tinkerer.sql +13 -0
- package/migrations/meta/0000_snapshot.json +757 -0
- package/migrations/meta/0001_snapshot.json +859 -0
- package/migrations/meta/_journal.json +20 -0
- package/package.json +58 -0
package/dist/schema.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { pgSchema, text, integer, boolean, jsonb, timestamp, primaryKey, uniqueIndex, index, bigserial } from "drizzle-orm/pg-core";
|
|
2
|
+
export const vendo = pgSchema("vendo");
|
|
3
|
+
export const automations = vendo.table("automations", {
|
|
4
|
+
id: text("id").primaryKey(),
|
|
5
|
+
tenantId: text("tenant_id").notNull(),
|
|
6
|
+
subject: text("subject").notNull(),
|
|
7
|
+
name: text("name").notNull(),
|
|
8
|
+
status: text("status").notNull(),
|
|
9
|
+
disabledReason: text("disabled_reason"),
|
|
10
|
+
spec: jsonb("spec").notNull(),
|
|
11
|
+
currentVersion: integer("current_version").notNull(),
|
|
12
|
+
triggerKind: text("trigger_kind").notNull(),
|
|
13
|
+
triggerKey: text("trigger_key"),
|
|
14
|
+
counters: jsonb("counters").notNull(),
|
|
15
|
+
createdFromThreadId: text("created_from_thread_id"),
|
|
16
|
+
createdAt: timestamp("created_at", { withTimezone: true, mode: "string" }).notNull(),
|
|
17
|
+
updatedAt: timestamp("updated_at", { withTimezone: true, mode: "string" }).notNull(),
|
|
18
|
+
}, (t) => [index("automations_scope_idx").on(t.tenantId, t.subject), index("automations_trigger_idx").on(t.triggerKind, t.triggerKey)]);
|
|
19
|
+
export const automationVersions = vendo.table("automation_versions", {
|
|
20
|
+
automationId: text("automation_id").notNull(),
|
|
21
|
+
version: integer("version").notNull(),
|
|
22
|
+
spec: jsonb("spec").notNull(),
|
|
23
|
+
dslVersion: integer("dsl_version").notNull(),
|
|
24
|
+
manifestHash: text("manifest_hash"),
|
|
25
|
+
grants: jsonb("grants").notNull(),
|
|
26
|
+
createdBy: text("created_by").notNull(),
|
|
27
|
+
createdAt: timestamp("created_at", { withTimezone: true, mode: "string" }).notNull(),
|
|
28
|
+
}, (t) => [primaryKey({ columns: [t.automationId, t.version] })]);
|
|
29
|
+
export const automationRuns = vendo.table("automation_runs", {
|
|
30
|
+
id: text("id").primaryKey(), // firingRunId — DB-level double-fire dedup
|
|
31
|
+
automationId: text("automation_id").notNull(),
|
|
32
|
+
tenantId: text("tenant_id").notNull(),
|
|
33
|
+
subject: text("subject").notNull(),
|
|
34
|
+
version: integer("version").notNull(),
|
|
35
|
+
manifestHash: text("manifest_hash"),
|
|
36
|
+
status: text("status").notNull(),
|
|
37
|
+
outcome: text("outcome"),
|
|
38
|
+
trigger: jsonb("trigger").notNull(),
|
|
39
|
+
steps: jsonb("steps").notNull(),
|
|
40
|
+
pendingApproval: jsonb("pending_approval"),
|
|
41
|
+
error: text("error"),
|
|
42
|
+
isTest: boolean("is_test").notNull(),
|
|
43
|
+
/** ENG-193 §4.6 — cumulative count of parked actions this run created. */
|
|
44
|
+
parkedCount: integer("parked_count").notNull().default(0),
|
|
45
|
+
startedAt: timestamp("started_at", { withTimezone: true, mode: "string" }).notNull(),
|
|
46
|
+
finishedAt: timestamp("finished_at", { withTimezone: true, mode: "string" }),
|
|
47
|
+
}, (t) => [index("runs_automation_idx").on(t.automationId, t.tenantId, t.subject)]);
|
|
48
|
+
/** ENG-193 §4.6 — parked automation drafts: an action a for_each iteration or
|
|
49
|
+
* agent step couldn't run unattended, resolved standalone later. Indexed
|
|
50
|
+
* columns cover the list filters; the full ParkedAction payload (frozen
|
|
51
|
+
* input, guard bindings, tier, hashes, resolution detail) lives in `record`
|
|
52
|
+
* jsonb, mirrored the way `saved_vendos.record` already is. */
|
|
53
|
+
export const parkedActions = vendo.table("parked_actions", {
|
|
54
|
+
id: text("id").primaryKey(),
|
|
55
|
+
tenantId: text("tenant_id").notNull(),
|
|
56
|
+
subject: text("subject").notNull(),
|
|
57
|
+
automationId: text("automation_id").notNull(),
|
|
58
|
+
runId: text("run_id").notNull(),
|
|
59
|
+
resolution: text("resolution"),
|
|
60
|
+
requestedAt: timestamp("requested_at", { withTimezone: true, mode: "string" }).notNull(),
|
|
61
|
+
record: jsonb("record").notNull(),
|
|
62
|
+
}, (t) => [index("parked_actions_scope_idx").on(t.tenantId, t.subject, t.automationId, t.runId)]);
|
|
63
|
+
export const decisions = vendo.table("decisions", {
|
|
64
|
+
tenantId: text("tenant_id").notNull(),
|
|
65
|
+
subject: text("subject").notNull(),
|
|
66
|
+
canonicalKey: text("canonical_key").notNull(),
|
|
67
|
+
decision: jsonb("decision").notNull(),
|
|
68
|
+
createdAt: timestamp("created_at", { withTimezone: true, mode: "string" }).notNull(),
|
|
69
|
+
}, (t) => [primaryKey({ columns: [t.tenantId, t.subject, t.canonicalKey] })]);
|
|
70
|
+
export const threads = vendo.table("threads", {
|
|
71
|
+
id: text("id").notNull(),
|
|
72
|
+
tenantId: text("tenant_id").notNull(),
|
|
73
|
+
subject: text("subject").notNull(),
|
|
74
|
+
title: text("title"),
|
|
75
|
+
nextSeq: integer("next_seq").notNull().default(0),
|
|
76
|
+
createdAt: timestamp("created_at", { withTimezone: true, mode: "string" }).notNull(),
|
|
77
|
+
updatedAt: timestamp("updated_at", { withTimezone: true, mode: "string" }).notNull(),
|
|
78
|
+
}, (t) => [primaryKey({ columns: [t.tenantId, t.subject, t.id] })]);
|
|
79
|
+
export const threadMessages = vendo.table("thread_messages", {
|
|
80
|
+
rowId: bigserial("row_id", { mode: "number" }).primaryKey(),
|
|
81
|
+
tenantId: text("tenant_id").notNull(),
|
|
82
|
+
subject: text("subject").notNull(),
|
|
83
|
+
threadId: text("thread_id").notNull(),
|
|
84
|
+
messageId: text("message_id").notNull(),
|
|
85
|
+
seq: integer("seq").notNull(),
|
|
86
|
+
message: jsonb("message").notNull(),
|
|
87
|
+
}, (t) => [
|
|
88
|
+
uniqueIndex("thread_messages_id_uq").on(t.tenantId, t.subject, t.threadId, t.messageId),
|
|
89
|
+
uniqueIndex("thread_messages_seq_uq").on(t.tenantId, t.subject, t.threadId, t.seq),
|
|
90
|
+
]);
|
|
91
|
+
export const savedVendos = vendo.table("saved_vendos", {
|
|
92
|
+
id: text("id").notNull(),
|
|
93
|
+
tenantId: text("tenant_id").notNull(),
|
|
94
|
+
subject: text("subject").notNull(),
|
|
95
|
+
record: jsonb("record").notNull(),
|
|
96
|
+
updatedAt: timestamp("updated_at", { withTimezone: true, mode: "string" }).notNull(),
|
|
97
|
+
}, (t) => [primaryKey({ columns: [t.tenantId, t.subject, t.id] })]);
|
|
98
|
+
export const connections = vendo.table("connections", {
|
|
99
|
+
toolkit: text("toolkit").notNull(),
|
|
100
|
+
tenantId: text("tenant_id").notNull(),
|
|
101
|
+
subject: text("subject").notNull(),
|
|
102
|
+
connectedAccountId: text("connected_account_id"),
|
|
103
|
+
status: text("status").notNull(),
|
|
104
|
+
createdAt: timestamp("created_at", { withTimezone: true, mode: "string" }).notNull(),
|
|
105
|
+
}, (t) => [
|
|
106
|
+
primaryKey({ columns: [t.tenantId, t.subject, t.toolkit] }),
|
|
107
|
+
index("connections_account_idx").on(t.connectedAccountId),
|
|
108
|
+
]);
|
|
109
|
+
/** Tiny operational KV (scheduler heartbeat, future flags). NOT for domain data. */
|
|
110
|
+
export const meta = vendo.table("meta", {
|
|
111
|
+
key: text("key").primaryKey(),
|
|
112
|
+
value: jsonb("value").notNull(),
|
|
113
|
+
updatedAt: timestamp("updated_at", { withTimezone: true, mode: "string" }).notNull(),
|
|
114
|
+
});
|
|
@@ -0,0 +1,209 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
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
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,110 @@
|
|
|
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");
|
|
@@ -0,0 +1,13 @@
|
|
|
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");
|