@intentface/latch-drizzle 0.9.1 → 0.11.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.
@@ -1,5 +1,6 @@
1
1
  import { desc, eq, inArray, and, like, sql } from "drizzle-orm";
2
2
  import { parseAgentRef } from "@intentface/latch-agent-builder";
3
+ import { isUniqueViolation } from "./adapter.shared.js";
3
4
  import * as sqliteSchema from "./schema.js";
4
5
  import * as pgSchema from "./schema.pg.js";
5
6
  /**
@@ -10,12 +11,101 @@ import * as pgSchema from "./schema.pg.js";
10
11
  function newId() {
11
12
  return `agt_${crypto.randomUUID().replace(/-/g, "").slice(0, 20)}`;
12
13
  }
14
+ function newVersionId() {
15
+ return `agv_${crypto.randomUUID().replace(/-/g, "").slice(0, 20)}`;
16
+ }
17
+ function newActivationId() {
18
+ return `aga_${crypto.randomUUID().replace(/-/g, "").slice(0, 20)}`;
19
+ }
20
+ /**
21
+ * How many times a publish retries when a concurrent one takes its number.
22
+ *
23
+ * Each contended round lets exactly one writer through, so a loser may need as
24
+ * many attempts as there are concurrent publishers. Real contention here is a
25
+ * couple of humans clicking Publish, so this is far above what can be reached —
26
+ * it exists only so the loop is bounded rather than as a tuned value.
27
+ */
28
+ const PUBLISH_ATTEMPTS = 20;
29
+ /**
30
+ * Jittered pause between publish retries. Without it, losers re-read and re-try
31
+ * in lockstep and collide again; a few random milliseconds desynchronises them.
32
+ */
33
+ const publishBackoff = () => new Promise((r) => setTimeout(r, Math.floor(Math.random() * 5) + 1));
34
+ /**
35
+ * Stable JSON for EQUALITY ONLY (never for storage): object keys sorted at every
36
+ * depth, and absent/null/empty-array treated alike.
37
+ *
38
+ * That last part is what makes "publishing an unchanged record appends nothing"
39
+ * actually hold. The same record reaches this function in three spellings — a
40
+ * caller's `AgentRecord` (absent keys are `undefined`), a row round-tripped
41
+ * through the live table (empty arrays are stored `null`), and the migration's
42
+ * backfilled v1 (built by `json_object`, so every absent column is an explicit
43
+ * `null`). Without flattening those, a no-op publish right after the backfill
44
+ * would append a duplicate version.
45
+ */
46
+ function canonicalize(value) {
47
+ if (Array.isArray(value))
48
+ return value.map(canonicalize);
49
+ if (value && typeof value === "object") {
50
+ const out = {};
51
+ for (const key of Object.keys(value).sort()) {
52
+ const v = value[key];
53
+ if (v === undefined || v === null)
54
+ continue;
55
+ if (Array.isArray(v) && v.length === 0)
56
+ continue;
57
+ out[key] = canonicalize(v);
58
+ }
59
+ return out;
60
+ }
61
+ return value;
62
+ }
63
+ const canonical = (rec) => JSON.stringify(canonicalize(rec));
64
+ /**
65
+ * Which top-level record fields differ between two versions. Compared through
66
+ * `canonicalize`, so the same spelling rules that decide "did anything change at
67
+ * all" also decide "which field changed" — otherwise a record could publish
68
+ * (unchanged overall) yet report fields as changed, or vice versa.
69
+ *
70
+ * `undefined` for the first version: there is nothing to have changed from.
71
+ */
72
+ function changedFieldsBetween(prev, next) {
73
+ if (!prev)
74
+ return undefined;
75
+ const a = canonicalize(prev);
76
+ const b = canonicalize(next);
77
+ const fields = [...new Set([...Object.keys(a), ...Object.keys(b)])]
78
+ .sort()
79
+ .filter((k) => JSON.stringify(a[k]) !== JSON.stringify(b[k]));
80
+ return fields.length ? fields : undefined;
81
+ }
82
+ /**
83
+ * A stored version payload back into an `AgentRecord`. Nulls become absent
84
+ * (see `canonicalize`) so a backfilled row and a published one round-trip to
85
+ * the same record. JSON columns arrive parsed from both dialects; a string is
86
+ * tolerated for hosts whose driver hands back raw text.
87
+ */
88
+ function recordFromJson(value) {
89
+ const raw = (typeof value === "string" ? JSON.parse(value) : value);
90
+ const out = {};
91
+ for (const [key, v] of Object.entries(raw ?? {})) {
92
+ if (v === null || v === undefined)
93
+ continue;
94
+ out[key] = v;
95
+ }
96
+ if (!Array.isArray(out.connections))
97
+ out.connections = [];
98
+ return out;
99
+ }
13
100
  /** Column value (INTEGER) ↔ AgentRecord.maxSteps: NULL=default, 0=unlimited, N=cap. */
14
101
  const maxStepsToCol = (m) => m === "unlimited" ? 0 : typeof m === "number" ? m : null;
15
102
  const maxStepsFromCol = (v) => v == null ? undefined : v === 0 ? "unlimited" : v;
16
103
  const ownerKeyOf = (segments) => segments.map(encodeURIComponent).join("/");
17
104
  function createStore(queries, opts) {
18
105
  const ownerKey = (p, scope) => ownerKeyOf(opts.owner(p, scope));
106
+ // The user-scope owner key is already per-person wherever the host's user
107
+ // scope carries a user id, so it is the right default editor identity.
108
+ const editorKey = (p) => opts.editorKey?.(p) ?? ownerKeyOf(opts.owner(p, "user"));
19
109
  const recordOf = (row) => ({
20
110
  name: row.name,
21
111
  title: row.title ?? undefined,
@@ -30,6 +120,18 @@ function createStore(queries, opts) {
30
120
  attachedEvals: (row.attachedEvals ?? undefined),
31
121
  scope: row.scope,
32
122
  });
123
+ /** The matched ROW, for callers that need more than the record (see `resolve`). */
124
+ async function findRow(p, name, scope) {
125
+ // Personal wins over org on a name clash — unless pinned to one scope.
126
+ const owners = scope === "org"
127
+ ? [ownerKey(p, "org")]
128
+ : scope === "user"
129
+ ? [ownerKey(p, "user")]
130
+ : [ownerKey(p, "user"), ownerKey(p, "org")];
131
+ const rows = await queries.byOwners(owners, name);
132
+ const personal = rows.find((r) => r.owner === ownerKey(p, "user"));
133
+ return personal ?? rows[0];
134
+ }
33
135
  async function find(p, name, scope) {
34
136
  // Personal wins over org on a name clash — unless pinned to one scope.
35
137
  const owners = scope === "org"
@@ -49,9 +151,10 @@ function createStore(queries, opts) {
49
151
  // it), so a same-named record in the OTHER scope can never shadow the
50
152
  // agent the binding was created for.
51
153
  const ref = parseAgentRef(name);
52
- const rec = await find(principal, ref.name, ref.scope);
53
- if (!rec)
154
+ const row = await findRow(principal, ref.name, ref.scope);
155
+ if (!row)
54
156
  return undefined;
157
+ const rec = recordOf(row);
55
158
  const decorate = opts.decorateResolve ?? ((cfg) => cfg);
56
159
  const sel = rec.tools;
57
160
  const approvalEntries = Object.entries(sel?.approval ?? {});
@@ -80,6 +183,10 @@ function createStore(queries, opts) {
80
183
  ]
81
184
  : undefined;
82
185
  return decorate({
186
+ // Which stored version this config is a copy of — recorded on the run
187
+ // and emitted as a trace tag, so an answer can be traced back to the
188
+ // configuration that produced it.
189
+ configVersion: row.publishedVersion ?? undefined,
83
190
  model: opts.modelFor(rec.modelId),
84
191
  instructions: rec.instructions,
85
192
  connections: rec.connections,
@@ -129,23 +236,209 @@ function createStore(queries, opts) {
129
236
  async listMemoryEnabledNames() {
130
237
  return queries.memoryEnabledNames();
131
238
  },
132
- async upsert(principal, rec) {
239
+ async upsert(principal, rec, opts) {
240
+ await publish(ownerKey(principal, rec.scope), rec, {
241
+ author: editorKey(principal),
242
+ ...opts,
243
+ });
244
+ },
245
+ async remove(principal, scope, name) {
246
+ const owner = ownerKey(principal, scope);
247
+ // Live row first: an interrupted delete then leaves orphan version rows
248
+ // (harmless — nothing resolves them) rather than a live agent whose
249
+ // history has silently vanished.
250
+ await queries.remove(owner, name);
251
+ await queries.removeVersions(owner, name);
252
+ await queries.removeActivations(owner, name);
253
+ await queries.removeDrafts(owner, name);
254
+ },
255
+ async listVersions(principal, name, scope) {
256
+ const owner = ownerKey(principal, scope);
257
+ const [rows, live] = await Promise.all([
258
+ queries.versions(owner, name),
259
+ queries.byOwners([owner], name),
260
+ ]);
261
+ const publishedVersion = live[0]?.publishedVersion ?? null;
262
+ return rows.map((r) => ({
263
+ version: r.version,
264
+ author: r.author ?? undefined,
265
+ source: (r.source ?? undefined),
266
+ message: r.changeMessage ?? undefined,
267
+ changedFields: (r.changedFields ?? undefined),
268
+ createdAt: r.createdAt,
269
+ published: r.version === publishedVersion,
270
+ }));
271
+ },
272
+ async getVersion(principal, name, scope, version) {
273
+ const row = await queries.versionAt(ownerKey(principal, scope), name, version);
274
+ return row ? recordFromJson(row.record) : undefined;
275
+ },
276
+ async activateVersion(principal, name, scope, version, opts) {
277
+ const owner = ownerKey(principal, scope);
278
+ const row = await queries.versionAt(owner, name, version);
279
+ if (!row)
280
+ throw new Error(`No version ${version} of agent "${name}".`);
281
+ // Pin the scope to the one asked for: it decides the owner key, and a
282
+ // record stored under a since-changed scope must not write elsewhere.
283
+ const rec = { ...recordFromJson(row.record), name, scope };
284
+ const now = Date.now();
285
+ // Pointer move only — no version is appended. Flipping between two
286
+ // configurations must not grow history by one duplicate row each time.
133
287
  await queries.upsert({
134
288
  id: newId(),
135
- owner: ownerKey(principal, rec.scope),
289
+ owner,
136
290
  rec,
137
291
  maxSteps: maxStepsToCol(rec.maxSteps),
292
+ now,
293
+ publishedVersion: version,
294
+ });
295
+ await recordActivation(owner, name, version, opts?.actor ?? editorKey(principal), "activate", now);
296
+ },
297
+ async listActivations(principal, name, scope, limit) {
298
+ const rows = await queries.activations(ownerKey(principal, scope), name, limit);
299
+ return rows.map((r) => ({
300
+ version: r.version,
301
+ actor: r.actor ?? undefined,
302
+ source: (r.source ?? undefined),
303
+ createdAt: r.createdAt,
304
+ }));
305
+ },
306
+ async getDraft(principal, name, scope) {
307
+ const row = await queries.draft(ownerKey(principal, scope), name, editorKey(principal));
308
+ if (!row)
309
+ return undefined;
310
+ return {
311
+ record: recordFromJson(row.record),
312
+ baseVersion: row.baseVersion ?? undefined,
313
+ updatedAt: row.updatedAt,
314
+ };
315
+ },
316
+ async saveDraft(principal, name, scope, rec, baseVersion) {
317
+ await queries.upsertDraft({
318
+ owner: ownerKey(principal, scope),
319
+ name,
320
+ editor: editorKey(principal),
321
+ // Pin identity to the arguments: a draft keyed (owner, name, editor)
322
+ // whose record claimed another name would publish to the wrong agent.
323
+ rec: { ...rec, name, scope },
324
+ baseVersion: baseVersion ?? null,
138
325
  now: Date.now(),
139
326
  });
140
327
  },
141
- async remove(principal, scope, name) {
142
- await queries.remove(ownerKey(principal, scope), name);
328
+ async discardDraft(principal, name, scope) {
329
+ await queries.removeDraft(ownerKey(principal, scope), name, editorKey(principal));
330
+ },
331
+ async listDrafts(principal) {
332
+ const mine = editorKey(principal);
333
+ const owners = [ownerKey(principal, "user"), ownerKey(principal, "org")];
334
+ const rows = await queries.draftsByOwners(owners);
335
+ return rows.map((r) => ({
336
+ name: r.name,
337
+ scope: r.owner === owners[1] ? "org" : "user",
338
+ mine: r.editor === mine,
339
+ updatedAt: r.updatedAt,
340
+ }));
143
341
  },
144
342
  };
343
+ /** Append to the publish timeline. Never fails the write it follows. */
344
+ async function recordActivation(owner, name, version, actor, source, now) {
345
+ try {
346
+ await queries.insertActivation({
347
+ id: newActivationId(),
348
+ owner,
349
+ name,
350
+ version,
351
+ actor: actor ?? null,
352
+ source,
353
+ now,
354
+ });
355
+ }
356
+ catch (error) {
357
+ // The timeline is an audit trail, not a correctness invariant: the live
358
+ // row and the version are already written, and losing one entry must not
359
+ // turn a successful publish into a failed one.
360
+ console.warn(`Could not record agent activation for "${name}":`, error);
361
+ }
362
+ }
363
+ /**
364
+ * Write the live row and append a version — the one path by which a record
365
+ * becomes current.
366
+ *
367
+ * Two ordering rules, neither of which needs a transaction (this store also
368
+ * serves better-sqlite3, whose transaction API rejects an async callback, and
369
+ * libsql, which opens one on a NEW connection — see `createSqliteAdapter`):
370
+ *
371
+ * 1. The VERSION row is written before the live row. Interrupted that way,
372
+ * history holds a version nothing points at — visible, inert, superseded by
373
+ * the next publish. The reverse order would leave the live row claiming a
374
+ * `published_version` that does not exist, breaking history and rollback.
375
+ * 2. The version NUMBER is `max + 1` read outside any lock, so two concurrent
376
+ * publishes can choose the same one. The UNIQUE (owner, name, version)
377
+ * index rejects the loser, which re-reads the head and retries — the same
378
+ * "let the index arbitrate" discipline the rest of this package uses.
379
+ */
380
+ async function publish(owner, rec, opts) {
381
+ const target = canonical(rec);
382
+ for (let attempt = 1; attempt <= PUBLISH_ATTEMPTS; attempt++) {
383
+ const head = (await queries.versions(owner, rec.name, 1))[0];
384
+ const now = Date.now();
385
+ // Unchanged content appends nothing, so history stays a list of real
386
+ // changes. The live row is still written: it reconciles a row that drifted
387
+ // from its version (a direct DB edit, an interrupted earlier publish).
388
+ if (head && canonical(recordFromJson(head.record)) === target) {
389
+ await queries.upsert({
390
+ id: newId(),
391
+ owner,
392
+ rec,
393
+ maxSteps: maxStepsToCol(rec.maxSteps),
394
+ now,
395
+ publishedVersion: head.version,
396
+ });
397
+ return;
398
+ }
399
+ const version = (head?.version ?? 0) + 1;
400
+ const message = opts?.message?.trim();
401
+ try {
402
+ await queries.insertVersion({
403
+ id: newVersionId(),
404
+ owner,
405
+ name: rec.name,
406
+ version,
407
+ rec,
408
+ author: opts?.author ?? null,
409
+ source: opts?.source ?? "api",
410
+ changedFields: changedFieldsBetween(head ? recordFromJson(head.record) : undefined, rec) ?? null,
411
+ changeMessage: message || null,
412
+ now,
413
+ });
414
+ }
415
+ catch (error) {
416
+ // Someone else took this number: back off, re-read the head, try again.
417
+ if (isUniqueViolation(error) && attempt < PUBLISH_ATTEMPTS) {
418
+ await publishBackoff();
419
+ continue;
420
+ }
421
+ throw error;
422
+ }
423
+ await queries.upsert({
424
+ id: newId(),
425
+ owner,
426
+ rec,
427
+ maxSteps: maxStepsToCol(rec.maxSteps),
428
+ now,
429
+ publishedVersion: version,
430
+ });
431
+ // A publish is also an activation: recording both here and in
432
+ // `activateVersion` is what makes the timeline complete.
433
+ await recordActivation(owner, rec.name, version, opts?.author, "publish", now);
434
+ return;
435
+ }
436
+ throw new Error(`Could not publish agent "${rec.name}": ${PUBLISH_ATTEMPTS} version numbers were taken concurrently.`);
437
+ }
145
438
  }
146
439
  /** Shared upsert `set` payload (column shapes match across dialects). */
147
440
  function upsertValues(row) {
148
- const { id, owner, rec, maxSteps, now } = row;
441
+ const { id, owner, rec, maxSteps, now, publishedVersion } = row;
149
442
  return {
150
443
  insert: {
151
444
  id,
@@ -162,6 +455,7 @@ function upsertValues(row) {
162
455
  attachedEvals: rec.attachedEvals?.length ? rec.attachedEvals : null,
163
456
  maxSteps,
164
457
  scope: rec.scope,
458
+ publishedVersion,
165
459
  createdAt: now,
166
460
  updatedAt: now,
167
461
  },
@@ -177,13 +471,32 @@ function upsertValues(row) {
177
471
  attachedEvals: rec.attachedEvals?.length ? rec.attachedEvals : null,
178
472
  maxSteps,
179
473
  scope: rec.scope,
474
+ publishedVersion,
180
475
  updatedAt: now,
181
476
  },
182
477
  };
183
478
  }
479
+ /** Shared version-row insert payload (column shapes match across dialects). */
480
+ function versionValues(row) {
481
+ return {
482
+ id: row.id,
483
+ owner: row.owner,
484
+ name: row.name,
485
+ version: row.version,
486
+ record: row.rec,
487
+ author: row.author,
488
+ source: row.source,
489
+ changedFields: row.changedFields,
490
+ changeMessage: row.changeMessage,
491
+ createdAt: row.now,
492
+ };
493
+ }
184
494
  /** `AgentBuilderStore` on a Drizzle SQLite database (libsql / better-sqlite3 / Turso). */
185
495
  export function createSqliteAgentBuilderStore(db, opts) {
186
496
  const t = sqliteSchema.agents;
497
+ const v = sqliteSchema.agentVersions;
498
+ const a = sqliteSchema.agentActivations;
499
+ const d = sqliteSchema.agentDrafts;
187
500
  return createStore({
188
501
  async byOwners(owners, name) {
189
502
  const where = name ? and(inArray(t.owner, owners), eq(t.name, name)) : inArray(t.owner, owners);
@@ -205,11 +518,109 @@ export function createSqliteAgentBuilderStore(db, opts) {
205
518
  async remove(owner, name) {
206
519
  await db.delete(t).where(and(eq(t.owner, owner), eq(t.name, name)));
207
520
  },
521
+ async versions(owner, name, limit) {
522
+ const q = db
523
+ .select({
524
+ version: v.version,
525
+ record: v.record,
526
+ author: v.author,
527
+ source: v.source,
528
+ changedFields: v.changedFields,
529
+ changeMessage: v.changeMessage,
530
+ createdAt: v.createdAt,
531
+ })
532
+ .from(v)
533
+ .where(and(eq(v.owner, owner), eq(v.name, name)))
534
+ .orderBy(desc(v.version));
535
+ return (limit ? await q.limit(limit) : await q);
536
+ },
537
+ async versionAt(owner, name, version) {
538
+ const rows = (await db
539
+ .select({
540
+ version: v.version,
541
+ record: v.record,
542
+ author: v.author,
543
+ source: v.source,
544
+ changedFields: v.changedFields,
545
+ changeMessage: v.changeMessage,
546
+ createdAt: v.createdAt,
547
+ })
548
+ .from(v)
549
+ .where(and(eq(v.owner, owner), eq(v.name, name), eq(v.version, version)))
550
+ .limit(1));
551
+ return rows[0];
552
+ },
553
+ async insertVersion(row) {
554
+ await db.insert(v).values(versionValues(row));
555
+ },
556
+ async removeVersions(owner, name) {
557
+ await db.delete(v).where(and(eq(v.owner, owner), eq(v.name, name)));
558
+ },
559
+ async insertActivation(row) {
560
+ await db.insert(a).values({
561
+ id: row.id,
562
+ owner: row.owner,
563
+ name: row.name,
564
+ version: row.version,
565
+ actor: row.actor,
566
+ source: row.source,
567
+ createdAt: row.now,
568
+ });
569
+ },
570
+ async activations(owner, name, limit) {
571
+ const q = db
572
+ .select({ version: a.version, actor: a.actor, source: a.source, createdAt: a.createdAt })
573
+ .from(a)
574
+ .where(and(eq(a.owner, owner), eq(a.name, name)))
575
+ .orderBy(desc(a.createdAt));
576
+ return (limit ? await q.limit(limit) : await q);
577
+ },
578
+ async removeActivations(owner, name) {
579
+ await db.delete(a).where(and(eq(a.owner, owner), eq(a.name, name)));
580
+ },
581
+ async draft(owner, name, editor) {
582
+ const rows = (await db
583
+ .select()
584
+ .from(d)
585
+ .where(and(eq(d.owner, owner), eq(d.name, name), eq(d.editor, editor)))
586
+ .limit(1));
587
+ return rows[0];
588
+ },
589
+ async upsertDraft(row) {
590
+ await db
591
+ .insert(d)
592
+ .values({
593
+ owner: row.owner,
594
+ name: row.name,
595
+ editor: row.editor,
596
+ record: row.rec,
597
+ baseVersion: row.baseVersion,
598
+ updatedAt: row.now,
599
+ })
600
+ .onConflictDoUpdate({
601
+ target: [d.owner, d.name, d.editor],
602
+ set: { record: row.rec, baseVersion: row.baseVersion, updatedAt: row.now },
603
+ });
604
+ },
605
+ async removeDraft(owner, name, editor) {
606
+ await db
607
+ .delete(d)
608
+ .where(and(eq(d.owner, owner), eq(d.name, name), eq(d.editor, editor)));
609
+ },
610
+ async draftsByOwners(owners) {
611
+ return (await db.select().from(d).where(inArray(d.owner, owners)));
612
+ },
613
+ async removeDrafts(owner, name) {
614
+ await db.delete(d).where(and(eq(d.owner, owner), eq(d.name, name)));
615
+ },
208
616
  }, opts);
209
617
  }
210
618
  /** `AgentBuilderStore` on a Drizzle Postgres database (node-postgres / postgres-js / PGlite). */
211
619
  export function createPostgresAgentBuilderStore(db, opts) {
212
620
  const t = pgSchema.agents;
621
+ const v = pgSchema.agentVersions;
622
+ const a = pgSchema.agentActivations;
623
+ const d = pgSchema.agentDrafts;
213
624
  return createStore({
214
625
  async byOwners(owners, name) {
215
626
  const where = name ? and(inArray(t.owner, owners), eq(t.name, name)) : inArray(t.owner, owners);
@@ -231,6 +642,101 @@ export function createPostgresAgentBuilderStore(db, opts) {
231
642
  async remove(owner, name) {
232
643
  await db.delete(t).where(and(eq(t.owner, owner), eq(t.name, name)));
233
644
  },
645
+ async versions(owner, name, limit) {
646
+ const q = db
647
+ .select({
648
+ version: v.version,
649
+ record: v.record,
650
+ author: v.author,
651
+ source: v.source,
652
+ changedFields: v.changedFields,
653
+ changeMessage: v.changeMessage,
654
+ createdAt: v.createdAt,
655
+ })
656
+ .from(v)
657
+ .where(and(eq(v.owner, owner), eq(v.name, name)))
658
+ .orderBy(desc(v.version));
659
+ return (limit ? await q.limit(limit) : await q);
660
+ },
661
+ async versionAt(owner, name, version) {
662
+ const rows = (await db
663
+ .select({
664
+ version: v.version,
665
+ record: v.record,
666
+ author: v.author,
667
+ source: v.source,
668
+ changedFields: v.changedFields,
669
+ changeMessage: v.changeMessage,
670
+ createdAt: v.createdAt,
671
+ })
672
+ .from(v)
673
+ .where(and(eq(v.owner, owner), eq(v.name, name), eq(v.version, version)))
674
+ .limit(1));
675
+ return rows[0];
676
+ },
677
+ async insertVersion(row) {
678
+ await db.insert(v).values(versionValues(row));
679
+ },
680
+ async removeVersions(owner, name) {
681
+ await db.delete(v).where(and(eq(v.owner, owner), eq(v.name, name)));
682
+ },
683
+ async insertActivation(row) {
684
+ await db.insert(a).values({
685
+ id: row.id,
686
+ owner: row.owner,
687
+ name: row.name,
688
+ version: row.version,
689
+ actor: row.actor,
690
+ source: row.source,
691
+ createdAt: row.now,
692
+ });
693
+ },
694
+ async activations(owner, name, limit) {
695
+ const q = db
696
+ .select({ version: a.version, actor: a.actor, source: a.source, createdAt: a.createdAt })
697
+ .from(a)
698
+ .where(and(eq(a.owner, owner), eq(a.name, name)))
699
+ .orderBy(desc(a.createdAt));
700
+ return (limit ? await q.limit(limit) : await q);
701
+ },
702
+ async removeActivations(owner, name) {
703
+ await db.delete(a).where(and(eq(a.owner, owner), eq(a.name, name)));
704
+ },
705
+ async draft(owner, name, editor) {
706
+ const rows = (await db
707
+ .select()
708
+ .from(d)
709
+ .where(and(eq(d.owner, owner), eq(d.name, name), eq(d.editor, editor)))
710
+ .limit(1));
711
+ return rows[0];
712
+ },
713
+ async upsertDraft(row) {
714
+ await db
715
+ .insert(d)
716
+ .values({
717
+ owner: row.owner,
718
+ name: row.name,
719
+ editor: row.editor,
720
+ record: row.rec,
721
+ baseVersion: row.baseVersion,
722
+ updatedAt: row.now,
723
+ })
724
+ .onConflictDoUpdate({
725
+ target: [d.owner, d.name, d.editor],
726
+ set: { record: row.rec, baseVersion: row.baseVersion, updatedAt: row.now },
727
+ });
728
+ },
729
+ async removeDraft(owner, name, editor) {
730
+ await db
731
+ .delete(d)
732
+ .where(and(eq(d.owner, owner), eq(d.name, name), eq(d.editor, editor)));
733
+ },
734
+ async draftsByOwners(owners) {
735
+ return (await db.select().from(d).where(inArray(d.owner, owners)));
736
+ },
737
+ async removeDrafts(owner, name) {
738
+ await db.delete(d).where(and(eq(d.owner, owner), eq(d.name, name)));
739
+ },
234
740
  }, opts);
235
741
  }
236
742
  //# sourceMappingURL=agent-store.js.map