Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 34 additions & 17 deletions supabase/functions/_shared/contracts/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,32 +51,45 @@ export interface ParseOptions {
prereadBody?: string;
}

export type ParseResult<V extends string, S extends Record<V, z.ZodTypeAny>> =
| {
/**
* Resultado de sucesso discriminado pela versão resolvida.
*
* Gera uma união `{ version: "1"; data: v1 } | { version: "2"; data: v2 } | ...`
* a partir de `C["versions"]`, de modo que `version` e `data` permaneçam
* correlacionados em tempo de compilação (cada versão expõe o shape do seu
* próprio schema, sem colapsar tudo no shape da versão default).
*
* Importante: indexamos os schemas diretamente (sem a interseção
* `ContractSchemas<V> & { versions: S }` usada anteriormente). Aquela interseção
* forçava `versions[k]` a virar `ZodTypeAny & ZodObject<...>`, fazendo o checker
* recursar no retorno de `ZodObject.deepPartial()` e falhar com TS2345
* (15 edge functions de contratos quebravam o `deno check`).
*/
type ParseSuccessByVersion<C extends ContractSchemas> = {
[K in keyof C["versions"] & string]: {
ok: true;
version: V;
/** Dados parseados; o tipo casa com o schema da versão resolvida. */
data: { [K in V]: z.infer<S[K]> }[V];
/** Headers que a resposta de sucesso deve incluir (versão, deprecation). */
version: K;
data: z.infer<C["versions"][K]>;
responseHeaders: Record<string, string>;
}
};
}[keyof C["versions"] & string];

export type ParseResult<C extends ContractSchemas = ContractSchemas> =
| ParseSuccessByVersion<C>
| { ok: false; response: Response };

/**
* Parseia, valida e versiona o body de uma requisição.
*/
export async function parseContract<
V extends string,
S extends Record<V, z.ZodTypeAny>,
>(
export async function parseContract<C extends ContractSchemas>(
req: Request,
schemas: ContractSchemas<V> & { versions: S },
schemas: C,
opts: ParseOptions = {},
): Promise<ParseResult<V, S>> {
): Promise<ParseResult<C>> {
const corsHeaders = opts.corsHeaders ?? {};

// 1. Resolver versão
const supportedVersions = Object.keys(schemas.versions) as V[];
const supportedVersions = Object.keys(schemas.versions);
const versionConfig: VersionConfig = {
supported: supportedVersions,
default: schemas.defaultVersion,
Expand All @@ -86,7 +99,7 @@ export async function parseContract<
if (!vRes.ok) return { ok: false, response: vRes.response };

const { version, responseHeaders } = vRes.resolved;
const schema = schemas.versions[version as V];
const schema = schemas.versions[version as keyof C["versions"]];

// 2. Ler body (uma única vez)
let rawText: string;
Expand Down Expand Up @@ -150,10 +163,14 @@ export async function parseContract<
};
}

// O TS nao consegue inferir, dentro do corpo generico, que este objeto
// satisfaz a uniao mapeada `ParseSuccessByVersion<C>` (a uniao colapsa para
// `never` sobre um `C` ainda nao resolvido). A validacao real foi feita por
// `resolveContractVersion` + `schema.safeParse`, entao o cast e seguro.
return {
ok: true,
version: version as V,
version,
data: result.data,
responseHeaders,
};
} as ParseResult<C>;
}