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/db/Db.ts
ADDED
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CamelCasePlugin,
|
|
3
|
+
type Compilable,
|
|
4
|
+
CompiledQuery,
|
|
5
|
+
type Dialect,
|
|
6
|
+
Kysely,
|
|
7
|
+
PostgresDialect,
|
|
8
|
+
sql,
|
|
9
|
+
} from 'kysely'
|
|
10
|
+
import * as pg from 'pg'
|
|
11
|
+
|
|
12
|
+
import type * as Organizations from './tables/organizations.js'
|
|
13
|
+
import type * as VerifiedTokens from './tables/verifiedTokens.js'
|
|
14
|
+
import type * as WebhookDeliveries from './tables/webhookDeliveries.js'
|
|
15
|
+
import type * as WebhookSubscriptions from './tables/webhookSubscriptions.js'
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Kysely schema with camelCase columns. {@link CamelCasePlugin} maps table
|
|
19
|
+
* records, queries, and {@link migrations} DDL to snake_case storage columns.
|
|
20
|
+
*/
|
|
21
|
+
export type Database = {
|
|
22
|
+
migrations: {
|
|
23
|
+
appliedAt: string
|
|
24
|
+
checksum: string
|
|
25
|
+
name: string
|
|
26
|
+
/** SQL as applied; null on rows written before the column existed. */
|
|
27
|
+
sql: string | null
|
|
28
|
+
}
|
|
29
|
+
organizations: Organizations.Table
|
|
30
|
+
verified_token_lists: VerifiedTokens.ListTable
|
|
31
|
+
verified_tokens: VerifiedTokens.Table
|
|
32
|
+
webhook_deliveries: WebhookDeliveries.Table
|
|
33
|
+
webhook_subscriptions: WebhookSubscriptions.Table
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* A SQL-backed authoritative store: the underlying Kysely handle (table
|
|
38
|
+
* repository modules query through it), schema migrations, and transactions.
|
|
39
|
+
*/
|
|
40
|
+
export type Db = {
|
|
41
|
+
/** Closes the underlying connection/pool. */
|
|
42
|
+
close(): Promise<void>
|
|
43
|
+
/**
|
|
44
|
+
* Underlying Kysely handle. Transaction-scoped {@link Db} values carry the
|
|
45
|
+
* transaction handle, so repositories called with `tx` run inside it. */
|
|
46
|
+
kysely: Kysely<Database>
|
|
47
|
+
/** Applies any pending schema migrations; safe to call repeatedly and concurrently. */
|
|
48
|
+
migrate(): Promise<void>
|
|
49
|
+
/**
|
|
50
|
+
* Runs `fn` in a transaction — commits on return, rolls back on throw. `fn`
|
|
51
|
+
* receives a transaction-scoped {@link Db}; pass it (not the outer `Db`) to
|
|
52
|
+
* repositories so every query inside runs in the transaction.
|
|
53
|
+
*/
|
|
54
|
+
transaction<value>(fn: (tx: Db) => Promise<value>): Promise<value>
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* A {@link Db}, or a factory for per-request Workers/Hyperdrive connections.
|
|
59
|
+
* Node can pass a long-lived singleton. Resolve at the leaf with {@link get}.
|
|
60
|
+
*/
|
|
61
|
+
export type Source = Db | (() => Db)
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Resolves a {@link Source} to a {@link Db}. Call at the request leaf so
|
|
65
|
+
* factories create per-request instances; singletons resolve to themselves.
|
|
66
|
+
*
|
|
67
|
+
* @param source - The database or factory.
|
|
68
|
+
* @returns The resolved {@link Db}.
|
|
69
|
+
*/
|
|
70
|
+
export function get(source: Source): Db {
|
|
71
|
+
return typeof source === 'function' ? source() : source
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* One schema migration. `up` builds a single idempotent Kysely DDL statement;
|
|
76
|
+
* {@link Db.migrate} compiles it for the checksum, then applies it.
|
|
77
|
+
*/
|
|
78
|
+
type Migration = {
|
|
79
|
+
/** Unique, ordered migration name (e.g. `0001_organizations`). */
|
|
80
|
+
name: string
|
|
81
|
+
/**
|
|
82
|
+
* Builds the migration's DDL against `db`. Use `.ifNotExists()` so the
|
|
83
|
+
* statement is safe to re-run without a wrapping transaction. */
|
|
84
|
+
up: (db: Kysely<Database>) => Compilable & { execute(): Promise<unknown> }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Ordered schema migrations for {@link Db.migrate}. Each `up` is one
|
|
89
|
+
* idempotent statement; compiled-SQL FNV-1a checksums reject edited applied
|
|
90
|
+
* migrations.
|
|
91
|
+
*/
|
|
92
|
+
export const migrations: readonly Migration[] = [
|
|
93
|
+
{
|
|
94
|
+
name: '0001_organizations',
|
|
95
|
+
up: (db) =>
|
|
96
|
+
db.schema
|
|
97
|
+
.createTable('organizations')
|
|
98
|
+
.ifNotExists()
|
|
99
|
+
.addColumn('id', 'text', (c) => c.primaryKey())
|
|
100
|
+
.addColumn('name', 'text', (c) => c.notNull())
|
|
101
|
+
.addColumn('createdBy', 'text')
|
|
102
|
+
.addColumn('createdAt', 'text', (c) => c.notNull())
|
|
103
|
+
.addColumn('updatedAt', 'text', (c) => c.notNull()),
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
name: '0002_webhook_subscriptions',
|
|
107
|
+
up: (db) =>
|
|
108
|
+
db.schema
|
|
109
|
+
.createTable('webhookSubscriptions')
|
|
110
|
+
.ifNotExists()
|
|
111
|
+
.addColumn('id', 'text', (c) => c.primaryKey())
|
|
112
|
+
.addColumn('ownerType', 'text', (c) => c.notNull())
|
|
113
|
+
.addColumn('ownerId', 'text', (c) => c.notNull())
|
|
114
|
+
.addColumn('chainId', 'integer', (c) => c.notNull())
|
|
115
|
+
.addColumn('eventType', 'text', (c) => c.notNull())
|
|
116
|
+
.addColumn('status', 'text', (c) => c.notNull().defaultTo('active'))
|
|
117
|
+
.addColumn('destination', 'jsonb', (c) => c.notNull())
|
|
118
|
+
.addColumn('filters', 'jsonb', (c) => c.notNull().defaultTo(sql`'{}'::jsonb`))
|
|
119
|
+
.addColumn('context', 'jsonb')
|
|
120
|
+
.addColumn('secret', 'text', (c) => c.notNull())
|
|
121
|
+
.addColumn('failureCount', 'integer', (c) => c.notNull().defaultTo(0))
|
|
122
|
+
.addColumn('lastDeliveryAt', 'text')
|
|
123
|
+
.addColumn('pollerCursor', 'text')
|
|
124
|
+
.addColumn('expiresAt', 'text')
|
|
125
|
+
.addColumn('createdAt', 'text', (c) => c.notNull())
|
|
126
|
+
.addColumn('updatedAt', 'text', (c) => c.notNull()),
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
name: '0003_webhook_deliveries',
|
|
130
|
+
up: (db) =>
|
|
131
|
+
db.schema
|
|
132
|
+
.createTable('webhookDeliveries')
|
|
133
|
+
.ifNotExists()
|
|
134
|
+
.addColumn('id', 'text', (c) => c.primaryKey())
|
|
135
|
+
.addColumn('subscriptionId', 'text', (c) =>
|
|
136
|
+
c.notNull().references('webhookSubscriptions.id').onDelete('cascade'),
|
|
137
|
+
)
|
|
138
|
+
.addColumn('eventId', 'text', (c) => c.notNull())
|
|
139
|
+
.addColumn('attempt', 'integer', (c) => c.notNull())
|
|
140
|
+
.addColumn('status', 'text', (c) => c.notNull())
|
|
141
|
+
.addColumn('requestUrl', 'text', (c) => c.notNull())
|
|
142
|
+
.addColumn('responseStatus', 'integer')
|
|
143
|
+
.addColumn('responseMs', 'integer')
|
|
144
|
+
.addColumn('error', 'text')
|
|
145
|
+
.addColumn('envelope', 'jsonb', (c) => c.notNull())
|
|
146
|
+
.addColumn('createdAt', 'text', (c) => c.notNull())
|
|
147
|
+
.addColumn('expiresAt', 'text', (c) => c.notNull()),
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
name: '0004_verified_token_lists',
|
|
151
|
+
up: (db) =>
|
|
152
|
+
db.schema
|
|
153
|
+
.createTable('verifiedTokenLists')
|
|
154
|
+
.ifNotExists()
|
|
155
|
+
.addColumn('chainId', 'integer', (c) => c.primaryKey())
|
|
156
|
+
.addColumn('version', 'text', (c) => c.notNull())
|
|
157
|
+
.addColumn('updatedAt', 'text', (c) => c.notNull()),
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
name: '0005_verified_tokens',
|
|
161
|
+
up: (db) =>
|
|
162
|
+
db.schema
|
|
163
|
+
.createTable('verifiedTokens')
|
|
164
|
+
.ifNotExists()
|
|
165
|
+
.addColumn('chainId', 'integer', (c) => c.notNull())
|
|
166
|
+
.addColumn('address', 'text', (c) => c.notNull())
|
|
167
|
+
.addColumn('symbol', 'text', (c) => c.notNull())
|
|
168
|
+
.addColumn('name', 'text', (c) => c.notNull())
|
|
169
|
+
.addColumn('currency', 'text', (c) => c.notNull())
|
|
170
|
+
.addColumn('decimals', 'integer', (c) => c.notNull())
|
|
171
|
+
.addColumn('logoUri', 'text')
|
|
172
|
+
.addColumn('position', 'integer', (c) => c.notNull())
|
|
173
|
+
.addPrimaryKeyConstraint('verified_tokens_pkey', ['chainId', 'address']),
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
// Case-insensitive symbol uniqueness per chain (`GET /tokens/:symbol`
|
|
177
|
+
// would otherwise be ambiguous).
|
|
178
|
+
name: '0006_verified_tokens_symbol_index',
|
|
179
|
+
up: (db) =>
|
|
180
|
+
db.schema
|
|
181
|
+
.createIndex('verified_tokens_symbol_idx')
|
|
182
|
+
.ifNotExists()
|
|
183
|
+
.unique()
|
|
184
|
+
.on('verifiedTokens')
|
|
185
|
+
.expression(sql`chain_id, lower(symbol)`),
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
// Owner list/count/cap scans and `id <` keyset paging.
|
|
189
|
+
name: '0007_webhook_subscriptions_owner_index',
|
|
190
|
+
up: (db) =>
|
|
191
|
+
db.schema
|
|
192
|
+
.createIndex('webhook_subscriptions_owner_idx')
|
|
193
|
+
.ifNotExists()
|
|
194
|
+
.on('webhookSubscriptions')
|
|
195
|
+
.columns(['ownerType', 'ownerId', 'id']),
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
// Partial index over active subscriptions: the poller's per-stream lookup.
|
|
199
|
+
// `sql.lit` keeps the predicate literal — DDL cannot carry parameters.
|
|
200
|
+
name: '0008_webhook_subscriptions_active_index',
|
|
201
|
+
up: (db) =>
|
|
202
|
+
db.schema
|
|
203
|
+
.createIndex('webhook_subscriptions_active_idx')
|
|
204
|
+
.ifNotExists()
|
|
205
|
+
.on('webhookSubscriptions')
|
|
206
|
+
.columns(['chainId', 'eventType'])
|
|
207
|
+
.where(sql.ref('status'), '=', sql.lit('active')),
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
name: '0009_webhook_subscriptions_expires_index',
|
|
211
|
+
up: (db) =>
|
|
212
|
+
db.schema
|
|
213
|
+
.createIndex('webhook_subscriptions_expires_idx')
|
|
214
|
+
.ifNotExists()
|
|
215
|
+
.on('webhookSubscriptions')
|
|
216
|
+
.column('expiresAt')
|
|
217
|
+
.where('expiresAt', 'is not', null),
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
// Per-subscription delivery-log paging.
|
|
221
|
+
name: '0010_webhook_deliveries_subscription_index',
|
|
222
|
+
up: (db) =>
|
|
223
|
+
db.schema
|
|
224
|
+
.createIndex('webhook_deliveries_subscription_idx')
|
|
225
|
+
.ifNotExists()
|
|
226
|
+
.on('webhookDeliveries')
|
|
227
|
+
.columns(['subscriptionId', 'id']),
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
name: '0011_webhook_deliveries_expires_index',
|
|
231
|
+
up: (db) =>
|
|
232
|
+
db.schema
|
|
233
|
+
.createIndex('webhook_deliveries_expires_idx')
|
|
234
|
+
.ifNotExists()
|
|
235
|
+
.on('webhookDeliveries')
|
|
236
|
+
.column('expiresAt'),
|
|
237
|
+
},
|
|
238
|
+
]
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Builds a {@link Db} for any Kysely {@link Dialect}. All backends share
|
|
242
|
+
* migrations, repositories, and the required {@link CamelCasePlugin} mapping.
|
|
243
|
+
*
|
|
244
|
+
* @param dialect - The Kysely dialect for the target backend.
|
|
245
|
+
* @returns The database.
|
|
246
|
+
*/
|
|
247
|
+
export function from(dialect: Dialect): Db {
|
|
248
|
+
// Builds Db over a top-level Kysely handle or transaction-scoped Kysely handle.
|
|
249
|
+
function build(kysely: Kysely<Database>): Db {
|
|
250
|
+
// Prevents same-handle nested transactions from deadlocking capped pools.
|
|
251
|
+
// Nested work must use the callback's scoped `tx`.
|
|
252
|
+
let active = false
|
|
253
|
+
return {
|
|
254
|
+
async close() {
|
|
255
|
+
await kysely.destroy()
|
|
256
|
+
},
|
|
257
|
+
kysely,
|
|
258
|
+
async migrate() {
|
|
259
|
+
// A session advisory lock serializes the full run, including bootstrap
|
|
260
|
+
// DDL and ledger inserts. Pin one connection and unlock in `finally`.
|
|
261
|
+
await kysely.connection().execute(async (db) => {
|
|
262
|
+
await sql`SELECT pg_advisory_lock(${migrateLockKey})`.execute(db)
|
|
263
|
+
try {
|
|
264
|
+
await sql`CREATE TABLE IF NOT EXISTS migrations (name TEXT PRIMARY KEY, checksum TEXT NOT NULL, applied_at TEXT NOT NULL)`.execute(
|
|
265
|
+
db,
|
|
266
|
+
)
|
|
267
|
+
await sql`ALTER TABLE migrations ADD COLUMN IF NOT EXISTS sql TEXT`.execute(db)
|
|
268
|
+
const applied = new Map(
|
|
269
|
+
(await db.selectFrom('migrations').select(['name', 'checksum', 'sql']).execute()).map(
|
|
270
|
+
(row) => [row.name, row] as const,
|
|
271
|
+
),
|
|
272
|
+
)
|
|
273
|
+
for (const migration of migrations) {
|
|
274
|
+
// Build once: compile (no I/O) for the checksum, execute to apply.
|
|
275
|
+
const statement = migration.up(db)
|
|
276
|
+
const compiled = statement.compile().sql
|
|
277
|
+
const digest = checksum(compiled)
|
|
278
|
+
const existing = applied.get(migration.name)
|
|
279
|
+
if (existing !== undefined) {
|
|
280
|
+
// The digest hashes kysely's rendered SQL, so a kysely upgrade
|
|
281
|
+
// can flip it with unchanged sources. The stored `sql` is what
|
|
282
|
+
// actually ran — diff it against the new render to tell render
|
|
283
|
+
// drift from a real edit (re-baseline procedure in AGENTS.md).
|
|
284
|
+
if (existing.checksum !== digest)
|
|
285
|
+
throw new Error(
|
|
286
|
+
`Migration "${migration.name}" was modified after being applied (checksum mismatch). Diff the migrations table's stored sql against the current render before re-baselining.`,
|
|
287
|
+
)
|
|
288
|
+
// Backfill rows from before the column existed; the matching
|
|
289
|
+
// checksum proves the current render is what was applied.
|
|
290
|
+
if (existing.sql === null)
|
|
291
|
+
await db
|
|
292
|
+
.updateTable('migrations')
|
|
293
|
+
.set({ sql: compiled })
|
|
294
|
+
.where('name', '=', migration.name)
|
|
295
|
+
.execute()
|
|
296
|
+
continue
|
|
297
|
+
}
|
|
298
|
+
await statement.execute()
|
|
299
|
+
await db
|
|
300
|
+
.insertInto('migrations')
|
|
301
|
+
.values({
|
|
302
|
+
appliedAt: new Date().toISOString(),
|
|
303
|
+
checksum: digest,
|
|
304
|
+
name: migration.name,
|
|
305
|
+
sql: compiled,
|
|
306
|
+
})
|
|
307
|
+
.execute()
|
|
308
|
+
}
|
|
309
|
+
} finally {
|
|
310
|
+
await sql`SELECT pg_advisory_unlock(${migrateLockKey})`.execute(db)
|
|
311
|
+
}
|
|
312
|
+
})
|
|
313
|
+
},
|
|
314
|
+
async transaction(fn) {
|
|
315
|
+
if (active) throw new Error('Db.transaction does not support nesting.')
|
|
316
|
+
active = true
|
|
317
|
+
try {
|
|
318
|
+
return await kysely.transaction().execute((trx) => fn(build(trx)))
|
|
319
|
+
} finally {
|
|
320
|
+
active = false
|
|
321
|
+
}
|
|
322
|
+
},
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return build(new Kysely<Database>({ dialect, plugins: [new CamelCasePlugin()] }))
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Creates a Postgres-backed {@link Db} via Kysely's `PostgresDialect`. Optional
|
|
330
|
+
* `schema` scopes every pooled connection and caps the pool for test isolation.
|
|
331
|
+
*
|
|
332
|
+
* @param options - Postgres options.
|
|
333
|
+
* @returns The database.
|
|
334
|
+
*/
|
|
335
|
+
export function postgres(options: postgres.Options): Db {
|
|
336
|
+
const { connectionString, schema } = options
|
|
337
|
+
const pool = new pg.Pool(schema ? { connectionString, max: 1 } : { connectionString })
|
|
338
|
+
return from(
|
|
339
|
+
new PostgresDialect({
|
|
340
|
+
pool,
|
|
341
|
+
...(schema
|
|
342
|
+
? {
|
|
343
|
+
onCreateConnection: async (connection) => {
|
|
344
|
+
await connection.executeQuery(
|
|
345
|
+
CompiledQuery.raw(`CREATE SCHEMA IF NOT EXISTS "${schema}"`),
|
|
346
|
+
)
|
|
347
|
+
await connection.executeQuery(CompiledQuery.raw(`SET search_path TO "${schema}"`))
|
|
348
|
+
},
|
|
349
|
+
}
|
|
350
|
+
: {}),
|
|
351
|
+
}),
|
|
352
|
+
)
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export declare namespace postgres {
|
|
356
|
+
/** Options for {@link postgres}. */
|
|
357
|
+
type Options = {
|
|
358
|
+
/** Postgres connection string (`postgresql://…`). */
|
|
359
|
+
connectionString: string
|
|
360
|
+
/** Schema to create and scope the session to (isolates concurrent tests). */
|
|
361
|
+
schema?: string | undefined
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Fixed key for the {@link Db.migrate} Postgres session advisory lock: the
|
|
367
|
+
* ASCII bytes of `"tapi"`, making all migrators serialize together.
|
|
368
|
+
*/
|
|
369
|
+
const migrateLockKey = 0x74617069
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Deterministic FNV-1a hash of migration SQL, so an applied migration whose
|
|
373
|
+
* source later changes is rejected (instead of silently diverging). Not
|
|
374
|
+
* cryptographic — only needs collision resistance for typos.
|
|
375
|
+
*/
|
|
376
|
+
function checksum(sql: string): string {
|
|
377
|
+
let hash = 0x811c9dc5
|
|
378
|
+
for (let i = 0; i < sql.length; i++) {
|
|
379
|
+
hash ^= sql.charCodeAt(i)
|
|
380
|
+
hash = Math.imul(hash, 0x01000193)
|
|
381
|
+
}
|
|
382
|
+
return (hash >>> 0).toString(16).padStart(8, '0')
|
|
383
|
+
}
|
package/src/db/Schema.ts
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import * as z from 'zod/mini'
|
|
2
|
+
|
|
3
|
+
import type * as Webhooks from '../internal/Webhooks.js'
|
|
4
|
+
|
|
5
|
+
// Row schemas for database tables (`zod/mini`), the source of truth each
|
|
6
|
+
// `tables/` module derives its `Table` type from. Nullable fields map to
|
|
7
|
+
// nullable columns; the domain layer converts `null` back to absent fields.
|
|
8
|
+
// Domain-owned unions and jsonb values stay typed by their owning module via
|
|
9
|
+
// `z.custom` (type-only imports) so the two cannot drift.
|
|
10
|
+
|
|
11
|
+
/** An organization, the top-level tenant that owns API keys. */
|
|
12
|
+
export const Organization = z.object({
|
|
13
|
+
createdAt: z.string().check(z.describe('When the organization was created (ISO 8601).')),
|
|
14
|
+
createdBy: z
|
|
15
|
+
.nullable(z.string())
|
|
16
|
+
.check(z.describe('Identity that created the organization (e.g. admin email), or null.')),
|
|
17
|
+
id: z
|
|
18
|
+
.string()
|
|
19
|
+
.check(
|
|
20
|
+
z.describe('Opaque organization id (`org_…`).'),
|
|
21
|
+
z.meta({ examples: ['org_1a2b3c4d5e6f7g8h9j0k1m2n'] }),
|
|
22
|
+
),
|
|
23
|
+
name: z
|
|
24
|
+
.string()
|
|
25
|
+
.check(z.describe('Human-readable organization name.'), z.meta({ examples: ['Acme, Inc.'] })),
|
|
26
|
+
updatedAt: z.string().check(z.describe('When the organization was last updated (ISO 8601).')),
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
/** Columns of the `organizations` table. */
|
|
30
|
+
export type Organization = z.output<typeof Organization>
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* A verified token: one row per curated token, ordered by `position` (list
|
|
34
|
+
* order is canonical and surfaced to clients). Addresses are stored lowercase;
|
|
35
|
+
* case-insensitive symbol uniqueness is enforced by a functional index on
|
|
36
|
+
* `lower(symbol)`.
|
|
37
|
+
*/
|
|
38
|
+
export const VerifiedToken = z.object({
|
|
39
|
+
address: z.string().check(z.describe('Verified TIP-20 token contract address (lowercase).')),
|
|
40
|
+
chainId: z.number().check(z.int(), z.describe('Chain the token belongs to.')),
|
|
41
|
+
currency: z.string().check(z.describe('Display currency, e.g. `USD`.')),
|
|
42
|
+
decimals: z.number().check(z.int(), z.describe('Decimal precision.')),
|
|
43
|
+
logoUri: z.nullable(z.string()).check(z.describe('Curated HTTPS logo URL, or null.')),
|
|
44
|
+
name: z.string().check(z.describe('Display name.')),
|
|
45
|
+
position: z
|
|
46
|
+
.number()
|
|
47
|
+
.check(z.int(), z.describe("Zero-based position in the chain's canonical list order.")),
|
|
48
|
+
symbol: z.string().check(z.describe('Ticker symbol.'), z.meta({ examples: ['USDC'] })),
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
/** Columns of the `verified_tokens` table. */
|
|
52
|
+
export type VerifiedToken = z.output<typeof VerifiedToken>
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* A chain's verified-token list metadata — the opaque `version` that drives
|
|
56
|
+
* the soft-refresh cache and `If-Match` preconditions, bumped on every publish.
|
|
57
|
+
*/
|
|
58
|
+
export const VerifiedTokenList = z.object({
|
|
59
|
+
chainId: z.number().check(z.int(), z.describe('Chain the list belongs to.')),
|
|
60
|
+
updatedAt: z.string().check(z.describe('ISO timestamp of the last publish.')),
|
|
61
|
+
version: z
|
|
62
|
+
.string()
|
|
63
|
+
.check(z.describe('Opaque list version (`<epoch-ms>_<hex>`); changes on every publish.')),
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
/** Columns of the `verified_token_lists` table. */
|
|
67
|
+
export type VerifiedTokenList = z.output<typeof VerifiedTokenList>
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* A webhook subscription. The `Webhooks.Owner` union maps to (`ownerType`,
|
|
71
|
+
* `ownerId`); jsonb object columns round-trip as parsed values. `pollerCursor`
|
|
72
|
+
* replaces the old per-subscription cursor key.
|
|
73
|
+
*/
|
|
74
|
+
export const WebhookSubscription = z.object({
|
|
75
|
+
chainId: z.number().check(z.int(), z.describe('Chain the subscription listens on.')),
|
|
76
|
+
context: z
|
|
77
|
+
.nullable(z.custom<Webhooks.Context>())
|
|
78
|
+
.check(z.describe('Human context (`jsonb`), or null.')),
|
|
79
|
+
createdAt: z.string().check(z.describe('ISO timestamp of creation.')),
|
|
80
|
+
destination: z
|
|
81
|
+
.custom<Webhooks.Destination>()
|
|
82
|
+
.check(z.describe('Delivery destination (`jsonb`).')),
|
|
83
|
+
eventType: z
|
|
84
|
+
.custom<Webhooks.EventType>()
|
|
85
|
+
.check(z.describe('Event type the subscription listens to.')),
|
|
86
|
+
expiresAt: z
|
|
87
|
+
.nullable(z.string())
|
|
88
|
+
.check(z.describe('ISO expiry timestamp (MPP-owned subscriptions), or null.')),
|
|
89
|
+
failureCount: z
|
|
90
|
+
.number()
|
|
91
|
+
.check(z.int(), z.describe('Consecutive delivery failures; drives auto-disable.')),
|
|
92
|
+
filters: z
|
|
93
|
+
.custom<Webhooks.Subscription['filters']>()
|
|
94
|
+
.check(z.describe('Event-type-specific filter predicates (`jsonb`).')),
|
|
95
|
+
id: z
|
|
96
|
+
.string()
|
|
97
|
+
.check(
|
|
98
|
+
z.describe('Subscription id (`wh_…`, lexically time-ordered).'),
|
|
99
|
+
z.meta({ examples: ['wh_0000019c1a2b3c4d5e6f7g8h'] }),
|
|
100
|
+
),
|
|
101
|
+
lastDeliveryAt: z
|
|
102
|
+
.nullable(z.string())
|
|
103
|
+
.check(z.describe('ISO timestamp of the last successful delivery, or null.')),
|
|
104
|
+
ownerId: z
|
|
105
|
+
.string()
|
|
106
|
+
.check(z.describe('Owner identifier: `orgId` for `api_key` owners, `payer` for `mpp`.')),
|
|
107
|
+
ownerType: z.custom<Webhooks.Owner['type']>().check(z.describe('Owner kind.')),
|
|
108
|
+
pollerCursor: z
|
|
109
|
+
.nullable(z.string())
|
|
110
|
+
.check(z.describe('Poller keyset cursor, or null before the first tick.')),
|
|
111
|
+
secret: z.string().check(z.describe('HMAC signing secret.')),
|
|
112
|
+
status: z.custom<Webhooks.Status>().check(z.describe('Lifecycle status.')),
|
|
113
|
+
updatedAt: z
|
|
114
|
+
.string()
|
|
115
|
+
.check(z.describe('ISO timestamp of the last mutation (`pollerCursor` writes excluded).')),
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
/** Columns of the `webhook_subscriptions` table. */
|
|
119
|
+
export type WebhookSubscription = z.output<typeof WebhookSubscription>
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* A webhook delivery attempt. Rows are invisible past `expiresAt` (the
|
|
123
|
+
* retention deadline that replaces the old store TTL) and pruned by the
|
|
124
|
+
* poller; deleting a subscription cascades to its rows.
|
|
125
|
+
*/
|
|
126
|
+
export const WebhookDelivery = z.object({
|
|
127
|
+
attempt: z
|
|
128
|
+
.number()
|
|
129
|
+
.check(
|
|
130
|
+
z.int(),
|
|
131
|
+
z.describe('Consecutive attempt number (1-based) at the time of the delivery.'),
|
|
132
|
+
),
|
|
133
|
+
createdAt: z.string().check(z.describe('ISO timestamp the attempt was recorded.')),
|
|
134
|
+
envelope: z
|
|
135
|
+
.custom<Webhooks.Envelope>()
|
|
136
|
+
.check(z.describe('The exact envelope that was sent (`jsonb`), for verbatim replay.')),
|
|
137
|
+
error: z
|
|
138
|
+
.nullable(z.string())
|
|
139
|
+
.check(z.describe('Failure reason when `status` is `failed`, or null.')),
|
|
140
|
+
eventId: z.string().check(z.describe('Idempotent event id the delivery carried (`evt_…`).')),
|
|
141
|
+
expiresAt: z
|
|
142
|
+
.string()
|
|
143
|
+
.check(z.describe('ISO retention deadline; rows past it are invisible and pruned.')),
|
|
144
|
+
id: z
|
|
145
|
+
.string()
|
|
146
|
+
.check(
|
|
147
|
+
z.describe('Delivery id (`whd_…`, lexically time-ordered).'),
|
|
148
|
+
z.meta({ examples: ['whd_0000019c1a2b3c4d5e6f7g8h'] }),
|
|
149
|
+
),
|
|
150
|
+
requestUrl: z.string().check(z.describe('Subscriber callback URL the attempt targeted.')),
|
|
151
|
+
responseMs: z
|
|
152
|
+
.nullable(z.number())
|
|
153
|
+
.check(z.describe('Wall-clock duration of the attempt in ms, or null.')),
|
|
154
|
+
responseStatus: z
|
|
155
|
+
.nullable(z.number())
|
|
156
|
+
.check(z.describe('HTTP response status, or null when no response was received.')),
|
|
157
|
+
status: z.custom<Webhooks.DeliveryStatus>().check(z.describe('Outcome status.')),
|
|
158
|
+
subscriptionId: z
|
|
159
|
+
.string()
|
|
160
|
+
.check(z.describe('Owning subscription id (`wh_…`); FK with `ON DELETE CASCADE`.')),
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
/** Columns of the `webhook_deliveries` table. */
|
|
164
|
+
export type WebhookDelivery = z.output<typeof WebhookDelivery>
|
|
@@ -0,0 +1,124 @@
|
|
|
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 './organizations.js'
|
|
6
|
+
|
|
7
|
+
const create = () => Db.postgres({ connectionString: postgresUrl, schema: `t_${nanoid()}` })
|
|
8
|
+
|
|
9
|
+
/** Redacts nondeterministic row fields (generated nanoid ids, timestamps). */
|
|
10
|
+
function redact(record: Organizations.Record) {
|
|
11
|
+
return {
|
|
12
|
+
...record,
|
|
13
|
+
createdAt: '<timestamp>',
|
|
14
|
+
id: /^org_[\w-]{21}$/.test(record.id) ? 'org_<id>' : record.id,
|
|
15
|
+
updatedAt: '<timestamp>',
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe('create', () => {
|
|
20
|
+
test('behavior: inserts with a generated id and defaults', async () => {
|
|
21
|
+
const db = create()
|
|
22
|
+
await db.migrate()
|
|
23
|
+
const record = await Organizations.create(db, { name: 'Acme, Inc.' })
|
|
24
|
+
expect(record.id).toMatch(/^org_/)
|
|
25
|
+
expect(redact(record)).toMatchInlineSnapshot(`
|
|
26
|
+
{
|
|
27
|
+
"createdAt": "<timestamp>",
|
|
28
|
+
"createdBy": null,
|
|
29
|
+
"id": "org_<id>",
|
|
30
|
+
"name": "Acme, Inc.",
|
|
31
|
+
"updatedAt": "<timestamp>",
|
|
32
|
+
}
|
|
33
|
+
`)
|
|
34
|
+
await db.close()
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test('behavior: preserves an explicit id and createdBy', async () => {
|
|
38
|
+
const db = create()
|
|
39
|
+
await db.migrate()
|
|
40
|
+
const record = await Organizations.create(db, {
|
|
41
|
+
createdBy: 'admin@tempo.xyz',
|
|
42
|
+
id: 'org_explicit',
|
|
43
|
+
name: 'Explicit',
|
|
44
|
+
})
|
|
45
|
+
expect(redact(record)).toMatchInlineSnapshot(`
|
|
46
|
+
{
|
|
47
|
+
"createdAt": "<timestamp>",
|
|
48
|
+
"createdBy": "admin@tempo.xyz",
|
|
49
|
+
"id": "org_explicit",
|
|
50
|
+
"name": "Explicit",
|
|
51
|
+
"updatedAt": "<timestamp>",
|
|
52
|
+
}
|
|
53
|
+
`)
|
|
54
|
+
await db.close()
|
|
55
|
+
})
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
describe('get', () => {
|
|
59
|
+
test('behavior: returns undefined when absent', async () => {
|
|
60
|
+
const db = create()
|
|
61
|
+
await db.migrate()
|
|
62
|
+
expect(await Organizations.get(db, 'org_missing')).toBeUndefined()
|
|
63
|
+
await db.close()
|
|
64
|
+
})
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
describe('list', () => {
|
|
68
|
+
test('behavior: lists newest first', async () => {
|
|
69
|
+
const db = create()
|
|
70
|
+
await db.migrate()
|
|
71
|
+
await Organizations.create(db, { id: 'org_a', name: 'A' })
|
|
72
|
+
// Distinct createdAt for a deterministic order.
|
|
73
|
+
await new Promise((resolve) => setTimeout(resolve, 5))
|
|
74
|
+
await Organizations.create(db, { id: 'org_b', name: 'B' })
|
|
75
|
+
expect((await Organizations.list(db)).map((record) => record.id)).toMatchInlineSnapshot(`
|
|
76
|
+
[
|
|
77
|
+
"org_b",
|
|
78
|
+
"org_a",
|
|
79
|
+
]
|
|
80
|
+
`)
|
|
81
|
+
await db.close()
|
|
82
|
+
})
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
describe('update', () => {
|
|
86
|
+
test('behavior: renames and bumps updatedAt', async () => {
|
|
87
|
+
const db = create()
|
|
88
|
+
await db.migrate()
|
|
89
|
+
const created = await Organizations.create(db, { id: 'org_1', name: 'Before' })
|
|
90
|
+
await new Promise((resolve) => setTimeout(resolve, 5))
|
|
91
|
+
const updated = await Organizations.update(db, 'org_1', { name: 'After' })
|
|
92
|
+
expect(updated?.name).toMatchInlineSnapshot(`"After"`)
|
|
93
|
+
expect(updated && updated.updatedAt > created.updatedAt).toBe(true)
|
|
94
|
+
await db.close()
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
test('behavior: returns undefined when absent', async () => {
|
|
98
|
+
const db = create()
|
|
99
|
+
await db.migrate()
|
|
100
|
+
expect(await Organizations.update(db, 'org_missing', { name: 'X' })).toBeUndefined()
|
|
101
|
+
await db.close()
|
|
102
|
+
})
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
describe('upsert', () => {
|
|
106
|
+
test('behavior: inserts when absent, defaulting name to the id', async () => {
|
|
107
|
+
const db = create()
|
|
108
|
+
await db.migrate()
|
|
109
|
+
await Organizations.upsert(db, { id: 'key_fallback' })
|
|
110
|
+
expect((await Organizations.get(db, 'key_fallback'))?.name).toMatchInlineSnapshot(
|
|
111
|
+
`"key_fallback"`,
|
|
112
|
+
)
|
|
113
|
+
await db.close()
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
test('behavior: leaves an existing row untouched', async () => {
|
|
117
|
+
const db = create()
|
|
118
|
+
await db.migrate()
|
|
119
|
+
await Organizations.create(db, { id: 'org_1', name: 'Original' })
|
|
120
|
+
await Organizations.upsert(db, { id: 'org_1', name: 'Clobbered' })
|
|
121
|
+
expect((await Organizations.get(db, 'org_1'))?.name).toMatchInlineSnapshot(`"Original"`)
|
|
122
|
+
await db.close()
|
|
123
|
+
})
|
|
124
|
+
})
|