From 6853666f8c1d843473d1971b32fa543afc96dbfa Mon Sep 17 00:00:00 2001 From: Vitor Date: Fri, 6 Dec 2024 15:29:10 -0300 Subject: [PATCH] fix: recursive delegation querying --- .../actions/delegations/get-delegation-db.ts | 102 ++++-------------- .../get-delegations-collection-db.ts | 23 +++- .../actions/delegations/get-delegations-db.ts | 12 ++- .../src/db/actions/delegations/utils.ts | 35 ++++++ apps/api-delegations/src/db/schema.ts | 7 +- packages/universal-types/package.json | 6 +- 6 files changed, 89 insertions(+), 96 deletions(-) create mode 100644 apps/api-delegations/src/db/actions/delegations/utils.ts diff --git a/apps/api-delegations/src/db/actions/delegations/get-delegation-db.ts b/apps/api-delegations/src/db/actions/delegations/get-delegation-db.ts index 67c6fcb1..8d97b764 100644 --- a/apps/api-delegations/src/db/actions/delegations/get-delegation-db.ts +++ b/apps/api-delegations/src/db/actions/delegations/get-delegation-db.ts @@ -1,92 +1,30 @@ import type { DelegationWithMetadata } from 'universal-types'; import type { GetDelegationParams } from '../../../validation.js'; import { db } from '../../index.js'; -import { ROOT_AUTHORITY } from 'universal-data'; +import { + MAX_DELEGATION_DEPTH, + buildAuthConfig, + replaceAuthKeys, +} from './utils.js'; -function addAuthorityDelegationAtLeaf( - root: DelegationWithMetadata, - newAuthorityDelegation: DelegationWithMetadata, -): DelegationWithMetadata { - // If we've reached a leaf where authorityDelegation is null, insert the new delegation here. - if (root.authorityDelegation === null) { - return { - ...root, - authorityDelegation: newAuthorityDelegation, - }; - } - - // Otherwise, recurse down one level and update that subtree - return { - ...root, - authorityDelegation: addAuthorityDelegationAtLeaf( - root.authorityDelegation, - newAuthorityDelegation, - ), - }; -} +type GetDelegationDbReturnType = Promise; export async function getDelegationDb({ hash, -}: GetDelegationParams): Promise { - return db.transaction(async (tx) => { - // Fetch the top-level (initial) delegation - const topDelegationDb = await tx.query.delegations.findFirst({ - where: (delegations, { eq }) => eq(delegations.hash, hash), - with: { caveats: true }, - }); - - if (!topDelegationDb) { - return undefined; - } - - // Initialize our top-level delegation with authorityDelegation = null - let delegation: DelegationWithMetadata = { - ...topDelegationDb, - authorityDelegation: null, - }; - - // If authority is the root authority, no further chaining is needed - if (delegation.authority === ROOT_AUTHORITY) { - return delegation; - } - - // Check for delegation loops - if (delegation.hash === delegation.authority) { - throw new Error('Delegation loop detected'); - } - - // Walk down the chain of authority delegations until we hit the root - let currentAuthority = delegation.authority; - - // TODO: replace loop with query using recursive CTE - while (currentAuthority !== ROOT_AUTHORITY) { - const nextDelegationDb = await tx.query.delegations.findFirst({ - where: (delegations, { eq }) => eq(delegations.hash, currentAuthority), - with: { caveats: true }, - }); - - if (!nextDelegationDb) { - // If we can't find the next delegation in the chain, stop and return what we have so far - break; - } - - // Check for delegation loops - if (nextDelegationDb.hash === nextDelegationDb.authority) { - throw new Error('Delegation loop detected'); - } - - const nextDelegation: DelegationWithMetadata = { - ...nextDelegationDb, - authorityDelegation: null, - }; - - // Insert this delegation at the leaf of our current chain - delegation = addAuthorityDelegationAtLeaf(delegation, nextDelegation); +}: GetDelegationParams): GetDelegationDbReturnType { + const delegation = await db.query.delegations.findFirst({ + where: (delegations, { eq }) => eq(delegations.hash, hash), + with: { + // Supports MAX_DELEGATION_DEPTH levels of parent recursion + caveats: true, + auth: buildAuthConfig(MAX_DELEGATION_DEPTH), + }, + }); - // Move on to the next authority in the chain - currentAuthority = nextDelegation.authority; - } + if (!delegation) { + return; + } - return delegation; - }); + // Recursively update auth for authorityDelegation + return replaceAuthKeys(delegation); } diff --git a/apps/api-delegations/src/db/actions/delegations/get-delegations-collection-db.ts b/apps/api-delegations/src/db/actions/delegations/get-delegations-collection-db.ts index b189c126..9bffe7a7 100644 --- a/apps/api-delegations/src/db/actions/delegations/get-delegations-collection-db.ts +++ b/apps/api-delegations/src/db/actions/delegations/get-delegations-collection-db.ts @@ -2,12 +2,27 @@ import { and } from 'drizzle-orm'; import type { Address } from 'viem'; import { db } from '../../index.js'; import { sqlLower } from '../../utils.js'; +import { + MAX_DELEGATION_DEPTH, + buildAuthConfig, + replaceAuthKeys, +} from './utils.js'; +import type { DelegationWithMetadata } from 'universal-types'; + +type GetDelegationsCollectionDbReturnType = { + delegate: DelegationWithMetadata[]; + delegator: DelegationWithMetadata[]; +}; export async function getDelegationsCollectionDb({ address, chainId, type, -}: { address: Address; chainId: number; type: string }) { +}: { + address: Address; + chainId: number; + type: string; +}): Promise { const lowercasedAddress = address.toLowerCase(); const [delegate, delegator] = await db.transaction(async (tx) => @@ -21,6 +36,7 @@ export async function getDelegationsCollectionDb({ ), with: { caveats: true, + auth: buildAuthConfig(MAX_DELEGATION_DEPTH), }, }), tx.query.delegations.findMany({ @@ -32,13 +48,14 @@ export async function getDelegationsCollectionDb({ ), with: { caveats: true, + auth: buildAuthConfig(MAX_DELEGATION_DEPTH), }, }), ]), ); return { - delegate, - delegator, + delegate: delegate.map(replaceAuthKeys), + delegator: delegator.map(replaceAuthKeys), }; } diff --git a/apps/api-delegations/src/db/actions/delegations/get-delegations-db.ts b/apps/api-delegations/src/db/actions/delegations/get-delegations-db.ts index 66b50e61..3d7f3b5e 100644 --- a/apps/api-delegations/src/db/actions/delegations/get-delegations-db.ts +++ b/apps/api-delegations/src/db/actions/delegations/get-delegations-db.ts @@ -4,6 +4,11 @@ import { db } from '../../index.js'; import { delegations } from '../../schema.js'; import { sqlLower } from '../../utils.js'; import type { DelegationWithMetadata } from 'universal-types'; +import { + MAX_DELEGATION_DEPTH, + buildAuthConfig, + replaceAuthKeys, +} from './utils.js'; export type GetDelegationsDbReturnType = DelegationWithMetadata[] | undefined; @@ -32,12 +37,9 @@ export async function getDelegationsDb({ where: (_, { and }) => and(...conditions), with: { caveats: true, + auth: buildAuthConfig(MAX_DELEGATION_DEPTH), }, }); - // TODO: add authorityDelegation to each delegation once the auxiliary chaining table is live - return delegationsDb.map((delegationDb) => ({ - ...delegationDb, - authorityDelegation: null, - })); + return delegationsDb.map(replaceAuthKeys); } diff --git a/apps/api-delegations/src/db/actions/delegations/utils.ts b/apps/api-delegations/src/db/actions/delegations/utils.ts new file mode 100644 index 00000000..f054190d --- /dev/null +++ b/apps/api-delegations/src/db/actions/delegations/utils.ts @@ -0,0 +1,35 @@ +import type { DelegationWithMetadata } from 'universal-types'; + +export type ReplaceAuthKeysParams = Omit< + DelegationWithMetadata, + 'authorityDelegation' +> & { + auth: ReplaceAuthKeysParams | null; +}; + +export function replaceAuthKeys( + delegation: ReplaceAuthKeysParams, +): DelegationWithMetadata { + const { auth, ...rest } = delegation; + + return { + ...rest, + authorityDelegation: auth ? replaceAuthKeys(auth) : null, + }; +} + +export const MAX_DELEGATION_DEPTH = 5; + +export type AuthConfig = true | { with: { caveats: true; auth: AuthConfig } }; + +export function buildAuthConfig(depth: number): AuthConfig { + if (depth === 0) { + return true; // at the bottom level, 'auth' is just 'true' + } + return { + with: { + caveats: true, + auth: buildAuthConfig(depth - 1), + }, + }; +} diff --git a/apps/api-delegations/src/db/schema.ts b/apps/api-delegations/src/db/schema.ts index 75ebfeee..971e7b5e 100644 --- a/apps/api-delegations/src/db/schema.ts +++ b/apps/api-delegations/src/db/schema.ts @@ -35,8 +35,13 @@ export const delegations = pgTable('delegations', { }); // Relations -export const delegationsRelations = relations(delegations, ({ many }) => ({ +export const delegationsRelations = relations(delegations, ({ one, many }) => ({ caveats: many(caveats), + // Uses auth instead of authorityDelegation due to Drizzle's query column length limit + auth: one(delegations, { + fields: [delegations.authority], + references: [delegations.hash], + }), })); // ---------------------------------------------- diff --git a/packages/universal-types/package.json b/packages/universal-types/package.json index 37380e60..2ce30937 100644 --- a/packages/universal-types/package.json +++ b/packages/universal-types/package.json @@ -3,11 +3,7 @@ "description": "Shared types for the Universal stack.", "version": "0.0.0", "license": "MIT", - "files": [ - "src/**/*.ts", - "!src/**/*.test.ts", - "!src/**/*.test-d.ts" - ], + "files": ["src/**/*.ts", "!src/**/*.test.ts", "!src/**/*.test-d.ts"], "sideEffects": false, "type": "module", "exports": {