@electric-ax/agents-server 0.4.19 → 0.5.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.
@@ -10,6 +10,7 @@ import {
10
10
  entityLineage,
11
11
  entityPermissionGrants,
12
12
  entityTypes,
13
+ pgSyncBridges,
13
14
  entityTypePermissionGrants,
14
15
  runnerRuntimeDiagnostics,
15
16
  runners,
@@ -42,8 +43,9 @@ import type {
42
43
  EntityTypePermission,
43
44
  EntityTypePermissionGrant,
44
45
  PermissionSubjectKind,
46
+ ExternallyWritableCollectionConfig,
45
47
  } from './electric-agents-types.js'
46
- import type { EntityTags } from '@electric-ax/agents-runtime'
48
+ import type { EntityTags, PgSyncOptions } from '@electric-ax/agents-runtime'
47
49
  import type { Principal } from './principal.js'
48
50
 
49
51
  export class EntityAlreadyExistsError extends Error {
@@ -73,6 +75,19 @@ export interface EntityBridgeRow {
73
75
  updatedAt: Date
74
76
  }
75
77
 
78
+ export interface PgSyncBridgeRow {
79
+ tenantId: string
80
+ sourceRef: string
81
+ options: PgSyncOptions
82
+ streamUrl: string
83
+ shapeHandle?: string
84
+ shapeOffset?: string
85
+ initialSnapshotComplete: boolean
86
+ lastTouchedAt: Date
87
+ createdAt: Date
88
+ updatedAt: Date
89
+ }
90
+
76
91
  export interface TagStreamOutboxRow {
77
92
  id: number
78
93
  tenantId: string
@@ -623,6 +638,13 @@ export class PostgresRegistry {
623
638
  )
624
639
  }
625
640
 
641
+ private pgSyncBridgeWhere(sourceRef: string) {
642
+ return and(
643
+ eq(pgSyncBridges.tenantId, this.tenantId),
644
+ eq(pgSyncBridges.sourceRef, sourceRef)
645
+ )
646
+ }
647
+
626
648
  async createEntityType(et: ElectricAgentsEntityType): Promise<void> {
627
649
  await this.db
628
650
  .insert(entityTypes)
@@ -633,6 +655,8 @@ export class PostgresRegistry {
633
655
  creationSchema: et.creation_schema ?? null,
634
656
  inboxSchemas: et.inbox_schemas ?? null,
635
657
  stateSchemas: et.state_schemas ?? null,
658
+ externallyWritableCollections:
659
+ et.externally_writable_collections ?? null,
636
660
  slashCommands: et.slash_commands ?? null,
637
661
  serveEndpoint: et.serve_endpoint ?? null,
638
662
  defaultDispatchPolicy: et.default_dispatch_policy ?? null,
@@ -647,6 +671,8 @@ export class PostgresRegistry {
647
671
  creationSchema: et.creation_schema ?? null,
648
672
  inboxSchemas: et.inbox_schemas ?? null,
649
673
  stateSchemas: et.state_schemas ?? null,
674
+ externallyWritableCollections:
675
+ et.externally_writable_collections ?? null,
650
676
  slashCommands: et.slash_commands ?? null,
651
677
  serveEndpoint: et.serve_endpoint ?? null,
652
678
  defaultDispatchPolicy: et.default_dispatch_policy ?? null,
@@ -670,6 +696,8 @@ export class PostgresRegistry {
670
696
  creationSchema: et.creation_schema ?? null,
671
697
  inboxSchemas: et.inbox_schemas ?? null,
672
698
  stateSchemas: et.state_schemas ?? null,
699
+ externallyWritableCollections:
700
+ et.externally_writable_collections ?? null,
673
701
  slashCommands: et.slash_commands ?? null,
674
702
  serveEndpoint: et.serve_endpoint ?? null,
675
703
  defaultDispatchPolicy: et.default_dispatch_policy ?? null,
@@ -712,6 +740,8 @@ export class PostgresRegistry {
712
740
  creationSchema: et.creation_schema ?? null,
713
741
  inboxSchemas: et.inbox_schemas ?? null,
714
742
  stateSchemas: et.state_schemas ?? null,
743
+ externallyWritableCollections:
744
+ et.externally_writable_collections ?? null,
715
745
  slashCommands: et.slash_commands ?? null,
716
746
  serveEndpoint: et.serve_endpoint ?? null,
717
747
  defaultDispatchPolicy: et.default_dispatch_policy ?? null,
@@ -1422,6 +1452,7 @@ export class PostgresRegistry {
1422
1452
  entity: ElectricAgentsEntity | null
1423
1453
  changed: boolean
1424
1454
  op?: `insert` | `update` | `delete`
1455
+ txid?: number
1425
1456
  }> {
1426
1457
  return this.mutateEntityTags(url, (oldTags) => {
1427
1458
  const previous = oldTags[key]
@@ -1440,7 +1471,11 @@ export class PostgresRegistry {
1440
1471
  async removeEntityTag(
1441
1472
  url: string,
1442
1473
  key: string
1443
- ): Promise<{ entity: ElectricAgentsEntity | null; changed: boolean }> {
1474
+ ): Promise<{
1475
+ entity: ElectricAgentsEntity | null
1476
+ changed: boolean
1477
+ txid?: number
1478
+ }> {
1444
1479
  return this.mutateEntityTags(url, (oldTags) => {
1445
1480
  if (!(key in oldTags)) return null
1446
1481
  const { [key]: _removed, ...remaining } = oldTags
@@ -1465,6 +1500,7 @@ export class PostgresRegistry {
1465
1500
  entity: ElectricAgentsEntity | null
1466
1501
  changed: boolean
1467
1502
  op?: `insert` | `update`
1503
+ txid?: number
1468
1504
  }> {
1469
1505
  return await this.db.transaction(async (tx) => {
1470
1506
  const [row] = await tx
@@ -1485,7 +1521,7 @@ export class PostgresRegistry {
1485
1521
 
1486
1522
  const nextTags = normalizeTags(mutation.nextTags)
1487
1523
  const updatedAt = Date.now()
1488
- await tx
1524
+ const [updateResult] = await tx
1489
1525
  .update(entities)
1490
1526
  .set({
1491
1527
  tags: nextTags,
@@ -1493,6 +1529,10 @@ export class PostgresRegistry {
1493
1529
  updatedAt,
1494
1530
  })
1495
1531
  .where(this.entityWhere(url))
1532
+ .returning({
1533
+ txid: sql<string>`pg_current_xact_id()::xid::text`,
1534
+ })
1535
+ const txid = updateResult ? parseInt(updateResult.txid) : undefined
1496
1536
 
1497
1537
  await tx.insert(tagStreamOutbox).values({
1498
1538
  tenantId: this.tenantId,
@@ -1513,10 +1553,103 @@ export class PostgresRegistry {
1513
1553
  entity,
1514
1554
  changed: true,
1515
1555
  ...(op === `insert` || op === `update` ? { op } : {}),
1556
+ ...(txid !== undefined ? { txid } : {}),
1516
1557
  }
1517
1558
  })
1518
1559
  }
1519
1560
 
1561
+ async upsertPgSyncBridge(row: {
1562
+ sourceRef: string
1563
+ options: PgSyncOptions
1564
+ streamUrl: string
1565
+ }): Promise<PgSyncBridgeRow> {
1566
+ await this.db
1567
+ .insert(pgSyncBridges)
1568
+ .values({
1569
+ tenantId: this.tenantId,
1570
+ sourceRef: row.sourceRef,
1571
+ options: row.options,
1572
+ streamUrl: row.streamUrl,
1573
+ lastTouchedAt: new Date(),
1574
+ updatedAt: new Date(),
1575
+ })
1576
+ .onConflictDoUpdate({
1577
+ target: [pgSyncBridges.tenantId, pgSyncBridges.sourceRef],
1578
+ set: {
1579
+ options: row.options,
1580
+ streamUrl: row.streamUrl,
1581
+ initialSnapshotComplete: false,
1582
+ lastTouchedAt: new Date(),
1583
+ updatedAt: new Date(),
1584
+ },
1585
+ })
1586
+
1587
+ const existing = await this.getPgSyncBridge(row.sourceRef)
1588
+ if (!existing)
1589
+ throw new Error(`Failed to load pgSync bridge ${row.sourceRef}`)
1590
+ return existing
1591
+ }
1592
+
1593
+ async getPgSyncBridge(sourceRef: string): Promise<PgSyncBridgeRow | null> {
1594
+ const rows = await this.db
1595
+ .select()
1596
+ .from(pgSyncBridges)
1597
+ .where(this.pgSyncBridgeWhere(sourceRef))
1598
+ .limit(1)
1599
+ return rows[0] ? this.rowToPgSyncBridge(rows[0]) : null
1600
+ }
1601
+
1602
+ async listPgSyncBridges(
1603
+ tenantId: string | null = this.tenantId
1604
+ ): Promise<Array<PgSyncBridgeRow>> {
1605
+ const rows =
1606
+ tenantId === null
1607
+ ? await this.db.select().from(pgSyncBridges)
1608
+ : await this.db
1609
+ .select()
1610
+ .from(pgSyncBridges)
1611
+ .where(eq(pgSyncBridges.tenantId, tenantId))
1612
+ return rows.map((row) => this.rowToPgSyncBridge(row))
1613
+ }
1614
+
1615
+ async touchPgSyncBridge(sourceRef: string): Promise<void> {
1616
+ await this.db
1617
+ .update(pgSyncBridges)
1618
+ .set({ lastTouchedAt: new Date(), updatedAt: new Date() })
1619
+ .where(this.pgSyncBridgeWhere(sourceRef))
1620
+ }
1621
+
1622
+ async updatePgSyncBridgeCursor(
1623
+ sourceRef: string,
1624
+ shapeHandle: string,
1625
+ shapeOffset: string,
1626
+ initialSnapshotComplete?: boolean
1627
+ ): Promise<void> {
1628
+ await this.db
1629
+ .update(pgSyncBridges)
1630
+ .set({
1631
+ shapeHandle,
1632
+ shapeOffset,
1633
+ ...(initialSnapshotComplete !== undefined
1634
+ ? { initialSnapshotComplete }
1635
+ : {}),
1636
+ updatedAt: new Date(),
1637
+ })
1638
+ .where(this.pgSyncBridgeWhere(sourceRef))
1639
+ }
1640
+
1641
+ async clearPgSyncBridgeCursor(sourceRef: string): Promise<void> {
1642
+ await this.db
1643
+ .update(pgSyncBridges)
1644
+ .set({
1645
+ shapeHandle: null,
1646
+ shapeOffset: null,
1647
+ initialSnapshotComplete: false,
1648
+ updatedAt: new Date(),
1649
+ })
1650
+ .where(this.pgSyncBridgeWhere(sourceRef))
1651
+ }
1652
+
1520
1653
  async upsertEntityBridge(row: {
1521
1654
  sourceRef: string
1522
1655
  tags: EntityTags
@@ -1833,6 +1966,11 @@ export class PostgresRegistry {
1833
1966
  state_schemas: row.stateSchemas as
1834
1967
  | Record<string, Record<string, unknown>>
1835
1968
  | undefined,
1969
+ externally_writable_collections:
1970
+ (row.externallyWritableCollections as Record<
1971
+ string,
1972
+ ExternallyWritableCollectionConfig
1973
+ > | null) ?? undefined,
1836
1974
  slash_commands:
1837
1975
  (row.slashCommands as ElectricAgentsEntityType[`slash_commands`]) ??
1838
1976
  undefined,
@@ -1911,6 +2049,23 @@ export class PostgresRegistry {
1911
2049
  }
1912
2050
  }
1913
2051
 
2052
+ private rowToPgSyncBridge(
2053
+ row: typeof pgSyncBridges.$inferSelect
2054
+ ): PgSyncBridgeRow {
2055
+ return {
2056
+ tenantId: row.tenantId,
2057
+ sourceRef: row.sourceRef,
2058
+ options: row.options as PgSyncOptions,
2059
+ streamUrl: row.streamUrl,
2060
+ shapeHandle: row.shapeHandle ?? undefined,
2061
+ shapeOffset: row.shapeOffset ?? undefined,
2062
+ initialSnapshotComplete: row.initialSnapshotComplete,
2063
+ lastTouchedAt: row.lastTouchedAt,
2064
+ createdAt: row.createdAt,
2065
+ updatedAt: row.updatedAt,
2066
+ }
2067
+ }
2068
+
1914
2069
  private rowToEntityBridge(
1915
2070
  row: typeof entityBridges.$inferSelect
1916
2071
  ): EntityBridgeRow {
@@ -10,6 +10,10 @@ export function isRecord(value: unknown): value is Record<string, unknown> {
10
10
  return typeof value === `object` && value !== null && !Array.isArray(value)
11
11
  }
12
12
 
13
+ function getPgSyncManifestStreamPath(sourceRef: string): string {
14
+ return `/_electric/pg-sync/${sourceRef}`
15
+ }
16
+
13
17
  export function extractManifestSourceUrl(
14
18
  manifest: Record<string, unknown> | undefined
15
19
  ): string | undefined {
@@ -57,6 +61,12 @@ export function extractManifestSourceUrl(
57
61
  : undefined
58
62
  }
59
63
 
64
+ if (manifest.sourceType === `pgSync`) {
65
+ return typeof manifest.sourceRef === `string`
66
+ ? getPgSyncManifestStreamPath(manifest.sourceRef)
67
+ : undefined
68
+ }
69
+
60
70
  if (manifest.sourceType === `webhook`) {
61
71
  if (typeof config?.streamUrl === `string`) return config.streamUrl
62
72
  if (typeof config?.endpointKey === `string`) {