UNPKG

@beignet/core

Version:

Core framework primitives for Beignet

244 lines 8.06 kB
import { assertErrorsAvoidCustomResponseStatuses, assertResponsesAvoidCatalogErrorStatuses, mergeCatalogErrors, responsesFromErrors, } from "./catalog-errors.js"; import { ContractBuilder } from "./contract-builder.js"; import { assertValidContractDeprecation, } from "./lifecycle.js"; import { mergeContractMeta } from "./metadata.js"; import { parsePathTemplate } from "./path-template.js"; import { generateContractName } from "./utils.js"; function joinPathPrefix(prefix, path) { const segments = [ ...prefix.split("/").filter(Boolean), ...path.split("/").filter(Boolean), ]; return `/${segments.join("/")}`; } /** * Feature-scoped factory for related HTTP contracts. * * Contract groups let a feature share a namespace, path prefix, headers, * responses, errors, and metadata across multiple endpoint contracts. The group * is immutable: every configuration method returns a new group. */ export class ContractGroup { _namespace; _meta; _responses; _headers; _pathPrefix; constructor(state = {}) { this._namespace = state.namespace ?? ""; this._meta = state.meta ?? {}; this._responses = state.responses ?? {}; this._headers = state.headers ?? null; this._pathPrefix = state.pathPrefix ?? ""; } /** * Set the namespace for contracts created from this group. * * The namespace prefixes contract names, not paths. Use `prefix(...)` for * path composition. */ namespace(ns) { return new ContractGroup({ namespace: ns, meta: this._meta, responses: this._responses, headers: this._headers, pathPrefix: this._pathPrefix, }); } /** * Add a path prefix to contracts created from this group. * * Prefixes compose immutably, so `prefix("/api").prefix("/v1")` produces * paths under `/api/v1`. */ prefix(pathPrefix) { parsePathTemplate(pathPrefix); const nextPrefix = joinPathPrefix(this._pathPrefix, pathPrefix); const normalizedPrefix = nextPrefix === "/" ? "" : nextPrefix; return new ContractGroup({ namespace: this._namespace, meta: this._meta, responses: this._responses, headers: this._headers, pathPrefix: normalizedPrefix, }); } /** * Merge shared metadata into contracts created from this group. */ meta(meta) { return new ContractGroup({ namespace: this._namespace, meta: mergeContractMeta(this._meta, meta), responses: this._responses, headers: this._headers, pathPrefix: this._pathPrefix, }); } /** * Mark every contract created from this group as deprecated. */ deprecated(deprecation) { assertValidContractDeprecation(deprecation, this._namespace || "group"); return new ContractGroup({ namespace: this._namespace, meta: mergeContractMeta(this._meta, { deprecation }), responses: this._responses, headers: this._headers, pathPrefix: this._pathPrefix, }); } /** * Add shared route-owned response schemas to contracts created from this group. * * Framework-owned responses, such as validation or auth hook failures, do not * need to be declared here. */ responses(responseSchemas) { assertResponsesAvoidCatalogErrorStatuses(this._meta, responseSchemas); return new ContractGroup({ namespace: this._namespace, meta: this._meta, responses: { ...this._responses, ...responseSchemas }, headers: this._headers, pathPrefix: this._pathPrefix, }); } /** * Declare shared route-owned application errors for contracts in this group. * * Catalog errors use Beignet's standard error envelope and remain separate * from framework-owned errors. Declarations merge with previously declared * group errors, and contracts created from the group merge these shared * errors with route-level `.errors()` declarations; later declarations win * when the same catalog key is declared twice. */ errors(errorDefs) { const errorResponses = responsesFromErrors(errorDefs); assertErrorsAvoidCustomResponseStatuses(this._meta, this._responses, errorResponses); return new ContractGroup({ namespace: this._namespace, meta: { ...this._meta, errors: mergeCatalogErrors(this._meta, errorDefs), }, responses: { ...this._responses, ...errorResponses }, headers: this._headers, pathPrefix: this._pathPrefix, }); } /** * Add a shared request header schema to contracts created from this group. */ headers(schema) { const existingHeaders = this._headers ? Array.isArray(this._headers) ? this._headers : [this._headers] : []; return new ContractGroup({ namespace: this._namespace, meta: this._meta, responses: this._responses, headers: [ ...existingHeaders, schema, ], pathPrefix: this._pathPrefix, }); } /** * Create a GET contract under this group. */ get(path, name) { return this.createBuilder("GET", path, name); } /** * Create a POST contract under this group. */ post(path, name) { return this.createBuilder("POST", path, name); } /** * Create a PUT contract under this group. */ put(path, name) { return this.createBuilder("PUT", path, name); } /** * Create a PATCH contract under this group. */ patch(path, name) { return this.createBuilder("PATCH", path, name); } /** * Create a DELETE contract under this group. */ delete(path, name) { return this.createBuilder("DELETE", path, name); } /** * Create a HEAD contract under this group. */ head(path, name) { return this.createBuilder("HEAD", path, name); } /** * Create an OPTIONS contract under this group. */ options(path, name) { return this.createBuilder("OPTIONS", path, name); } /** * Internal helper to create a contract builder with shared config */ createBuilder(method, path, name) { parsePathTemplate(path); const fullPath = (this._pathPrefix ? joinPathPrefix(this._pathPrefix, path) : path); parsePathTemplate(fullPath); const contractName = name || generateContractName(method, fullPath); const fullName = this._namespace ? `${this._namespace}.${contractName}` : contractName; return new ContractBuilder({ kind: "http", name: fullName, namespace: this._namespace || undefined, localName: contractName, method, path: fullPath, pathParams: null, query: null, headers: this._headers, body: null, responses: { ...this._responses }, metadata: { ...this._meta }, }); } } /** * Create a new feature contract group. * * Start here for most feature HTTP surfaces, then add a namespace and path * prefix before defining individual contracts. * * @example * ```ts * const todos = defineContractGroup() * .namespace("todos") * .prefix("/api/todos") * .meta({ auth: "required" }) * .responses({ * 401: z.object({ message: z.literal("Unauthorized") }), * }); * * const getTodo = todos.get("/:id")... * ``` * * @returns An empty immutable contract group. */ export function defineContractGroup() { return new ContractGroup(); } //# sourceMappingURL=contract-group.js.map