import { logger } from "@tailor-platform/sdk/runtime"; import type { InsufficientPermissionError } from "./errors"; import { UnauthenticatedError } from "./errors"; import { inputAttr } from "./logAttrs"; import { requirePermission } from "./permissions"; import { err, type Result } from "./result"; import type { CallerContext, QueryContext, ReadonlyDB } from "./types"; /** * Queries carry no permission gate of their own — access is decided at the edge, * by the resolver's `permission` (or the namespace `defaultPermission`) in * `tailor.config.ts`. An implementation therefore receives {@link CallerContext}, * whose `actorId` is `null` for an anonymous caller, and a query that scopes its * result to the caller has to say what that means. */ export type Query = ( db: ReadonlyDB, input: TInput, ctx: CallerContext, ) => Promise; /** * A gated query reports two failures of its own — no actor, and no permission — so * its implementation has to return a `Result` for those to have somewhere to go. */ type GatedResult> = | TResult | Result> | Result>; type Impl = ( db: ReadonlyDB, input: TInput, ctx: CallerContext, ) => Promise; // Overload: name only (no permission) export function defineQuery( name: string, impl: Impl, ): Query; // Overload: name + permission. The gate rejects a caller with no actor, so this // implementation takes QueryContext rather than CallerContext, and returns a // `Result` so the gate's own failures have somewhere to go. export function defineQuery>( name: string, permission: string, impl: (db: ReadonlyDB, input: TInput, ctx: QueryContext) => Promise, ): Query>; // Implementation export function defineQuery( name: string, ...rest: | [Impl] | [ permission: string, impl: (db: ReadonlyDB, input: TInput, ctx: QueryContext) => Promise, ] // oxlint-disable-next-line typescript/no-explicit-any -- generic return type ): Query { if (rest.length === 2) { const [permission, impl] = rest; return async (db, input, ctx) => { const { actorId } = ctx; if (actorId === null) { logger.debug("query unauthenticated", { query: name }); return err(new UnauthenticatedError()); } const authenticated: QueryContext = { ...ctx, actorId }; const check = requirePermission(authenticated, permission); if (!check.ok) { logger.debug("query permission denied", { query: name, permission }); return check; } logger.debug("query start", { query: name, input: inputAttr(input) }); const start = Date.now(); const result = await impl(db, input, authenticated); const durationMs = Date.now() - start; logger.debug("query ok", { query: name, durationMs }); return result; }; } const [impl] = rest; return async (db, input, ctx) => { logger.debug("query start", { query: name, input: inputAttr(input) }); const start = Date.now(); const result = await impl(db, input, ctx); const durationMs = Date.now() - start; logger.debug("query ok", { query: name, durationMs }); return result; }; }