/** * The kinds Rebase ships, and the constructors a project declares them with. * * Each kind is registered rather than hardcoded, so a fourth one arrives * without editing a manifest schema, a validator and a switch statement. That * cost is precisely why databases and buckets ended up declared in different * files with different rules — the cheapest thing to do was always to bolt the * new kind onto whichever home was nearest. * * A kind owns its engine list. `custom:` is always accepted, so a build * that ships an engine this package has never heard of says so at the call site * instead of looking like a typo of one that exists. */ import { type DeclareOptions, type ResourceDeclaration, type ResourceHandle } from "./resources.js"; import { type DataSourceDefinition } from "./data_source.js"; import { type StorageSourceDefinition } from "./storage_source.js"; /** Options a database accepts beyond the common ones. */ export interface DatabaseOptions extends DeclareOptions { /** * The physical database or schema within the engine, when it differs from * the engine's own default. Threaded to drivers as `databaseId`. */ databaseId?: string; /** Directory of migration files, relative to the config directory. */ migrations?: string; /** * Server extensions Rebase may install on this database. * * A permission, not a request: naming one grants leave to run * `CREATE EXTENSION IF NOT EXISTS `, and Rebase issues it only when * something in the schema actually needs it. Naming an extension nothing * needs installs nothing. * * It has to be said out loud because installing an extension is a decision * with a deployment behind it — the image has to ship the library, the role * has to be allowed to install it, and a managed provider has to have it on * an allow-list. Rebase cannot see any of that from inside the connection, * so the answer comes from whoever chose the database. * * Today `vector` is the one that matters: a `{ type: "vector" }` property * compiles to a `VECTOR(n)` column, which does not exist until pgvector is * installed. Without this, Rebase creates the column and lets Postgres * refuse, naming the option. * * ```ts * export const main = database({ extensions: ["vector"] }); * ``` * * `pg_trgm` and `unaccent` are not on this list and need no permission: a * `search` block installs them unasked, because they are contrib modules * present in every Postgres distribution. pgvector is a separate build that * a stock `postgres:18` does not carry. */ extensions?: string[]; } /** A database handle. Collections point at it via `dataSource`. */ export type DatabaseHandle = ResourceHandle; /** * Declare a database. * * ```ts * export const main = database(); // the default one * export const analytics = database("analytics"); // reads DATABASE_URL__ANALYTICS * export const withPgv = database({ extensions: ["vector"] }); // the default one, configured * ``` * * The third form exists because the default database has no name to pass, and * the alternative was `database("(default)", { … })` — writing out an internal * sentinel to reach the options. A key is a string and options are an object, * so the two can never be confused for one another. */ export declare function database(options?: DatabaseOptions): DatabaseHandle; export declare function database(key?: string, options?: DatabaseOptions): DatabaseHandle; /** * The extensions the project's databases gave Rebase leave to install. * * A flat union rather than a per-database answer, because the surfaces that ask * — `rebase db push` and the boot schema-ensure — drive one connection and * generate one `schema.sql` for every collection regardless of `dataSource`. * Splitting the permission by data source would be a distinction the rest of * that pipeline does not make, and a false precision is worse than none. * * Empty for a project that declared nothing, which is every project that has * not opted in — so this reads as a refusal by default, on purpose. */ export declare function declaredDatabaseExtensions(): readonly string[]; /** Options a bucket accepts beyond the common ones. */ export interface BucketOptions extends DeclareOptions { /** * Whether objects are world-readable by default. * * Declared rather than inferred from the engine, because the two have * disagreed before: a private object served through a cacheable public URL * is a data leak that nothing errors on. */ publicRead?: boolean; /** Key prefix within the bucket, for sharing one bucket between sources. */ prefix?: string; /** * Serve unqualified uploads — a storage property with no `storageSource` — * from this bucket. * * A project that declares only `bucket("media")` has no default bucket, and * the registry used to promote the one it found with a warning. That is a * decision about where a user's files land, made by the framework, on the * strength of declaration order; it also produced two different * destinations either side of a deploy, because the synthesized local * default is dropped in production and the promotion is not. So it is now * a boot error, and this is one of the two ways to answer it — the other * being `bucket()`, which declares the default bucket itself. */ default?: boolean; /** * The credential set this bucket signs with, when several share one. * * `bucket("media", { engine: "s3", account: "minio" })` keeps reading its own * `S3_BUCKET__MEDIA` — the bucket name is what distinguishes one source from * another and never falls back — while the provider-level variables * (`S3_ACCESS_KEY_ID`, `S3_SECRET_ACCESS_KEY`, `S3_ENDPOINT`, `S3_REGION`, * `S3_FORCE_PATH_STYLE`) fall back to `__MINIO` when no per-key value is set. * * Fifteen buckets on one install go from ninety variables to eighteen, and * rotating the key becomes one edit. A per-bucket value still wins, so a * single source can move to another provider without breaking the rest off * their shared account. */ account?: string; } /** A bucket handle. Storage properties point at it via `storageSource`. */ export type BucketHandle = ResourceHandle; /** * Declare a bucket. * * ```ts * export const uploads = bucket({ engine: "s3" }); // the default one * export const media = bucket("media", { transport: "direct" }); * ``` * * `transport: "direct"` means a provider SDK talks to the bucket and the * backend is not in the upload path. * * The options-only form exists for the same reason `database`'s does: the * default bucket has no name to pass, and without it the only way to configure * one was `bucket("(default)", { … })` — writing out an internal sentinel to * reach the options. Passing options where a key belongs used to throw "a * bucket needs a non-empty key", which names neither the mistake nor the fix. */ export declare function bucket(options?: BucketOptions): BucketHandle; export declare function bucket(key?: string, options?: BucketOptions): BucketHandle; /** * How hard the runtime tries to deliver. * * Only `at-least-once` is implemented, and it is the honest name for what a * retrying queue does: a handler must tolerate seeing the same event twice. * `at-most-once` is listed so a future transport can offer it without the * option changing shape, and is refused today rather than silently upgraded. */ export type TopicDelivery = "at-least-once" | "at-most-once"; /** Options a topic accepts beyond the common ones. */ export interface TopicOptions extends DeclareOptions { delivery?: TopicDelivery; /** Attempts per subscription before a message is left failed. Default 5. */ maxAttempts?: number; } /** * What a subscription does with an event. * * `attempt` counts from 1. Worth branching on: the first delivery and the * fourth are the same call, but the fourth is where it is worth logging loudly. */ export type TopicHandler = (event: T, context: { attempt: number; topic: string; subscription: string; }) => Promise | void; /** A declared subscription, as recorded in the graph and wired at boot. */ export interface TopicSubscription { topic: string; name: string; handler: TopicHandler; maxAttempts?: number; } /** * What a topic publishes through. * * Installed by `@rebasepro/server` at boot. Absent — in the CLI evaluating * config to derive the graph, or in a unit test — publishing throws a message * naming the cause, rather than resolving and dropping the event. A publish * that silently does nothing is the failure mode a queue exists to prevent. */ export interface TopicRuntime { publish(topic: string, event: unknown): Promise; } /** Install the transport topics publish through. Called by the server at boot. */ export declare function setTopicRuntime(runtime: TopicRuntime | null): void; /** Every declared subscription, for the worker to wire and the graph to record. */ export declare function declaredSubscriptions(topic?: string): TopicSubscription[]; /** Forget declared subscriptions. For tests, alongside `resetDeclaredResources`. */ export declare function resetDeclaredSubscriptions(): void; /** A topic handle, carrying its payload type. */ export interface TopicHandle extends ResourceHandle { /** * Publish an event. * * Resolves once the event is durably recorded for every subscription, not * once they have run. Enqueued inside a transaction that rolls back, it was * never published. */ publish(event: T): Promise; /** * Declare a subscription. * * The name is its identity: it is what the job row records, what a retry * counts against, and what a second subscription must not collide with. */ subscription(name: string, handler: TopicHandler, options?: { maxAttempts?: number; }): void; } /** * Declare a topic. * * ```ts * export const signups = topic<{ userId: string }>("signups"); * signups.subscription("send-welcome", async (event) => { … }); * await signups.publish({ userId }); * ``` */ export declare function topic(key: string, options?: TopicOptions): TopicHandle; /** What a cron declaration records, beyond its handler. */ export interface CronResourceOptions extends DeclareOptions { /** Five-field cron expression, e.g. `0 3 * * *`. */ schedule: string; /** * IANA zone the schedule is read in, e.g. `Europe/Madrid`. * * Without it the schedule is read in the process's own zone, which is * whatever the host happens to be set to — UTC in nearly every container, * the developer's own on a laptop. "3 AM" then means two different hours * either side of a deploy. Naming the zone makes the declaration mean one * thing everywhere. */ timezone?: string; description?: string; enabled?: boolean; timeoutSeconds?: number; catchUpWindowSeconds?: number; } /** * Declare a cron, as the scheduler's `defineCron` does on its way through. * * Projects do not call this: `defineCron` in `@rebasepro/server` does, so a * cron file is both the handler and the declaration — one file, one name, and * the graph derived from it says what a host needs to know without evaluating * the handler. Exported so the derive step and the scheduler spell the * declaration identically. */ export declare function declareCron(name: string, options: CronResourceOptions): ResourceHandle; /** * What a function declaration records. * * Recorded by the derive step from the bundler's static analysis rather than * by evaluating the function module: a function's handler is a Hono app that * only needs to exist at request time, and evaluating it at build time would * run its module-scope code in a process with none of its environment. */ export interface FunctionResourceOptions extends DeclareOptions { /** Path inside the project, so a host can point at the file. */ file?: string; /** `false` when the source imports a Node built-in or a package that needs one. */ portable?: boolean; /** Why it is not portable — one short phrase per reason. */ requires?: string[]; } /** Declare a function. Called by the derive step, not by projects. */ export declare function declareFunction(name: string, options?: FunctionResourceOptions): ResourceHandle; /** Options a queue accepts beyond the common ones. */ export interface QueueOptions extends DeclareOptions { /** Attempts before a job is left failed. Default 5. */ maxAttempts?: number; } /** What a queue's handler receives. `attempt` counts from 1. */ export type QueueHandler = (payload: T, context: { attempt: number; queue: string; jobId: string; }) => Promise | void; /** Per-job options at enqueue time. */ export interface QueueEnqueueOptions { /** Earliest time the job may run. Defaults to now. */ runAt?: Date; /** Attempts for this job, overriding the queue's. */ maxAttempts?: number; } /** * What a queue enqueues through. * * Installed by `@rebasepro/server` at boot, alongside the topic runtime. * Absent — config evaluated by the CLI, a unit test — enqueueing throws with * the cause named, rather than resolving and dropping the job. */ export interface QueueRuntime { enqueue(queue: string, payload: unknown, options?: QueueEnqueueOptions): Promise<{ id: string; }>; } /** Install the transport queues enqueue through. Called by the server at boot. */ export declare function setQueueRuntime(runtime: QueueRuntime | null): void; /** A queue's handler, as recorded for the worker to wire. */ export interface QueueConsumer { queue: string; handler: QueueHandler; } /** Every declared queue handler, for the worker to wire. */ export declare function declaredQueueConsumers(): QueueConsumer[]; /** Forget declared queue handlers. For tests, alongside `resetDeclaredResources`. */ export declare function resetDeclaredQueueConsumers(): void; /** A queue handle, carrying its payload type. */ export interface QueueHandle extends ResourceHandle { /** * Put a job on the queue. * * Resolves once the job is durably recorded, not once it has run. A row * insert, so enqueued inside a transaction that rolls back it was never * enqueued. */ enqueue(payload: T, options?: QueueEnqueueOptions): Promise<{ id: string; }>; /** * Declare the handler. * * One per queue: a queue is a work list with one consumer, which is what * separates it from a topic. Work that several things must react to is a * topic with several subscriptions. */ handler(fn: QueueHandler): void; } /** * Declare a queue. * * ```ts * export const thumbnails = queue<{ key: string }>("thumbnails"); * thumbnails.handler(async ({ key }) => { … }); * await thumbnails.enqueue({ key }, { runAt: new Date(Date.now() + 60_000) }); * ``` * * The difference from a topic is the number of consumers: a queue has one, a * topic fans out to every subscription. Both ride on the durable job queue, so * declaring either turns it on. */ export declare function queue(key: string, options?: QueueOptions): QueueHandle; /** * One declaration, as the data layer's definition. * * There is exactly one of these per kind, and everything that needs a * definition goes through it — the frontend, the managed runtime's boot path, * and an ejected project's own entrypoint. That is not tidiness: the mapping * used to exist twice, once here and once in `@rebasepro/server`'s * `graphToStorageSources`, and the two disagreed. The server's copy carried a * bucket's `account`; this one dropped it, so a bucket declared with shared * credentials resolved them on the managed runtime and resolved *nothing* in an * ejected backend — the source was skipped and every upload to it answered 501. * * A field-by-field map is one line away from that failure at all times, so * there is now one line to keep right instead of two to keep equal. */ export declare function resourceToDataSource(declaration: ResourceDeclaration): DataSourceDefinition; /** One declaration, as the storage layer's definition. See {@link resourceToDataSource}. */ export declare function resourceToStorageSource(declaration: ResourceDeclaration): StorageSourceDefinition; /** * The declared databases, as definitions. * * Both the frontend and a project's own backend entrypoint read this. The * frontend needs to know which sources exist and how they are reached — a * `direct`-transport source is one the browser talks to itself — and it imports * the same config package the backend does. Without these it would mean writing * the list a second time, by hand, next to the declarations, which is precisely * the two-homes problem this model removed everywhere else. * * ```tsx * import "../config/resources"; // registers them * import { declaredDataSources, declaredStorageSources } from "@rebasepro/types"; * * * ``` * * The import is what registers them, so a bundler that drops an unused module * would leave this empty — hence the side-effect import above rather than a * bare re-export. */ export declare function declaredDataSources(): DataSourceDefinition[]; /** The declared buckets, as definitions. */ export declare function declaredStorageSources(): StorageSourceDefinition[];