Initial Commit
This commit is contained in:
122
packages/api-stack/api/api-plugin-tenant/src/schemas/options.ts
Normal file
122
packages/api-stack/api/api-plugin-tenant/src/schemas/options.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
// schema: boundary-only — TenantStrategySpecSchema is exposed for
|
||||
// consumers building stack-specific factories on top of the plugin's
|
||||
// option shape. The plugin itself parses options via
|
||||
// TenantPluginOptionsSchema (which embeds the strategy schema), so
|
||||
// `TenantStrategySpecSchema.parse(...)` does not appear at the plugin's
|
||||
// own call sites.
|
||||
import z from "zod";
|
||||
|
||||
/**
|
||||
* Strategy spec schemas — one per resolver type. Composed into a Zod
|
||||
* discriminated union so the plugin can route each spec to the right
|
||||
* concrete `TenantResolver` implementation.
|
||||
*/
|
||||
const HeaderStrategySchema = z.object({
|
||||
type: z.literal("header"),
|
||||
headerName: z.string().min(1),
|
||||
});
|
||||
|
||||
const SubdomainStrategySchema = z.object({
|
||||
type: z.literal("subdomain"),
|
||||
baseDomain: z.string().min(1),
|
||||
});
|
||||
|
||||
const DomainStrategySchema = z.object({ type: z.literal("domain") });
|
||||
|
||||
const PathStrategySchema = z.object({
|
||||
type: z.literal("path"),
|
||||
prefix: z.string().regex(/^\//u, "prefix must start with '/'"),
|
||||
});
|
||||
|
||||
const JWTStrategySchema = z.object({
|
||||
type: z.literal("jwt"),
|
||||
claimName: z.string().min(1),
|
||||
});
|
||||
|
||||
const QueryStrategySchema = z.object({
|
||||
type: z.literal("query"),
|
||||
paramName: z.string().min(1),
|
||||
});
|
||||
|
||||
const CookieStrategySchema = z.object({
|
||||
type: z.literal("cookie"),
|
||||
cookieName: z.string().min(1),
|
||||
});
|
||||
|
||||
export const TenantStrategySpecSchema = z.discriminatedUnion("type", [
|
||||
HeaderStrategySchema,
|
||||
SubdomainStrategySchema,
|
||||
DomainStrategySchema,
|
||||
PathStrategySchema,
|
||||
JWTStrategySchema,
|
||||
QueryStrategySchema,
|
||||
CookieStrategySchema,
|
||||
]);
|
||||
|
||||
const CacheSchema = z.object({
|
||||
ttlMs: z.number().int().positive(),
|
||||
negativeTtlMs: z.number().int().positive().optional(),
|
||||
maxEntries: z.number().int().positive().optional(),
|
||||
});
|
||||
|
||||
const RateLimitSchema = z.object({
|
||||
maxRequests: z.number().int().positive(),
|
||||
windowMs: z.number().int().positive(),
|
||||
store: z
|
||||
.custom<unknown>(
|
||||
(val) => val !== null && typeof val === "object",
|
||||
"store must be a RateLimitStore object"
|
||||
)
|
||||
.optional(),
|
||||
keyPrefix: z.string().min(1).optional(),
|
||||
});
|
||||
|
||||
const StoreSchema = z.custom<unknown>(
|
||||
(val) =>
|
||||
val !== null &&
|
||||
typeof val === "object" &&
|
||||
typeof (val as { getById?: unknown }).getById === "function" &&
|
||||
typeof (val as { create?: unknown }).create === "function",
|
||||
"store must implement the TenantStore interface (getById/create/update/delete)"
|
||||
);
|
||||
|
||||
const LoggerSchema = z.custom<unknown>(
|
||||
(val) =>
|
||||
val !== null &&
|
||||
typeof val === "object" &&
|
||||
typeof (val as { info?: unknown }).info === "function",
|
||||
"logger must be a LoggerLike with debug/info/warn/error"
|
||||
);
|
||||
|
||||
/**
|
||||
* Zod schema for `TenantPluginOptions`. Validated by `createTenantPlugin`
|
||||
* at the factory boundary — invalid input throws
|
||||
* `TenantPluginError(INVALID_INPUT)` carrying the `ZodError` as `cause`.
|
||||
*
|
||||
* `routePrefix` MUST start with `/` and MUST NOT end with `/` (unless it
|
||||
* is exactly `/`).
|
||||
*/
|
||||
export const TenantPluginOptionsSchema = z.object({
|
||||
strategies: z.array(TenantStrategySpecSchema).min(1),
|
||||
cache: CacheSchema.optional(),
|
||||
required: z.boolean().default(false),
|
||||
excludePaths: z.array(z.string()).optional(),
|
||||
allowSuspended: z.boolean().default(false),
|
||||
routePrefix: z
|
||||
.string()
|
||||
.regex(/^\/[A-Za-z0-9\-_/]*[A-Za-z0-9\-_]$|^\/$/u, {
|
||||
message:
|
||||
"routePrefix must start with '/' and not end with '/' (unless it is exactly '/')",
|
||||
})
|
||||
.default("/tenants"),
|
||||
store: StoreSchema.optional(),
|
||||
logger: LoggerSchema.optional(),
|
||||
rateLimit: RateLimitSchema.optional(),
|
||||
});
|
||||
|
||||
export type TenantPluginOptionsInput = z.input<
|
||||
typeof TenantPluginOptionsSchema
|
||||
>;
|
||||
export type TenantPluginOptionsResolved = z.output<
|
||||
typeof TenantPluginOptionsSchema
|
||||
>;
|
||||
Reference in New Issue
Block a user