@beignet/core
Version:
Core framework primitives for Beignet
244 lines • 8.06 kB
JavaScript
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