tapimo 0.4.0 → 0.4.3
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 +1 -1
- package/dist/ApiKey.d.ts +1 -9
- package/dist/ApiKey.d.ts.map +1 -1
- package/dist/ApiKey.js +2 -3
- package/dist/ApiKey.js.map +1 -1
- package/dist/ApiKeys.d.ts +53 -27
- package/dist/ApiKeys.d.ts.map +1 -1
- package/dist/ApiKeys.js +32 -17
- package/dist/ApiKeys.js.map +1 -1
- package/dist/App.d.ts +2451 -1533
- package/dist/App.d.ts.map +1 -1
- package/dist/App.js +23 -9
- package/dist/App.js.map +1 -1
- package/dist/Assets.d.ts +10 -0
- package/dist/Assets.d.ts.map +1 -1
- package/dist/Assets.js +4 -0
- package/dist/Assets.js.map +1 -1
- package/dist/Auth.d.ts +2 -1
- package/dist/Auth.d.ts.map +1 -1
- package/dist/Auth.js +2 -1
- package/dist/Auth.js.map +1 -1
- package/dist/Scope.d.ts +40 -15
- package/dist/Scope.d.ts.map +1 -1
- package/dist/Scope.js +44 -13
- package/dist/Scope.js.map +1 -1
- package/dist/admin/App.d.ts +560 -9
- package/dist/admin/App.d.ts.map +1 -1
- package/dist/admin/App.js +38 -6
- package/dist/admin/App.js.map +1 -1
- package/dist/admin/apps/api-keys.d.ts +10 -24
- package/dist/admin/apps/api-keys.d.ts.map +1 -1
- package/dist/admin/apps/api-keys.js +10 -3
- package/dist/admin/apps/api-keys.js.map +1 -1
- package/dist/admin/apps/organizations.d.ts +833 -0
- package/dist/admin/apps/organizations.d.ts.map +1 -0
- package/dist/admin/apps/organizations.js +733 -0
- package/dist/admin/apps/organizations.js.map +1 -0
- package/dist/admin/apps/scopes.d.ts +8 -0
- package/dist/admin/apps/scopes.d.ts.map +1 -1
- package/dist/admin/ui.generated.d.ts +1 -1
- package/dist/admin/ui.generated.d.ts.map +1 -1
- package/dist/admin/ui.generated.js +53 -13
- package/dist/admin/ui.generated.js.map +1 -1
- package/dist/analytics/Analytics.d.ts +10 -0
- package/dist/analytics/Analytics.d.ts.map +1 -1
- package/dist/analytics/Analytics.js +8 -0
- package/dist/analytics/Analytics.js.map +1 -1
- package/dist/analytics/tables/requestEvents.d.ts +9 -0
- package/dist/analytics/tables/requestEvents.d.ts.map +1 -1
- package/dist/analytics/tables/requestEvents.js +14 -0
- package/dist/analytics/tables/requestEvents.js.map +1 -1
- package/dist/apps/data/App.d.ts +1354 -984
- package/dist/apps/data/App.d.ts.map +1 -1
- package/dist/apps/data/App.js +20 -3
- package/dist/apps/data/App.js.map +1 -1
- package/dist/apps/data/FundingRouteProviders.d.ts +94 -5
- package/dist/apps/data/FundingRouteProviders.d.ts.map +1 -1
- package/dist/apps/data/FundingRouteProviders.js +196 -5
- package/dist/apps/data/FundingRouteProviders.js.map +1 -1
- package/dist/apps/data/routes/activities.d.ts +38 -38
- package/dist/apps/data/routes/addresses.d.ts +24 -24
- package/dist/apps/data/routes/balances.d.ts +45 -45
- package/dist/apps/data/routes/blocks.d.ts +39 -39
- package/dist/apps/data/routes/coingecko.d.ts +176 -176
- package/dist/apps/data/routes/exchanges.d.ts +453 -453
- package/dist/apps/data/routes/fee-amm.d.ts +101 -101
- package/dist/apps/data/routes/funding.d.ts +730 -138
- package/dist/apps/data/routes/funding.d.ts.map +1 -1
- package/dist/apps/data/routes/funding.js +234 -0
- package/dist/apps/data/routes/funding.js.map +1 -1
- package/dist/apps/data/routes/indexer.d.ts +5 -5
- package/dist/apps/data/routes/rpc.d.ts +31 -31
- package/dist/apps/data/routes/verified-tokens.d.ts +81 -81
- package/dist/apps/management/App.d.ts +1156 -615
- package/dist/apps/management/App.d.ts.map +1 -1
- package/dist/apps/management/App.js +2 -2
- package/dist/apps/management/Billing.d.ts +3 -0
- package/dist/apps/management/Billing.d.ts.map +1 -1
- package/dist/apps/management/Billing.js +7 -2
- package/dist/apps/management/Billing.js.map +1 -1
- package/dist/apps/management/routes/api-keys.d.ts +106 -90
- package/dist/apps/management/routes/api-keys.d.ts.map +1 -1
- package/dist/apps/management/routes/api-keys.js +41 -30
- package/dist/apps/management/routes/api-keys.js.map +1 -1
- package/dist/apps/management/routes/billing.d.ts +278 -146
- package/dist/apps/management/routes/billing.d.ts.map +1 -1
- package/dist/apps/management/routes/billing.js +14 -14
- package/dist/apps/management/routes/billing.js.map +1 -1
- package/dist/apps/management/routes/invitations.d.ts +93 -20
- package/dist/apps/management/routes/invitations.d.ts.map +1 -1
- package/dist/apps/management/routes/invitations.js +12 -15
- package/dist/apps/management/routes/invitations.js.map +1 -1
- package/dist/apps/management/routes/me.d.ts +13 -13
- package/dist/apps/management/routes/members.d.ts +89 -18
- package/dist/apps/management/routes/members.d.ts.map +1 -1
- package/dist/apps/management/routes/members.js +10 -10
- package/dist/apps/management/routes/members.js.map +1 -1
- package/dist/apps/management/routes/orgs.d.ts +130 -70
- package/dist/apps/management/routes/orgs.d.ts.map +1 -1
- package/dist/apps/management/routes/orgs.js +19 -19
- package/dist/apps/management/routes/orgs.js.map +1 -1
- package/dist/apps/management/routes/projects.d.ts +137 -48
- package/dist/apps/management/routes/projects.d.ts.map +1 -1
- package/dist/apps/management/routes/projects.js +12 -13
- package/dist/apps/management/routes/projects.js.map +1 -1
- package/dist/apps/management/routes/scopes.d.ts +14 -28
- package/dist/apps/management/routes/scopes.d.ts.map +1 -1
- package/dist/apps/management/routes/scopes.js +6 -7
- package/dist/apps/management/routes/scopes.js.map +1 -1
- package/dist/apps/management/routes/usage.d.ts +63 -19
- package/dist/apps/management/routes/usage.d.ts.map +1 -1
- package/dist/apps/management/routes/usage.js +9 -10
- package/dist/apps/management/routes/usage.js.map +1 -1
- package/dist/apps/mcp.d.ts.map +1 -1
- package/dist/apps/mcp.js +35 -3
- package/dist/apps/mcp.js.map +1 -1
- package/dist/apps/relay/App.d.ts +26 -4
- package/dist/apps/relay/App.d.ts.map +1 -1
- package/dist/apps/relay/App.js +17 -14
- package/dist/apps/relay/App.js.map +1 -1
- package/dist/db/Db.d.ts +5 -3
- package/dist/db/Db.d.ts.map +1 -1
- package/dist/db/Db.js +52 -0
- package/dist/db/Db.js.map +1 -1
- package/dist/db/Schema.d.ts +13 -0
- package/dist/db/Schema.d.ts.map +1 -1
- package/dist/db/Schema.js +16 -1
- package/dist/db/Schema.js.map +1 -1
- package/dist/db/tables/adminAuditLogs.d.ts +27 -0
- package/dist/db/tables/adminAuditLogs.d.ts.map +1 -0
- package/dist/db/tables/adminAuditLogs.js +19 -0
- package/dist/db/tables/adminAuditLogs.js.map +1 -0
- package/dist/db/tables/invitations.d.ts +1 -1
- package/dist/db/tables/invitations.d.ts.map +1 -1
- package/dist/db/tables/memberships.d.ts +32 -4
- package/dist/db/tables/memberships.d.ts.map +1 -1
- package/dist/db/tables/memberships.js +31 -0
- package/dist/db/tables/memberships.js.map +1 -1
- package/dist/db/tables/organizations.d.ts +43 -1
- package/dist/db/tables/organizations.d.ts.map +1 -1
- package/dist/db/tables/organizations.js +103 -0
- package/dist/db/tables/organizations.js.map +1 -1
- package/dist/db/tables/projects.d.ts +25 -0
- package/dist/db/tables/projects.d.ts.map +1 -1
- package/dist/db/tables/projects.js +22 -0
- package/dist/db/tables/projects.js.map +1 -1
- package/dist/db/tables/webhookDeliveries.d.ts +26 -0
- package/dist/db/tables/webhookDeliveries.d.ts.map +1 -1
- package/dist/db/tables/webhookDeliveries.js +37 -0
- package/dist/db/tables/webhookDeliveries.js.map +1 -1
- package/dist/handlers/relay.d.ts +13 -10
- package/dist/handlers/relay.d.ts.map +1 -1
- package/dist/handlers/relay.js +46 -16
- package/dist/handlers/relay.js.map +1 -1
- package/dist/internal/Auth.d.ts +13 -18
- package/dist/internal/Auth.d.ts.map +1 -1
- package/dist/internal/Auth.js +78 -58
- package/dist/internal/Auth.js.map +1 -1
- package/dist/internal/Cursor.d.ts +1 -1
- package/dist/internal/Cursor.d.ts.map +1 -1
- package/dist/internal/Cursor.js +4 -0
- package/dist/internal/Cursor.js.map +1 -1
- package/dist/internal/EdgeCache.d.ts.map +1 -1
- package/dist/internal/EdgeCache.js +2 -0
- package/dist/internal/EdgeCache.js.map +1 -1
- package/dist/internal/FundingRoutes.d.ts +170 -0
- package/dist/internal/FundingRoutes.d.ts.map +1 -1
- package/dist/internal/FundingRoutes.js +68 -6
- package/dist/internal/FundingRoutes.js.map +1 -1
- package/dist/internal/Log.d.ts +16 -1
- package/dist/internal/Log.d.ts.map +1 -1
- package/dist/internal/Log.js +5 -3
- package/dist/internal/Log.js.map +1 -1
- package/package.json +2 -2
- package/src/ApiKey.ts +2 -3
- package/src/ApiKeys.ts +95 -17
- package/src/App.test.ts +44 -2
- package/src/App.ts +29 -9
- package/src/Assets.ts +8 -0
- package/src/Auth.test-d.ts +36 -0
- package/src/Auth.ts +20 -1
- package/src/Client.test-d.ts +64 -17
- package/src/Scope.test.ts +34 -0
- package/src/Scope.ts +53 -17
- package/src/admin/App.test.ts +33 -3
- package/src/admin/App.ts +49 -14
- package/src/admin/apps/api-keys.ts +10 -3
- package/src/admin/apps/organizations.test.ts +455 -0
- package/src/admin/apps/organizations.ts +953 -0
- package/src/admin/apps/scopes.test.ts +10 -0
- package/src/admin/ui/plugin.ts +1 -1
- package/src/admin/ui/src/components/MobileProfile.tsx +2 -0
- package/src/admin/ui/src/components/ThemeToggle.tsx +20 -0
- package/src/admin/ui/src/lib/api-keys.ts +36 -19
- package/src/admin/ui/src/lib/config.ts +2 -3
- package/src/admin/ui/src/lib/organizations.test.ts +89 -0
- package/src/admin/ui/src/lib/organizations.ts +262 -0
- package/src/admin/ui/src/lib/query-client.ts +3 -1
- package/src/admin/ui/src/lib/theme.ts +57 -0
- package/src/admin/ui/src/routeTree.gen.ts +47 -4
- package/src/admin/ui/src/routes/__root.tsx +18 -3
- package/src/admin/ui/src/routes/api-keys.tsx +52 -7
- package/src/admin/ui/src/routes/index.tsx +1 -1
- package/src/admin/ui/src/routes/organizations.tsx +158 -0
- package/src/admin/ui/src/routes/organizations_.$orgId.tsx +710 -0
- package/src/admin/ui/src/start.ts +6 -0
- package/src/admin/ui.generated.ts +63 -13
- package/src/analytics/Analytics.ts +25 -0
- package/src/analytics/tables/requestEvents.ts +33 -0
- package/src/apps/composition.test.ts +82 -2
- package/src/apps/data/App.ts +24 -3
- package/src/apps/data/FundingRouteProviders.test.ts +204 -0
- package/src/apps/data/FundingRouteProviders.ts +294 -9
- package/src/apps/data/routes/funding.test.ts +90 -23
- package/src/apps/data/routes/funding.ts +315 -0
- package/src/apps/data/routes/rpc.test.ts +33 -0
- package/src/apps/management/App.ts +2 -2
- package/src/apps/management/Billing.ts +10 -2
- package/src/apps/management/routes/api-keys.test.ts +137 -1
- package/src/apps/management/routes/api-keys.ts +63 -38
- package/src/apps/management/routes/billing.ts +14 -14
- package/src/apps/management/routes/invitations.test.ts +102 -1
- package/src/apps/management/routes/invitations.ts +13 -15
- package/src/apps/management/routes/members.ts +10 -10
- package/src/apps/management/routes/orgs.test.ts +78 -16
- package/src/apps/management/routes/orgs.ts +21 -20
- package/src/apps/management/routes/projects.ts +12 -13
- package/src/apps/management/routes/scopes.test.ts +16 -0
- package/src/apps/management/routes/scopes.ts +6 -7
- package/src/apps/management/routes/usage.ts +9 -14
- package/src/apps/mcp.test.ts +25 -1
- package/src/apps/mcp.ts +43 -3
- package/src/apps/relay/App.test.ts +23 -18
- package/src/apps/relay/App.ts +35 -14
- package/src/db/Db.ts +59 -3
- package/src/db/Schema.ts +20 -1
- package/src/db/tables/adminAuditLogs.ts +47 -0
- package/src/db/tables/invitations.ts +1 -1
- package/src/db/tables/memberships.ts +59 -3
- package/src/db/tables/organizations.ts +148 -1
- package/src/db/tables/projects.ts +47 -0
- package/src/db/tables/webhookDeliveries.ts +83 -0
- package/src/handlers/relay.test-d.ts +8 -0
- package/src/handlers/relay.test.ts +2 -2
- package/src/handlers/relay.ts +62 -22
- package/src/internal/Auth.test.ts +174 -7
- package/src/internal/Auth.ts +90 -62
- package/src/internal/Cursor.test.ts +10 -0
- package/src/internal/Cursor.ts +5 -1
- package/src/internal/EdgeCache.test.ts +11 -0
- package/src/internal/EdgeCache.ts +2 -0
- package/src/internal/FundingRoutes.test.ts +26 -0
- package/src/internal/FundingRoutes.ts +187 -18
- package/src/internal/Log.test.ts +41 -3
- package/src/internal/Log.ts +23 -4
|
@@ -29,14 +29,14 @@ const senderAccount = Tempo.accounts[9]!
|
|
|
29
29
|
/** Super admin secret configured on the end-to-end billing app. */
|
|
30
30
|
const superAdminSecret = 'tempo:sk:a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1'
|
|
31
31
|
|
|
32
|
-
/** Keys covering the sponsorship policy matrix
|
|
32
|
+
/** Keys covering the sponsorship policy matrix. */
|
|
33
33
|
const keys = [
|
|
34
34
|
{
|
|
35
35
|
id: 'key_sponsor',
|
|
36
36
|
orgId: 'org_1',
|
|
37
37
|
projectId: 'prj_1',
|
|
38
38
|
environment: 'sandbox',
|
|
39
|
-
scopes: ['rpc-relay:
|
|
39
|
+
scopes: ['rpc-relay:sponsor'],
|
|
40
40
|
token: 'secret_sponsor',
|
|
41
41
|
},
|
|
42
42
|
{
|
|
@@ -44,7 +44,7 @@ const keys = [
|
|
|
44
44
|
orgId: 'org_1',
|
|
45
45
|
projectId: 'prj_1',
|
|
46
46
|
environment: 'production',
|
|
47
|
-
scopes: ['rpc-relay:
|
|
47
|
+
scopes: ['rpc-relay:sponsor'],
|
|
48
48
|
token: 'secret_sponsor_prod',
|
|
49
49
|
},
|
|
50
50
|
{
|
|
@@ -75,7 +75,7 @@ const keys = [
|
|
|
75
75
|
id: 'key_unattributed',
|
|
76
76
|
orgId: 'org_1',
|
|
77
77
|
environment: 'sandbox',
|
|
78
|
-
scopes: ['rpc-relay:
|
|
78
|
+
scopes: ['rpc-relay:sponsor'],
|
|
79
79
|
token: 'secret_unattributed',
|
|
80
80
|
},
|
|
81
81
|
] as const satisfies readonly TestApp.kvStore.Key[]
|
|
@@ -169,7 +169,7 @@ describe.skipIf(runtime.mode !== 'localnet')('behavior: sponsorship', () => {
|
|
|
169
169
|
expect(row?.orgId).toMatchInlineSnapshot(`"org_2"`)
|
|
170
170
|
})
|
|
171
171
|
|
|
172
|
-
test('behavior: keys
|
|
172
|
+
test('behavior: read-only keys cannot access the sponsor route', async () => {
|
|
173
173
|
const db = TestApp.database()
|
|
174
174
|
const app = TestApp.create({
|
|
175
175
|
auth: { keys },
|
|
@@ -177,14 +177,15 @@ describe.skipIf(runtime.mode !== 'localnet')('behavior: sponsorship', () => {
|
|
|
177
177
|
relay: { feePayer: { account: feePayerAccount } },
|
|
178
178
|
})
|
|
179
179
|
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
expect(
|
|
180
|
+
const response = await app.fetch(
|
|
181
|
+
new Request('http://tempo-api.test/rpc/sponsor', {
|
|
182
|
+
body: JSON.stringify({ id: 1, jsonrpc: '2.0', method: 'eth_chainId' }),
|
|
183
|
+
headers: { 'content-type': 'application/json', 'tempo-api-key': 'secret_unscoped' },
|
|
184
|
+
method: 'POST',
|
|
185
|
+
}),
|
|
186
|
+
)
|
|
187
|
+
expect(response.status).toBe(403)
|
|
188
|
+
expect(await response.json()).toMatchObject({ error: { code: 'api_key_forbidden' } })
|
|
188
189
|
expect(await SponsoredTransactions.listPending(db)).toHaveLength(0)
|
|
189
190
|
})
|
|
190
191
|
|
|
@@ -288,12 +289,16 @@ describe.skipIf(runtime.mode !== 'localnet')('behavior: sponsorship', () => {
|
|
|
288
289
|
nonce: '0x0',
|
|
289
290
|
},
|
|
290
291
|
],
|
|
291
|
-
token: '
|
|
292
|
+
token: 'secret_sponsor',
|
|
292
293
|
})
|
|
293
294
|
const result = body.result as unknown as
|
|
294
|
-
| {
|
|
295
|
+
| {
|
|
296
|
+
capabilities?: { balanceDiffs?: unknown; sponsored?: boolean }
|
|
297
|
+
tx?: { feePayerSignature?: string | null }
|
|
298
|
+
}
|
|
295
299
|
| undefined
|
|
296
300
|
expect(result?.tx?.feePayerSignature ?? null).toBeNull()
|
|
301
|
+
expect(result?.capabilities?.balanceDiffs).toBeUndefined()
|
|
297
302
|
expect(result?.capabilities?.sponsored).toBe(false)
|
|
298
303
|
expect(await SponsoredTransactions.listPending(db)).toHaveLength(0)
|
|
299
304
|
})
|
|
@@ -629,7 +634,7 @@ describe.skipIf(runtime.mode !== 'localnet')('behavior: sponsorship (viem)', ()
|
|
|
629
634
|
chain: Tempo.chain,
|
|
630
635
|
transport: withRelay(
|
|
631
636
|
Relay.http(),
|
|
632
|
-
http(`${server.url}/rpc/
|
|
637
|
+
http(`${server.url}/rpc/sponsor`, {
|
|
633
638
|
fetchOptions: { headers: { 'tempo-api-key': 'secret_sponsor' } },
|
|
634
639
|
}),
|
|
635
640
|
policy ? { policy } : {},
|
|
@@ -785,13 +790,13 @@ async function signSponsorable(
|
|
|
785
790
|
})
|
|
786
791
|
}
|
|
787
792
|
|
|
788
|
-
/** Posts one JSON-RPC request to the composed app's
|
|
793
|
+
/** Posts one JSON-RPC request to the composed app's sponsor mount. */
|
|
789
794
|
async function relayRequest(
|
|
790
795
|
app: ReturnType<typeof TestApp.create>,
|
|
791
796
|
options: { method: string; params: readonly unknown[]; token?: string | undefined },
|
|
792
797
|
) {
|
|
793
798
|
const response = await app.fetch(
|
|
794
|
-
new Request('http://tempo-api.test/rpc/
|
|
799
|
+
new Request('http://tempo-api.test/rpc/sponsor', {
|
|
795
800
|
body: JSON.stringify({ id: 1, jsonrpc: '2.0', method: options.method, params: options.params }), // prettier-ignore
|
|
796
801
|
headers: {
|
|
797
802
|
'content-type': 'application/json',
|
package/src/apps/relay/App.ts
CHANGED
|
@@ -18,16 +18,17 @@ import * as Viem from '../../internal/Viem.js'
|
|
|
18
18
|
import * as Scope from '../../Scope.js'
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
|
-
* The relay route group
|
|
22
|
-
* after `data()`,
|
|
23
|
-
*
|
|
21
|
+
* The relay route group exposes wallet features at `/rpc/relay` and fee
|
|
22
|
+
* sponsorship at `/rpc/sponsor`. Mount it after `data()`, which publishes
|
|
23
|
+
* chain clients and supported chains.
|
|
24
24
|
*
|
|
25
25
|
* ```ts
|
|
26
26
|
* App.create(options).route('/', data()).route('/', relay())
|
|
27
27
|
* ```
|
|
28
28
|
*/
|
|
29
29
|
export function relay(options: relay.Options = {}) {
|
|
30
|
-
const
|
|
30
|
+
const handler_relay = middleware({ ...options, features: 'all', path: 'rpc/relay' })
|
|
31
|
+
const handler_sponsor = middleware({ ...options, path: 'rpc/sponsor' })
|
|
31
32
|
return new Hono<App.Environment>()
|
|
32
33
|
.use(
|
|
33
34
|
'/rpc/relay',
|
|
@@ -39,8 +40,20 @@ export function relay(options: relay.Options = {}) {
|
|
|
39
40
|
Auth.policy({ apiKey: { scopes: ['rpc-relay:read'] }, mpp: false, public: false }),
|
|
40
41
|
OpenApi.describeRoute({ hide: true, summary: 'Relay RPC' }),
|
|
41
42
|
)
|
|
42
|
-
.all('/rpc/relay',
|
|
43
|
-
.all('/rpc/relay/:chainId',
|
|
43
|
+
.all('/rpc/relay', handler_relay)
|
|
44
|
+
.all('/rpc/relay/:chainId', handler_relay)
|
|
45
|
+
.use(
|
|
46
|
+
'/rpc/sponsor',
|
|
47
|
+
Auth.policy({ apiKey: { scopes: ['rpc-relay:sponsor'] }, mpp: false, public: false }),
|
|
48
|
+
OpenApi.describeRoute({ hide: true, summary: 'Sponsor RPC' }),
|
|
49
|
+
)
|
|
50
|
+
.use(
|
|
51
|
+
'/rpc/sponsor/*',
|
|
52
|
+
Auth.policy({ apiKey: { scopes: ['rpc-relay:sponsor'] }, mpp: false, public: false }),
|
|
53
|
+
OpenApi.describeRoute({ hide: true, summary: 'Sponsor RPC' }),
|
|
54
|
+
)
|
|
55
|
+
.all('/rpc/sponsor', handler_sponsor)
|
|
56
|
+
.all('/rpc/sponsor/:chainId', handler_sponsor)
|
|
44
57
|
}
|
|
45
58
|
|
|
46
59
|
export declare namespace relay {
|
|
@@ -58,11 +71,9 @@ export declare namespace relay {
|
|
|
58
71
|
}
|
|
59
72
|
}
|
|
60
73
|
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
// clients are memoized upstream, so per-request handler construction is cheap.
|
|
65
|
-
function middleware(options: relay.Options = {}) {
|
|
74
|
+
// Builds request-aware RPC middleware. Viem clients are memoized upstream, so
|
|
75
|
+
// constructing `Handler.relay` per request stays cheap.
|
|
76
|
+
function middleware(options: middleware.Options) {
|
|
66
77
|
// Platform-wide per-transaction cap; org settings tighten it, never loosen.
|
|
67
78
|
const txFeeLimit = options.feePayer
|
|
68
79
|
? Billing.toBaseUnits(options.feePayer.txFeeLimit ?? '1.00')
|
|
@@ -215,12 +226,12 @@ function middleware(options: relay.Options = {}) {
|
|
|
215
226
|
|
|
216
227
|
// Dispatching a standalone Hono via `fetch(c.req.raw)` bypasses Hono's
|
|
217
228
|
// mount-path stripping, so its internal routes register at the request's
|
|
218
|
-
// full external path (`basePath` +
|
|
229
|
+
// full external path (`basePath` + the RPC mount).
|
|
219
230
|
const handler = Handler.relay({
|
|
220
231
|
cache: c.get('store'),
|
|
221
|
-
features:
|
|
232
|
+
...(options.features ? { features: options.features } : {}),
|
|
222
233
|
getClient: (chainId) => c.get('getClient')(chainId as Viem.ChainId | undefined),
|
|
223
|
-
path: Path.join(c.get('basePath'),
|
|
234
|
+
path: Path.join(c.get('basePath'), options.path),
|
|
224
235
|
...(feePayer ? { feePayer } : {}),
|
|
225
236
|
...(verifiedTokens
|
|
226
237
|
? {
|
|
@@ -234,3 +245,13 @@ function middleware(options: relay.Options = {}) {
|
|
|
234
245
|
return handler.fetch(c.req.raw)
|
|
235
246
|
})
|
|
236
247
|
}
|
|
248
|
+
|
|
249
|
+
declare namespace middleware {
|
|
250
|
+
/** Internal relay mount configuration. */
|
|
251
|
+
type Options = relay.Options & {
|
|
252
|
+
/** Optional wallet feature bundle. */
|
|
253
|
+
features?: Handler.relay.Options['features']
|
|
254
|
+
/** RPC mount path. */
|
|
255
|
+
path: 'rpc/relay' | 'rpc/sponsor'
|
|
256
|
+
}
|
|
257
|
+
}
|
package/src/db/Db.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
} from 'kysely'
|
|
11
11
|
import * as pg from 'pg'
|
|
12
12
|
|
|
13
|
+
import type * as AdminAuditLogs from './tables/adminAuditLogs.js'
|
|
13
14
|
import type * as BillingSettings from './tables/billingSettings.js'
|
|
14
15
|
import type * as Invitations from './tables/invitations.js'
|
|
15
16
|
import type * as Memberships from './tables/memberships.js'
|
|
@@ -28,6 +29,10 @@ import type * as WebhookSubscriptions from './tables/webhookSubscriptions.js'
|
|
|
28
29
|
* records, queries, and {@link migrations} DDL to snake_case storage columns.
|
|
29
30
|
*/
|
|
30
31
|
export type Database = {
|
|
32
|
+
admin_audit_logs: AdminAuditLogs.Table
|
|
33
|
+
billing_settings: BillingSettings.Table
|
|
34
|
+
invitations: Invitations.Table
|
|
35
|
+
memberships: Memberships.Table
|
|
31
36
|
migrations: {
|
|
32
37
|
appliedAt: string
|
|
33
38
|
checksum: string
|
|
@@ -35,9 +40,6 @@ export type Database = {
|
|
|
35
40
|
/** SQL as applied; null on rows written before the column existed. */
|
|
36
41
|
sql: string | null
|
|
37
42
|
}
|
|
38
|
-
billing_settings: BillingSettings.Table
|
|
39
|
-
invitations: Invitations.Table
|
|
40
|
-
memberships: Memberships.Table
|
|
41
43
|
organizations: Organizations.Table
|
|
42
44
|
projects: Projects.Table
|
|
43
45
|
request_usage_buckets: RequestUsage.BucketTable
|
|
@@ -520,6 +522,60 @@ export const migrations: readonly Migration[] = [
|
|
|
520
522
|
// Partial: the reporter scans only unreported (pending) rows.
|
|
521
523
|
up: (db) => raw(db, sql`CREATE INDEX IF NOT EXISTS request_usage_meter_events_pending_idx ON request_usage_meter_events (first_attempted_at, identifier) WHERE state = 'pending'`), // prettier-ignore
|
|
522
524
|
},
|
|
525
|
+
{
|
|
526
|
+
// Supports the admin organization's newest-first keyset pagination.
|
|
527
|
+
name: '0045_organizations_admin_list_index',
|
|
528
|
+
up: (db) => raw(db, sql`CREATE INDEX IF NOT EXISTS organizations_admin_list_idx ON organizations (created_at DESC, id DESC)`), // prettier-ignore
|
|
529
|
+
},
|
|
530
|
+
{
|
|
531
|
+
// Prefix search keeps organization-name lookup index-backed.
|
|
532
|
+
name: '0046_organizations_name_prefix_index',
|
|
533
|
+
up: (db) => raw(db, sql`CREATE INDEX IF NOT EXISTS organizations_name_prefix_idx ON organizations (lower(name) text_pattern_ops)`), // prettier-ignore
|
|
534
|
+
},
|
|
535
|
+
{
|
|
536
|
+
// Prefix search keeps member-email lookup index-backed.
|
|
537
|
+
name: '0047_users_email_prefix_index',
|
|
538
|
+
up: (db) => raw(db, sql`CREATE INDEX IF NOT EXISTS users_email_prefix_idx ON users (lower(email) text_pattern_ops) WHERE email IS NOT NULL`), // prettier-ignore
|
|
539
|
+
},
|
|
540
|
+
{
|
|
541
|
+
// Exact owner lookup for organizations created by a user.
|
|
542
|
+
name: '0048_organizations_user_index',
|
|
543
|
+
up: (db) => raw(db, sql`CREATE INDEX IF NOT EXISTS organizations_user_idx ON organizations (user_id) WHERE user_id IS NOT NULL`), // prettier-ignore
|
|
544
|
+
},
|
|
545
|
+
{
|
|
546
|
+
// Supports oldest-first admin membership keyset pagination.
|
|
547
|
+
name: '0049_memberships_admin_list_index',
|
|
548
|
+
up: (db) => raw(db, sql`CREATE INDEX IF NOT EXISTS memberships_admin_list_idx ON memberships (org_id, created_at, user_id)`), // prettier-ignore
|
|
549
|
+
},
|
|
550
|
+
{
|
|
551
|
+
// Supports newest-first admin project keyset pagination.
|
|
552
|
+
name: '0050_projects_admin_list_index',
|
|
553
|
+
up: (db) => raw(db, sql`CREATE INDEX IF NOT EXISTS projects_admin_list_idx ON projects (org_id, created_at DESC, id DESC)`), // prettier-ignore
|
|
554
|
+
},
|
|
555
|
+
{
|
|
556
|
+
name: '0051_admin_audit_logs',
|
|
557
|
+
up: (db) =>
|
|
558
|
+
db.schema
|
|
559
|
+
.createTable('adminAuditLogs')
|
|
560
|
+
.ifNotExists()
|
|
561
|
+
.addColumn('id', 'text', (c) => c.primaryKey())
|
|
562
|
+
.addColumn('actor', 'text', (c) => c.notNull())
|
|
563
|
+
.addColumn('method', 'text', (c) => c.notNull())
|
|
564
|
+
.addColumn('path', 'text', (c) => c.notNull())
|
|
565
|
+
.addColumn('query', 'text')
|
|
566
|
+
.addColumn('requestId', 'text', (c) => c.notNull())
|
|
567
|
+
.addColumn('status', 'integer', (c) => c.notNull())
|
|
568
|
+
.addColumn('createdAt', 'text', (c) => c.notNull()),
|
|
569
|
+
},
|
|
570
|
+
{
|
|
571
|
+
name: '0052_admin_audit_logs_created_index',
|
|
572
|
+
up: (db) =>
|
|
573
|
+
db.schema
|
|
574
|
+
.createIndex('admin_audit_logs_created_idx')
|
|
575
|
+
.ifNotExists()
|
|
576
|
+
.on('adminAuditLogs')
|
|
577
|
+
.columns(['createdAt', 'id']),
|
|
578
|
+
},
|
|
523
579
|
]
|
|
524
580
|
|
|
525
581
|
/**
|
package/src/db/Schema.ts
CHANGED
|
@@ -2,6 +2,25 @@ import * as z from 'zod/mini'
|
|
|
2
2
|
|
|
3
3
|
import type * as Webhooks from '../internal/Webhooks.js'
|
|
4
4
|
|
|
5
|
+
/** One authenticated administrator access to a protected admin API route. */
|
|
6
|
+
export const AdminAuditLog = z.object({
|
|
7
|
+
actor: z.string().check(z.describe('Verified administrator email.')),
|
|
8
|
+
createdAt: z.string().check(z.describe('When the access completed (ISO 8601).')),
|
|
9
|
+
id: z.string().check(z.describe('Opaque audit event id (`aud_…`).')),
|
|
10
|
+
method: z.string().check(z.describe('HTTP method used for the access.')),
|
|
11
|
+
path: z.string().check(z.describe('Admin API path accessed, without query parameters.')),
|
|
12
|
+
query: z
|
|
13
|
+
.nullable(z.string())
|
|
14
|
+
.check(z.describe('Query string used for the lookup, or null when absent.')),
|
|
15
|
+
requestId: z.string().check(z.describe('Request id correlating the access with request logs.')),
|
|
16
|
+
status: z
|
|
17
|
+
.number()
|
|
18
|
+
.check(z.int(), z.describe('HTTP response status returned to the administrator.')),
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
/** Columns of the `admin_audit_logs` table. */
|
|
22
|
+
export type AdminAuditLog = z.output<typeof AdminAuditLog>
|
|
23
|
+
|
|
5
24
|
// Row schemas for database tables (`zod/mini`), the source of truth each
|
|
6
25
|
// `tables/` module derives its `Table` type from. Nullable fields map to
|
|
7
26
|
// nullable columns; the domain layer converts `null` back to absent fields.
|
|
@@ -49,7 +68,7 @@ export const Invitation = z.object({
|
|
|
49
68
|
z.describe('Opaque invitation id (`inv_…`).'),
|
|
50
69
|
z.meta({ examples: ['inv_1a2b3c4d5e6f7g8h9j0k1m2n'] }),
|
|
51
70
|
),
|
|
52
|
-
invitedBy: z.string().check(z.describe('User id
|
|
71
|
+
invitedBy: z.string().check(z.describe('User or API key id that created the invitation.')),
|
|
53
72
|
orgId: z.string().check(z.describe('Organization id (`org_…`) the invitation joins.')),
|
|
54
73
|
revokedAt: z
|
|
55
74
|
.nullable(z.string())
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { Selectable } from 'kysely'
|
|
2
|
+
|
|
3
|
+
import * as Id from '../../internal/Id.js'
|
|
4
|
+
import type * as Db from '../Db.js'
|
|
5
|
+
import type * as db_Schema from '../Schema.js'
|
|
6
|
+
|
|
7
|
+
/** Columns of the `admin_audit_logs` table. */
|
|
8
|
+
export type Table = db_Schema.AdminAuditLog
|
|
9
|
+
|
|
10
|
+
/** One stored administrator access event. */
|
|
11
|
+
export type Record = Selectable<Table>
|
|
12
|
+
|
|
13
|
+
/** Records an authenticated access to a protected admin API route. */
|
|
14
|
+
export function insert(db: Db.Db, input: insert.Input): Promise<Record> {
|
|
15
|
+
return db.kysely
|
|
16
|
+
.insertInto('admin_audit_logs')
|
|
17
|
+
.values({
|
|
18
|
+
actor: input.actor,
|
|
19
|
+
createdAt: new Date().toISOString(),
|
|
20
|
+
id: Id.generate('aud'),
|
|
21
|
+
method: input.method,
|
|
22
|
+
path: input.path,
|
|
23
|
+
query: input.query ?? null,
|
|
24
|
+
requestId: input.requestId,
|
|
25
|
+
status: input.status,
|
|
26
|
+
})
|
|
27
|
+
.returningAll()
|
|
28
|
+
.executeTakeFirstOrThrow()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export declare namespace insert {
|
|
32
|
+
/** Fields captured for one administrator access. */
|
|
33
|
+
type Input = {
|
|
34
|
+
/** Verified administrator email. */
|
|
35
|
+
actor: string
|
|
36
|
+
/** HTTP method used for the access. */
|
|
37
|
+
method: string
|
|
38
|
+
/** Admin API path accessed, without query parameters. */
|
|
39
|
+
path: string
|
|
40
|
+
/** Query string used for the lookup. */
|
|
41
|
+
query?: string | undefined
|
|
42
|
+
/** Request id correlating the access with request logs. */
|
|
43
|
+
requestId: string
|
|
44
|
+
/** HTTP response status returned to the administrator. */
|
|
45
|
+
status: number
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -60,7 +60,7 @@ export declare namespace create {
|
|
|
60
60
|
email: string
|
|
61
61
|
/** When the invitation expires (ISO 8601). */
|
|
62
62
|
expiresAt: string
|
|
63
|
-
/** User id
|
|
63
|
+
/** User or API key id creating the invitation. */
|
|
64
64
|
invitedBy: string
|
|
65
65
|
/** Organization id (`org_…`) the invitation joins. */
|
|
66
66
|
orgId: string
|
|
@@ -9,6 +9,9 @@ export type Table = db_Schema.Membership
|
|
|
9
9
|
/** A stored membership row. */
|
|
10
10
|
export type Record = Selectable<Table>
|
|
11
11
|
|
|
12
|
+
/** A membership with the member's resolved user identity. */
|
|
13
|
+
export type DetailedRecord = Record & { address: string | null; email: string | null }
|
|
14
|
+
|
|
12
15
|
/** A membership role. */
|
|
13
16
|
export type Role = Record['role']
|
|
14
17
|
|
|
@@ -93,20 +96,73 @@ export function listByOrg(db: Db.Db, orgId: string): Promise<Record[]> {
|
|
|
93
96
|
* @param orgId - The organization id (`org_…`).
|
|
94
97
|
* @returns The records with `address`/`email` from the member's user row.
|
|
95
98
|
*/
|
|
96
|
-
export function listByOrgDetailed(
|
|
99
|
+
export function listByOrgDetailed(db: Db.Db, orgId: string): Promise<DetailedRecord[]> {
|
|
100
|
+
return db.kysely
|
|
101
|
+
.selectFrom('memberships')
|
|
102
|
+
.innerJoin('users', 'users.id', 'memberships.userId')
|
|
103
|
+
.where('memberships.orgId', '=', orgId)
|
|
104
|
+
.selectAll('memberships')
|
|
105
|
+
.select(['users.address', 'users.email'])
|
|
106
|
+
.orderBy('memberships.createdAt', 'asc')
|
|
107
|
+
.execute()
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Lists a bounded page of an organization's memberships with member identity,
|
|
112
|
+
* oldest first.
|
|
113
|
+
*
|
|
114
|
+
* @param db - The database.
|
|
115
|
+
* @param orgId - The organization id.
|
|
116
|
+
* @param options - Keyset and page-size options.
|
|
117
|
+
* @returns At most `limit + 1` rows for next-cursor derivation.
|
|
118
|
+
*/
|
|
119
|
+
export function listByOrgDetailedPage(
|
|
97
120
|
db: Db.Db,
|
|
98
121
|
orgId: string,
|
|
99
|
-
|
|
100
|
-
|
|
122
|
+
options: listByOrgDetailedPage.Options,
|
|
123
|
+
): Promise<DetailedRecord[]> {
|
|
124
|
+
const cursor = options.cursor
|
|
125
|
+
let query = db.kysely
|
|
101
126
|
.selectFrom('memberships')
|
|
102
127
|
.innerJoin('users', 'users.id', 'memberships.userId')
|
|
103
128
|
.where('memberships.orgId', '=', orgId)
|
|
104
129
|
.selectAll('memberships')
|
|
105
130
|
.select(['users.address', 'users.email'])
|
|
131
|
+
if (cursor)
|
|
132
|
+
query = query.where((eb) =>
|
|
133
|
+
eb.or([
|
|
134
|
+
eb('memberships.createdAt', '>', cursor.createdAt),
|
|
135
|
+
eb.and([
|
|
136
|
+
eb('memberships.createdAt', '=', cursor.createdAt),
|
|
137
|
+
eb('memberships.userId', '>', cursor.userId),
|
|
138
|
+
]),
|
|
139
|
+
]),
|
|
140
|
+
)
|
|
141
|
+
return query
|
|
106
142
|
.orderBy('memberships.createdAt', 'asc')
|
|
143
|
+
.orderBy('memberships.userId', 'asc')
|
|
144
|
+
.limit(options.limit + 1)
|
|
107
145
|
.execute()
|
|
108
146
|
}
|
|
109
147
|
|
|
148
|
+
export declare namespace listByOrgDetailedPage {
|
|
149
|
+
/** Cursor fields for the last membership on the previous page. */
|
|
150
|
+
type Cursor = {
|
|
151
|
+
/** Membership creation time. */
|
|
152
|
+
createdAt: string
|
|
153
|
+
/** Member user id, used as a deterministic tie-breaker. */
|
|
154
|
+
userId: string
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Options for {@link listByOrgDetailedPage}. */
|
|
158
|
+
type Options = {
|
|
159
|
+
/** Last membership returned by the previous page. */
|
|
160
|
+
cursor?: Cursor | undefined
|
|
161
|
+
/** Requested page size. */
|
|
162
|
+
limit: number
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
110
166
|
/**
|
|
111
167
|
* Changes a member's role atomically, refusing to demote the last owner. The
|
|
112
168
|
* org's owner rows are locked for the transaction, so a concurrent demotion or
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { sql, type Selectable } from 'kysely'
|
|
2
2
|
|
|
3
3
|
import * as Id from '../../internal/Id.js'
|
|
4
4
|
import type * as Db from '../Db.js'
|
|
@@ -133,6 +133,39 @@ export function get(db: Db.Db, id: string): Promise<Record | undefined> {
|
|
|
133
133
|
return db.kysely.selectFrom('organizations').selectAll().where('id', '=', id).executeTakeFirst()
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
+
/**
|
|
137
|
+
* Reads an organization with current member and project counts.
|
|
138
|
+
*
|
|
139
|
+
* @param db - The database.
|
|
140
|
+
* @param id - The organization id.
|
|
141
|
+
* @returns The organization summary, or `undefined` when absent.
|
|
142
|
+
*/
|
|
143
|
+
export async function getSummary(db: Db.Db, id: string): Promise<search.Result | undefined> {
|
|
144
|
+
const record = await db.kysely
|
|
145
|
+
.selectFrom('organizations')
|
|
146
|
+
.selectAll('organizations')
|
|
147
|
+
.select((eb) => [
|
|
148
|
+
eb
|
|
149
|
+
.selectFrom('memberships')
|
|
150
|
+
.select((builder) => builder.fn.countAll<number>().as('count'))
|
|
151
|
+
.whereRef('memberships.orgId', '=', 'organizations.id')
|
|
152
|
+
.as('memberCount'),
|
|
153
|
+
eb
|
|
154
|
+
.selectFrom('projects')
|
|
155
|
+
.select((builder) => builder.fn.countAll<number>().as('count'))
|
|
156
|
+
.whereRef('projects.orgId', '=', 'organizations.id')
|
|
157
|
+
.as('projectCount'),
|
|
158
|
+
])
|
|
159
|
+
.where('organizations.id', '=', id)
|
|
160
|
+
.executeTakeFirst()
|
|
161
|
+
if (!record) return undefined
|
|
162
|
+
return {
|
|
163
|
+
...record,
|
|
164
|
+
memberCount: Number(record.memberCount),
|
|
165
|
+
projectCount: Number(record.projectCount),
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
136
169
|
/**
|
|
137
170
|
* Lists organizations, newest first.
|
|
138
171
|
*
|
|
@@ -143,6 +176,120 @@ export function list(db: Db.Db): Promise<Record[]> {
|
|
|
143
176
|
return db.kysely.selectFrom('organizations').selectAll().orderBy('createdAt', 'desc').execute()
|
|
144
177
|
}
|
|
145
178
|
|
|
179
|
+
/**
|
|
180
|
+
* Searches organizations for the admin lookup surface, newest first. Results
|
|
181
|
+
* include member and project counts and use a stable `(createdAt, id)` keyset.
|
|
182
|
+
*
|
|
183
|
+
* @param db - The database.
|
|
184
|
+
* @param options - Search, keyset, and page-size options.
|
|
185
|
+
* @returns At most `limit + 1` rows so the caller can derive a next cursor.
|
|
186
|
+
*/
|
|
187
|
+
export async function search(db: Db.Db, options: search.Options): Promise<search.Result[]> {
|
|
188
|
+
const { cursor, limit, query: searchQuery } = options
|
|
189
|
+
let query = db.kysely
|
|
190
|
+
.selectFrom('organizations')
|
|
191
|
+
.selectAll('organizations')
|
|
192
|
+
.select((eb) => [
|
|
193
|
+
eb
|
|
194
|
+
.selectFrom('memberships')
|
|
195
|
+
.select((builder) => builder.fn.countAll<number>().as('count'))
|
|
196
|
+
.whereRef('memberships.orgId', '=', 'organizations.id')
|
|
197
|
+
.as('memberCount'),
|
|
198
|
+
eb
|
|
199
|
+
.selectFrom('projects')
|
|
200
|
+
.select((builder) => builder.fn.countAll<number>().as('count'))
|
|
201
|
+
.whereRef('projects.orgId', '=', 'organizations.id')
|
|
202
|
+
.as('projectCount'),
|
|
203
|
+
])
|
|
204
|
+
|
|
205
|
+
if (searchQuery?.startsWith('org_')) query = query.where('organizations.id', '=', searchQuery)
|
|
206
|
+
else if (searchQuery?.startsWith('usr_'))
|
|
207
|
+
query = query.where((eb) =>
|
|
208
|
+
eb.or([
|
|
209
|
+
eb('organizations.userId', '=', searchQuery),
|
|
210
|
+
eb(
|
|
211
|
+
'organizations.id',
|
|
212
|
+
'in',
|
|
213
|
+
eb
|
|
214
|
+
.selectFrom('memberships')
|
|
215
|
+
.select('memberships.orgId')
|
|
216
|
+
.where('memberships.userId', '=', searchQuery),
|
|
217
|
+
),
|
|
218
|
+
]),
|
|
219
|
+
)
|
|
220
|
+
else if (searchQuery) {
|
|
221
|
+
const pattern = `${searchQuery
|
|
222
|
+
.toLowerCase()
|
|
223
|
+
.replaceAll('\\', '\\\\')
|
|
224
|
+
.replaceAll('%', '\\%')
|
|
225
|
+
.replaceAll('_', '\\_')}%`
|
|
226
|
+
query = query.where((eb) =>
|
|
227
|
+
eb.or([
|
|
228
|
+
sql<boolean>`lower(${sql.ref('organizations.name')}) LIKE ${pattern} ESCAPE '\\'`,
|
|
229
|
+
eb(
|
|
230
|
+
'organizations.id',
|
|
231
|
+
'in',
|
|
232
|
+
eb
|
|
233
|
+
.selectFrom('memberships')
|
|
234
|
+
.innerJoin('users', 'users.id', 'memberships.userId')
|
|
235
|
+
.select('memberships.orgId')
|
|
236
|
+
.where(sql<boolean>`lower(${sql.ref('users.email')}) LIKE ${pattern} ESCAPE '\\'`),
|
|
237
|
+
),
|
|
238
|
+
]),
|
|
239
|
+
)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (cursor)
|
|
243
|
+
query = query.where((eb) =>
|
|
244
|
+
eb.or([
|
|
245
|
+
eb('organizations.createdAt', '<', cursor.createdAt),
|
|
246
|
+
eb.and([
|
|
247
|
+
eb('organizations.createdAt', '=', cursor.createdAt),
|
|
248
|
+
eb('organizations.id', '<', cursor.id),
|
|
249
|
+
]),
|
|
250
|
+
]),
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
const records = await query
|
|
254
|
+
.orderBy('organizations.createdAt', 'desc')
|
|
255
|
+
.orderBy('organizations.id', 'desc')
|
|
256
|
+
.limit(limit + 1)
|
|
257
|
+
.execute()
|
|
258
|
+
return records.map((record) => ({
|
|
259
|
+
...record,
|
|
260
|
+
memberCount: Number(record.memberCount),
|
|
261
|
+
projectCount: Number(record.projectCount),
|
|
262
|
+
}))
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export declare namespace search {
|
|
266
|
+
/** Cursor fields for the last organization returned by the previous page. */
|
|
267
|
+
type Cursor = {
|
|
268
|
+
/** Organization creation time. */
|
|
269
|
+
createdAt: string
|
|
270
|
+
/** Organization id, used as a deterministic tie-breaker. */
|
|
271
|
+
id: string
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** Options for {@link search}. */
|
|
275
|
+
type Options = {
|
|
276
|
+
/** Last organization returned by the previous page. */
|
|
277
|
+
cursor?: Cursor | undefined
|
|
278
|
+
/** Requested page size. */
|
|
279
|
+
limit: number
|
|
280
|
+
/** Exact resource id or case-insensitive name/email prefix search. */
|
|
281
|
+
query?: string | undefined
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** Organization summary returned by {@link search}. */
|
|
285
|
+
type Result = Record & {
|
|
286
|
+
/** Number of current organization members. */
|
|
287
|
+
memberCount: number
|
|
288
|
+
/** Number of projects owned by the organization. */
|
|
289
|
+
projectCount: number
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
146
293
|
/**
|
|
147
294
|
* Lists organizations owned by `userId`, newest first.
|
|
148
295
|
*
|