tapimo 0.0.2 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -2
- package/dist/ApiKey.d.ts +12 -11
- package/dist/ApiKey.d.ts.map +1 -1
- package/dist/ApiKey.js +10 -12
- package/dist/ApiKey.js.map +1 -1
- package/dist/ApiKeys.d.ts +139 -0
- package/dist/ApiKeys.d.ts.map +1 -0
- package/dist/ApiKeys.js +166 -0
- package/dist/ApiKeys.js.map +1 -0
- package/dist/App.d.ts +920 -910
- package/dist/App.d.ts.map +1 -1
- package/dist/App.js +36 -12
- package/dist/App.js.map +1 -1
- package/dist/Handler.d.ts +86 -0
- package/dist/Handler.d.ts.map +1 -0
- package/dist/Handler.js +83 -0
- package/dist/Handler.js.map +1 -0
- package/dist/Scope.d.ts +4 -1
- package/dist/Scope.d.ts.map +1 -1
- package/dist/Scope.js +5 -1
- package/dist/Scope.js.map +1 -1
- package/dist/Webhooks.d.ts +3 -5
- package/dist/Webhooks.d.ts.map +1 -1
- package/dist/Webhooks.js +9 -2
- package/dist/Webhooks.js.map +1 -1
- package/dist/admin/Access.d.ts +1 -3
- package/dist/admin/Access.d.ts.map +1 -1
- package/dist/admin/Access.js +1 -3
- package/dist/admin/Access.js.map +1 -1
- package/dist/admin/App.d.ts +98 -209
- package/dist/admin/App.d.ts.map +1 -1
- package/dist/admin/App.js +2 -4
- package/dist/admin/App.js.map +1 -1
- package/dist/admin/apps/api-keys.d.ts +7 -5
- package/dist/admin/apps/api-keys.d.ts.map +1 -1
- package/dist/admin/apps/api-keys.js +8 -9
- package/dist/admin/apps/api-keys.js.map +1 -1
- package/dist/admin/apps/scopes.d.ts +3 -0
- package/dist/admin/apps/scopes.d.ts.map +1 -1
- package/dist/admin/apps/verified-tokens.d.ts +101 -215
- package/dist/admin/apps/verified-tokens.d.ts.map +1 -1
- package/dist/admin/apps/verified-tokens.js +17 -38
- package/dist/admin/apps/verified-tokens.js.map +1 -1
- package/dist/admin/ui.generated.d.ts +1 -1
- package/dist/admin/ui.generated.js +1 -1
- package/dist/apps/balances.d.ts +2 -2
- package/dist/apps/receipts.d.ts +43 -43
- package/dist/apps/tokens.d.ts +454 -454
- package/dist/apps/transactions.d.ts +43 -43
- package/dist/apps/transfers.d.ts +29 -29
- package/dist/apps/webhooks.d.ts +5 -2
- package/dist/apps/webhooks.d.ts.map +1 -1
- package/dist/apps/webhooks.js +38 -34
- package/dist/apps/webhooks.js.map +1 -1
- package/dist/{Cli.d.ts → cli/Cli.d.ts} +13 -2
- package/dist/cli/Cli.d.ts.map +1 -0
- package/dist/cli/Cli.js +19 -0
- package/dist/cli/Cli.js.map +1 -0
- package/dist/cli/bin.d.ts +3 -0
- package/dist/cli/bin.d.ts.map +1 -0
- package/dist/cli/bin.js +4 -0
- package/dist/cli/bin.js.map +1 -0
- package/dist/cli/commands/admin.d.ts +15 -0
- package/dist/cli/commands/admin.d.ts.map +1 -0
- package/dist/cli/commands/admin.js +60 -0
- package/dist/cli/commands/admin.js.map +1 -0
- package/dist/cloudflare.d.ts +7 -0
- package/dist/cloudflare.d.ts.map +1 -1
- package/dist/cloudflare.js +9 -0
- package/dist/cloudflare.js.map +1 -1
- package/dist/db/Db.d.ts +103 -0
- package/dist/db/Db.d.ts.map +1 -0
- package/dist/db/Db.js +274 -0
- package/dist/db/Db.js.map +1 -0
- package/dist/db/Schema.d.ts +88 -0
- package/dist/db/Schema.d.ts.map +1 -0
- package/dist/db/Schema.js +130 -0
- package/dist/db/Schema.js.map +1 -0
- package/dist/db/tables/organizations.d.ts +79 -0
- package/dist/db/tables/organizations.d.ts.map +1 -0
- package/dist/db/tables/organizations.js +81 -0
- package/dist/db/tables/organizations.js.map +1 -0
- package/dist/db/tables/verifiedTokens.d.ts +53 -0
- package/dist/db/tables/verifiedTokens.d.ts.map +1 -0
- package/dist/db/tables/verifiedTokens.js +74 -0
- package/dist/db/tables/verifiedTokens.js.map +1 -0
- package/dist/db/tables/webhookDeliveries.d.ts +71 -0
- package/dist/db/tables/webhookDeliveries.d.ts.map +1 -0
- package/dist/db/tables/webhookDeliveries.js +119 -0
- package/dist/db/tables/webhookDeliveries.js.map +1 -0
- package/dist/db/tables/webhookSubscriptions.d.ts +158 -0
- package/dist/db/tables/webhookSubscriptions.d.ts.map +1 -0
- package/dist/db/tables/webhookSubscriptions.js +272 -0
- package/dist/db/tables/webhookSubscriptions.js.map +1 -0
- package/dist/handlers/internal/executionError.d.ts +25 -0
- package/dist/handlers/internal/executionError.d.ts.map +1 -0
- package/dist/handlers/internal/executionError.js +184 -0
- package/dist/handlers/internal/executionError.js.map +1 -0
- package/dist/handlers/internal/multisig.d.ts +237 -0
- package/dist/handlers/internal/multisig.d.ts.map +1 -0
- package/dist/handlers/internal/multisig.js +567 -0
- package/dist/handlers/internal/multisig.js.map +1 -0
- package/dist/handlers/internal/requestListener.d.ts +124 -0
- package/dist/handlers/internal/requestListener.d.ts.map +1 -0
- package/dist/handlers/internal/requestListener.js +173 -0
- package/dist/handlers/internal/requestListener.js.map +1 -0
- package/dist/handlers/internal/sponsorship.d.ts +83 -0
- package/dist/handlers/internal/sponsorship.d.ts.map +1 -0
- package/dist/handlers/internal/sponsorship.js +103 -0
- package/dist/handlers/internal/sponsorship.js.map +1 -0
- package/dist/handlers/internal/types.d.ts +284 -0
- package/dist/handlers/internal/types.d.ts.map +1 -0
- package/dist/handlers/internal/types.js +2 -0
- package/dist/handlers/internal/types.js.map +1 -0
- package/dist/handlers/internal/utils.d.ts +34 -0
- package/dist/handlers/internal/utils.d.ts.map +1 -0
- package/dist/handlers/internal/utils.js +131 -0
- package/dist/handlers/internal/utils.js.map +1 -0
- package/dist/handlers/relay.d.ts +119 -0
- package/dist/handlers/relay.d.ts.map +1 -0
- package/dist/handlers/relay.js +1264 -0
- package/dist/handlers/relay.js.map +1 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/internal/Auth.d.ts +16 -10
- package/dist/internal/Auth.d.ts.map +1 -1
- package/dist/internal/Auth.js +72 -12
- package/dist/internal/Auth.js.map +1 -1
- package/dist/internal/RpcSchema.d.ts +10 -0
- package/dist/internal/RpcSchema.d.ts.map +1 -0
- package/dist/internal/RpcSchema.js +2 -0
- package/dist/internal/RpcSchema.js.map +1 -0
- package/dist/internal/Store.d.ts +42 -3
- package/dist/internal/Store.d.ts.map +1 -1
- package/dist/internal/Store.js +65 -3
- package/dist/internal/Store.js.map +1 -1
- package/dist/internal/VerifiedTokens.d.ts +15 -34
- package/dist/internal/VerifiedTokens.d.ts.map +1 -1
- package/dist/internal/VerifiedTokens.js +115 -159
- package/dist/internal/VerifiedTokens.js.map +1 -1
- package/dist/internal/Webhooks.d.ts +49 -35
- package/dist/internal/Webhooks.d.ts.map +1 -1
- package/dist/internal/Webhooks.js +92 -205
- package/dist/internal/Webhooks.js.map +1 -1
- package/package.json +7 -2
- package/src/ApiKey.test.ts +3 -3
- package/src/ApiKey.ts +10 -12
- package/src/ApiKeys.test.ts +175 -0
- package/src/ApiKeys.ts +205 -0
- package/src/App.test-d.ts +1 -1
- package/src/App.test.ts +10768 -351
- package/src/App.ts +79 -36
- package/src/Client.test-d.ts +1 -1
- package/src/Handler.ts +154 -0
- package/src/Scope.ts +5 -1
- package/src/Webhooks.test.ts +8 -7
- package/src/Webhooks.ts +11 -5
- package/src/admin/Access.ts +1 -3
- package/src/admin/App.ts +19 -24
- package/src/admin/apps/api-keys.test.ts +19 -18
- package/src/admin/apps/api-keys.ts +8 -9
- package/src/admin/apps/scopes.test.ts +4 -0
- package/src/admin/apps/verified-tokens.test.ts +3 -30
- package/src/admin/apps/verified-tokens.ts +17 -36
- package/src/admin/ui.generated.ts +1 -1
- package/src/apps/balances.test.ts +4 -3
- package/src/apps/blocks.test.ts +1 -1
- package/src/apps/exchanges.test.ts +1 -1
- package/src/apps/indexer.test.ts +1 -1
- package/src/apps/receipts.test.ts +1 -1
- package/src/apps/rpc.test.ts +100 -10
- package/src/apps/tokenlist.test.ts +5 -7
- package/src/apps/tokens.test.ts +4 -3
- package/src/apps/transactions.test.ts +1 -1
- package/src/apps/transfers.test.ts +1 -1
- package/src/apps/verified-tokens.test.ts +24 -12
- package/src/apps/webhooks.test.ts +130 -143
- package/src/apps/webhooks.ts +45 -47
- package/src/{Cli.test.ts → cli/Cli.test.ts} +10 -9
- package/src/{Cli.ts → cli/Cli.ts} +16 -9
- package/src/cli/bin.ts +4 -0
- package/src/cli/commands/admin.test.ts +71 -0
- package/src/cli/commands/admin.ts +60 -0
- package/src/cloudflare.ts +10 -0
- package/src/db/Db.test.ts +84 -0
- package/src/db/Db.ts +383 -0
- package/src/db/Schema.ts +164 -0
- package/src/db/tables/organizations.test.ts +124 -0
- package/src/db/tables/organizations.ts +127 -0
- package/src/db/tables/verifiedTokens.ts +111 -0
- package/src/db/tables/webhookDeliveries.ts +159 -0
- package/src/db/tables/webhookSubscriptions.ts +358 -0
- package/src/handlers/internal/executionError.ts +205 -0
- package/src/handlers/internal/multisig.localnet.test.ts +146 -0
- package/src/handlers/internal/multisig.test.ts +744 -0
- package/src/handlers/internal/multisig.ts +858 -0
- package/src/handlers/internal/relay.localnet.test.ts +2035 -0
- package/src/handlers/internal/requestListener.ts +273 -0
- package/src/handlers/internal/sponsorship.ts +186 -0
- package/src/handlers/internal/types.ts +378 -0
- package/src/handlers/internal/utils.ts +153 -0
- package/src/handlers/relay.ts +1605 -0
- package/src/index.ts +4 -2
- package/src/internal/Auth.test.ts +21 -21
- package/src/internal/Auth.ts +92 -19
- package/src/internal/RateLimit.test.ts +1 -1
- package/src/internal/RpcSchema.ts +10 -0
- package/src/internal/Store.test.ts +98 -10
- package/src/internal/Store.ts +112 -6
- package/src/internal/VerifiedTokens.test.ts +99 -115
- package/src/internal/VerifiedTokens.ts +123 -175
- package/src/internal/Webhooks.test.ts +107 -107
- package/src/internal/Webhooks.ts +102 -220
- package/dist/ApiKeySource.d.ts +0 -166
- package/dist/ApiKeySource.d.ts.map +0 -1
- package/dist/ApiKeySource.js +0 -234
- package/dist/ApiKeySource.js.map +0 -1
- package/dist/Cli.d.ts.map +0 -1
- package/dist/Cli.js +0 -13
- package/dist/Cli.js.map +0 -1
- package/seeds/31318/icons/0x20c0000000000000000000000000000000000000.svg +0 -1
- package/seeds/31318/icons/0x20c0000000000000000000000000000000000001.svg +0 -1
- package/seeds/31318/icons/0x20c0000000000000000000000000000000000002.svg +0 -1
- package/seeds/31318/icons/0x20c0000000000000000000000000000000000003.svg +0 -1
- package/seeds/31318/verified-tokens.json +0 -30
- package/seeds/4217/icons/0x20c0000000000000000000000000000000000000.svg +0 -25
- package/seeds/4217/icons/0x20c000000000000000000000048c8f36df1c9a4a.svg +0 -1
- package/seeds/4217/icons/0x20c0000000000000000000000520792dcccccccc.svg +0 -1
- package/seeds/4217/icons/0x20c0000000000000000000000a6da882d075a4c3.svg +0 -4
- package/seeds/4217/icons/0x20c000000000000000000000111111111e910f0f.svg +0 -30
- package/seeds/4217/icons/0x20c00000000000000000000014f22ca97301eb73.svg +0 -1
- package/seeds/4217/icons/0x20c0000000000000000000001621e21f71cf12fb.svg +0 -1
- package/seeds/4217/icons/0x20c0000000000000000000002f52d5cc21a3207b.svg +0 -1
- package/seeds/4217/icons/0x20c0000000000000000000003158081efd85bfc2.svg +0 -11
- package/seeds/4217/icons/0x20c0000000000000000000003554d28269e0f3c2.svg +0 -1
- package/seeds/4217/icons/0x20c000000000000000000000383a23bacb546ab9.svg +0 -1
- package/seeds/4217/icons/0x20c00000000000000000000042109aef2f8b28e1.png +0 -0
- package/seeds/4217/icons/0x20c0000000000000000000005c0bac7cef389a11.svg +0 -1
- package/seeds/4217/icons/0x20c0000000000000000000006fd9a167923ba194.png +0 -0
- package/seeds/4217/icons/0x20c0000000000000000000007f7ba549dd0251b9.svg +0 -1
- package/seeds/4217/icons/0x20c0000000000000000000008191667423f70e67.svg +0 -5
- package/seeds/4217/icons/0x20c0000000000000000000008ee4fcff88888888.svg +0 -1
- package/seeds/4217/icons/0x20c0000000000000000000009a4a4b17e0dc6651.svg +0 -1
- package/seeds/4217/icons/0x20c000000000000000000000ab02d39df30bd17e.svg +0 -1
- package/seeds/4217/icons/0x20c000000000000000000000ae247a1130450f09.svg +0 -1
- package/seeds/4217/icons/0x20c000000000000000000000aeed2ec36a54d0e5.svg +0 -1
- package/seeds/4217/icons/0x20c000000000000000000000b9537d11c60e8b50.svg +0 -1
- package/seeds/4217/icons/0x20c000000000000000000000bd95bfb69fbe6ce3.svg +0 -1
- package/seeds/4217/icons/0x20c000000000000000000000c412ec89d0c08be5.svg +0 -1
- package/seeds/4217/icons/0x20c000000000000000000000f047dd7018e50367.png +0 -0
- package/seeds/4217/verified-tokens.json +0 -177
- package/seeds/42431/icons/0x20c0000000000000000000000000000000000000.svg +0 -25
- package/seeds/42431/icons/0x20c0000000000000000000000000000000000001.svg +0 -1
- package/seeds/42431/icons/0x20c0000000000000000000000000000000000002.svg +0 -1
- package/seeds/42431/icons/0x20c0000000000000000000000000000000000003.svg +0 -1
- package/seeds/42431/icons/0x20c0000000000000000000009e8d7eb59b783726.svg +0 -1
- package/seeds/42431/icons/0x20c000000000000000000000d72572838bbee59c.svg +0 -1
- package/seeds/42431/verified-tokens.json +0 -44
- package/seeds/openrpc/README.md +0 -27
- package/seeds/openrpc/execution-apis.json +0 -8989
- package/src/ApiKeySource.test-d.ts +0 -11
- package/src/ApiKeySource.test.ts +0 -295
- package/src/ApiKeySource.ts +0 -303
package/src/apps/webhooks.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type * as App from '../App.js'
|
|
|
6
6
|
import * as Auth from '../internal/Auth.js'
|
|
7
7
|
import * as Cache from '../internal/Cache.js'
|
|
8
8
|
import * as Cursor from '../internal/Cursor.js'
|
|
9
|
+
import * as Db from '../db/Db.js'
|
|
9
10
|
import * as OpenApi from '../internal/OpenApi.js'
|
|
10
11
|
import * as Response from '../internal/Response.js'
|
|
11
12
|
import * as Schema from '../internal/Schema.js'
|
|
@@ -1306,7 +1307,7 @@ export function webhooks(options: webhooks.Options = {}) {
|
|
|
1306
1307
|
})
|
|
1307
1308
|
|
|
1308
1309
|
const webhook = c.get('webhook')!
|
|
1309
|
-
const
|
|
1310
|
+
const db = Db.get(c.get('db'))
|
|
1310
1311
|
const owner = ownerFor(c)
|
|
1311
1312
|
if (!owner) return unauthorized(c)
|
|
1312
1313
|
|
|
@@ -1315,7 +1316,7 @@ export function webhooks(options: webhooks.Options = {}) {
|
|
|
1315
1316
|
|
|
1316
1317
|
try {
|
|
1317
1318
|
const subscription = await Webhooks.createSubscription(
|
|
1318
|
-
|
|
1319
|
+
db,
|
|
1319
1320
|
{
|
|
1320
1321
|
chainId,
|
|
1321
1322
|
...(body.context === undefined ? {} : { context: body.context }),
|
|
@@ -1365,7 +1366,7 @@ export function webhooks(options: webhooks.Options = {}) {
|
|
|
1365
1366
|
message: 'Check the query parameters and try again.',
|
|
1366
1367
|
})
|
|
1367
1368
|
|
|
1368
|
-
const
|
|
1369
|
+
const db = Db.get(c.get('db'))
|
|
1369
1370
|
const owner = ownerFor(c)
|
|
1370
1371
|
if (!owner) return unauthorized(c)
|
|
1371
1372
|
|
|
@@ -1375,12 +1376,12 @@ export function webhooks(options: webhooks.Options = {}) {
|
|
|
1375
1376
|
// run it concurrently with the page list. Best-effort: a failure
|
|
1376
1377
|
// omits `meta` rather than failing the page.
|
|
1377
1378
|
const countPromise = include.includes('totalCount')
|
|
1378
|
-
? Webhooks.countSubscriptions(
|
|
1379
|
+
? Webhooks.countSubscriptions(db, owner).catch(() => undefined)
|
|
1379
1380
|
: undefined
|
|
1380
1381
|
|
|
1381
1382
|
// Fetch one extra to detect a further page without a second round-trip.
|
|
1382
1383
|
// `page` translates to a positional slice of `(page - 1) * limit` rows.
|
|
1383
|
-
const rows = await Webhooks.listSubscriptions(
|
|
1384
|
+
const rows = await Webhooks.listSubscriptions(db, owner, {
|
|
1384
1385
|
cursor,
|
|
1385
1386
|
limit: limit + 1,
|
|
1386
1387
|
offset: page !== undefined && page > 1 ? (page - 1) * limit : undefined,
|
|
@@ -1445,13 +1446,13 @@ export function webhooks(options: webhooks.Options = {}) {
|
|
|
1445
1446
|
message: 'Check the path parameters and try again.',
|
|
1446
1447
|
})
|
|
1447
1448
|
|
|
1448
|
-
const
|
|
1449
|
+
const db = Db.get(c.get('db'))
|
|
1449
1450
|
const owner = ownerFor(c)
|
|
1450
1451
|
if (!owner) return unauthorized(c)
|
|
1451
1452
|
|
|
1452
1453
|
const { id } = c.req.valid('param')
|
|
1453
1454
|
try {
|
|
1454
|
-
const subscription = await Webhooks.getSubscription(
|
|
1455
|
+
const subscription = await Webhooks.getSubscription(db, owner, id)
|
|
1455
1456
|
if (!subscription) return notFound(c)
|
|
1456
1457
|
return c.json(
|
|
1457
1458
|
Response.validated(schema.getWebhook.Response, toPublic(subscription)),
|
|
@@ -1506,7 +1507,7 @@ export function webhooks(options: webhooks.Options = {}) {
|
|
|
1506
1507
|
message: 'Check the query parameters and try again.',
|
|
1507
1508
|
})
|
|
1508
1509
|
|
|
1509
|
-
const
|
|
1510
|
+
const db = Db.get(c.get('db'))
|
|
1510
1511
|
const owner = ownerFor(c)
|
|
1511
1512
|
if (!owner) return unauthorized(c)
|
|
1512
1513
|
|
|
@@ -1515,19 +1516,19 @@ export function webhooks(options: webhooks.Options = {}) {
|
|
|
1515
1516
|
try {
|
|
1516
1517
|
// Owner-scope first: resolving the subscription under the caller's owner
|
|
1517
1518
|
// key 404s ids they don't own (indistinguishable from "doesn't exist").
|
|
1518
|
-
const subscription = await Webhooks.getSubscription(
|
|
1519
|
+
const subscription = await Webhooks.getSubscription(db, owner, id)
|
|
1519
1520
|
if (!subscription) return notFound(c)
|
|
1520
1521
|
|
|
1521
1522
|
// `totalCount` is opt-in and exact (the delivery log is TTL-bounded);
|
|
1522
1523
|
// run it concurrently with the page list. Best-effort: a failure
|
|
1523
1524
|
// omits `meta` rather than failing the page.
|
|
1524
1525
|
const countPromise = include.includes('totalCount')
|
|
1525
|
-
? Webhooks.countDeliveries(
|
|
1526
|
+
? Webhooks.countDeliveries(db, id).catch(() => undefined)
|
|
1526
1527
|
: undefined
|
|
1527
1528
|
|
|
1528
1529
|
// Fetch one extra to detect a further page without a second round-trip.
|
|
1529
1530
|
// `page` translates to a positional slice of `(page - 1) * limit` rows.
|
|
1530
|
-
const rows = await Webhooks.listDeliveries(
|
|
1531
|
+
const rows = await Webhooks.listDeliveries(db, id, {
|
|
1531
1532
|
cursor,
|
|
1532
1533
|
limit: limit + 1,
|
|
1533
1534
|
offset: page !== undefined && page > 1 ? (page - 1) * limit : undefined,
|
|
@@ -1589,20 +1590,20 @@ export function webhooks(options: webhooks.Options = {}) {
|
|
|
1589
1590
|
message: 'Check the path parameters and try again.',
|
|
1590
1591
|
})
|
|
1591
1592
|
|
|
1592
|
-
const
|
|
1593
|
+
const db = Db.get(c.get('db'))
|
|
1593
1594
|
const owner = ownerFor(c)
|
|
1594
1595
|
if (!owner) return unauthorized(c)
|
|
1595
1596
|
|
|
1596
1597
|
const { id } = c.req.valid('param')
|
|
1597
1598
|
try {
|
|
1598
1599
|
// Owner-scope first so unowned/unknown ids are indistinguishable (404).
|
|
1599
|
-
const subscription = await Webhooks.getSubscription(
|
|
1600
|
+
const subscription = await Webhooks.getSubscription(db, owner, id)
|
|
1600
1601
|
if (!subscription) return notFound(c)
|
|
1601
1602
|
|
|
1602
1603
|
// Inline delivery (not the queue) so the caller gets an immediate
|
|
1603
1604
|
// result. `ping` never mutates the subscription's failure/lifecycle
|
|
1604
1605
|
// state — a test must not disable a healthy endpoint.
|
|
1605
|
-
const { envelope, result } = await Webhooks.ping(
|
|
1606
|
+
const { envelope, result } = await Webhooks.ping(db, subscription)
|
|
1606
1607
|
return c.json(
|
|
1607
1608
|
Response.validated(schema.pingWebhook.Response, {
|
|
1608
1609
|
delivered: result.ok,
|
|
@@ -1653,17 +1654,17 @@ export function webhooks(options: webhooks.Options = {}) {
|
|
|
1653
1654
|
message: 'Check the path parameters and try again.',
|
|
1654
1655
|
})
|
|
1655
1656
|
|
|
1656
|
-
const
|
|
1657
|
+
const db = Db.get(c.get('db'))
|
|
1657
1658
|
const owner = ownerFor(c)
|
|
1658
1659
|
if (!owner) return unauthorized(c)
|
|
1659
1660
|
|
|
1660
1661
|
const { deliveryId, id } = c.req.valid('param')
|
|
1661
1662
|
try {
|
|
1662
1663
|
// Owner-scope the subscription first so unowned ids 404 like elsewhere.
|
|
1663
|
-
const subscription = await Webhooks.getSubscription(
|
|
1664
|
+
const subscription = await Webhooks.getSubscription(db, owner, id)
|
|
1664
1665
|
if (!subscription) return notFound(c)
|
|
1665
1666
|
|
|
1666
|
-
const delivery = await Webhooks.getDelivery(
|
|
1667
|
+
const delivery = await Webhooks.getDelivery(db, id, deliveryId)
|
|
1667
1668
|
if (!delivery)
|
|
1668
1669
|
return Response.error(c, {
|
|
1669
1670
|
code: 'delivery_not_found',
|
|
@@ -1673,7 +1674,7 @@ export function webhooks(options: webhooks.Options = {}) {
|
|
|
1673
1674
|
|
|
1674
1675
|
// Replay the exact persisted envelope inline. Unlike `ping`, this is a
|
|
1675
1676
|
// real event, so the outcome updates the subscription's failure state.
|
|
1676
|
-
const result = await Webhooks.deliverAndRecord(
|
|
1677
|
+
const result = await Webhooks.deliverAndRecord(db, subscription, delivery.envelope)
|
|
1677
1678
|
return c.json(
|
|
1678
1679
|
Response.validated(schema.retryWebhookDelivery.Response, {
|
|
1679
1680
|
delivered: result.ok,
|
|
@@ -1733,7 +1734,7 @@ export function webhooks(options: webhooks.Options = {}) {
|
|
|
1733
1734
|
message: 'Check the request body and try again.',
|
|
1734
1735
|
})
|
|
1735
1736
|
|
|
1736
|
-
const
|
|
1737
|
+
const db = Db.get(c.get('db'))
|
|
1737
1738
|
const owner = ownerFor(c)
|
|
1738
1739
|
if (!owner) return unauthorized(c)
|
|
1739
1740
|
|
|
@@ -1746,11 +1747,11 @@ export function webhooks(options: webhooks.Options = {}) {
|
|
|
1746
1747
|
// persisting — otherwise a bad filter would silently widen the match
|
|
1747
1748
|
// set at scan time.
|
|
1748
1749
|
if (patch.filters !== undefined) {
|
|
1749
|
-
const existing = await Webhooks.getSubscription(
|
|
1750
|
+
const existing = await Webhooks.getSubscription(db, owner, id)
|
|
1750
1751
|
if (!existing) return notFound(c)
|
|
1751
1752
|
parseFilters(existing.eventType, patch.filters)
|
|
1752
1753
|
}
|
|
1753
|
-
const subscription = await Webhooks.updateSubscription(
|
|
1754
|
+
const subscription = await Webhooks.updateSubscription(db, owner, id, patch)
|
|
1754
1755
|
if (!subscription) return notFound(c)
|
|
1755
1756
|
return c.json(
|
|
1756
1757
|
Response.validated(schema.updateWebhook.Response, toPublic(subscription)),
|
|
@@ -1796,13 +1797,13 @@ export function webhooks(options: webhooks.Options = {}) {
|
|
|
1796
1797
|
message: 'Check the path parameters and try again.',
|
|
1797
1798
|
})
|
|
1798
1799
|
|
|
1799
|
-
const
|
|
1800
|
+
const db = Db.get(c.get('db'))
|
|
1800
1801
|
const owner = ownerFor(c)
|
|
1801
1802
|
if (!owner) return unauthorized(c)
|
|
1802
1803
|
|
|
1803
1804
|
const { id } = c.req.valid('param')
|
|
1804
1805
|
try {
|
|
1805
|
-
const deleted = await Webhooks.deleteSubscription(
|
|
1806
|
+
const deleted = await Webhooks.deleteSubscription(db, owner, id)
|
|
1806
1807
|
if (!deleted) return notFound(c)
|
|
1807
1808
|
return c.json(Response.validated(schema.deleteWebhook.Response, { id }), 200)
|
|
1808
1809
|
} catch (cause) {
|
|
@@ -1874,12 +1875,13 @@ type ScanPage = {
|
|
|
1874
1875
|
export async function poll(options: poll.Options): Promise<poll.Result> {
|
|
1875
1876
|
const {
|
|
1876
1877
|
batchSize = 100,
|
|
1878
|
+
cache: cache_metadata = Store.memory(),
|
|
1877
1879
|
chainId,
|
|
1878
1880
|
client,
|
|
1879
1881
|
confirmations = 2,
|
|
1882
|
+
db,
|
|
1880
1883
|
eventTypes = ['token:transfer', 'transaction:included', 'log:emitted', 'block:created'],
|
|
1881
1884
|
maxBlocks = 1_000,
|
|
1882
|
-
store,
|
|
1883
1885
|
tidx,
|
|
1884
1886
|
} = options
|
|
1885
1887
|
const now = options.now ?? (() => new Date())
|
|
@@ -1888,7 +1890,7 @@ export async function poll(options: poll.Options): Promise<poll.Result> {
|
|
|
1888
1890
|
// Queue producer instead and the consumer runs `deliverAndRecord`.
|
|
1889
1891
|
const dispatch =
|
|
1890
1892
|
options.dispatch ??
|
|
1891
|
-
(({ envelope, subscription }) => Webhooks.deliverAndRecord(
|
|
1893
|
+
(({ envelope, subscription }) => Webhooks.deliverAndRecord(db, subscription, envelope))
|
|
1892
1894
|
|
|
1893
1895
|
// Head resolution is lazy and per-source. RPC types work back from the live
|
|
1894
1896
|
// RPC head; the legacy TIDX types from the indexed head (never ahead of the
|
|
@@ -1901,6 +1903,9 @@ export async function poll(options: poll.Options): Promise<poll.Result> {
|
|
|
1901
1903
|
(tidxHead ??= tidx ? await getIndexedHead(tidx, chainId) : await resolveRpcHead())
|
|
1902
1904
|
|
|
1903
1905
|
const rpcSafeBlock = Math.max(0, (await resolveRpcHead()) - confirmations)
|
|
1906
|
+
// Reclaim expired rows (MPP-owned subscriptions, aged delivery logs) once
|
|
1907
|
+
// per tick; reads filter expiry at query time, so this is hygiene only.
|
|
1908
|
+
await Webhooks.pruneExpired(db, { now }).catch(() => {})
|
|
1904
1909
|
// Per-tick lazy cache so multiple subscriptions of the same/related types
|
|
1905
1910
|
// share fetched blocks and receipts instead of refetching the same window.
|
|
1906
1911
|
const cache = createChainCache(client)
|
|
@@ -1916,19 +1921,15 @@ export async function poll(options: poll.Options): Promise<poll.Result> {
|
|
|
1916
1921
|
for (const eventType of eventTypes) {
|
|
1917
1922
|
const legacy = eventType === 'token:transfer'
|
|
1918
1923
|
const safeBlock = legacy ? Math.max(0, (await resolveTidxHead()) - confirmations) : rpcSafeBlock
|
|
1919
|
-
const subscriptions = await Webhooks.listActive(
|
|
1924
|
+
const subscriptions = await Webhooks.listActive(db, chainId, eventType)
|
|
1920
1925
|
for (const subscription of subscriptions) {
|
|
1921
1926
|
result.subscriptions++
|
|
1922
|
-
const cursor = await Webhooks.getCursor(
|
|
1927
|
+
const cursor = await Webhooks.getCursor(db, subscription.id)
|
|
1923
1928
|
if (cursor === null) {
|
|
1924
1929
|
// First time we've seen this subscription: anchor it at the head of the
|
|
1925
1930
|
// confirmed window so we never backfill from genesis. Events start
|
|
1926
1931
|
// flowing from the next tick onward.
|
|
1927
|
-
await Webhooks.setCursor(
|
|
1928
|
-
store,
|
|
1929
|
-
subscription.id,
|
|
1930
|
-
Cursor.encode([safeBlock, endOfBlockIndex]),
|
|
1931
|
-
)
|
|
1932
|
+
await Webhooks.setCursor(db, subscription.id, Cursor.encode([safeBlock, endOfBlockIndex]))
|
|
1932
1933
|
continue
|
|
1933
1934
|
}
|
|
1934
1935
|
const page = await scanSubscription({
|
|
@@ -1951,7 +1952,7 @@ export async function poll(options: poll.Options): Promise<poll.Result> {
|
|
|
1951
1952
|
// payloads) can render `amount` in token units. Non-fatal on failure.
|
|
1952
1953
|
const data =
|
|
1953
1954
|
eventType === 'token:transfer'
|
|
1954
|
-
? await withTokenMetadata(item.data, { chainId, client
|
|
1955
|
+
? await withTokenMetadata(item.data, { cache: cache_metadata, chainId, client })
|
|
1955
1956
|
: item.data
|
|
1956
1957
|
const envelope = Webhooks.buildEnvelope({
|
|
1957
1958
|
blockNumber: item.cursor[0],
|
|
@@ -1976,21 +1977,16 @@ export async function poll(options: poll.Options): Promise<poll.Result> {
|
|
|
1976
1977
|
// matches); keyset scanners advance past the last dispatched row, or — on
|
|
1977
1978
|
// a complete empty page — to the end of the confirmed window.
|
|
1978
1979
|
if (failed) {
|
|
1979
|
-
if (accepted) await Webhooks.setCursor(
|
|
1980
|
+
if (accepted) await Webhooks.setCursor(db, subscription.id, Cursor.encode([...accepted]))
|
|
1980
1981
|
} else if (page.scannedThrough !== undefined)
|
|
1981
1982
|
await Webhooks.setCursor(
|
|
1982
|
-
|
|
1983
|
+
db,
|
|
1983
1984
|
subscription.id,
|
|
1984
1985
|
Cursor.encode([page.scannedThrough, endOfBlockIndex]),
|
|
1985
1986
|
)
|
|
1986
|
-
else if (accepted)
|
|
1987
|
-
await Webhooks.setCursor(store, subscription.id, Cursor.encode([...accepted]))
|
|
1987
|
+
else if (accepted) await Webhooks.setCursor(db, subscription.id, Cursor.encode([...accepted]))
|
|
1988
1988
|
else if (!page.hasMore)
|
|
1989
|
-
await Webhooks.setCursor(
|
|
1990
|
-
store,
|
|
1991
|
-
subscription.id,
|
|
1992
|
-
Cursor.encode([safeBlock, endOfBlockIndex]),
|
|
1993
|
-
)
|
|
1989
|
+
await Webhooks.setCursor(db, subscription.id, Cursor.encode([safeBlock, endOfBlockIndex]))
|
|
1994
1990
|
}
|
|
1995
1991
|
}
|
|
1996
1992
|
|
|
@@ -2009,12 +2005,16 @@ export declare namespace poll {
|
|
|
2009
2005
|
type Options = {
|
|
2010
2006
|
/** Max matching rows scanned per subscription per tick (default 100). */
|
|
2011
2007
|
batchSize?: number | undefined
|
|
2008
|
+
/** Cache store for the token-metadata memoize (defaults to a fresh in-memory store; pass a persistent one to share lookups across ticks). */
|
|
2009
|
+
cache?: Store.Store | undefined
|
|
2012
2010
|
/** Chain to poll. Callers loop over chains. */
|
|
2013
2011
|
chainId: number
|
|
2014
2012
|
/** viem RPC client for the chain (used by the RPC monitoring scanners). */
|
|
2015
2013
|
client: Viem.getClient.ReturnType
|
|
2016
2014
|
/** Reorg-safety window: only emit blocks this far behind head (default 2). */
|
|
2017
2015
|
confirmations?: number | undefined
|
|
2016
|
+
/** Database holding subscriptions + cursors. */
|
|
2017
|
+
db: Db.Db
|
|
2018
2018
|
/**
|
|
2019
2019
|
* Delivery transport. Defaults to inline {@link Webhooks.deliverAndRecord} (self-host);
|
|
2020
2020
|
* on Cloudflare pass a Queue producer so retry/backoff/DLQ are native.
|
|
@@ -2028,8 +2028,6 @@ export declare namespace poll {
|
|
|
2028
2028
|
maxBlocks?: number | undefined
|
|
2029
2029
|
/** Clock for envelope timestamps; injectable for tests. */
|
|
2030
2030
|
now?: (() => Date) | undefined
|
|
2031
|
-
/** Webhook state store (subscriptions + cursors). */
|
|
2032
|
-
store: Store.State
|
|
2033
2031
|
/** TIDX query client for the legacy `token:transfer` type. */
|
|
2034
2032
|
tidx?: Tidx.Client | undefined
|
|
2035
2033
|
}
|
|
@@ -2059,9 +2057,9 @@ export declare namespace poll {
|
|
|
2059
2057
|
async function withTokenMetadata(
|
|
2060
2058
|
data: unknown,
|
|
2061
2059
|
deps: {
|
|
2060
|
+
cache: Store.Store
|
|
2062
2061
|
chainId: number
|
|
2063
2062
|
client: Viem.getClient.ReturnType
|
|
2064
|
-
store: Store.State
|
|
2065
2063
|
},
|
|
2066
2064
|
): Promise<unknown> {
|
|
2067
2065
|
if (typeof data !== 'object' || data === null) return data
|
|
@@ -2079,7 +2077,7 @@ async function withTokenMetadata(
|
|
|
2079
2077
|
*/
|
|
2080
2078
|
async function resolveTokenMetadata(
|
|
2081
2079
|
address: string,
|
|
2082
|
-
deps: { chainId: number; client: Viem.getClient.ReturnType
|
|
2080
|
+
deps: { cache: Store.Store; chainId: number; client: Viem.getClient.ReturnType },
|
|
2083
2081
|
): Promise<{ decimals: number; symbol: string } | undefined> {
|
|
2084
2082
|
try {
|
|
2085
2083
|
return await Store.memoize(
|
|
@@ -2089,7 +2087,7 @@ async function resolveTokenMetadata(
|
|
|
2089
2087
|
},
|
|
2090
2088
|
{
|
|
2091
2089
|
key: `webhook:token-meta:v1:${deps.chainId}:${address}`,
|
|
2092
|
-
store: deps.
|
|
2090
|
+
store: deps.cache,
|
|
2093
2091
|
ttl: Ttl.minutes(5),
|
|
2094
2092
|
},
|
|
2095
2093
|
)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { Cli } from '
|
|
2
|
-
|
|
3
|
-
import * as
|
|
1
|
+
import { Cli } from 'tapimo'
|
|
2
|
+
|
|
3
|
+
import * as TestApp from '../../test/App.js'
|
|
4
|
+
import * as Runtime from '../../test/runtime.js'
|
|
4
5
|
|
|
5
6
|
const apiUrl = 'https://api.tempo-api.test'
|
|
6
7
|
|
|
@@ -12,7 +13,7 @@ describe('from', () => {
|
|
|
12
13
|
const cli = Cli.from(apiUrl, { version: '1.2.3' })
|
|
13
14
|
await serve(cli, ['--help'])
|
|
14
15
|
|
|
15
|
-
expect(cli.name).toMatchInlineSnapshot(`"
|
|
16
|
+
expect(cli.name).toMatchInlineSnapshot(`"tapimo"`)
|
|
16
17
|
expect(cli.description).toMatchInlineSnapshot(`"Tempo API CLI"`)
|
|
17
18
|
} finally {
|
|
18
19
|
fetch.mockRestore()
|
|
@@ -20,11 +21,11 @@ describe('from', () => {
|
|
|
20
21
|
})
|
|
21
22
|
|
|
22
23
|
test('serves the hosted Tempo API command', async () => {
|
|
23
|
-
// Verified tokens are
|
|
24
|
-
//
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
)
|
|
24
|
+
// Verified tokens are database-backed (no bundled fallback), so seed the
|
|
25
|
+
// db for the default chain to exercise the `--verified` listing.
|
|
26
|
+
const db = TestApp.database()
|
|
27
|
+
await TestApp.verifiedSeed(db, Runtime.get().chainId)
|
|
28
|
+
const fetch = mockHostedApi(TestApp.create({ db }))
|
|
28
29
|
|
|
29
30
|
try {
|
|
30
31
|
const cli = Cli.from(apiUrl)
|
|
@@ -1,22 +1,29 @@
|
|
|
1
1
|
import { Cli as incur_Cli, Fetch, type Openapi } from 'incur'
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
import * as Admin from './commands/admin.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Creates an incur CLI backed by a hosted Tempo API, plus the operator
|
|
7
|
+
* commands (`<cli> admin …`).
|
|
8
|
+
*/
|
|
4
9
|
export function from(url: string | URL, options: from.Options = {}) {
|
|
5
10
|
const {
|
|
6
11
|
description = 'Tempo API CLI',
|
|
7
|
-
name = '
|
|
12
|
+
name = 'tapimo',
|
|
8
13
|
openapi = 'openapi.json',
|
|
9
14
|
request,
|
|
10
15
|
version = '0.0.0',
|
|
11
16
|
} = options
|
|
12
17
|
|
|
13
|
-
return incur_Cli
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
return incur_Cli
|
|
19
|
+
.create(name, {
|
|
20
|
+
description,
|
|
21
|
+
fetch: Fetch.fromRequest(url, request),
|
|
22
|
+
openapi,
|
|
23
|
+
openapiConfig: options.openapiConfig ?? { mode: 'namespace' },
|
|
24
|
+
version,
|
|
25
|
+
})
|
|
26
|
+
.command(Admin.admin)
|
|
20
27
|
}
|
|
21
28
|
|
|
22
29
|
export declare namespace from {
|
package/src/cli/bin.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { nanoid } from 'nanoid'
|
|
2
|
+
|
|
3
|
+
import * as TestCli from '../../../test/cli.js'
|
|
4
|
+
|
|
5
|
+
describe('admin', () => {
|
|
6
|
+
describe('migrate', () => {
|
|
7
|
+
test('behavior: applies migrations', async () => {
|
|
8
|
+
const { code, envelope } = await TestCli.run([
|
|
9
|
+
'admin',
|
|
10
|
+
'migrate',
|
|
11
|
+
'--db-schema',
|
|
12
|
+
`t_${nanoid()}`,
|
|
13
|
+
])
|
|
14
|
+
expect(code).toBe(0)
|
|
15
|
+
expect(envelope).toMatchInlineSnapshot(`
|
|
16
|
+
{
|
|
17
|
+
"data": {
|
|
18
|
+
"migrated": true,
|
|
19
|
+
},
|
|
20
|
+
"ok": true,
|
|
21
|
+
}
|
|
22
|
+
`)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
test('behavior: applies migrations from DATABASE_URL without a schema option', async () => {
|
|
26
|
+
const { code, envelope } = await TestCli.run(['admin', 'migrate'])
|
|
27
|
+
expect(code).toBe(0)
|
|
28
|
+
expect(envelope).toEqual({ data: { migrated: true }, ok: true })
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test('behavior: is idempotent across runs', async () => {
|
|
32
|
+
const schema = `t_${nanoid()}`
|
|
33
|
+
expect((await TestCli.run(['admin', 'migrate', '--db-schema', schema])).code).toBe(0)
|
|
34
|
+
const { code, envelope } = await TestCli.run(['admin', 'migrate', '--db-schema', schema])
|
|
35
|
+
expect(code).toBe(0)
|
|
36
|
+
expect(envelope).toEqual({ data: { migrated: true }, ok: true })
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
test('error: a missing connection string exits 1', async () => {
|
|
40
|
+
const { code, envelope } = await TestCli.run(['admin', 'migrate'], {
|
|
41
|
+
DATABASE_URL: undefined,
|
|
42
|
+
})
|
|
43
|
+
expect(code).toBe(1)
|
|
44
|
+
expect(envelope).toMatchInlineSnapshot(`
|
|
45
|
+
{
|
|
46
|
+
"error": {
|
|
47
|
+
"code": "missing_database_url",
|
|
48
|
+
"message": "Set --database-url or DATABASE_URL to the Postgres connection string.",
|
|
49
|
+
},
|
|
50
|
+
"ok": false,
|
|
51
|
+
}
|
|
52
|
+
`)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test('error: a migration failure exits 1', async () => {
|
|
56
|
+
const { code, envelope } = await TestCli.run(
|
|
57
|
+
[
|
|
58
|
+
'admin',
|
|
59
|
+
'migrate',
|
|
60
|
+
'--database-url',
|
|
61
|
+
'postgres://postgres:postgres@127.0.0.1:1/tempo-api',
|
|
62
|
+
'--db-schema',
|
|
63
|
+
`t_${nanoid()}`,
|
|
64
|
+
],
|
|
65
|
+
{ DATABASE_URL: undefined },
|
|
66
|
+
)
|
|
67
|
+
expect(code).toBe(1)
|
|
68
|
+
expect(envelope.error.code).toBe('migrate_failed')
|
|
69
|
+
})
|
|
70
|
+
})
|
|
71
|
+
})
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Cli as incur_Cli, z } from 'incur'
|
|
2
|
+
|
|
3
|
+
import * as Db from '../../db/Db.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Applies pending Postgres migrations, connecting directly via
|
|
7
|
+
* `--database-url` or `DATABASE_URL` (never through Hyperdrive — the migrate
|
|
8
|
+
* advisory lock needs session semantics); `--db-schema` scopes the session.
|
|
9
|
+
*/
|
|
10
|
+
const migrate = incur_Cli.create('migrate', {
|
|
11
|
+
description: 'Apply pending database migrations.',
|
|
12
|
+
env: z.object({
|
|
13
|
+
DATABASE_URL: z
|
|
14
|
+
.string()
|
|
15
|
+
.optional()
|
|
16
|
+
.describe('Postgres connection string. Used when --database-url is omitted.'),
|
|
17
|
+
}),
|
|
18
|
+
options: z.object({
|
|
19
|
+
databaseUrl: z
|
|
20
|
+
.string()
|
|
21
|
+
.optional()
|
|
22
|
+
.describe('Postgres connection string. Overrides DATABASE_URL.'),
|
|
23
|
+
dbSchema: z.string().optional().describe('Postgres schema to create and scope the session to.'),
|
|
24
|
+
}),
|
|
25
|
+
async run(c) {
|
|
26
|
+
const connectionString = c.options.databaseUrl ?? c.env.DATABASE_URL
|
|
27
|
+
if (!connectionString)
|
|
28
|
+
return c.error({
|
|
29
|
+
code: 'missing_database_url',
|
|
30
|
+
message: 'Set --database-url or DATABASE_URL to the Postgres connection string.',
|
|
31
|
+
})
|
|
32
|
+
const db = Db.postgres({
|
|
33
|
+
connectionString,
|
|
34
|
+
...(c.options.dbSchema ? { schema: c.options.dbSchema } : {}),
|
|
35
|
+
})
|
|
36
|
+
try {
|
|
37
|
+
await db.migrate()
|
|
38
|
+
return { migrated: true }
|
|
39
|
+
} catch (error) {
|
|
40
|
+
/* v8 ignore next */
|
|
41
|
+
const message = error instanceof Error ? error.message : 'Migration failed.'
|
|
42
|
+
return c.error({
|
|
43
|
+
code: 'migrate_failed',
|
|
44
|
+
message,
|
|
45
|
+
})
|
|
46
|
+
} finally {
|
|
47
|
+
await db.close()
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Operator commands that run against a deployment's infrastructure rather
|
|
54
|
+
* than the hosted API (database migrations, …).
|
|
55
|
+
*/
|
|
56
|
+
export const admin = incur_Cli
|
|
57
|
+
.create('admin', {
|
|
58
|
+
description: 'Operator commands for a Tempo API deployment.',
|
|
59
|
+
})
|
|
60
|
+
.command(migrate)
|
package/src/cloudflare.ts
CHANGED
|
@@ -52,4 +52,14 @@ export class DurableObject extends CloudflareDurableObject {
|
|
|
52
52
|
async put(key: string, value: string, options?: Store.Store.PutOptions) {
|
|
53
53
|
await this.#store.put(key, value, options)
|
|
54
54
|
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Atomically compares-and-swaps a value.
|
|
58
|
+
*
|
|
59
|
+
* The compare and write run on the object's single thread behind its input
|
|
60
|
+
* gate, so concurrent callers cannot interleave between them.
|
|
61
|
+
*/
|
|
62
|
+
async swap(key: string, expected: null | string, next: string, options?: Store.Store.PutOptions) {
|
|
63
|
+
return this.#store.swap!(key, expected, next, options)
|
|
64
|
+
}
|
|
55
65
|
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { nanoid } from 'nanoid'
|
|
2
|
+
|
|
3
|
+
import { postgresUrl } from '../../test/containers.js'
|
|
4
|
+
import * as Db from './Db.js'
|
|
5
|
+
import * as Organizations from './tables/organizations.js'
|
|
6
|
+
|
|
7
|
+
const create = () => Db.postgres({ connectionString: postgresUrl, schema: `t_${nanoid()}` })
|
|
8
|
+
|
|
9
|
+
describe('postgres', () => {
|
|
10
|
+
test('behavior: migrate is idempotent', async () => {
|
|
11
|
+
const db = create()
|
|
12
|
+
await db.migrate()
|
|
13
|
+
await db.migrate()
|
|
14
|
+
expect(await Organizations.get(db, 'missing')).toBeUndefined()
|
|
15
|
+
await db.close()
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
test('behavior: creates a Postgres db without schema scoping', async () => {
|
|
19
|
+
const db = Db.postgres({ connectionString: postgresUrl })
|
|
20
|
+
await db.close()
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
test('behavior: concurrent migrate tolerates the ledger race', async () => {
|
|
24
|
+
const schema = `t_${nanoid()}`
|
|
25
|
+
// Pre-create the schema so concurrent migrate races only on the
|
|
26
|
+
// `migrations` ledger, not `CREATE SCHEMA`.
|
|
27
|
+
const seed = Db.postgres({ connectionString: postgresUrl, schema })
|
|
28
|
+
await Organizations.get(seed, 'x').catch(() => {})
|
|
29
|
+
await seed.close()
|
|
30
|
+
|
|
31
|
+
// Several runners migrate the same fresh schema at once (mirrors parallel
|
|
32
|
+
// deploys). The session advisory lock serializes them; none throw.
|
|
33
|
+
const dbs = Array.from({ length: 5 }, () =>
|
|
34
|
+
Db.postgres({ connectionString: postgresUrl, schema }),
|
|
35
|
+
)
|
|
36
|
+
await Promise.all(dbs.map((db) => db.migrate()))
|
|
37
|
+
const created = await Organizations.create(dbs[0]!, { id: 'org_race', name: 'Race' })
|
|
38
|
+
expect(created.id).toBe('org_race')
|
|
39
|
+
await Promise.all(dbs.map((db) => db.close()))
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
test('error: rejects a modified applied migration', async () => {
|
|
43
|
+
const db = create()
|
|
44
|
+
await db.migrate()
|
|
45
|
+
await db.kysely
|
|
46
|
+
.updateTable('migrations')
|
|
47
|
+
.set({ checksum: '00000000' })
|
|
48
|
+
.where('name', '=', Db.migrations[0]!.name)
|
|
49
|
+
.execute()
|
|
50
|
+
await expect(db.migrate()).rejects.toThrow(/checksum mismatch/)
|
|
51
|
+
await db.close()
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test('behavior: transaction rolls back on throw', async () => {
|
|
55
|
+
const db = create()
|
|
56
|
+
await db.migrate()
|
|
57
|
+
await expect(
|
|
58
|
+
db.transaction(async (tx) => {
|
|
59
|
+
await Organizations.create(tx, { id: 'org_2', name: 'Roll' })
|
|
60
|
+
throw new Error('boom')
|
|
61
|
+
}),
|
|
62
|
+
).rejects.toThrow('boom')
|
|
63
|
+
expect(await Organizations.get(db, 'org_2')).toBeUndefined()
|
|
64
|
+
await db.close()
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
test('error: transaction rejects nesting', async () => {
|
|
68
|
+
const db = create()
|
|
69
|
+
await db.migrate()
|
|
70
|
+
await expect(db.transaction(() => db.transaction(async () => 1))).rejects.toThrow(/nesting/)
|
|
71
|
+
await db.close()
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test('behavior: transaction commits and returns the value', async () => {
|
|
75
|
+
const db = create()
|
|
76
|
+
await db.migrate()
|
|
77
|
+
const id = await db.transaction(
|
|
78
|
+
async (tx) => (await Organizations.create(tx, { id: 'org_3', name: 'Commit' })).id,
|
|
79
|
+
)
|
|
80
|
+
expect(id).toBe('org_3')
|
|
81
|
+
expect((await Organizations.get(db, 'org_3'))?.name).toBe('Commit')
|
|
82
|
+
await db.close()
|
|
83
|
+
})
|
|
84
|
+
})
|