@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.
- package/dist/entrypoint.js +692 -45
- package/dist/index.cjs +678 -41
- package/dist/index.d.cts +2519 -2216
- package/dist/index.d.ts +2518 -2217
- package/dist/index.js +679 -42
- package/drizzle/0015_pg_sync_bridges.sql +14 -0
- package/drizzle/0016_entity_type_externally_writable_collections.sql +1 -0
- package/drizzle/meta/_journal.json +14 -0
- package/package.json +6 -6
- package/src/db/schema.ts +32 -0
- package/src/electric-agents-types.ts +23 -0
- package/src/entity-manager.ts +160 -29
- package/src/entity-registry.ts +158 -3
- package/src/manifest-side-effects.ts +10 -0
- package/src/pg-sync-bridge-manager.ts +552 -0
- package/src/routing/context.ts +2 -0
- package/src/routing/entities-router.ts +89 -18
- package/src/routing/entity-types-router.ts +56 -0
- package/src/routing/global-router.ts +3 -0
- package/src/routing/hooks.ts +7 -0
- package/src/routing/internal-router.ts +2 -0
- package/src/routing/pg-sync-router.ts +113 -0
- package/src/runtime.ts +20 -1
- package/src/scheduler.ts +26 -0
- package/src/server.ts +4 -0
- package/src/standalone-runtime.ts +16 -0
- package/src/utils/server-utils.ts +97 -1
- package/src/wake-registry.ts +27 -2
package/dist/index.cjs
CHANGED
|
@@ -61,6 +61,7 @@ __export(schema_exports, {
|
|
|
61
61
|
entityPermissionGrants: () => entityPermissionGrants,
|
|
62
62
|
entityTypePermissionGrants: () => entityTypePermissionGrants,
|
|
63
63
|
entityTypes: () => entityTypes,
|
|
64
|
+
pgSyncBridges: () => pgSyncBridges,
|
|
64
65
|
runnerRuntimeDiagnostics: () => runnerRuntimeDiagnostics,
|
|
65
66
|
runners: () => runners,
|
|
66
67
|
scheduledTasks: () => scheduledTasks,
|
|
@@ -78,6 +79,7 @@ const entityTypes = (0, drizzle_orm_pg_core.pgTable)(`entity_types`, {
|
|
|
78
79
|
creationSchema: (0, drizzle_orm_pg_core.jsonb)(`creation_schema`),
|
|
79
80
|
inboxSchemas: (0, drizzle_orm_pg_core.jsonb)(`inbox_schemas`),
|
|
80
81
|
stateSchemas: (0, drizzle_orm_pg_core.jsonb)(`state_schemas`),
|
|
82
|
+
externallyWritableCollections: (0, drizzle_orm_pg_core.jsonb)(`externally_writable_collections`).$type(),
|
|
81
83
|
slashCommands: (0, drizzle_orm_pg_core.jsonb)(`slash_commands`),
|
|
82
84
|
serveEndpoint: (0, drizzle_orm_pg_core.text)(`serve_endpoint`),
|
|
83
85
|
defaultDispatchPolicy: (0, drizzle_orm_pg_core.jsonb)(`default_dispatch_policy`),
|
|
@@ -382,6 +384,18 @@ const scheduledTasks = (0, drizzle_orm_pg_core.pgTable)(`scheduled_tasks`, {
|
|
|
382
384
|
(0, drizzle_orm_pg_core.index)(`idx_scheduled_tasks_manifest_pending`).on(table.tenantId, table.ownerEntityUrl, table.manifestKey).where(drizzle_orm.sql`${table.kind} = 'delayed_send' AND ${table.completedAt} IS NULL AND ${table.manifestKey} IS NOT NULL`),
|
|
383
385
|
(0, drizzle_orm_pg_core.index)(`idx_scheduled_tasks_stale_claims`).on(table.tenantId, table.claimedAt).where(drizzle_orm.sql`${table.completedAt} IS NULL AND ${table.claimedAt} IS NOT NULL`)
|
|
384
386
|
]);
|
|
387
|
+
const pgSyncBridges = (0, drizzle_orm_pg_core.pgTable)(`pg_sync_bridges`, {
|
|
388
|
+
tenantId: (0, drizzle_orm_pg_core.text)(`tenant_id`).notNull().default(`default`),
|
|
389
|
+
sourceRef: (0, drizzle_orm_pg_core.text)(`source_ref`).notNull(),
|
|
390
|
+
options: (0, drizzle_orm_pg_core.jsonb)(`options`).notNull(),
|
|
391
|
+
streamUrl: (0, drizzle_orm_pg_core.text)(`stream_url`).notNull(),
|
|
392
|
+
shapeHandle: (0, drizzle_orm_pg_core.text)(`shape_handle`),
|
|
393
|
+
shapeOffset: (0, drizzle_orm_pg_core.text)(`shape_offset`),
|
|
394
|
+
initialSnapshotComplete: (0, drizzle_orm_pg_core.boolean)(`initial_snapshot_complete`).notNull().default(false),
|
|
395
|
+
lastTouchedAt: (0, drizzle_orm_pg_core.timestamp)(`last_touched_at`, { withTimezone: true }).notNull().defaultNow(),
|
|
396
|
+
createdAt: (0, drizzle_orm_pg_core.timestamp)(`created_at`, { withTimezone: true }).notNull().defaultNow(),
|
|
397
|
+
updatedAt: (0, drizzle_orm_pg_core.timestamp)(`updated_at`, { withTimezone: true }).notNull().defaultNow()
|
|
398
|
+
}, (table) => [(0, drizzle_orm_pg_core.primaryKey)({ columns: [table.tenantId, table.sourceRef] }), (0, drizzle_orm_pg_core.unique)(`uq_pg_sync_bridges_stream_url`).on(table.tenantId, table.streamUrl)]);
|
|
385
399
|
const entityBridges = (0, drizzle_orm_pg_core.pgTable)(`entity_bridges`, {
|
|
386
400
|
tenantId: (0, drizzle_orm_pg_core.text)(`tenant_id`).notNull().default(`default`),
|
|
387
401
|
sourceRef: (0, drizzle_orm_pg_core.text)(`source_ref`).notNull(),
|
|
@@ -842,6 +856,9 @@ var PostgresRegistry = class {
|
|
|
842
856
|
entityBridgeWhere(sourceRef) {
|
|
843
857
|
return (0, drizzle_orm.and)((0, drizzle_orm.eq)(entityBridges.tenantId, this.tenantId), (0, drizzle_orm.eq)(entityBridges.sourceRef, sourceRef));
|
|
844
858
|
}
|
|
859
|
+
pgSyncBridgeWhere(sourceRef) {
|
|
860
|
+
return (0, drizzle_orm.and)((0, drizzle_orm.eq)(pgSyncBridges.tenantId, this.tenantId), (0, drizzle_orm.eq)(pgSyncBridges.sourceRef, sourceRef));
|
|
861
|
+
}
|
|
845
862
|
async createEntityType(et) {
|
|
846
863
|
await this.db.insert(entityTypes).values({
|
|
847
864
|
tenantId: this.tenantId,
|
|
@@ -850,6 +867,7 @@ var PostgresRegistry = class {
|
|
|
850
867
|
creationSchema: et.creation_schema ?? null,
|
|
851
868
|
inboxSchemas: et.inbox_schemas ?? null,
|
|
852
869
|
stateSchemas: et.state_schemas ?? null,
|
|
870
|
+
externallyWritableCollections: et.externally_writable_collections ?? null,
|
|
853
871
|
slashCommands: et.slash_commands ?? null,
|
|
854
872
|
serveEndpoint: et.serve_endpoint ?? null,
|
|
855
873
|
defaultDispatchPolicy: et.default_dispatch_policy ?? null,
|
|
@@ -863,6 +881,7 @@ var PostgresRegistry = class {
|
|
|
863
881
|
creationSchema: et.creation_schema ?? null,
|
|
864
882
|
inboxSchemas: et.inbox_schemas ?? null,
|
|
865
883
|
stateSchemas: et.state_schemas ?? null,
|
|
884
|
+
externallyWritableCollections: et.externally_writable_collections ?? null,
|
|
866
885
|
slashCommands: et.slash_commands ?? null,
|
|
867
886
|
serveEndpoint: et.serve_endpoint ?? null,
|
|
868
887
|
defaultDispatchPolicy: et.default_dispatch_policy ?? null,
|
|
@@ -881,6 +900,7 @@ var PostgresRegistry = class {
|
|
|
881
900
|
creationSchema: et.creation_schema ?? null,
|
|
882
901
|
inboxSchemas: et.inbox_schemas ?? null,
|
|
883
902
|
stateSchemas: et.state_schemas ?? null,
|
|
903
|
+
externallyWritableCollections: et.externally_writable_collections ?? null,
|
|
884
904
|
slashCommands: et.slash_commands ?? null,
|
|
885
905
|
serveEndpoint: et.serve_endpoint ?? null,
|
|
886
906
|
defaultDispatchPolicy: et.default_dispatch_policy ?? null,
|
|
@@ -908,6 +928,7 @@ var PostgresRegistry = class {
|
|
|
908
928
|
creationSchema: et.creation_schema ?? null,
|
|
909
929
|
inboxSchemas: et.inbox_schemas ?? null,
|
|
910
930
|
stateSchemas: et.state_schemas ?? null,
|
|
931
|
+
externallyWritableCollections: et.externally_writable_collections ?? null,
|
|
911
932
|
slashCommands: et.slash_commands ?? null,
|
|
912
933
|
serveEndpoint: et.serve_endpoint ?? null,
|
|
913
934
|
defaultDispatchPolicy: et.default_dispatch_policy ?? null,
|
|
@@ -1311,11 +1332,12 @@ var PostgresRegistry = class {
|
|
|
1311
1332
|
};
|
|
1312
1333
|
const nextTags = (0, __electric_ax_agents_runtime.normalizeTags)(mutation.nextTags);
|
|
1313
1334
|
const updatedAt = Date.now();
|
|
1314
|
-
await tx.update(entities).set({
|
|
1335
|
+
const [updateResult] = await tx.update(entities).set({
|
|
1315
1336
|
tags: nextTags,
|
|
1316
1337
|
tagsIndex: (0, __electric_ax_agents_runtime.buildTagsIndex)(nextTags),
|
|
1317
1338
|
updatedAt
|
|
1318
|
-
}).where(this.entityWhere(url));
|
|
1339
|
+
}).where(this.entityWhere(url)).returning({ txid: drizzle_orm.sql`pg_current_xact_id()::xid::text` });
|
|
1340
|
+
const txid = updateResult ? parseInt(updateResult.txid) : void 0;
|
|
1319
1341
|
await tx.insert(tagStreamOutbox).values({
|
|
1320
1342
|
tenantId: this.tenantId,
|
|
1321
1343
|
entityUrl: url,
|
|
@@ -1333,10 +1355,63 @@ var PostgresRegistry = class {
|
|
|
1333
1355
|
return {
|
|
1334
1356
|
entity,
|
|
1335
1357
|
changed: true,
|
|
1336
|
-
...op === `insert` || op === `update` ? { op } : {}
|
|
1358
|
+
...op === `insert` || op === `update` ? { op } : {},
|
|
1359
|
+
...txid !== void 0 ? { txid } : {}
|
|
1337
1360
|
};
|
|
1338
1361
|
});
|
|
1339
1362
|
}
|
|
1363
|
+
async upsertPgSyncBridge(row) {
|
|
1364
|
+
await this.db.insert(pgSyncBridges).values({
|
|
1365
|
+
tenantId: this.tenantId,
|
|
1366
|
+
sourceRef: row.sourceRef,
|
|
1367
|
+
options: row.options,
|
|
1368
|
+
streamUrl: row.streamUrl,
|
|
1369
|
+
lastTouchedAt: new Date(),
|
|
1370
|
+
updatedAt: new Date()
|
|
1371
|
+
}).onConflictDoUpdate({
|
|
1372
|
+
target: [pgSyncBridges.tenantId, pgSyncBridges.sourceRef],
|
|
1373
|
+
set: {
|
|
1374
|
+
options: row.options,
|
|
1375
|
+
streamUrl: row.streamUrl,
|
|
1376
|
+
initialSnapshotComplete: false,
|
|
1377
|
+
lastTouchedAt: new Date(),
|
|
1378
|
+
updatedAt: new Date()
|
|
1379
|
+
}
|
|
1380
|
+
});
|
|
1381
|
+
const existing = await this.getPgSyncBridge(row.sourceRef);
|
|
1382
|
+
if (!existing) throw new Error(`Failed to load pgSync bridge ${row.sourceRef}`);
|
|
1383
|
+
return existing;
|
|
1384
|
+
}
|
|
1385
|
+
async getPgSyncBridge(sourceRef) {
|
|
1386
|
+
const rows = await this.db.select().from(pgSyncBridges).where(this.pgSyncBridgeWhere(sourceRef)).limit(1);
|
|
1387
|
+
return rows[0] ? this.rowToPgSyncBridge(rows[0]) : null;
|
|
1388
|
+
}
|
|
1389
|
+
async listPgSyncBridges(tenantId = this.tenantId) {
|
|
1390
|
+
const rows = tenantId === null ? await this.db.select().from(pgSyncBridges) : await this.db.select().from(pgSyncBridges).where((0, drizzle_orm.eq)(pgSyncBridges.tenantId, tenantId));
|
|
1391
|
+
return rows.map((row) => this.rowToPgSyncBridge(row));
|
|
1392
|
+
}
|
|
1393
|
+
async touchPgSyncBridge(sourceRef) {
|
|
1394
|
+
await this.db.update(pgSyncBridges).set({
|
|
1395
|
+
lastTouchedAt: new Date(),
|
|
1396
|
+
updatedAt: new Date()
|
|
1397
|
+
}).where(this.pgSyncBridgeWhere(sourceRef));
|
|
1398
|
+
}
|
|
1399
|
+
async updatePgSyncBridgeCursor(sourceRef, shapeHandle, shapeOffset, initialSnapshotComplete) {
|
|
1400
|
+
await this.db.update(pgSyncBridges).set({
|
|
1401
|
+
shapeHandle,
|
|
1402
|
+
shapeOffset,
|
|
1403
|
+
...initialSnapshotComplete !== void 0 ? { initialSnapshotComplete } : {},
|
|
1404
|
+
updatedAt: new Date()
|
|
1405
|
+
}).where(this.pgSyncBridgeWhere(sourceRef));
|
|
1406
|
+
}
|
|
1407
|
+
async clearPgSyncBridgeCursor(sourceRef) {
|
|
1408
|
+
await this.db.update(pgSyncBridges).set({
|
|
1409
|
+
shapeHandle: null,
|
|
1410
|
+
shapeOffset: null,
|
|
1411
|
+
initialSnapshotComplete: false,
|
|
1412
|
+
updatedAt: new Date()
|
|
1413
|
+
}).where(this.pgSyncBridgeWhere(sourceRef));
|
|
1414
|
+
}
|
|
1340
1415
|
async upsertEntityBridge(row) {
|
|
1341
1416
|
await this.db.insert(entityBridges).values({
|
|
1342
1417
|
tenantId: this.tenantId,
|
|
@@ -1499,6 +1574,7 @@ var PostgresRegistry = class {
|
|
|
1499
1574
|
creation_schema: row.creationSchema,
|
|
1500
1575
|
inbox_schemas: row.inboxSchemas,
|
|
1501
1576
|
state_schemas: row.stateSchemas,
|
|
1577
|
+
externally_writable_collections: row.externallyWritableCollections ?? void 0,
|
|
1502
1578
|
slash_commands: row.slashCommands ?? void 0,
|
|
1503
1579
|
serve_endpoint: row.serveEndpoint ?? void 0,
|
|
1504
1580
|
default_dispatch_policy: row.defaultDispatchPolicy ?? void 0,
|
|
@@ -1556,6 +1632,20 @@ var PostgresRegistry = class {
|
|
|
1556
1632
|
updated_at: row.updatedAt
|
|
1557
1633
|
};
|
|
1558
1634
|
}
|
|
1635
|
+
rowToPgSyncBridge(row) {
|
|
1636
|
+
return {
|
|
1637
|
+
tenantId: row.tenantId,
|
|
1638
|
+
sourceRef: row.sourceRef,
|
|
1639
|
+
options: row.options,
|
|
1640
|
+
streamUrl: row.streamUrl,
|
|
1641
|
+
shapeHandle: row.shapeHandle ?? void 0,
|
|
1642
|
+
shapeOffset: row.shapeOffset ?? void 0,
|
|
1643
|
+
initialSnapshotComplete: row.initialSnapshotComplete,
|
|
1644
|
+
lastTouchedAt: row.lastTouchedAt,
|
|
1645
|
+
createdAt: row.createdAt,
|
|
1646
|
+
updatedAt: row.updatedAt
|
|
1647
|
+
};
|
|
1648
|
+
}
|
|
1559
1649
|
rowToEntityBridge(row) {
|
|
1560
1650
|
return {
|
|
1561
1651
|
tenantId: row.tenantId,
|
|
@@ -3203,6 +3293,9 @@ function assertSharedSandboxColocated(key, chosenIsRemote, dispatchPolicy) {
|
|
|
3203
3293
|
function isRecord$1(value) {
|
|
3204
3294
|
return typeof value === `object` && value !== null && !Array.isArray(value);
|
|
3205
3295
|
}
|
|
3296
|
+
function getPgSyncManifestStreamPath(sourceRef) {
|
|
3297
|
+
return `/_electric/pg-sync/${sourceRef}`;
|
|
3298
|
+
}
|
|
3206
3299
|
function extractManifestSourceUrl(manifest) {
|
|
3207
3300
|
if (!manifest) return void 0;
|
|
3208
3301
|
if (manifest.kind === `child` || manifest.kind === `observe`) return typeof manifest.entity_url === `string` ? manifest.entity_url : void 0;
|
|
@@ -3215,6 +3308,7 @@ function extractManifestSourceUrl(manifest) {
|
|
|
3215
3308
|
}
|
|
3216
3309
|
if (manifest.sourceType === `entities`) return typeof manifest.sourceRef === `string` ? `/_entities/${manifest.sourceRef}` : void 0;
|
|
3217
3310
|
if (manifest.sourceType === `db`) return typeof manifest.sourceRef === `string` ? (0, __electric_ax_agents_runtime.getSharedStateStreamPath)(manifest.sourceRef) : void 0;
|
|
3311
|
+
if (manifest.sourceType === `pgSync`) return typeof manifest.sourceRef === `string` ? getPgSyncManifestStreamPath(manifest.sourceRef) : void 0;
|
|
3218
3312
|
if (manifest.sourceType === `webhook`) {
|
|
3219
3313
|
if (typeof config?.streamUrl === `string`) return config.streamUrl;
|
|
3220
3314
|
if (typeof config?.endpointKey === `string`) return (0, __electric_ax_agents_runtime.getWebhookStreamPath)(config.endpointKey, typeof config.bucket === `string` ? config.bucket : void 0);
|
|
@@ -3329,6 +3423,13 @@ function isRecord(value) {
|
|
|
3329
3423
|
function cloneRecord(value) {
|
|
3330
3424
|
return JSON.parse(JSON.stringify(value));
|
|
3331
3425
|
}
|
|
3426
|
+
function withOptionalTxid(entity, txid) {
|
|
3427
|
+
if (txid === void 0) return entity;
|
|
3428
|
+
return {
|
|
3429
|
+
...entity,
|
|
3430
|
+
txid
|
|
3431
|
+
};
|
|
3432
|
+
}
|
|
3332
3433
|
/**
|
|
3333
3434
|
* Orchestrates the Electric Agents entity lifecycle: register types, spawn, send, kill.
|
|
3334
3435
|
*
|
|
@@ -3403,6 +3504,7 @@ var EntityManager = class {
|
|
|
3403
3504
|
creation_schema: req.creation_schema,
|
|
3404
3505
|
inbox_schemas: req.inbox_schemas,
|
|
3405
3506
|
state_schemas: req.state_schemas,
|
|
3507
|
+
externally_writable_collections: req.externally_writable_collections,
|
|
3406
3508
|
slash_commands: req.slash_commands,
|
|
3407
3509
|
serve_endpoint: req.serve_endpoint,
|
|
3408
3510
|
default_dispatch_policy: defaultDispatchPolicy,
|
|
@@ -4470,15 +4572,17 @@ var EntityManager = class {
|
|
|
4470
4572
|
await this.registry.updateStatus(entityUrl, `idle`);
|
|
4471
4573
|
await this.entityBridgeManager?.onEntityChanged(entityUrl);
|
|
4472
4574
|
}
|
|
4575
|
+
const txid = crypto.randomUUID();
|
|
4473
4576
|
const envelope = __electric_ax_agents_runtime.entityStateSchema.inbox.insert({
|
|
4474
4577
|
key,
|
|
4475
|
-
value
|
|
4578
|
+
value,
|
|
4579
|
+
headers: { txid }
|
|
4476
4580
|
});
|
|
4477
4581
|
const encoded = this.encodeChangeEvent(envelope);
|
|
4478
4582
|
try {
|
|
4479
4583
|
if (opts?.producerId) {
|
|
4480
4584
|
await this.streamClient.appendIdempotent(entity.streams.main, encoded, { producerId: opts.producerId });
|
|
4481
|
-
return;
|
|
4585
|
+
return { txid };
|
|
4482
4586
|
}
|
|
4483
4587
|
await this.streamClient.append(entity.streams.main, encoded);
|
|
4484
4588
|
if (entity.type === `principal` && req.type === `update_identity`) {
|
|
@@ -4486,14 +4590,50 @@ var EntityManager = class {
|
|
|
4486
4590
|
await this.streamClient.append(entity.streams.main, this.encodeChangeEvent({
|
|
4487
4591
|
type: `identity`,
|
|
4488
4592
|
key: `self`,
|
|
4489
|
-
value: identity
|
|
4593
|
+
value: identity,
|
|
4594
|
+
headers: { txid }
|
|
4490
4595
|
}));
|
|
4491
4596
|
}
|
|
4597
|
+
return { txid };
|
|
4492
4598
|
} catch (err) {
|
|
4493
4599
|
if (this.isClosedStreamError(err)) throw new ElectricAgentsError(ErrCodeNotRunning, `Entity is stopped`, 409);
|
|
4494
4600
|
throw err;
|
|
4495
4601
|
}
|
|
4496
4602
|
}
|
|
4603
|
+
async writeCollection(entityUrl, collection, req) {
|
|
4604
|
+
const entity = await this.registry.getEntity(entityUrl);
|
|
4605
|
+
if (!entity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
|
|
4606
|
+
const { externallyWritableCollections } = await this.getEffectiveSchemas(entity);
|
|
4607
|
+
const config = externallyWritableCollections?.[collection];
|
|
4608
|
+
if (!config) throw new ElectricAgentsError(ErrCodeUnauthorized, `Collection "${collection}" is not writable`, 403);
|
|
4609
|
+
const allowedOperations = config.operations ?? [`insert`];
|
|
4610
|
+
if (!allowedOperations.includes(req.operation)) throw new ElectricAgentsError(ErrCodeUnauthorized, `Operation "${req.operation}" is not allowed on collection "${collection}"`, 403);
|
|
4611
|
+
if (rejectsNormalWrites(entity.status)) throw new ElectricAgentsError(ErrCodeNotRunning, `Entity is not accepting writes`, 409);
|
|
4612
|
+
if (this.isForkWorkLockedEntity(entityUrl)) this.assertEntityNotForkWorkLocked(entityUrl);
|
|
4613
|
+
if (req.operation !== `delete` && (req.value === void 0 || req.value === null)) throw new ElectricAgentsError(ErrCodeInvalidRequest, `value is required for ${req.operation}`, 400);
|
|
4614
|
+
if (req.operation !== `insert` && !req.key) throw new ElectricAgentsError(ErrCodeInvalidRequest, `key is required for ${req.operation}`, 400);
|
|
4615
|
+
const key = req.key ?? `${collection}-${(0, node_crypto.randomUUID)()}`;
|
|
4616
|
+
const event = {
|
|
4617
|
+
type: config.type,
|
|
4618
|
+
key,
|
|
4619
|
+
headers: {
|
|
4620
|
+
operation: req.operation,
|
|
4621
|
+
timestamp: new Date().toISOString(),
|
|
4622
|
+
principal: req.principal
|
|
4623
|
+
}
|
|
4624
|
+
};
|
|
4625
|
+
if (req.operation !== `delete`) event.value = req.value;
|
|
4626
|
+
const validationError = await this.validateWriteEvent(entity, event);
|
|
4627
|
+
if (validationError) throw new ElectricAgentsError(validationError.code, validationError.message, validationError.status);
|
|
4628
|
+
const encoded = this.encodeChangeEvent(event);
|
|
4629
|
+
try {
|
|
4630
|
+
await this.streamClient.append(entity.streams.main, encoded);
|
|
4631
|
+
} catch (err) {
|
|
4632
|
+
if (this.isClosedStreamError(err)) throw new ElectricAgentsError(ErrCodeNotRunning, `Entity is stopped`, 409);
|
|
4633
|
+
throw err;
|
|
4634
|
+
}
|
|
4635
|
+
return { key };
|
|
4636
|
+
}
|
|
4497
4637
|
async updateInboxMessage(entityUrl, key, req) {
|
|
4498
4638
|
const entity = await this.registry.getEntity(entityUrl);
|
|
4499
4639
|
if (!entity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
|
|
@@ -4509,18 +4649,26 @@ var EntityManager = class {
|
|
|
4509
4649
|
if (req.status === `cancelled`) value.cancelled_at = now;
|
|
4510
4650
|
}
|
|
4511
4651
|
if (Object.keys(value).length === 0) throw new ElectricAgentsError(ErrCodeInvalidRequest, `No inbox fields to update`, 400);
|
|
4652
|
+
const txid = crypto.randomUUID();
|
|
4512
4653
|
const envelope = __electric_ax_agents_runtime.entityStateSchema.inbox.update({
|
|
4513
4654
|
key,
|
|
4514
|
-
value
|
|
4655
|
+
value,
|
|
4656
|
+
headers: { txid }
|
|
4515
4657
|
});
|
|
4516
4658
|
await this.streamClient.append(entity.streams.main, this.encodeChangeEvent(envelope));
|
|
4659
|
+
return { txid };
|
|
4517
4660
|
}
|
|
4518
4661
|
async deleteInboxMessage(entityUrl, key) {
|
|
4519
4662
|
const entity = await this.registry.getEntity(entityUrl);
|
|
4520
4663
|
if (!entity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
|
|
4521
4664
|
if (rejectsNormalWrites(entity.status)) throw new ElectricAgentsError(ErrCodeNotRunning, `Entity is not accepting writes`, 409);
|
|
4522
|
-
const
|
|
4665
|
+
const txid = crypto.randomUUID();
|
|
4666
|
+
const envelope = __electric_ax_agents_runtime.entityStateSchema.inbox.delete({
|
|
4667
|
+
key,
|
|
4668
|
+
headers: { txid }
|
|
4669
|
+
});
|
|
4523
4670
|
await this.streamClient.append(entity.streams.main, this.encodeChangeEvent(envelope));
|
|
4671
|
+
return { txid };
|
|
4524
4672
|
}
|
|
4525
4673
|
isAttachmentStreamPath(path$2) {
|
|
4526
4674
|
return /^\/[^/]+\/[^/]+\/attachments\/[^/]+$/.test(path$2);
|
|
@@ -4609,28 +4757,26 @@ var EntityManager = class {
|
|
|
4609
4757
|
await this.streamClient.delete(attachment.streamPath).catch(() => void 0);
|
|
4610
4758
|
return { txid };
|
|
4611
4759
|
}
|
|
4612
|
-
async setTag(entityUrl, key, req
|
|
4760
|
+
async setTag(entityUrl, key, req) {
|
|
4613
4761
|
const entity = await this.registry.getEntity(entityUrl);
|
|
4614
4762
|
if (!entity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
|
|
4615
|
-
if (!this.isValidWriteToken(entity, token)) throw new ElectricAgentsError(ErrCodeUnauthorized, `Invalid write token`, 401);
|
|
4616
4763
|
if (rejectsNormalWrites(entity.status)) throw new ElectricAgentsError(ErrCodeNotRunning, `Entity is not accepting writes`, 409);
|
|
4617
4764
|
if (typeof req.value !== `string`) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Tag values must be strings`, 400);
|
|
4618
4765
|
const result = await this.registry.setEntityTag(entityUrl, key, req.value);
|
|
4619
4766
|
const updated = result.entity;
|
|
4620
4767
|
if (!updated) throw new ElectricAgentsError(ErrCodeEntityPersistFailed, `Entity not found after tag write`, 500);
|
|
4621
4768
|
if (result.changed && this.entityBridgeManager) await this.entityBridgeManager.onEntityChanged(entityUrl);
|
|
4622
|
-
return updated;
|
|
4769
|
+
return withOptionalTxid(updated, result.txid);
|
|
4623
4770
|
}
|
|
4624
|
-
async deleteTag(entityUrl, key
|
|
4771
|
+
async deleteTag(entityUrl, key) {
|
|
4625
4772
|
const entity = await this.registry.getEntity(entityUrl);
|
|
4626
4773
|
if (!entity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
|
|
4627
|
-
if (!this.isValidWriteToken(entity, token)) throw new ElectricAgentsError(ErrCodeUnauthorized, `Invalid write token`, 401);
|
|
4628
4774
|
if (rejectsNormalWrites(entity.status)) throw new ElectricAgentsError(ErrCodeNotRunning, `Entity is not accepting writes`, 409);
|
|
4629
4775
|
const result = await this.registry.removeEntityTag(entityUrl, key);
|
|
4630
4776
|
const updated = result.entity;
|
|
4631
4777
|
if (!updated) throw new ElectricAgentsError(ErrCodeEntityPersistFailed, `Entity not found after tag delete`, 500);
|
|
4632
4778
|
if (result.changed && this.entityBridgeManager) await this.entityBridgeManager.onEntityChanged(entityUrl);
|
|
4633
|
-
return updated;
|
|
4779
|
+
return withOptionalTxid(updated, result.txid);
|
|
4634
4780
|
}
|
|
4635
4781
|
async ensureEntitiesMembershipStream(tags, principal) {
|
|
4636
4782
|
if (!this.entityBridgeManager) throw new Error(`Entity bridge manager not configured`);
|
|
@@ -5180,7 +5326,8 @@ var EntityManager = class {
|
|
|
5180
5326
|
async getEffectiveSchemas(entity) {
|
|
5181
5327
|
if (!entity.type) return {
|
|
5182
5328
|
inboxSchemas: entity.inbox_schemas,
|
|
5183
|
-
stateSchemas: entity.state_schemas
|
|
5329
|
+
stateSchemas: entity.state_schemas,
|
|
5330
|
+
externallyWritableCollections: void 0
|
|
5184
5331
|
};
|
|
5185
5332
|
const latestType = await this.registry.getEntityType(entity.type);
|
|
5186
5333
|
return {
|
|
@@ -5191,7 +5338,8 @@ var EntityManager = class {
|
|
|
5191
5338
|
stateSchemas: latestType?.state_schemas ? {
|
|
5192
5339
|
...entity.state_schemas ?? {},
|
|
5193
5340
|
...latestType.state_schemas
|
|
5194
|
-
} : entity.state_schemas
|
|
5341
|
+
} : entity.state_schemas,
|
|
5342
|
+
externallyWritableCollections: latestType?.externally_writable_collections
|
|
5195
5343
|
};
|
|
5196
5344
|
}
|
|
5197
5345
|
isClosedStreamError(err) {
|
|
@@ -5426,6 +5574,9 @@ function isPermanentElectricAgentsError(err) {
|
|
|
5426
5574
|
const name = typeof err === `object` && err !== null && `name` in err ? err.name : void 0;
|
|
5427
5575
|
return name === `ElectricAgentsError` && typeof status$4 === `number` && status$4 >= 400 && status$4 < 500;
|
|
5428
5576
|
}
|
|
5577
|
+
function cronTaskStreamPath(payload) {
|
|
5578
|
+
return typeof payload.streamPath === `string` ? payload.streamPath : null;
|
|
5579
|
+
}
|
|
5429
5580
|
function normalizeTask(row) {
|
|
5430
5581
|
return {
|
|
5431
5582
|
id: Number(row.id),
|
|
@@ -5768,6 +5919,15 @@ var Scheduler = class {
|
|
|
5768
5919
|
`;
|
|
5769
5920
|
if (completed.length === 0) return;
|
|
5770
5921
|
const nextFireAt = (0, __electric_ax_agents_runtime.getNextCronFireAt)(task.cronExpression, task.cronTimezone, task.fireAt);
|
|
5922
|
+
const streamPath = cronTaskStreamPath(task.payload);
|
|
5923
|
+
const subscriberRows = streamPath ? await sql$2`
|
|
5924
|
+
select 1 as exists
|
|
5925
|
+
from wake_registrations
|
|
5926
|
+
where tenant_id = ${tenantId}
|
|
5927
|
+
and source_url = ${streamPath}
|
|
5928
|
+
limit 1
|
|
5929
|
+
` : [];
|
|
5930
|
+
if (subscriberRows.length === 0) return;
|
|
5771
5931
|
await sql$2`
|
|
5772
5932
|
insert into scheduled_tasks (
|
|
5773
5933
|
tenant_id,
|
|
@@ -5867,6 +6027,308 @@ var Scheduler = class {
|
|
|
5867
6027
|
}
|
|
5868
6028
|
};
|
|
5869
6029
|
|
|
6030
|
+
//#endregion
|
|
6031
|
+
//#region src/pg-sync-bridge-manager.ts
|
|
6032
|
+
const PG_SYNC_ELECTRIC_SHAPE_URL = process.env.ELECTRIC_AGENTS_PG_SYNC_ELECTRIC_URL ?? `http://localhost:3000/v1/shape`;
|
|
6033
|
+
const DEFAULT_RETRY_INITIAL_DELAY_MS = 1e3;
|
|
6034
|
+
const DEFAULT_RETRY_MAX_DELAY_MS = 3e4;
|
|
6035
|
+
function buildElectricShapeParams(options) {
|
|
6036
|
+
return {
|
|
6037
|
+
table: options.table,
|
|
6038
|
+
...options.columns !== void 0 ? { columns: [...options.columns] } : {},
|
|
6039
|
+
...options.where !== void 0 ? { where: options.where } : {},
|
|
6040
|
+
...options.params !== void 0 ? { params: Array.isArray(options.params) ? [...options.params] : { ...options.params } } : {},
|
|
6041
|
+
...options.replica !== void 0 ? { replica: options.replica } : {},
|
|
6042
|
+
...options.metadata?.tenantId ? { electric_agents_tenant_id: options.metadata.tenantId } : {},
|
|
6043
|
+
...options.metadata?.principalKind ? { electric_agents_principal_kind: options.metadata.principalKind } : {},
|
|
6044
|
+
...options.metadata?.principalId ? { electric_agents_principal_id: options.metadata.principalId } : {},
|
|
6045
|
+
...options.metadata?.principalKey ? { electric_agents_principal_key: options.metadata.principalKey } : {},
|
|
6046
|
+
...options.metadata?.principalUrl ? { electric_agents_principal_url: options.metadata.principalUrl } : {},
|
|
6047
|
+
...options.metadata?.entityUrl ? { electric_agents_entity_url: options.metadata.entityUrl } : {},
|
|
6048
|
+
...options.metadata?.entityType ? { electric_agents_entity_type: options.metadata.entityType } : {},
|
|
6049
|
+
...options.metadata?.streamPath ? { electric_agents_stream_path: options.metadata.streamPath } : {},
|
|
6050
|
+
...options.metadata?.runtimeConsumerId ? { electric_agents_runtime_consumer_id: options.metadata.runtimeConsumerId } : {},
|
|
6051
|
+
...options.metadata?.wakeId ? { electric_agents_wake_id: options.metadata.wakeId } : {}
|
|
6052
|
+
};
|
|
6053
|
+
}
|
|
6054
|
+
function jsonSafe(value) {
|
|
6055
|
+
if (typeof value === `bigint`) return value.toString();
|
|
6056
|
+
if (value === null || typeof value !== `object`) return value;
|
|
6057
|
+
if (Array.isArray(value)) return value.map(jsonSafe);
|
|
6058
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, jsonSafe(item)]));
|
|
6059
|
+
}
|
|
6060
|
+
function stableJson(value) {
|
|
6061
|
+
if (typeof value === `bigint`) return JSON.stringify(value.toString());
|
|
6062
|
+
if (value === null || typeof value !== `object`) return JSON.stringify(value);
|
|
6063
|
+
if (Array.isArray(value)) return `[${value.map(stableJson).join(`,`)}]`;
|
|
6064
|
+
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`).join(`,`)}}`;
|
|
6065
|
+
}
|
|
6066
|
+
function parseElectricOffset(offset) {
|
|
6067
|
+
if (offset === `-1`) return offset;
|
|
6068
|
+
return /^\d+_\d+$/.test(offset) ? offset : null;
|
|
6069
|
+
}
|
|
6070
|
+
function rowKeyForMessage(message) {
|
|
6071
|
+
const headers = message.headers;
|
|
6072
|
+
const candidate = headers.key ?? headers.rowKey ?? message.value?.id ?? message.value?.key ?? message.old_value?.id ?? message.old_value?.key;
|
|
6073
|
+
return candidate === void 0 ? void 0 : stableJson(candidate);
|
|
6074
|
+
}
|
|
6075
|
+
function pgSyncMessageToDurableEvent(message, optionsOrSourceRef) {
|
|
6076
|
+
const operation = message.headers.operation;
|
|
6077
|
+
if (operation !== `insert` && operation !== `update` && operation !== `delete`) return null;
|
|
6078
|
+
const sourceRef = typeof optionsOrSourceRef === `string` ? optionsOrSourceRef : (0, __electric_ax_agents_runtime.sourceRefForPgSync)(optionsOrSourceRef);
|
|
6079
|
+
const rowKey = rowKeyForMessage(message);
|
|
6080
|
+
const offset = message.headers.offset;
|
|
6081
|
+
if (typeof offset !== `string` || offset.length === 0) return null;
|
|
6082
|
+
const messageKeyPart = offset;
|
|
6083
|
+
const messageKey = `${sourceRef}:${operation}:${messageKeyPart}`;
|
|
6084
|
+
const timestamp$1 = new Date().toISOString();
|
|
6085
|
+
const oldValue = message.old_value;
|
|
6086
|
+
const safeValue = jsonSafe(message.value);
|
|
6087
|
+
const safeOldValue = jsonSafe(oldValue);
|
|
6088
|
+
const safeHeaders = jsonSafe(message.headers);
|
|
6089
|
+
return {
|
|
6090
|
+
type: `pg_sync_change`,
|
|
6091
|
+
key: messageKey,
|
|
6092
|
+
value: {
|
|
6093
|
+
key: messageKey,
|
|
6094
|
+
table: typeof optionsOrSourceRef === `string` ? void 0 : optionsOrSourceRef.table,
|
|
6095
|
+
operation,
|
|
6096
|
+
...rowKey !== void 0 ? { rowKey } : {},
|
|
6097
|
+
...message.value !== void 0 ? { value: safeValue } : {},
|
|
6098
|
+
...oldValue !== void 0 ? { oldValue: safeOldValue } : {},
|
|
6099
|
+
headers: safeHeaders,
|
|
6100
|
+
...typeof offset === `string` ? { offset } : {},
|
|
6101
|
+
receivedAt: timestamp$1
|
|
6102
|
+
},
|
|
6103
|
+
headers: {
|
|
6104
|
+
operation,
|
|
6105
|
+
timestamp: timestamp$1
|
|
6106
|
+
}
|
|
6107
|
+
};
|
|
6108
|
+
}
|
|
6109
|
+
function cursorFromRow(row) {
|
|
6110
|
+
return row?.shapeHandle && row.shapeOffset ? {
|
|
6111
|
+
handle: row.shapeHandle,
|
|
6112
|
+
offset: row.shapeOffset,
|
|
6113
|
+
initialSnapshotComplete: row.initialSnapshotComplete
|
|
6114
|
+
} : void 0;
|
|
6115
|
+
}
|
|
6116
|
+
var PgSyncBridge = class {
|
|
6117
|
+
producer = null;
|
|
6118
|
+
unsubscribe = null;
|
|
6119
|
+
abortController = null;
|
|
6120
|
+
skipChangesUntilUpToDate = false;
|
|
6121
|
+
recovering = false;
|
|
6122
|
+
committedCursor;
|
|
6123
|
+
retryAttempt = 0;
|
|
6124
|
+
constructor(sourceRef, streamUrl, options, resolvedSource, retry, streamClient, registry, evaluateWakes, initialCursor) {
|
|
6125
|
+
this.sourceRef = sourceRef;
|
|
6126
|
+
this.streamUrl = streamUrl;
|
|
6127
|
+
this.options = options;
|
|
6128
|
+
this.resolvedSource = resolvedSource;
|
|
6129
|
+
this.retry = retry;
|
|
6130
|
+
this.streamClient = streamClient;
|
|
6131
|
+
this.registry = registry;
|
|
6132
|
+
this.evaluateWakes = evaluateWakes;
|
|
6133
|
+
this.initialCursor = initialCursor;
|
|
6134
|
+
this.committedCursor = initialCursor;
|
|
6135
|
+
}
|
|
6136
|
+
async start() {
|
|
6137
|
+
if (!this.producer) this.producer = new __durable_streams_client.IdempotentProducer(new __durable_streams_client.DurableStream({
|
|
6138
|
+
url: `${this.streamClient.baseUrl}${this.streamUrl}`,
|
|
6139
|
+
contentType: `application/json`
|
|
6140
|
+
}), `pg-sync-bridge-${this.sourceRef}`);
|
|
6141
|
+
if (this.initialCursor) {
|
|
6142
|
+
const offset = parseElectricOffset(this.initialCursor.offset);
|
|
6143
|
+
if (offset) {
|
|
6144
|
+
this.startStream(offset, this.initialCursor.handle, !this.initialCursor.initialSnapshotComplete);
|
|
6145
|
+
return;
|
|
6146
|
+
}
|
|
6147
|
+
}
|
|
6148
|
+
await this.registry?.clearPgSyncBridgeCursor(this.sourceRef);
|
|
6149
|
+
this.startStream(`now`, void 0, true);
|
|
6150
|
+
}
|
|
6151
|
+
async stop() {
|
|
6152
|
+
this.unsubscribe?.();
|
|
6153
|
+
this.abortController?.abort();
|
|
6154
|
+
this.unsubscribe = null;
|
|
6155
|
+
this.abortController = null;
|
|
6156
|
+
try {
|
|
6157
|
+
await this.producer?.flush();
|
|
6158
|
+
} finally {
|
|
6159
|
+
await this.producer?.detach();
|
|
6160
|
+
this.producer = null;
|
|
6161
|
+
}
|
|
6162
|
+
}
|
|
6163
|
+
startStream(offset, handle, skipChangesUntilUpToDate = false, log = offset === `now` ? `changes_only` : `full`) {
|
|
6164
|
+
this.unsubscribe?.();
|
|
6165
|
+
this.abortController?.abort();
|
|
6166
|
+
this.skipChangesUntilUpToDate = skipChangesUntilUpToDate;
|
|
6167
|
+
this.abortController = new AbortController();
|
|
6168
|
+
const stream = new __electric_sql_client.ShapeStream({
|
|
6169
|
+
url: this.resolvedSource.url,
|
|
6170
|
+
params: buildElectricShapeParams(this.options),
|
|
6171
|
+
offset,
|
|
6172
|
+
log,
|
|
6173
|
+
...handle ? { handle } : {},
|
|
6174
|
+
signal: this.abortController.signal
|
|
6175
|
+
});
|
|
6176
|
+
this.unsubscribe = stream.subscribe(async (messages) => {
|
|
6177
|
+
try {
|
|
6178
|
+
for (const message of messages) {
|
|
6179
|
+
if ((0, __electric_sql_client.isControlMessage)(message)) {
|
|
6180
|
+
if (message.headers.control === `must-refetch`) {
|
|
6181
|
+
await this.registry?.clearPgSyncBridgeCursor(this.sourceRef);
|
|
6182
|
+
this.startStream(`now`, void 0, true);
|
|
6183
|
+
return;
|
|
6184
|
+
}
|
|
6185
|
+
if (message.headers.control === `up-to-date`) {
|
|
6186
|
+
this.skipChangesUntilUpToDate = false;
|
|
6187
|
+
await this.persistCursor(stream, true);
|
|
6188
|
+
continue;
|
|
6189
|
+
}
|
|
6190
|
+
await this.persistCursor(stream);
|
|
6191
|
+
continue;
|
|
6192
|
+
}
|
|
6193
|
+
if (!(0, __electric_sql_client.isChangeMessage)(message)) continue;
|
|
6194
|
+
if (!this.skipChangesUntilUpToDate) {
|
|
6195
|
+
const event = pgSyncMessageToDurableEvent(message, this.options);
|
|
6196
|
+
if (event) {
|
|
6197
|
+
if (!this.producer) throw new Error(`pg-sync producer is not started`);
|
|
6198
|
+
await this.producer.append(JSON.stringify(event));
|
|
6199
|
+
await this.producer.flush?.();
|
|
6200
|
+
await this.evaluateWakes?.(this.streamUrl, event);
|
|
6201
|
+
}
|
|
6202
|
+
}
|
|
6203
|
+
await this.persistCursor(stream);
|
|
6204
|
+
this.retryAttempt = 0;
|
|
6205
|
+
}
|
|
6206
|
+
} catch (error) {
|
|
6207
|
+
serverLog.warn(`[pg-sync-bridge] subscription callback failed for ${this.sourceRef}:`, error);
|
|
6208
|
+
await this.recoverStream();
|
|
6209
|
+
}
|
|
6210
|
+
}, (error) => {
|
|
6211
|
+
if (this.abortController?.signal.aborted) return;
|
|
6212
|
+
serverLog.warn(`[pg-sync-bridge] subscription failed for ${this.sourceRef}:`, error);
|
|
6213
|
+
this.recoverStream();
|
|
6214
|
+
});
|
|
6215
|
+
}
|
|
6216
|
+
async recoverStream() {
|
|
6217
|
+
if (this.recovering) return;
|
|
6218
|
+
this.recovering = true;
|
|
6219
|
+
try {
|
|
6220
|
+
const attempt = this.retryAttempt++;
|
|
6221
|
+
const baseDelay = Math.min(this.retry.initialDelayMs * 2 ** attempt, this.retry.maxDelayMs);
|
|
6222
|
+
const jitter = Math.floor(baseDelay * .2 * this.retry.random());
|
|
6223
|
+
const delay = baseDelay + jitter;
|
|
6224
|
+
if (delay > 0) await this.retry.sleep(delay);
|
|
6225
|
+
const offset = this.committedCursor ? parseElectricOffset(this.committedCursor.offset) : null;
|
|
6226
|
+
if (offset && this.committedCursor) this.startStream(offset, this.committedCursor.handle, !this.committedCursor.initialSnapshotComplete);
|
|
6227
|
+
else {
|
|
6228
|
+
await this.registry?.clearPgSyncBridgeCursor(this.sourceRef);
|
|
6229
|
+
this.startStream(`now`, void 0, true);
|
|
6230
|
+
}
|
|
6231
|
+
} finally {
|
|
6232
|
+
this.recovering = false;
|
|
6233
|
+
}
|
|
6234
|
+
}
|
|
6235
|
+
async persistCursor(stream, initialSnapshotComplete = !this.skipChangesUntilUpToDate) {
|
|
6236
|
+
const shapeHandle = stream.shapeHandle;
|
|
6237
|
+
const shapeOffset = stream.lastOffset;
|
|
6238
|
+
if (!shapeHandle || !shapeOffset || shapeOffset === `-1`) return;
|
|
6239
|
+
await this.registry?.updatePgSyncBridgeCursor(this.sourceRef, shapeHandle, shapeOffset, initialSnapshotComplete);
|
|
6240
|
+
this.committedCursor = {
|
|
6241
|
+
handle: shapeHandle,
|
|
6242
|
+
offset: shapeOffset,
|
|
6243
|
+
initialSnapshotComplete
|
|
6244
|
+
};
|
|
6245
|
+
}
|
|
6246
|
+
};
|
|
6247
|
+
var PgSyncBridgeManager = class {
|
|
6248
|
+
bridges = new Map();
|
|
6249
|
+
starting = new Map();
|
|
6250
|
+
url;
|
|
6251
|
+
retry;
|
|
6252
|
+
constructor(streamClient, evaluateWakes, registry, options = {}) {
|
|
6253
|
+
this.streamClient = streamClient;
|
|
6254
|
+
this.evaluateWakes = evaluateWakes;
|
|
6255
|
+
this.registry = registry;
|
|
6256
|
+
this.url = options.url ?? PG_SYNC_ELECTRIC_SHAPE_URL;
|
|
6257
|
+
this.retry = {
|
|
6258
|
+
initialDelayMs: options.retry?.initialDelayMs ?? DEFAULT_RETRY_INITIAL_DELAY_MS,
|
|
6259
|
+
maxDelayMs: options.retry?.maxDelayMs ?? DEFAULT_RETRY_MAX_DELAY_MS,
|
|
6260
|
+
random: options.retry?.random ?? Math.random,
|
|
6261
|
+
sleep: options.retry?.sleep ?? ((ms) => new Promise((resolve$1) => setTimeout(resolve$1, ms)))
|
|
6262
|
+
};
|
|
6263
|
+
}
|
|
6264
|
+
async start() {
|
|
6265
|
+
const rows = await this.registry?.listPgSyncBridges?.();
|
|
6266
|
+
if (!rows) return;
|
|
6267
|
+
await Promise.all(rows.map((row) => this.ensureBridge(row).catch((error) => {
|
|
6268
|
+
serverLog.warn(`[pg-sync-bridge] failed to start ${row.sourceRef}:`, error);
|
|
6269
|
+
})));
|
|
6270
|
+
}
|
|
6271
|
+
async register(options, metadata) {
|
|
6272
|
+
const mergedMetadata = {
|
|
6273
|
+
...options.metadata,
|
|
6274
|
+
...metadata
|
|
6275
|
+
};
|
|
6276
|
+
const canonicalOptions = {
|
|
6277
|
+
...(0, __electric_ax_agents_runtime.canonicalPgSyncOptions)(options),
|
|
6278
|
+
...Object.keys(mergedMetadata).length > 0 ? { metadata: mergedMetadata } : {}
|
|
6279
|
+
};
|
|
6280
|
+
const resolvedSource = this.resolveSource(canonicalOptions);
|
|
6281
|
+
const sourceRef = (0, __electric_ax_agents_runtime.sourceRefForPgSync)(canonicalOptions);
|
|
6282
|
+
const streamUrl = (0, __electric_ax_agents_runtime.getPgSyncStreamPath)(sourceRef, this.registry?.tenantId);
|
|
6283
|
+
const row = await this.registry?.upsertPgSyncBridge({
|
|
6284
|
+
sourceRef,
|
|
6285
|
+
options: canonicalOptions,
|
|
6286
|
+
streamUrl
|
|
6287
|
+
});
|
|
6288
|
+
await this.streamClient.ensure(streamUrl, { contentType: `application/json` });
|
|
6289
|
+
if (!this.bridges.has(sourceRef)) {
|
|
6290
|
+
let start = this.starting.get(sourceRef);
|
|
6291
|
+
if (!start) {
|
|
6292
|
+
start = (async () => {
|
|
6293
|
+
const bridge = new PgSyncBridge(sourceRef, streamUrl, canonicalOptions, resolvedSource, this.retry, this.streamClient, this.registry, this.evaluateWakes, cursorFromRow(row));
|
|
6294
|
+
await bridge.start();
|
|
6295
|
+
this.bridges.set(sourceRef, bridge);
|
|
6296
|
+
})().finally(() => this.starting.delete(sourceRef));
|
|
6297
|
+
this.starting.set(sourceRef, start);
|
|
6298
|
+
}
|
|
6299
|
+
await start;
|
|
6300
|
+
}
|
|
6301
|
+
return {
|
|
6302
|
+
sourceRef,
|
|
6303
|
+
streamUrl
|
|
6304
|
+
};
|
|
6305
|
+
}
|
|
6306
|
+
async ensureBridge(row) {
|
|
6307
|
+
if (this.bridges.has(row.sourceRef)) return;
|
|
6308
|
+
let start = this.starting.get(row.sourceRef);
|
|
6309
|
+
if (!start) {
|
|
6310
|
+
start = (async () => {
|
|
6311
|
+
await this.streamClient.ensure(row.streamUrl, { contentType: `application/json` });
|
|
6312
|
+
const canonicalOptions = (0, __electric_ax_agents_runtime.canonicalPgSyncOptions)(row.options);
|
|
6313
|
+
const resolvedSource = this.resolveSource(canonicalOptions);
|
|
6314
|
+
const bridge = new PgSyncBridge(row.sourceRef, row.streamUrl, canonicalOptions, resolvedSource, this.retry, this.streamClient, this.registry, this.evaluateWakes, cursorFromRow(row));
|
|
6315
|
+
await bridge.start();
|
|
6316
|
+
this.bridges.set(row.sourceRef, bridge);
|
|
6317
|
+
})().finally(() => this.starting.delete(row.sourceRef));
|
|
6318
|
+
this.starting.set(row.sourceRef, start);
|
|
6319
|
+
}
|
|
6320
|
+
await start;
|
|
6321
|
+
}
|
|
6322
|
+
resolveSource(options) {
|
|
6323
|
+
return { url: options.url ?? this.url };
|
|
6324
|
+
}
|
|
6325
|
+
async stop() {
|
|
6326
|
+
await Promise.allSettled(this.starting.values());
|
|
6327
|
+
await Promise.all([...this.bridges.values()].map((bridge) => bridge.stop()));
|
|
6328
|
+
this.bridges.clear();
|
|
6329
|
+
}
|
|
6330
|
+
};
|
|
6331
|
+
|
|
5870
6332
|
//#endregion
|
|
5871
6333
|
//#region src/runtime.ts
|
|
5872
6334
|
function omitUndefined(value) {
|
|
@@ -5881,6 +6343,7 @@ var ElectricAgentsTenantRuntime = class {
|
|
|
5881
6343
|
wakeRegistry;
|
|
5882
6344
|
scheduler;
|
|
5883
6345
|
entityBridgeManager;
|
|
6346
|
+
pgSyncBridgeManager;
|
|
5884
6347
|
claimWriteTokens;
|
|
5885
6348
|
manager;
|
|
5886
6349
|
constructor(options) {
|
|
@@ -5905,9 +6368,10 @@ var ElectricAgentsTenantRuntime = class {
|
|
|
5905
6368
|
writeTokenValidator: (entity, token) => this.claimWriteTokens.isValid(this.serviceId, entity.streams.main, token),
|
|
5906
6369
|
stopWakeRegistryOnShutdown: options.stopWakeRegistryOnShutdown ?? false
|
|
5907
6370
|
});
|
|
6371
|
+
this.pgSyncBridgeManager = options.pgSyncBridgeManager ?? new PgSyncBridgeManager(this.streamClient, (sourceUrl, event) => this.manager.evaluateWakes(sourceUrl, event), this.registry, options.pgSync);
|
|
5908
6372
|
}
|
|
5909
6373
|
async stop() {
|
|
5910
|
-
await this.manager.shutdown();
|
|
6374
|
+
await Promise.all([this.manager.shutdown(), this.pgSyncBridgeManager.stop()]);
|
|
5911
6375
|
}
|
|
5912
6376
|
async rehydrateCronSchedules() {
|
|
5913
6377
|
const rows = await this.db.select({ sourceUrl: wakeRegistrations.sourceUrl }).from(wakeRegistrations).where((0, drizzle_orm.eq)(wakeRegistrations.tenantId, this.serviceId));
|
|
@@ -6671,7 +7135,10 @@ var WakeRegistry = class {
|
|
|
6671
7135
|
}
|
|
6672
7136
|
if (!(0, __electric_sql_client.isChangeMessage)(message)) return;
|
|
6673
7137
|
if (message.headers.operation === `delete`) {
|
|
6674
|
-
|
|
7138
|
+
const oldValue = message.old_value;
|
|
7139
|
+
const oldId = Number(oldValue?.id);
|
|
7140
|
+
if (Number.isFinite(oldId)) this.removeCachedRegistrationByDbId(oldId);
|
|
7141
|
+
else this.resetCachedRegistrations();
|
|
6675
7142
|
return;
|
|
6676
7143
|
}
|
|
6677
7144
|
this.upsertCachedRegistration(this.normalizeShapeRow(message.value));
|
|
@@ -6782,9 +7249,9 @@ var WakeRegistry = class {
|
|
|
6782
7249
|
matchCondition(reg, event) {
|
|
6783
7250
|
if (reg.condition === `runFinished`) {
|
|
6784
7251
|
if (event.type !== `run`) return null;
|
|
6785
|
-
const value = event.value;
|
|
7252
|
+
const value$1 = event.value;
|
|
6786
7253
|
const headers$1 = event.headers;
|
|
6787
|
-
const status$4 = value?.status;
|
|
7254
|
+
const status$4 = value$1?.status;
|
|
6788
7255
|
const operation$1 = headers$1?.operation;
|
|
6789
7256
|
if (operation$1 !== `update`) return null;
|
|
6790
7257
|
if (status$4 !== `completed` && status$4 !== `failed`) return null;
|
|
@@ -6807,13 +7274,15 @@ var WakeRegistry = class {
|
|
|
6807
7274
|
}
|
|
6808
7275
|
const kind = operation === `delete` ? `delete` : operation === `update` ? `update` : `insert`;
|
|
6809
7276
|
if (condition.ops && condition.ops.length > 0 && !condition.ops.includes(kind)) return null;
|
|
7277
|
+
const value = event.value;
|
|
6810
7278
|
const change = {
|
|
6811
7279
|
collection: eventType,
|
|
6812
7280
|
kind,
|
|
6813
7281
|
key: event.key || ``
|
|
6814
7282
|
};
|
|
7283
|
+
if (value && `value` in value) change.value = value.value;
|
|
7284
|
+
if (value && `oldValue` in value) change.oldValue = value.oldValue;
|
|
6815
7285
|
if (eventType === `inbox`) {
|
|
6816
|
-
const value = event.value;
|
|
6817
7286
|
if (typeof value?.from === `string`) change.from = value.from;
|
|
6818
7287
|
if (typeof value?.from_principal === `string`) change.from_principal = value.from_principal;
|
|
6819
7288
|
if (typeof value?.from_agent === `string`) change.from_agent = value.from_agent;
|
|
@@ -7217,6 +7686,22 @@ function resolveDurableStreamsRoutingAdapter(adapter, _durableStreamsUrl) {
|
|
|
7217
7686
|
|
|
7218
7687
|
//#endregion
|
|
7219
7688
|
//#region src/utils/server-utils.ts
|
|
7689
|
+
/**
|
|
7690
|
+
* Raised when an Electric shape proxy request must be rejected for security
|
|
7691
|
+
* reasons (an un-scoped table, or a client `where` clause that could escape the
|
|
7692
|
+
* enforced per-tenant/per-principal scoping). The global `errorMapper` hook
|
|
7693
|
+
* maps this to an HTTP error response. Defined here (rather than reusing
|
|
7694
|
+
* `ElectricAgentsError`) to keep this module free of the heavy entity-manager
|
|
7695
|
+
* import graph.
|
|
7696
|
+
*/
|
|
7697
|
+
var ElectricProxyError = class extends Error {
|
|
7698
|
+
constructor(code, message, status$4) {
|
|
7699
|
+
super(message);
|
|
7700
|
+
this.code = code;
|
|
7701
|
+
this.status = status$4;
|
|
7702
|
+
this.name = `ElectricProxyError`;
|
|
7703
|
+
}
|
|
7704
|
+
};
|
|
7220
7705
|
function buildElectricProxyTarget(options) {
|
|
7221
7706
|
const targetPath = options.incomingUrl.pathname.replace(`/_electric/electric`, ``);
|
|
7222
7707
|
const target = electricUrlWithPath(options.electricUrl, targetPath);
|
|
@@ -7226,7 +7711,12 @@ function buildElectricProxyTarget(options) {
|
|
|
7226
7711
|
applyElectricUrlQueryParams(target, options.electricUrl);
|
|
7227
7712
|
if (targetPath !== `/v1/shape`) return target;
|
|
7228
7713
|
if (options.electricSecret) target.searchParams.set(`secret`, options.electricSecret);
|
|
7229
|
-
const
|
|
7714
|
+
const clientWhere = options.incomingUrl.searchParams.get(`where`);
|
|
7715
|
+
if (clientWhere !== null && !isSelfContainedWhereClause(clientWhere)) throw new ElectricProxyError(`INVALID_WHERE`, `Invalid where clause`, 400);
|
|
7716
|
+
const tableParams = options.incomingUrl.searchParams.getAll(`table`);
|
|
7717
|
+
if (tableParams.length !== 1) throw new ElectricProxyError(`TABLE_NOT_ALLOWED`, `Table is not available through the Electric proxy`, 403);
|
|
7718
|
+
const table = tableParams[0];
|
|
7719
|
+
target.searchParams.set(`table`, table);
|
|
7230
7720
|
if (table === `entities`) {
|
|
7231
7721
|
target.searchParams.set(`columns`, `"tenant_id","url","type","status","dispatch_policy","tags","spawn_args","sandbox","parent","created_by","type_revision","inbox_schemas","state_schemas","created_at","updated_at"`);
|
|
7232
7722
|
applyShapeWhere(target, buildReadableEntitiesWhere({
|
|
@@ -7284,9 +7774,39 @@ function buildElectricProxyTarget(options) {
|
|
|
7284
7774
|
principalKind: options.principalKind ?? ``,
|
|
7285
7775
|
permissionBypass: options.permissionBypass
|
|
7286
7776
|
}));
|
|
7287
|
-
}
|
|
7777
|
+
} else throw new ElectricProxyError(`TABLE_NOT_ALLOWED`, `Table is not available through the Electric proxy`, 403);
|
|
7288
7778
|
return target;
|
|
7289
7779
|
}
|
|
7780
|
+
/**
|
|
7781
|
+
* Returns true when a client-supplied Electric `where` clause is self-contained:
|
|
7782
|
+
* its parentheses are balanced, never close below the top level, all string
|
|
7783
|
+
* (`'`) and identifier (`"`) literals are terminated, and it contains no SQL
|
|
7784
|
+
* comment markers. Such a clause cannot break out of the `(...)` group it is
|
|
7785
|
+
* wrapped in when AND-combined with the enforced scoping predicate, nor comment
|
|
7786
|
+
* out the trailing paren the proxy appends. Characters inside string/identifier
|
|
7787
|
+
* literals are ignored. Comment markers are rejected unconditionally (even where
|
|
7788
|
+
* harmless) as a conservative defensive measure; dollar-quoted and `E''` strings
|
|
7789
|
+
* are not modeled and only ever cause fail-safe over-rejection, never a bypass.
|
|
7790
|
+
*/
|
|
7791
|
+
function isSelfContainedWhereClause(where) {
|
|
7792
|
+
let depth = 0;
|
|
7793
|
+
let quote = null;
|
|
7794
|
+
for (let i = 0; i < where.length; i++) {
|
|
7795
|
+
const ch = where[i];
|
|
7796
|
+
if (quote !== null) {
|
|
7797
|
+
if (ch === quote) if (where[i + 1] === quote) i++;
|
|
7798
|
+
else quote = null;
|
|
7799
|
+
continue;
|
|
7800
|
+
}
|
|
7801
|
+
if (ch === `'` || ch === `"`) quote = ch;
|
|
7802
|
+
else if (ch === `(`) depth++;
|
|
7803
|
+
else if (ch === `)`) {
|
|
7804
|
+
depth--;
|
|
7805
|
+
if (depth < 0) return false;
|
|
7806
|
+
} else if (ch === `-` && where[i + 1] === `-` || ch === `/` && where[i + 1] === `*`) return false;
|
|
7807
|
+
}
|
|
7808
|
+
return depth === 0 && quote === null;
|
|
7809
|
+
}
|
|
7290
7810
|
function buildReadableEntitiesWhere(options) {
|
|
7291
7811
|
const tenant = sqlStringLiteral(options.tenantId);
|
|
7292
7812
|
if (options.permissionBypass) return `tenant_id = ${tenant}`;
|
|
@@ -8057,6 +8577,15 @@ const spawnBodySchema = __sinclair_typebox.Type.Object({
|
|
|
8057
8577
|
manifestKey: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String())
|
|
8058
8578
|
}))
|
|
8059
8579
|
});
|
|
8580
|
+
const writeCollectionBodySchema = __sinclair_typebox.Type.Object({
|
|
8581
|
+
operation: __sinclair_typebox.Type.Union([
|
|
8582
|
+
__sinclair_typebox.Type.Literal(`insert`),
|
|
8583
|
+
__sinclair_typebox.Type.Literal(`update`),
|
|
8584
|
+
__sinclair_typebox.Type.Literal(`delete`)
|
|
8585
|
+
]),
|
|
8586
|
+
key: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
8587
|
+
value: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Record(__sinclair_typebox.Type.String(), __sinclair_typebox.Type.Unknown()))
|
|
8588
|
+
}, { additionalProperties: false });
|
|
8060
8589
|
const sendBodySchema = __sinclair_typebox.Type.Object({
|
|
8061
8590
|
payload: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Unknown()),
|
|
8062
8591
|
key: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
@@ -8189,6 +8718,7 @@ entitiesRouter.head(`/:type/:instanceId`, withExistingEntity, withEntityPermissi
|
|
|
8189
8718
|
entitiesRouter.delete(`/:type/:instanceId`, withExistingEntity, withEntityPermission(`delete`), killEntity);
|
|
8190
8719
|
entitiesRouter.post(`/:type/:instanceId/signal`, withExistingEntity, withSchema(signalBodySchema), withEntityPermission(`signal`), signalEntity);
|
|
8191
8720
|
entitiesRouter.post(`/:type/:instanceId/send`, withExistingEntity, withSchema(sendBodySchema), withEntityPermission(`write`), sendEntity);
|
|
8721
|
+
entitiesRouter.post(`/:type/:instanceId/collections/:collection`, withExistingEntity, withSchema(writeCollectionBodySchema), withEntityPermission(`write`), writeCollection);
|
|
8192
8722
|
entitiesRouter.post(`/:type/:instanceId/attachments`, withExistingEntity, withEntityPermission(`write`), createAttachment);
|
|
8193
8723
|
entitiesRouter.get(`/:type/:instanceId/attachments/:attachmentId`, withExistingEntity, withEntityPermission(`read`), readAttachment);
|
|
8194
8724
|
entitiesRouter.delete(`/:type/:instanceId/attachments/:attachmentId`, withExistingEntity, withEntityPermission(`write`), deleteAttachment);
|
|
@@ -8294,7 +8824,7 @@ async function parseAttachmentForm(request) {
|
|
|
8294
8824
|
};
|
|
8295
8825
|
}
|
|
8296
8826
|
function contentDisposition(filename) {
|
|
8297
|
-
const fallback = filename.replace(/["
|
|
8827
|
+
const fallback = filename.replace(/[^\x20-\x7e]|["\\]/g, `_`);
|
|
8298
8828
|
return `attachment; filename="${fallback}"; filename*=UTF-8''${encodeURIComponent(filename)}`;
|
|
8299
8829
|
}
|
|
8300
8830
|
function rejectPrincipalEntityMutation(request, action) {
|
|
@@ -8492,22 +9022,28 @@ async function deleteEventSourceSubscription(request, ctx) {
|
|
|
8492
9022
|
const result = await ctx.entityManager.deleteEventSourceSubscription(entityUrl, { id: decodeURIComponent(request.params.subscriptionId) });
|
|
8493
9023
|
return (0, itty_router.json)(result);
|
|
8494
9024
|
}
|
|
9025
|
+
function tagResponseBody(entity) {
|
|
9026
|
+
const publicEntity = toPublicEntity(entity);
|
|
9027
|
+
if (entity.txid !== void 0) return {
|
|
9028
|
+
...publicEntity,
|
|
9029
|
+
txid: entity.txid
|
|
9030
|
+
};
|
|
9031
|
+
return publicEntity;
|
|
9032
|
+
}
|
|
8495
9033
|
async function setTag(request, ctx) {
|
|
8496
9034
|
const principalMutationError = rejectPrincipalEntityMutation(request, `tag updated`);
|
|
8497
9035
|
if (principalMutationError) return principalMutationError;
|
|
8498
9036
|
const parsed = routeBody(request);
|
|
8499
9037
|
const { entityUrl } = requireExistingEntityRoute(request);
|
|
8500
|
-
const
|
|
8501
|
-
|
|
8502
|
-
return (0, itty_router.json)(toPublicEntity(updated));
|
|
9038
|
+
const updated = await ctx.entityManager.setTag(entityUrl, decodeURIComponent(request.params.tagKey), { value: parsed.value });
|
|
9039
|
+
return (0, itty_router.json)(tagResponseBody(updated));
|
|
8503
9040
|
}
|
|
8504
9041
|
async function deleteTag(request, ctx) {
|
|
8505
9042
|
const principalMutationError = rejectPrincipalEntityMutation(request, `tag deleted`);
|
|
8506
9043
|
if (principalMutationError) return principalMutationError;
|
|
8507
9044
|
const { entityUrl } = requireExistingEntityRoute(request);
|
|
8508
|
-
const
|
|
8509
|
-
|
|
8510
|
-
return (0, itty_router.json)(toPublicEntity(updated));
|
|
9045
|
+
const updated = await ctx.entityManager.deleteTag(entityUrl, decodeURIComponent(request.params.tagKey));
|
|
9046
|
+
return (0, itty_router.json)(tagResponseBody(updated));
|
|
8511
9047
|
}
|
|
8512
9048
|
async function forkEntity(request, ctx) {
|
|
8513
9049
|
const principalMutationError = rejectPrincipalEntityMutation(request, `forked`);
|
|
@@ -8574,9 +9110,29 @@ async function sendEntity(request, ctx) {
|
|
|
8574
9110
|
mode: parsed.mode,
|
|
8575
9111
|
position: parsed.position
|
|
8576
9112
|
};
|
|
8577
|
-
if (parsed.afterMs && parsed.afterMs > 0)
|
|
8578
|
-
|
|
8579
|
-
|
|
9113
|
+
if (parsed.afterMs && parsed.afterMs > 0) {
|
|
9114
|
+
await ctx.entityManager.enqueueDelayedSend(entityUrl, sendReq, new Date(Date.now() + parsed.afterMs));
|
|
9115
|
+
return (0, itty_router.status)(204);
|
|
9116
|
+
}
|
|
9117
|
+
const result = await ctx.entityManager.send(entityUrl, sendReq);
|
|
9118
|
+
return (0, itty_router.json)(result);
|
|
9119
|
+
}
|
|
9120
|
+
async function writeCollection(request, ctx) {
|
|
9121
|
+
const parsed = routeBody(request);
|
|
9122
|
+
await ctx.entityManager.ensurePrincipal(ctx.principal);
|
|
9123
|
+
const { entityUrl } = requireExistingEntityRoute(request);
|
|
9124
|
+
const collection = request.params.collection;
|
|
9125
|
+
const result = await ctx.entityManager.writeCollection(entityUrl, collection, {
|
|
9126
|
+
operation: parsed.operation,
|
|
9127
|
+
key: parsed.key,
|
|
9128
|
+
value: parsed.value,
|
|
9129
|
+
principal: {
|
|
9130
|
+
url: ctx.principal.url,
|
|
9131
|
+
kind: ctx.principal.kind,
|
|
9132
|
+
id: ctx.principal.id
|
|
9133
|
+
}
|
|
9134
|
+
});
|
|
9135
|
+
return (0, itty_router.json)(result, { status: parsed.operation === `insert` ? 201 : 200 });
|
|
8580
9136
|
}
|
|
8581
9137
|
async function createAttachment(request, ctx) {
|
|
8582
9138
|
const principalMutationError = rejectPrincipalEntityMutation(request, `given attachments`);
|
|
@@ -8619,13 +9175,13 @@ async function deleteAttachment(request, ctx) {
|
|
|
8619
9175
|
async function updateInboxMessage(request, ctx) {
|
|
8620
9176
|
const parsed = routeBody(request);
|
|
8621
9177
|
const { entityUrl } = requireExistingEntityRoute(request);
|
|
8622
|
-
await ctx.entityManager.updateInboxMessage(entityUrl, decodeURIComponent(request.params.messageKey), parsed);
|
|
8623
|
-
return (0, itty_router.
|
|
9178
|
+
const result = await ctx.entityManager.updateInboxMessage(entityUrl, decodeURIComponent(request.params.messageKey), parsed);
|
|
9179
|
+
return (0, itty_router.json)(result);
|
|
8624
9180
|
}
|
|
8625
9181
|
async function deleteInboxMessage(request, ctx) {
|
|
8626
9182
|
const { entityUrl } = requireExistingEntityRoute(request);
|
|
8627
|
-
await ctx.entityManager.deleteInboxMessage(entityUrl, decodeURIComponent(request.params.messageKey));
|
|
8628
|
-
return (0, itty_router.
|
|
9183
|
+
const result = await ctx.entityManager.deleteInboxMessage(entityUrl, decodeURIComponent(request.params.messageKey));
|
|
9184
|
+
return (0, itty_router.json)(result);
|
|
8629
9185
|
}
|
|
8630
9186
|
async function spawnEntity(request, ctx) {
|
|
8631
9187
|
const parsed = routeBody(request);
|
|
@@ -8675,8 +9231,13 @@ async function spawnEntity(request, ctx) {
|
|
|
8675
9231
|
headers: { "x-write-token": entity.write_token }
|
|
8676
9232
|
});
|
|
8677
9233
|
}
|
|
8678
|
-
function getEntity(request) {
|
|
8679
|
-
|
|
9234
|
+
async function getEntity(request, ctx) {
|
|
9235
|
+
const { entity } = requireExistingEntityRoute(request);
|
|
9236
|
+
const entityType = entity.type ? await ctx.entityManager.registry.getEntityType(entity.type) : null;
|
|
9237
|
+
return (0, itty_router.json)({
|
|
9238
|
+
...toPublicEntity(entity),
|
|
9239
|
+
...entityType?.externally_writable_collections && { externally_writable_collections: entityType.externally_writable_collections }
|
|
9240
|
+
});
|
|
8680
9241
|
}
|
|
8681
9242
|
function headEntity() {
|
|
8682
9243
|
return (0, itty_router.status)(200);
|
|
@@ -8711,6 +9272,16 @@ async function signalEntity(request, ctx) {
|
|
|
8711
9272
|
//#region src/routing/entity-types-router.ts
|
|
8712
9273
|
const jsonObjectSchema = __sinclair_typebox.Type.Record(__sinclair_typebox.Type.String(), __sinclair_typebox.Type.Unknown());
|
|
8713
9274
|
const schemaMapSchema = __sinclair_typebox.Type.Record(__sinclair_typebox.Type.String(), jsonObjectSchema);
|
|
9275
|
+
const externallyWritableCollectionsSchema = __sinclair_typebox.Type.Record(__sinclair_typebox.Type.String(), __sinclair_typebox.Type.Object({
|
|
9276
|
+
type: __sinclair_typebox.Type.String(),
|
|
9277
|
+
contract: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
9278
|
+
operations: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Array(__sinclair_typebox.Type.Union([
|
|
9279
|
+
__sinclair_typebox.Type.Literal(`insert`),
|
|
9280
|
+
__sinclair_typebox.Type.Literal(`update`),
|
|
9281
|
+
__sinclair_typebox.Type.Literal(`delete`)
|
|
9282
|
+
]))),
|
|
9283
|
+
principalColumn: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String())
|
|
9284
|
+
}, { additionalProperties: false }));
|
|
8714
9285
|
const slashCommandArgumentSchema = __sinclair_typebox.Type.Object({
|
|
8715
9286
|
name: __sinclair_typebox.Type.String(),
|
|
8716
9287
|
type: __sinclair_typebox.Type.Union([
|
|
@@ -8741,7 +9312,8 @@ const registerEntityTypeBodySchema = __sinclair_typebox.Type.Object({
|
|
|
8741
9312
|
slash_commands: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Array(slashCommandSchema)),
|
|
8742
9313
|
serve_endpoint: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
8743
9314
|
default_dispatch_policy: __sinclair_typebox.Type.Optional(dispatchPolicySchema),
|
|
8744
|
-
permission_grants: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Array(typePermissionGrantInputSchema))
|
|
9315
|
+
permission_grants: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Array(typePermissionGrantInputSchema)),
|
|
9316
|
+
externally_writable_collections: __sinclair_typebox.Type.Optional(externallyWritableCollectionsSchema)
|
|
8745
9317
|
}, { additionalProperties: false });
|
|
8746
9318
|
const amendEntityTypeSchemasBodySchema = __sinclair_typebox.Type.Object({
|
|
8747
9319
|
inbox_schemas: __sinclair_typebox.Type.Optional(schemaMapSchema),
|
|
@@ -8872,7 +9444,20 @@ function parseExpiresAt(value) {
|
|
|
8872
9444
|
if (Number.isNaN(expiresAt.getTime())) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Invalid expires_at timestamp`, 400);
|
|
8873
9445
|
return expiresAt;
|
|
8874
9446
|
}
|
|
9447
|
+
/**
|
|
9448
|
+
* The `comments` collection name is reserved for the canonical comments
|
|
9449
|
+
* contract: the UI keys its comment affordances on it, so a divergent
|
|
9450
|
+
* collection registered under that name (or the contract mounted under
|
|
9451
|
+
* another name) would break that assumption silently.
|
|
9452
|
+
*/
|
|
9453
|
+
function validateExternallyWritableCollections(collections) {
|
|
9454
|
+
for (const [name, config] of Object.entries(collections ?? {})) {
|
|
9455
|
+
if (name === `comments` && config.contract !== __electric_ax_agents_runtime.COMMENTS_CONTRACT) throw new ElectricAgentsError(ErrCodeInvalidRequest, `The externally-writable collection name "comments" is reserved for the "${__electric_ax_agents_runtime.COMMENTS_CONTRACT}" contract`, 400);
|
|
9456
|
+
if (config.contract === __electric_ax_agents_runtime.COMMENTS_CONTRACT && name !== `comments`) throw new ElectricAgentsError(ErrCodeInvalidRequest, `The "${__electric_ax_agents_runtime.COMMENTS_CONTRACT}" contract must be registered under the collection name "comments"`, 400);
|
|
9457
|
+
}
|
|
9458
|
+
}
|
|
8875
9459
|
function normalizeEntityTypeRequest(parsed) {
|
|
9460
|
+
validateExternallyWritableCollections(parsed.externally_writable_collections);
|
|
8876
9461
|
const serveEndpoint = rewriteLoopbackWebhookUrl(parsed.serve_endpoint);
|
|
8877
9462
|
return {
|
|
8878
9463
|
name: parsed.name ?? ``,
|
|
@@ -8886,7 +9471,8 @@ function normalizeEntityTypeRequest(parsed) {
|
|
|
8886
9471
|
type: `webhook`,
|
|
8887
9472
|
url: serveEndpoint
|
|
8888
9473
|
}] } : void 0),
|
|
8889
|
-
permission_grants: parsed.permission_grants
|
|
9474
|
+
permission_grants: parsed.permission_grants,
|
|
9475
|
+
externally_writable_collections: parsed.externally_writable_collections
|
|
8890
9476
|
};
|
|
8891
9477
|
}
|
|
8892
9478
|
function toPublicEntityType(entityType) {
|
|
@@ -8896,6 +9482,49 @@ function toPublicEntityType(entityType) {
|
|
|
8896
9482
|
};
|
|
8897
9483
|
}
|
|
8898
9484
|
|
|
9485
|
+
//#endregion
|
|
9486
|
+
//#region src/routing/pg-sync-router.ts
|
|
9487
|
+
const pgSyncOptionsSchema = __sinclair_typebox.Type.Object({
|
|
9488
|
+
url: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
9489
|
+
table: __sinclair_typebox.Type.String(),
|
|
9490
|
+
columns: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Array(__sinclair_typebox.Type.String())),
|
|
9491
|
+
where: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
9492
|
+
params: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Union([__sinclair_typebox.Type.Array(__sinclair_typebox.Type.String()), __sinclair_typebox.Type.Record(__sinclair_typebox.Type.String(), __sinclair_typebox.Type.String())])),
|
|
9493
|
+
replica: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Union([__sinclair_typebox.Type.Literal(`default`), __sinclair_typebox.Type.Literal(`full`)]))
|
|
9494
|
+
});
|
|
9495
|
+
const pgSyncRequestMetadataSchema = __sinclair_typebox.Type.Object({
|
|
9496
|
+
entityUrl: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
9497
|
+
entityType: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
9498
|
+
streamPath: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
9499
|
+
runtimeConsumerId: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
9500
|
+
wakeId: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String())
|
|
9501
|
+
});
|
|
9502
|
+
const pgSyncRegisterBodySchema = __sinclair_typebox.Type.Object({
|
|
9503
|
+
options: pgSyncOptionsSchema,
|
|
9504
|
+
metadata: __sinclair_typebox.Type.Optional(pgSyncRequestMetadataSchema)
|
|
9505
|
+
});
|
|
9506
|
+
const pgSyncRouter = (0, itty_router.Router)({ base: `/_electric/pg-sync` });
|
|
9507
|
+
pgSyncRouter.post(`/register`, withSchema(pgSyncRegisterBodySchema), registerPgSync);
|
|
9508
|
+
async function registerPgSync(request, ctx) {
|
|
9509
|
+
const { options, metadata } = routeBody(request);
|
|
9510
|
+
if (options.table.trim() === ``) return apiError(400, ErrCodeInvalidRequest, `pgSync table must be non-empty`);
|
|
9511
|
+
if (!ctx.pgSyncBridgeManager) return apiError(503, ErrCodeInvalidRequest, `pgSync bridge manager is not configured`);
|
|
9512
|
+
try {
|
|
9513
|
+
const requestMetadata$1 = {
|
|
9514
|
+
tenantId: ctx.service,
|
|
9515
|
+
principalKind: ctx.principal.kind,
|
|
9516
|
+
principalId: ctx.principal.id,
|
|
9517
|
+
principalKey: ctx.principal.key,
|
|
9518
|
+
principalUrl: ctx.principal.url,
|
|
9519
|
+
...metadata ?? {}
|
|
9520
|
+
};
|
|
9521
|
+
const result = await ctx.pgSyncBridgeManager.register(options, requestMetadata$1);
|
|
9522
|
+
return (0, itty_router.json)(result);
|
|
9523
|
+
} catch (error) {
|
|
9524
|
+
return apiError(500, ErrCodeInvalidRequest, `pgSync registration failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
9525
|
+
}
|
|
9526
|
+
}
|
|
9527
|
+
|
|
8899
9528
|
//#endregion
|
|
8900
9529
|
//#region src/routing/hooks.ts
|
|
8901
9530
|
const SPAN_KEY = Symbol(`agents-server.otel-span`);
|
|
@@ -8970,6 +9599,10 @@ function errorMapper(err, req) {
|
|
|
8970
9599
|
});
|
|
8971
9600
|
}
|
|
8972
9601
|
if (err instanceof ElectricAgentsError) return apiError(err.status, err.code, err.message, err.details);
|
|
9602
|
+
if (err instanceof ElectricProxyError) {
|
|
9603
|
+
serverLog.warn(`[agent-server] Electric proxy rejected request (${err.code}): ${req.url}`);
|
|
9604
|
+
return apiError(err.status, err.code, err.message);
|
|
9605
|
+
}
|
|
8973
9606
|
serverLog.error(`[agent-server] Unhandled error:`, err);
|
|
8974
9607
|
return apiError(500, `INTERNAL_SERVER_ERROR`, `Internal server error`);
|
|
8975
9608
|
}
|
|
@@ -9420,6 +10053,7 @@ internalRouter.all(`/runners`, runnersRouter.fetch);
|
|
|
9420
10053
|
internalRouter.all(`/runners/*`, runnersRouter.fetch);
|
|
9421
10054
|
internalRouter.all(`/entities/*`, entitiesRouter.fetch);
|
|
9422
10055
|
internalRouter.all(`/entity-types/*`, entityTypesRouter.fetch);
|
|
10056
|
+
internalRouter.all(`/pg-sync/*`, pgSyncRouter.fetch);
|
|
9423
10057
|
internalRouter.all(`/observations/*`, observationsRouter.fetch);
|
|
9424
10058
|
internalRouter.get(`/electric/*`, electricProxyRouter.fetch);
|
|
9425
10059
|
internalRouter.all(`*`, () => (0, itty_router.status)(404));
|
|
@@ -9803,6 +10437,9 @@ const globalRouter = (0, itty_router.AutoRouter)({
|
|
|
9803
10437
|
finally: [otelEndSpan, applyCors]
|
|
9804
10438
|
});
|
|
9805
10439
|
globalRouter.all(`/_electric/shared-state/*`, durableStreamsRouter.fetch);
|
|
10440
|
+
globalRouter.all(`/_electric/pg-sync/register`, internalRouter.fetch);
|
|
10441
|
+
globalRouter.get(`/_electric/pg-sync/*`, durableStreamsRouter.fetch);
|
|
10442
|
+
globalRouter.head(`/_electric/pg-sync/*`, durableStreamsRouter.fetch);
|
|
9806
10443
|
globalRouter.all(`/_electric/*`, internalRouter.fetch);
|
|
9807
10444
|
globalRouter.all(`*`, durableStreamsRouter.fetch);
|
|
9808
10445
|
|