@electric-ax/agents-server 0.4.20 → 0.5.1
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 +1003 -834
- package/dist/index.cjs +241 -72
- package/dist/index.d.cts +2507 -2440
- package/dist/index.d.ts +2506 -2441
- package/dist/index.js +242 -73
- package/drizzle/0016_entity_type_externally_writable_collections.sql +1 -0
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +5 -5
- package/src/db/schema.ts +4 -0
- package/src/electric-agents-types.ts +23 -0
- package/src/entity-manager.ts +157 -7
- package/src/entity-registry.ts +25 -1
- package/src/index.ts +6 -6
- package/src/manifest-side-effects.ts +2 -6
- package/src/pg-sync-bridge-manager.ts +147 -47
- package/src/routing/context.ts +11 -11
- package/src/routing/entities-router.ts +112 -30
- package/src/routing/entity-types-router.ts +56 -0
- package/src/routing/internal-router.ts +9 -7
- package/src/routing/pg-sync-router.ts +14 -1
- package/src/server.ts +8 -8
- package/src/wake-registry.ts +2 -0
package/dist/index.cjs
CHANGED
|
@@ -79,6 +79,7 @@ const entityTypes = (0, drizzle_orm_pg_core.pgTable)(`entity_types`, {
|
|
|
79
79
|
creationSchema: (0, drizzle_orm_pg_core.jsonb)(`creation_schema`),
|
|
80
80
|
inboxSchemas: (0, drizzle_orm_pg_core.jsonb)(`inbox_schemas`),
|
|
81
81
|
stateSchemas: (0, drizzle_orm_pg_core.jsonb)(`state_schemas`),
|
|
82
|
+
externallyWritableCollections: (0, drizzle_orm_pg_core.jsonb)(`externally_writable_collections`).$type(),
|
|
82
83
|
slashCommands: (0, drizzle_orm_pg_core.jsonb)(`slash_commands`),
|
|
83
84
|
serveEndpoint: (0, drizzle_orm_pg_core.text)(`serve_endpoint`),
|
|
84
85
|
defaultDispatchPolicy: (0, drizzle_orm_pg_core.jsonb)(`default_dispatch_policy`),
|
|
@@ -866,6 +867,7 @@ var PostgresRegistry = class {
|
|
|
866
867
|
creationSchema: et.creation_schema ?? null,
|
|
867
868
|
inboxSchemas: et.inbox_schemas ?? null,
|
|
868
869
|
stateSchemas: et.state_schemas ?? null,
|
|
870
|
+
externallyWritableCollections: et.externally_writable_collections ?? null,
|
|
869
871
|
slashCommands: et.slash_commands ?? null,
|
|
870
872
|
serveEndpoint: et.serve_endpoint ?? null,
|
|
871
873
|
defaultDispatchPolicy: et.default_dispatch_policy ?? null,
|
|
@@ -879,6 +881,7 @@ var PostgresRegistry = class {
|
|
|
879
881
|
creationSchema: et.creation_schema ?? null,
|
|
880
882
|
inboxSchemas: et.inbox_schemas ?? null,
|
|
881
883
|
stateSchemas: et.state_schemas ?? null,
|
|
884
|
+
externallyWritableCollections: et.externally_writable_collections ?? null,
|
|
882
885
|
slashCommands: et.slash_commands ?? null,
|
|
883
886
|
serveEndpoint: et.serve_endpoint ?? null,
|
|
884
887
|
defaultDispatchPolicy: et.default_dispatch_policy ?? null,
|
|
@@ -897,6 +900,7 @@ var PostgresRegistry = class {
|
|
|
897
900
|
creationSchema: et.creation_schema ?? null,
|
|
898
901
|
inboxSchemas: et.inbox_schemas ?? null,
|
|
899
902
|
stateSchemas: et.state_schemas ?? null,
|
|
903
|
+
externallyWritableCollections: et.externally_writable_collections ?? null,
|
|
900
904
|
slashCommands: et.slash_commands ?? null,
|
|
901
905
|
serveEndpoint: et.serve_endpoint ?? null,
|
|
902
906
|
defaultDispatchPolicy: et.default_dispatch_policy ?? null,
|
|
@@ -924,6 +928,7 @@ var PostgresRegistry = class {
|
|
|
924
928
|
creationSchema: et.creation_schema ?? null,
|
|
925
929
|
inboxSchemas: et.inbox_schemas ?? null,
|
|
926
930
|
stateSchemas: et.state_schemas ?? null,
|
|
931
|
+
externallyWritableCollections: et.externally_writable_collections ?? null,
|
|
927
932
|
slashCommands: et.slash_commands ?? null,
|
|
928
933
|
serveEndpoint: et.serve_endpoint ?? null,
|
|
929
934
|
defaultDispatchPolicy: et.default_dispatch_policy ?? null,
|
|
@@ -1368,7 +1373,6 @@ var PostgresRegistry = class {
|
|
|
1368
1373
|
set: {
|
|
1369
1374
|
options: row.options,
|
|
1370
1375
|
streamUrl: row.streamUrl,
|
|
1371
|
-
initialSnapshotComplete: false,
|
|
1372
1376
|
lastTouchedAt: new Date(),
|
|
1373
1377
|
updatedAt: new Date()
|
|
1374
1378
|
}
|
|
@@ -1407,6 +1411,9 @@ var PostgresRegistry = class {
|
|
|
1407
1411
|
updatedAt: new Date()
|
|
1408
1412
|
}).where(this.pgSyncBridgeWhere(sourceRef));
|
|
1409
1413
|
}
|
|
1414
|
+
async deletePgSyncBridge(sourceRef) {
|
|
1415
|
+
await this.db.delete(pgSyncBridges).where(this.pgSyncBridgeWhere(sourceRef));
|
|
1416
|
+
}
|
|
1410
1417
|
async upsertEntityBridge(row) {
|
|
1411
1418
|
await this.db.insert(entityBridges).values({
|
|
1412
1419
|
tenantId: this.tenantId,
|
|
@@ -1569,6 +1576,7 @@ var PostgresRegistry = class {
|
|
|
1569
1576
|
creation_schema: row.creationSchema,
|
|
1570
1577
|
inbox_schemas: row.inboxSchemas,
|
|
1571
1578
|
state_schemas: row.stateSchemas,
|
|
1579
|
+
externally_writable_collections: row.externallyWritableCollections ?? void 0,
|
|
1572
1580
|
slash_commands: row.slashCommands ?? void 0,
|
|
1573
1581
|
serve_endpoint: row.serveEndpoint ?? void 0,
|
|
1574
1582
|
default_dispatch_policy: row.defaultDispatchPolicy ?? void 0,
|
|
@@ -3287,9 +3295,6 @@ function assertSharedSandboxColocated(key, chosenIsRemote, dispatchPolicy) {
|
|
|
3287
3295
|
function isRecord$1(value) {
|
|
3288
3296
|
return typeof value === `object` && value !== null && !Array.isArray(value);
|
|
3289
3297
|
}
|
|
3290
|
-
function getPgSyncManifestStreamPath(sourceRef) {
|
|
3291
|
-
return `/_electric/pg-sync/${sourceRef}`;
|
|
3292
|
-
}
|
|
3293
3298
|
function extractManifestSourceUrl(manifest) {
|
|
3294
3299
|
if (!manifest) return void 0;
|
|
3295
3300
|
if (manifest.kind === `child` || manifest.kind === `observe`) return typeof manifest.entity_url === `string` ? manifest.entity_url : void 0;
|
|
@@ -3302,7 +3307,7 @@ function extractManifestSourceUrl(manifest) {
|
|
|
3302
3307
|
}
|
|
3303
3308
|
if (manifest.sourceType === `entities`) return typeof manifest.sourceRef === `string` ? `/_entities/${manifest.sourceRef}` : void 0;
|
|
3304
3309
|
if (manifest.sourceType === `db`) return typeof manifest.sourceRef === `string` ? (0, __electric_ax_agents_runtime.getSharedStateStreamPath)(manifest.sourceRef) : void 0;
|
|
3305
|
-
if (manifest.sourceType === `pgSync`) return typeof
|
|
3310
|
+
if (manifest.sourceType === `pgSync`) return typeof config?.streamUrl === `string` ? config.streamUrl : void 0;
|
|
3306
3311
|
if (manifest.sourceType === `webhook`) {
|
|
3307
3312
|
if (typeof config?.streamUrl === `string`) return config.streamUrl;
|
|
3308
3313
|
if (typeof config?.endpointKey === `string`) return (0, __electric_ax_agents_runtime.getWebhookStreamPath)(config.endpointKey, typeof config.bucket === `string` ? config.bucket : void 0);
|
|
@@ -3498,6 +3503,7 @@ var EntityManager = class {
|
|
|
3498
3503
|
creation_schema: req.creation_schema,
|
|
3499
3504
|
inbox_schemas: req.inbox_schemas,
|
|
3500
3505
|
state_schemas: req.state_schemas,
|
|
3506
|
+
externally_writable_collections: req.externally_writable_collections,
|
|
3501
3507
|
slash_commands: req.slash_commands,
|
|
3502
3508
|
serve_endpoint: req.serve_endpoint,
|
|
3503
3509
|
default_dispatch_policy: defaultDispatchPolicy,
|
|
@@ -4593,6 +4599,40 @@ var EntityManager = class {
|
|
|
4593
4599
|
throw err;
|
|
4594
4600
|
}
|
|
4595
4601
|
}
|
|
4602
|
+
async writeCollection(entityUrl, collection, req) {
|
|
4603
|
+
const entity = await this.registry.getEntity(entityUrl);
|
|
4604
|
+
if (!entity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
|
|
4605
|
+
const { externallyWritableCollections } = await this.getEffectiveSchemas(entity);
|
|
4606
|
+
const config = externallyWritableCollections?.[collection];
|
|
4607
|
+
if (!config) throw new ElectricAgentsError(ErrCodeUnauthorized, `Collection "${collection}" is not writable`, 403);
|
|
4608
|
+
const allowedOperations = config.operations ?? [`insert`];
|
|
4609
|
+
if (!allowedOperations.includes(req.operation)) throw new ElectricAgentsError(ErrCodeUnauthorized, `Operation "${req.operation}" is not allowed on collection "${collection}"`, 403);
|
|
4610
|
+
if (rejectsNormalWrites(entity.status)) throw new ElectricAgentsError(ErrCodeNotRunning, `Entity is not accepting writes`, 409);
|
|
4611
|
+
if (this.isForkWorkLockedEntity(entityUrl)) this.assertEntityNotForkWorkLocked(entityUrl);
|
|
4612
|
+
if (req.operation !== `delete` && (req.value === void 0 || req.value === null)) throw new ElectricAgentsError(ErrCodeInvalidRequest, `value is required for ${req.operation}`, 400);
|
|
4613
|
+
if (req.operation !== `insert` && !req.key) throw new ElectricAgentsError(ErrCodeInvalidRequest, `key is required for ${req.operation}`, 400);
|
|
4614
|
+
const key = req.key ?? `${collection}-${(0, node_crypto.randomUUID)()}`;
|
|
4615
|
+
const event = {
|
|
4616
|
+
type: config.type,
|
|
4617
|
+
key,
|
|
4618
|
+
headers: {
|
|
4619
|
+
operation: req.operation,
|
|
4620
|
+
timestamp: new Date().toISOString(),
|
|
4621
|
+
principal: req.principal
|
|
4622
|
+
}
|
|
4623
|
+
};
|
|
4624
|
+
if (req.operation !== `delete`) event.value = req.value;
|
|
4625
|
+
const validationError = await this.validateWriteEvent(entity, event);
|
|
4626
|
+
if (validationError) throw new ElectricAgentsError(validationError.code, validationError.message, validationError.status);
|
|
4627
|
+
const encoded = this.encodeChangeEvent(event);
|
|
4628
|
+
try {
|
|
4629
|
+
await this.streamClient.append(entity.streams.main, encoded);
|
|
4630
|
+
} catch (err) {
|
|
4631
|
+
if (this.isClosedStreamError(err)) throw new ElectricAgentsError(ErrCodeNotRunning, `Entity is stopped`, 409);
|
|
4632
|
+
throw err;
|
|
4633
|
+
}
|
|
4634
|
+
return { key };
|
|
4635
|
+
}
|
|
4596
4636
|
async updateInboxMessage(entityUrl, key, req) {
|
|
4597
4637
|
const entity = await this.registry.getEntity(entityUrl);
|
|
4598
4638
|
if (!entity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
|
|
@@ -4859,7 +4899,7 @@ var EntityManager = class {
|
|
|
4859
4899
|
await this.writeManifestEntry(entityUrl, manifestKey, `delete`, void 0, { txid });
|
|
4860
4900
|
return { txid };
|
|
4861
4901
|
}
|
|
4862
|
-
async
|
|
4902
|
+
async upsertWebhookSourceSubscription(entityUrl, req) {
|
|
4863
4903
|
const manifestKey = req.subscription.manifestKey;
|
|
4864
4904
|
const txid = (0, node_crypto.randomUUID)();
|
|
4865
4905
|
await this.writeManifestEntry(entityUrl, manifestKey, `upsert`, req.manifest, { txid });
|
|
@@ -4881,8 +4921,20 @@ var EntityManager = class {
|
|
|
4881
4921
|
subscription: req.subscription
|
|
4882
4922
|
};
|
|
4883
4923
|
}
|
|
4884
|
-
async
|
|
4885
|
-
const manifestKey = (0, __electric_ax_agents_runtime.
|
|
4924
|
+
async deleteWebhookSourceSubscription(entityUrl, req) {
|
|
4925
|
+
const manifestKey = (0, __electric_ax_agents_runtime.webhookSourceSubscriptionManifestKey)(req.id);
|
|
4926
|
+
const txid = (0, node_crypto.randomUUID)();
|
|
4927
|
+
await this.writeManifestEntry(entityUrl, manifestKey, `delete`, void 0, { txid });
|
|
4928
|
+
await this.wakeRegistry.unregisterByManifestKey(entityUrl, manifestKey, this.tenantId);
|
|
4929
|
+
return { txid };
|
|
4930
|
+
}
|
|
4931
|
+
/**
|
|
4932
|
+
* Stop this entity observing a pg-sync source: drop its manifest entry and
|
|
4933
|
+
* the wake it anchors. The shared pg-sync bridge (keyed by sourceRef, not by
|
|
4934
|
+
* subscriber) is intentionally left running for any other observers.
|
|
4935
|
+
*/
|
|
4936
|
+
async deletePgSyncObservation(entityUrl, req) {
|
|
4937
|
+
const manifestKey = `source:pgSync:${req.sourceRef}`;
|
|
4886
4938
|
const txid = (0, node_crypto.randomUUID)();
|
|
4887
4939
|
await this.writeManifestEntry(entityUrl, manifestKey, `delete`, void 0, { txid });
|
|
4888
4940
|
await this.wakeRegistry.unregisterByManifestKey(entityUrl, manifestKey, this.tenantId);
|
|
@@ -5285,7 +5337,8 @@ var EntityManager = class {
|
|
|
5285
5337
|
async getEffectiveSchemas(entity) {
|
|
5286
5338
|
if (!entity.type) return {
|
|
5287
5339
|
inboxSchemas: entity.inbox_schemas,
|
|
5288
|
-
stateSchemas: entity.state_schemas
|
|
5340
|
+
stateSchemas: entity.state_schemas,
|
|
5341
|
+
externallyWritableCollections: void 0
|
|
5289
5342
|
};
|
|
5290
5343
|
const latestType = await this.registry.getEntityType(entity.type);
|
|
5291
5344
|
return {
|
|
@@ -5296,7 +5349,8 @@ var EntityManager = class {
|
|
|
5296
5349
|
stateSchemas: latestType?.state_schemas ? {
|
|
5297
5350
|
...entity.state_schemas ?? {},
|
|
5298
5351
|
...latestType.state_schemas
|
|
5299
|
-
} : entity.state_schemas
|
|
5352
|
+
} : entity.state_schemas,
|
|
5353
|
+
externallyWritableCollections: latestType?.externally_writable_collections
|
|
5300
5354
|
};
|
|
5301
5355
|
}
|
|
5302
5356
|
isClosedStreamError(err) {
|
|
@@ -5986,9 +6040,13 @@ var Scheduler = class {
|
|
|
5986
6040
|
|
|
5987
6041
|
//#endregion
|
|
5988
6042
|
//#region src/pg-sync-bridge-manager.ts
|
|
5989
|
-
|
|
6043
|
+
/** Registration was rejected because the source itself is invalid — map to a 4xx. */
|
|
6044
|
+
var PgSyncSourceValidationError = class extends Error {
|
|
6045
|
+
name = `PgSyncSourceValidationError`;
|
|
6046
|
+
};
|
|
5990
6047
|
const DEFAULT_RETRY_INITIAL_DELAY_MS = 1e3;
|
|
5991
6048
|
const DEFAULT_RETRY_MAX_DELAY_MS = 3e4;
|
|
6049
|
+
const DEFAULT_PROBE_TIMEOUT_MS = 1e4;
|
|
5992
6050
|
function buildElectricShapeParams(options) {
|
|
5993
6051
|
return {
|
|
5994
6052
|
table: options.table,
|
|
@@ -6008,6 +6066,31 @@ function buildElectricShapeParams(options) {
|
|
|
6008
6066
|
...options.metadata?.wakeId ? { electric_agents_wake_id: options.metadata.wakeId } : {}
|
|
6009
6067
|
};
|
|
6010
6068
|
}
|
|
6069
|
+
/**
|
|
6070
|
+
* Build the one-shot URL used to validate a shape source at registration
|
|
6071
|
+
* time. Approximates the query-param encoding of the Electric TS client
|
|
6072
|
+
* (arrays comma-joined, where-clause params as `params[n]`) — unlike the
|
|
6073
|
+
* client it does not quote column identifiers, so probe and stream encoding
|
|
6074
|
+
* can diverge for exotic column names.
|
|
6075
|
+
*/
|
|
6076
|
+
function buildShapeProbeUrl(sourceUrl, options) {
|
|
6077
|
+
let url;
|
|
6078
|
+
try {
|
|
6079
|
+
url = new URL(sourceUrl);
|
|
6080
|
+
} catch {
|
|
6081
|
+
throw new PgSyncSourceValidationError(`pgSync url "${sourceUrl}" is not a valid URL`);
|
|
6082
|
+
}
|
|
6083
|
+
if (url.protocol !== `http:` && url.protocol !== `https:`) throw new PgSyncSourceValidationError(`pgSync url "${sourceUrl}" must be an HTTP(S) Electric shape endpoint, not a database connection string`);
|
|
6084
|
+
for (const [key, value] of Object.entries(buildElectricShapeParams(options))) {
|
|
6085
|
+
if (value === void 0 || value === null) continue;
|
|
6086
|
+
if (Array.isArray(value)) if (key === `params`) value.forEach((item, index$1) => url.searchParams.set(`params[${index$1 + 1}]`, String(item)));
|
|
6087
|
+
else url.searchParams.set(key, value.join(`,`));
|
|
6088
|
+
else if (typeof value === `object`) for (const [k, v] of Object.entries(value)) url.searchParams.set(`${key}[${k}]`, String(v));
|
|
6089
|
+
else url.searchParams.set(key, String(value));
|
|
6090
|
+
}
|
|
6091
|
+
url.searchParams.set(`offset`, `now`);
|
|
6092
|
+
return url;
|
|
6093
|
+
}
|
|
6011
6094
|
function jsonSafe(value) {
|
|
6012
6095
|
if (typeof value === `bigint`) return value.toString();
|
|
6013
6096
|
if (value === null || typeof value !== `object`) return value;
|
|
@@ -6029,37 +6112,19 @@ function rowKeyForMessage(message) {
|
|
|
6029
6112
|
const candidate = headers.key ?? headers.rowKey ?? message.value?.id ?? message.value?.key ?? message.old_value?.id ?? message.old_value?.key;
|
|
6030
6113
|
return candidate === void 0 ? void 0 : stableJson(candidate);
|
|
6031
6114
|
}
|
|
6032
|
-
function pgSyncMessageToDurableEvent(message
|
|
6115
|
+
function pgSyncMessageToDurableEvent(message) {
|
|
6033
6116
|
const operation = message.headers.operation;
|
|
6034
6117
|
if (operation !== `insert` && operation !== `update` && operation !== `delete`) return null;
|
|
6035
|
-
const
|
|
6036
|
-
|
|
6037
|
-
const
|
|
6038
|
-
if (typeof offset !== `string` || offset.length === 0) return null;
|
|
6039
|
-
const messageKeyPart = offset;
|
|
6040
|
-
const messageKey = `${sourceRef}:${operation}:${messageKeyPart}`;
|
|
6041
|
-
const timestamp$1 = new Date().toISOString();
|
|
6042
|
-
const oldValue = message.old_value;
|
|
6043
|
-
const safeValue = jsonSafe(message.value);
|
|
6044
|
-
const safeOldValue = jsonSafe(oldValue);
|
|
6045
|
-
const safeHeaders = jsonSafe(message.headers);
|
|
6118
|
+
const key = message.key ?? (typeof message.headers.key === `string` ? message.headers.key : void 0) ?? rowKeyForMessage(message);
|
|
6119
|
+
if (!key) return null;
|
|
6120
|
+
const safeMessage = jsonSafe(message);
|
|
6046
6121
|
return {
|
|
6047
6122
|
type: `pg_sync_change`,
|
|
6048
|
-
key
|
|
6049
|
-
value:
|
|
6050
|
-
key: messageKey,
|
|
6051
|
-
table: typeof optionsOrSourceRef === `string` ? void 0 : optionsOrSourceRef.table,
|
|
6052
|
-
operation,
|
|
6053
|
-
...rowKey !== void 0 ? { rowKey } : {},
|
|
6054
|
-
...message.value !== void 0 ? { value: safeValue } : {},
|
|
6055
|
-
...oldValue !== void 0 ? { oldValue: safeOldValue } : {},
|
|
6056
|
-
headers: safeHeaders,
|
|
6057
|
-
...typeof offset === `string` ? { offset } : {},
|
|
6058
|
-
receivedAt: timestamp$1
|
|
6059
|
-
},
|
|
6123
|
+
key,
|
|
6124
|
+
value: safeMessage,
|
|
6060
6125
|
headers: {
|
|
6061
|
-
|
|
6062
|
-
|
|
6126
|
+
...jsonSafe(message.headers),
|
|
6127
|
+
operation
|
|
6063
6128
|
}
|
|
6064
6129
|
};
|
|
6065
6130
|
}
|
|
@@ -6149,13 +6214,13 @@ var PgSyncBridge = class {
|
|
|
6149
6214
|
}
|
|
6150
6215
|
if (!(0, __electric_sql_client.isChangeMessage)(message)) continue;
|
|
6151
6216
|
if (!this.skipChangesUntilUpToDate) {
|
|
6152
|
-
const event = pgSyncMessageToDurableEvent(message
|
|
6217
|
+
const event = pgSyncMessageToDurableEvent(message);
|
|
6153
6218
|
if (event) {
|
|
6154
6219
|
if (!this.producer) throw new Error(`pg-sync producer is not started`);
|
|
6155
6220
|
await this.producer.append(JSON.stringify(event));
|
|
6156
6221
|
await this.producer.flush?.();
|
|
6157
6222
|
await this.evaluateWakes?.(this.streamUrl, event);
|
|
6158
|
-
}
|
|
6223
|
+
} else serverLog.warn(`[pg-sync-bridge] dropped change message for ${this.sourceRef} (unknown operation or missing row key):`, message.headers);
|
|
6159
6224
|
}
|
|
6160
6225
|
await this.persistCursor(stream);
|
|
6161
6226
|
this.retryAttempt = 0;
|
|
@@ -6204,13 +6269,15 @@ var PgSyncBridge = class {
|
|
|
6204
6269
|
var PgSyncBridgeManager = class {
|
|
6205
6270
|
bridges = new Map();
|
|
6206
6271
|
starting = new Map();
|
|
6207
|
-
url;
|
|
6208
6272
|
retry;
|
|
6273
|
+
fetchFn;
|
|
6274
|
+
probeTimeoutMs;
|
|
6209
6275
|
constructor(streamClient, evaluateWakes, registry, options = {}) {
|
|
6210
6276
|
this.streamClient = streamClient;
|
|
6211
6277
|
this.evaluateWakes = evaluateWakes;
|
|
6212
6278
|
this.registry = registry;
|
|
6213
|
-
this.
|
|
6279
|
+
this.fetchFn = options.fetchFn;
|
|
6280
|
+
this.probeTimeoutMs = options.probeTimeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS;
|
|
6214
6281
|
this.retry = {
|
|
6215
6282
|
initialDelayMs: options.retry?.initialDelayMs ?? DEFAULT_RETRY_INITIAL_DELAY_MS,
|
|
6216
6283
|
maxDelayMs: options.retry?.maxDelayMs ?? DEFAULT_RETRY_MAX_DELAY_MS,
|
|
@@ -6221,9 +6288,16 @@ var PgSyncBridgeManager = class {
|
|
|
6221
6288
|
async start() {
|
|
6222
6289
|
const rows = await this.registry?.listPgSyncBridges?.();
|
|
6223
6290
|
if (!rows) return;
|
|
6224
|
-
await Promise.all(rows.map(
|
|
6225
|
-
|
|
6226
|
-
|
|
6291
|
+
await Promise.all(rows.map(async (row) => {
|
|
6292
|
+
if (!row.options.url) {
|
|
6293
|
+
serverLog.warn(`[pg-sync-bridge] deleting registration ${row.sourceRef}: it predates required source URLs; re-register the observation with an explicit Electric shape URL`);
|
|
6294
|
+
await this.registry?.deletePgSyncBridge?.(row.sourceRef);
|
|
6295
|
+
return;
|
|
6296
|
+
}
|
|
6297
|
+
await this.ensureBridge(row).catch((error) => {
|
|
6298
|
+
serverLog.warn(`[pg-sync-bridge] failed to start ${row.sourceRef}:`, error);
|
|
6299
|
+
});
|
|
6300
|
+
}));
|
|
6227
6301
|
}
|
|
6228
6302
|
async register(options, metadata) {
|
|
6229
6303
|
const mergedMetadata = {
|
|
@@ -6237,6 +6311,7 @@ var PgSyncBridgeManager = class {
|
|
|
6237
6311
|
const resolvedSource = this.resolveSource(canonicalOptions);
|
|
6238
6312
|
const sourceRef = (0, __electric_ax_agents_runtime.sourceRefForPgSync)(canonicalOptions);
|
|
6239
6313
|
const streamUrl = (0, __electric_ax_agents_runtime.getPgSyncStreamPath)(sourceRef, this.registry?.tenantId);
|
|
6314
|
+
if (!this.bridges.has(sourceRef) && !this.starting.has(sourceRef)) await this.probeSource(resolvedSource, canonicalOptions);
|
|
6240
6315
|
const row = await this.registry?.upsertPgSyncBridge({
|
|
6241
6316
|
sourceRef,
|
|
6242
6317
|
options: canonicalOptions,
|
|
@@ -6277,7 +6352,32 @@ var PgSyncBridgeManager = class {
|
|
|
6277
6352
|
await start;
|
|
6278
6353
|
}
|
|
6279
6354
|
resolveSource(options) {
|
|
6280
|
-
|
|
6355
|
+
if (!options.url) throw new PgSyncSourceValidationError(`pgSync source url is required; no server default is configured`);
|
|
6356
|
+
return { url: options.url };
|
|
6357
|
+
}
|
|
6358
|
+
/**
|
|
6359
|
+
* One-shot fetch of the shape log before a bridge is created, so a bad
|
|
6360
|
+
* URL or rejected shape fails the registration instead of dying silently
|
|
6361
|
+
* in the bridge's retry loop.
|
|
6362
|
+
*/
|
|
6363
|
+
async probeSource(source, options) {
|
|
6364
|
+
const probeUrl = buildShapeProbeUrl(source.url, options);
|
|
6365
|
+
const fetchFn = this.fetchFn ?? globalThis.fetch;
|
|
6366
|
+
let response;
|
|
6367
|
+
try {
|
|
6368
|
+
response = await fetchFn(probeUrl, { signal: AbortSignal.timeout(this.probeTimeoutMs) });
|
|
6369
|
+
} catch (error) {
|
|
6370
|
+
throw new PgSyncSourceValidationError(`pgSync source at ${source.url} is unreachable: ${error instanceof Error ? error.message : String(error)}`);
|
|
6371
|
+
}
|
|
6372
|
+
if (!response.ok) {
|
|
6373
|
+
const body = (await response.text().catch(() => `<failed to read body>`)).slice(0, 500);
|
|
6374
|
+
throw new PgSyncSourceValidationError(`pgSync source at ${source.url} rejected the shape request (${response.status})${body ? `: ${body}` : ``}`);
|
|
6375
|
+
}
|
|
6376
|
+
if (!response.headers.get(`electric-handle`)) {
|
|
6377
|
+
const suggestion = new URL(source.url);
|
|
6378
|
+
suggestion.pathname = `/v1/shape`;
|
|
6379
|
+
throw new PgSyncSourceValidationError(`pgSync source at ${source.url} responded but is not a shape log (missing electric-handle header) — the Electric shape API is usually served at ${suggestion.origin}/v1/shape`);
|
|
6380
|
+
}
|
|
6281
6381
|
}
|
|
6282
6382
|
async stop() {
|
|
6283
6383
|
await Promise.allSettled(this.starting.values());
|
|
@@ -7239,6 +7339,7 @@ var WakeRegistry = class {
|
|
|
7239
7339
|
};
|
|
7240
7340
|
if (value && `value` in value) change.value = value.value;
|
|
7241
7341
|
if (value && `oldValue` in value) change.oldValue = value.oldValue;
|
|
7342
|
+
else if (value && `old_value` in value) change.oldValue = value.old_value;
|
|
7242
7343
|
if (eventType === `inbox`) {
|
|
7243
7344
|
if (typeof value?.from === `string`) change.from = value.from;
|
|
7244
7345
|
if (typeof value?.from_principal === `string`) change.from_principal = value.from_principal;
|
|
@@ -8534,6 +8635,15 @@ const spawnBodySchema = __sinclair_typebox.Type.Object({
|
|
|
8534
8635
|
manifestKey: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String())
|
|
8535
8636
|
}))
|
|
8536
8637
|
});
|
|
8638
|
+
const writeCollectionBodySchema = __sinclair_typebox.Type.Object({
|
|
8639
|
+
operation: __sinclair_typebox.Type.Union([
|
|
8640
|
+
__sinclair_typebox.Type.Literal(`insert`),
|
|
8641
|
+
__sinclair_typebox.Type.Literal(`update`),
|
|
8642
|
+
__sinclair_typebox.Type.Literal(`delete`)
|
|
8643
|
+
]),
|
|
8644
|
+
key: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
8645
|
+
value: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Record(__sinclair_typebox.Type.String(), __sinclair_typebox.Type.Unknown()))
|
|
8646
|
+
}, { additionalProperties: false });
|
|
8537
8647
|
const sendBodySchema = __sinclair_typebox.Type.Object({
|
|
8538
8648
|
payload: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Unknown()),
|
|
8539
8649
|
key: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
@@ -8643,8 +8753,8 @@ const subscriptionLifetimeSchema = __sinclair_typebox.Type.Union([
|
|
|
8643
8753
|
}),
|
|
8644
8754
|
__sinclair_typebox.Type.Object({ kind: __sinclair_typebox.Type.Literal(`manual`) })
|
|
8645
8755
|
]);
|
|
8646
|
-
const
|
|
8647
|
-
|
|
8756
|
+
const webhookSourceSubscriptionBodySchema = __sinclair_typebox.Type.Object({
|
|
8757
|
+
webhookKey: __sinclair_typebox.Type.String(),
|
|
8648
8758
|
bucketKey: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
8649
8759
|
params: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Record(__sinclair_typebox.Type.String(), __sinclair_typebox.Type.Unknown())),
|
|
8650
8760
|
filterKey: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
@@ -8666,6 +8776,7 @@ entitiesRouter.head(`/:type/:instanceId`, withExistingEntity, withEntityPermissi
|
|
|
8666
8776
|
entitiesRouter.delete(`/:type/:instanceId`, withExistingEntity, withEntityPermission(`delete`), killEntity);
|
|
8667
8777
|
entitiesRouter.post(`/:type/:instanceId/signal`, withExistingEntity, withSchema(signalBodySchema), withEntityPermission(`signal`), signalEntity);
|
|
8668
8778
|
entitiesRouter.post(`/:type/:instanceId/send`, withExistingEntity, withSchema(sendBodySchema), withEntityPermission(`write`), sendEntity);
|
|
8779
|
+
entitiesRouter.post(`/:type/:instanceId/collections/:collection`, withExistingEntity, withSchema(writeCollectionBodySchema), withEntityPermission(`write`), writeCollection);
|
|
8669
8780
|
entitiesRouter.post(`/:type/:instanceId/attachments`, withExistingEntity, withEntityPermission(`write`), createAttachment);
|
|
8670
8781
|
entitiesRouter.get(`/:type/:instanceId/attachments/:attachmentId`, withExistingEntity, withEntityPermission(`read`), readAttachment);
|
|
8671
8782
|
entitiesRouter.delete(`/:type/:instanceId/attachments/:attachmentId`, withExistingEntity, withEntityPermission(`write`), deleteAttachment);
|
|
@@ -8676,8 +8787,9 @@ entitiesRouter.post(`/:type/:instanceId/tags/:tagKey`, withExistingEntity, withS
|
|
|
8676
8787
|
entitiesRouter.delete(`/:type/:instanceId/tags/:tagKey`, withExistingEntity, withEntityPermission(`write`), deleteTag);
|
|
8677
8788
|
entitiesRouter.put(`/:type/:instanceId/schedules/:scheduleId`, withExistingEntity, withSchema(scheduleBodySchema), withEntityPermission(`schedule`), upsertSchedule);
|
|
8678
8789
|
entitiesRouter.delete(`/:type/:instanceId/schedules/:scheduleId`, withExistingEntity, withEntityPermission(`schedule`), deleteSchedule);
|
|
8679
|
-
entitiesRouter.put(`/:type/:instanceId/
|
|
8680
|
-
entitiesRouter.delete(`/:type/:instanceId/
|
|
8790
|
+
entitiesRouter.put(`/:type/:instanceId/webhook-source-subscriptions/:subscriptionId`, withExistingEntity, withSchema(webhookSourceSubscriptionBodySchema), withEntityPermission(`write`), upsertWebhookSourceSubscription);
|
|
8791
|
+
entitiesRouter.delete(`/:type/:instanceId/webhook-source-subscriptions/:subscriptionId`, withExistingEntity, withEntityPermission(`write`), deleteWebhookSourceSubscription);
|
|
8792
|
+
entitiesRouter.delete(`/:type/:instanceId/pg-sync-observations/:sourceRef`, withExistingEntity, withEntityPermission(`write`), deletePgSyncObservation);
|
|
8681
8793
|
entitiesRouter.get(`/:type/:instanceId/grants`, withExistingEntity, withEntityPermission(`manage`), listEntityPermissionGrants);
|
|
8682
8794
|
entitiesRouter.post(`/:type/:instanceId/grants`, withExistingEntity, withSchema(entityPermissionGrantInputSchema), withEntityPermission(`manage`), createEntityPermissionGrant);
|
|
8683
8795
|
entitiesRouter.delete(`/:type/:instanceId/grants/:grantId`, withExistingEntity, withEntityPermission(`manage`), deleteEntityPermissionGrant);
|
|
@@ -8928,22 +9040,22 @@ async function deleteSchedule(request, ctx) {
|
|
|
8928
9040
|
const result = await ctx.entityManager.deleteSchedule(entityUrl, { id: decodeURIComponent(request.params.scheduleId) });
|
|
8929
9041
|
return (0, itty_router.json)(result);
|
|
8930
9042
|
}
|
|
8931
|
-
async function
|
|
8932
|
-
const principalMutationError = rejectPrincipalEntityMutation(request, `subscribed to
|
|
9043
|
+
async function upsertWebhookSourceSubscription(request, ctx) {
|
|
9044
|
+
const principalMutationError = rejectPrincipalEntityMutation(request, `subscribed to webhook sources`);
|
|
8933
9045
|
if (principalMutationError) return principalMutationError;
|
|
8934
|
-
const catalog = ctx.
|
|
8935
|
-
if (!catalog) return apiError(404, ErrCodeNotFound, `No
|
|
9046
|
+
const catalog = ctx.webhookSources;
|
|
9047
|
+
if (!catalog) return apiError(404, ErrCodeNotFound, `No webhook source catalog is configured`);
|
|
8936
9048
|
const { entityUrl } = requireExistingEntityRoute(request);
|
|
8937
9049
|
const parsed = routeBody(request);
|
|
8938
|
-
const source = await catalog.
|
|
8939
|
-
if (!source) return apiError(404, ErrCodeNotFound, `
|
|
9050
|
+
const source = await catalog.getWebhookSource(parsed.webhookKey);
|
|
9051
|
+
if (!source) return apiError(404, ErrCodeNotFound, `Webhook source "${parsed.webhookKey}" not found`);
|
|
8940
9052
|
if (parsed.lifetime?.kind === `expires_at`) {
|
|
8941
9053
|
const expiresAt = new Date(parsed.lifetime.at);
|
|
8942
9054
|
if (Number.isNaN(expiresAt.getTime())) return apiError(400, ErrCodeInvalidRequest, `Invalid expires_at lifetime timestamp`);
|
|
8943
9055
|
}
|
|
8944
9056
|
let resolved;
|
|
8945
9057
|
try {
|
|
8946
|
-
resolved = (0, __electric_ax_agents_runtime.
|
|
9058
|
+
resolved = (0, __electric_ax_agents_runtime.resolveWebhookSourceSubscription)({
|
|
8947
9059
|
contract: source,
|
|
8948
9060
|
entityUrl,
|
|
8949
9061
|
request: {
|
|
@@ -8955,18 +9067,25 @@ async function upsertEventSourceSubscription(request, ctx) {
|
|
|
8955
9067
|
} catch (error) {
|
|
8956
9068
|
return apiError(400, ErrCodeInvalidRequest, error instanceof Error ? error.message : String(error));
|
|
8957
9069
|
}
|
|
8958
|
-
await ctx.
|
|
8959
|
-
const result = await ctx.entityManager.
|
|
9070
|
+
await ctx.ensureWebhookSourceWakeSource?.(resolved.subscription.sourceUrl);
|
|
9071
|
+
const result = await ctx.entityManager.upsertWebhookSourceSubscription(entityUrl, {
|
|
8960
9072
|
subscription: resolved.subscription,
|
|
8961
|
-
manifest: (0, __electric_ax_agents_runtime.
|
|
9073
|
+
manifest: (0, __electric_ax_agents_runtime.buildWebhookSourceManifestEntry)(resolved)
|
|
8962
9074
|
});
|
|
8963
9075
|
return (0, itty_router.json)(result);
|
|
8964
9076
|
}
|
|
8965
|
-
async function
|
|
8966
|
-
const principalMutationError = rejectPrincipalEntityMutation(request, `unsubscribed from
|
|
9077
|
+
async function deleteWebhookSourceSubscription(request, ctx) {
|
|
9078
|
+
const principalMutationError = rejectPrincipalEntityMutation(request, `unsubscribed from webhook sources`);
|
|
9079
|
+
if (principalMutationError) return principalMutationError;
|
|
9080
|
+
const { entityUrl } = requireExistingEntityRoute(request);
|
|
9081
|
+
const result = await ctx.entityManager.deleteWebhookSourceSubscription(entityUrl, { id: decodeURIComponent(request.params.subscriptionId) });
|
|
9082
|
+
return (0, itty_router.json)(result);
|
|
9083
|
+
}
|
|
9084
|
+
async function deletePgSyncObservation(request, ctx) {
|
|
9085
|
+
const principalMutationError = rejectPrincipalEntityMutation(request, `unobserved a pg-sync source`);
|
|
8967
9086
|
if (principalMutationError) return principalMutationError;
|
|
8968
9087
|
const { entityUrl } = requireExistingEntityRoute(request);
|
|
8969
|
-
const result = await ctx.entityManager.
|
|
9088
|
+
const result = await ctx.entityManager.deletePgSyncObservation(entityUrl, { sourceRef: decodeURIComponent(request.params.sourceRef) });
|
|
8970
9089
|
return (0, itty_router.json)(result);
|
|
8971
9090
|
}
|
|
8972
9091
|
function tagResponseBody(entity) {
|
|
@@ -9064,6 +9183,23 @@ async function sendEntity(request, ctx) {
|
|
|
9064
9183
|
const result = await ctx.entityManager.send(entityUrl, sendReq);
|
|
9065
9184
|
return (0, itty_router.json)(result);
|
|
9066
9185
|
}
|
|
9186
|
+
async function writeCollection(request, ctx) {
|
|
9187
|
+
const parsed = routeBody(request);
|
|
9188
|
+
await ctx.entityManager.ensurePrincipal(ctx.principal);
|
|
9189
|
+
const { entityUrl } = requireExistingEntityRoute(request);
|
|
9190
|
+
const collection = request.params.collection;
|
|
9191
|
+
const result = await ctx.entityManager.writeCollection(entityUrl, collection, {
|
|
9192
|
+
operation: parsed.operation,
|
|
9193
|
+
key: parsed.key,
|
|
9194
|
+
value: parsed.value,
|
|
9195
|
+
principal: {
|
|
9196
|
+
url: ctx.principal.url,
|
|
9197
|
+
kind: ctx.principal.kind,
|
|
9198
|
+
id: ctx.principal.id
|
|
9199
|
+
}
|
|
9200
|
+
});
|
|
9201
|
+
return (0, itty_router.json)(result, { status: parsed.operation === `insert` ? 201 : 200 });
|
|
9202
|
+
}
|
|
9067
9203
|
async function createAttachment(request, ctx) {
|
|
9068
9204
|
const principalMutationError = rejectPrincipalEntityMutation(request, `given attachments`);
|
|
9069
9205
|
if (principalMutationError) return principalMutationError;
|
|
@@ -9161,8 +9297,13 @@ async function spawnEntity(request, ctx) {
|
|
|
9161
9297
|
headers: { "x-write-token": entity.write_token }
|
|
9162
9298
|
});
|
|
9163
9299
|
}
|
|
9164
|
-
function getEntity(request) {
|
|
9165
|
-
|
|
9300
|
+
async function getEntity(request, ctx) {
|
|
9301
|
+
const { entity } = requireExistingEntityRoute(request);
|
|
9302
|
+
const entityType = entity.type ? await ctx.entityManager.registry.getEntityType(entity.type) : null;
|
|
9303
|
+
return (0, itty_router.json)({
|
|
9304
|
+
...toPublicEntity(entity),
|
|
9305
|
+
...entityType?.externally_writable_collections && { externally_writable_collections: entityType.externally_writable_collections }
|
|
9306
|
+
});
|
|
9166
9307
|
}
|
|
9167
9308
|
function headEntity() {
|
|
9168
9309
|
return (0, itty_router.status)(200);
|
|
@@ -9197,6 +9338,16 @@ async function signalEntity(request, ctx) {
|
|
|
9197
9338
|
//#region src/routing/entity-types-router.ts
|
|
9198
9339
|
const jsonObjectSchema = __sinclair_typebox.Type.Record(__sinclair_typebox.Type.String(), __sinclair_typebox.Type.Unknown());
|
|
9199
9340
|
const schemaMapSchema = __sinclair_typebox.Type.Record(__sinclair_typebox.Type.String(), jsonObjectSchema);
|
|
9341
|
+
const externallyWritableCollectionsSchema = __sinclair_typebox.Type.Record(__sinclair_typebox.Type.String(), __sinclair_typebox.Type.Object({
|
|
9342
|
+
type: __sinclair_typebox.Type.String(),
|
|
9343
|
+
contract: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
9344
|
+
operations: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Array(__sinclair_typebox.Type.Union([
|
|
9345
|
+
__sinclair_typebox.Type.Literal(`insert`),
|
|
9346
|
+
__sinclair_typebox.Type.Literal(`update`),
|
|
9347
|
+
__sinclair_typebox.Type.Literal(`delete`)
|
|
9348
|
+
]))),
|
|
9349
|
+
principalColumn: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String())
|
|
9350
|
+
}, { additionalProperties: false }));
|
|
9200
9351
|
const slashCommandArgumentSchema = __sinclair_typebox.Type.Object({
|
|
9201
9352
|
name: __sinclair_typebox.Type.String(),
|
|
9202
9353
|
type: __sinclair_typebox.Type.Union([
|
|
@@ -9227,7 +9378,8 @@ const registerEntityTypeBodySchema = __sinclair_typebox.Type.Object({
|
|
|
9227
9378
|
slash_commands: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Array(slashCommandSchema)),
|
|
9228
9379
|
serve_endpoint: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
9229
9380
|
default_dispatch_policy: __sinclair_typebox.Type.Optional(dispatchPolicySchema),
|
|
9230
|
-
permission_grants: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Array(typePermissionGrantInputSchema))
|
|
9381
|
+
permission_grants: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Array(typePermissionGrantInputSchema)),
|
|
9382
|
+
externally_writable_collections: __sinclair_typebox.Type.Optional(externallyWritableCollectionsSchema)
|
|
9231
9383
|
}, { additionalProperties: false });
|
|
9232
9384
|
const amendEntityTypeSchemasBodySchema = __sinclair_typebox.Type.Object({
|
|
9233
9385
|
inbox_schemas: __sinclair_typebox.Type.Optional(schemaMapSchema),
|
|
@@ -9358,7 +9510,20 @@ function parseExpiresAt(value) {
|
|
|
9358
9510
|
if (Number.isNaN(expiresAt.getTime())) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Invalid expires_at timestamp`, 400);
|
|
9359
9511
|
return expiresAt;
|
|
9360
9512
|
}
|
|
9513
|
+
/**
|
|
9514
|
+
* The `comments` collection name is reserved for the canonical comments
|
|
9515
|
+
* contract: the UI keys its comment affordances on it, so a divergent
|
|
9516
|
+
* collection registered under that name (or the contract mounted under
|
|
9517
|
+
* another name) would break that assumption silently.
|
|
9518
|
+
*/
|
|
9519
|
+
function validateExternallyWritableCollections(collections) {
|
|
9520
|
+
for (const [name, config] of Object.entries(collections ?? {})) {
|
|
9521
|
+
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);
|
|
9522
|
+
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);
|
|
9523
|
+
}
|
|
9524
|
+
}
|
|
9361
9525
|
function normalizeEntityTypeRequest(parsed) {
|
|
9526
|
+
validateExternallyWritableCollections(parsed.externally_writable_collections);
|
|
9362
9527
|
const serveEndpoint = rewriteLoopbackWebhookUrl(parsed.serve_endpoint);
|
|
9363
9528
|
return {
|
|
9364
9529
|
name: parsed.name ?? ``,
|
|
@@ -9372,7 +9537,8 @@ function normalizeEntityTypeRequest(parsed) {
|
|
|
9372
9537
|
type: `webhook`,
|
|
9373
9538
|
url: serveEndpoint
|
|
9374
9539
|
}] } : void 0),
|
|
9375
|
-
permission_grants: parsed.permission_grants
|
|
9540
|
+
permission_grants: parsed.permission_grants,
|
|
9541
|
+
externally_writable_collections: parsed.externally_writable_collections
|
|
9376
9542
|
};
|
|
9377
9543
|
}
|
|
9378
9544
|
function toPublicEntityType(entityType) {
|
|
@@ -9385,7 +9551,7 @@ function toPublicEntityType(entityType) {
|
|
|
9385
9551
|
//#endregion
|
|
9386
9552
|
//#region src/routing/pg-sync-router.ts
|
|
9387
9553
|
const pgSyncOptionsSchema = __sinclair_typebox.Type.Object({
|
|
9388
|
-
url: __sinclair_typebox.Type.
|
|
9554
|
+
url: __sinclair_typebox.Type.String(),
|
|
9389
9555
|
table: __sinclair_typebox.Type.String(),
|
|
9390
9556
|
columns: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.Array(__sinclair_typebox.Type.String())),
|
|
9391
9557
|
where: __sinclair_typebox.Type.Optional(__sinclair_typebox.Type.String()),
|
|
@@ -9407,6 +9573,7 @@ const pgSyncRouter = (0, itty_router.Router)({ base: `/_electric/pg-sync` });
|
|
|
9407
9573
|
pgSyncRouter.post(`/register`, withSchema(pgSyncRegisterBodySchema), registerPgSync);
|
|
9408
9574
|
async function registerPgSync(request, ctx) {
|
|
9409
9575
|
const { options, metadata } = routeBody(request);
|
|
9576
|
+
if (options.url.trim() === ``) return apiError(400, ErrCodeInvalidRequest, `pgSync url must be non-empty`);
|
|
9410
9577
|
if (options.table.trim() === ``) return apiError(400, ErrCodeInvalidRequest, `pgSync table must be non-empty`);
|
|
9411
9578
|
if (!ctx.pgSyncBridgeManager) return apiError(503, ErrCodeInvalidRequest, `pgSync bridge manager is not configured`);
|
|
9412
9579
|
try {
|
|
@@ -9421,6 +9588,8 @@ async function registerPgSync(request, ctx) {
|
|
|
9421
9588
|
const result = await ctx.pgSyncBridgeManager.register(options, requestMetadata$1);
|
|
9422
9589
|
return (0, itty_router.json)(result);
|
|
9423
9590
|
} catch (error) {
|
|
9591
|
+
if (error instanceof PgSyncSourceValidationError) return apiError(400, ErrCodeInvalidRequest, error.message);
|
|
9592
|
+
serverLog.error(`[pg-sync] registration failed for table "${options.table}":`, error);
|
|
9424
9593
|
return apiError(500, ErrCodeInvalidRequest, `pgSync registration failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
9425
9594
|
}
|
|
9426
9595
|
}
|
|
@@ -9945,7 +10114,7 @@ const wakeCallbackBodySchema = __sinclair_typebox.Type.Object({
|
|
|
9945
10114
|
const DS_SUBSCRIPTION_CALLBACK_PREFIX = `ds-subscription:`;
|
|
9946
10115
|
const internalRouter = (0, itty_router.Router)({ base: `/_electric` });
|
|
9947
10116
|
internalRouter.get(`/health`, () => (0, itty_router.json)({ status: `ok` }));
|
|
9948
|
-
internalRouter.get(`/
|
|
10117
|
+
internalRouter.get(`/webhook-sources`, listWebhookSources);
|
|
9949
10118
|
internalRouter.post(`/wake`, withSchema(wakeRegistrationBodySchema), registerWake);
|
|
9950
10119
|
internalRouter.post(`/subscription-webhooks/:subscriptionId`, subscriptionWebhook);
|
|
9951
10120
|
internalRouter.post(`/wake-callbacks/:consumerId`, wakeCallback);
|
|
@@ -10067,11 +10236,11 @@ async function registerWake(request, ctx) {
|
|
|
10067
10236
|
await ctx.entityManager.registerWake(opts);
|
|
10068
10237
|
return (0, itty_router.status)(204);
|
|
10069
10238
|
}
|
|
10070
|
-
async function
|
|
10071
|
-
const
|
|
10072
|
-
return (0, itty_router.json)({
|
|
10239
|
+
async function listWebhookSources(_request, ctx) {
|
|
10240
|
+
const webhookSources = ctx.webhookSources ? await ctx.webhookSources.listWebhookSources() : [];
|
|
10241
|
+
return (0, itty_router.json)({ webhookSources: webhookSources.filter(isAgentVisibleWebhookSource) });
|
|
10073
10242
|
}
|
|
10074
|
-
function
|
|
10243
|
+
function isAgentVisibleWebhookSource(source) {
|
|
10075
10244
|
return source.agentVisible === true && source.status === `active`;
|
|
10076
10245
|
}
|
|
10077
10246
|
async function subscriptionWebhook(request, ctx) {
|