@fabrika/platform-node 0.9.0-dev.1 → 0.9.0-dev.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +7 -6
- package/types/src/application-postgres-grants.d.ts +19 -0
- package/types/src/application-postgres-migrations.d.ts +59 -0
- package/types/src/application-postgres-roles.d.ts +33 -0
- package/types/src/application-schema.d.ts +3 -0
- package/types/src/asset-server-fs.d.ts +16 -0
- package/types/src/blob-s3.d.ts +34 -0
- package/types/src/connection-capacity.d.ts +29 -0
- package/types/src/database-code.d.ts +16 -0
- package/types/src/file-table-ddl.d.ts +36 -0
- package/types/src/index.d.ts +15 -0
- package/types/src/job-queue-postgres.d.ts +106 -0
- package/types/src/migration-classify.d.ts +105 -0
- package/types/src/migration-sql.d.ts +63 -0
- package/types/src/pg-catalog-names.d.ts +11 -0
- package/types/src/placeholders.d.ts +13 -0
- package/types/src/postgres-migration-error.d.ts +4 -0
- package/types/src/postgres-migrations.d.ts +63 -0
- package/types/src/postgres-test-provisioner.d.ts +37 -0
- package/types/src/service-postgres-migrations.d.ts +25 -0
- package/types/src/sql-names.d.ts +101 -0
- package/types/src/sql-postgres.d.ts +51 -0
- package/types/src/sql.d.ts +6 -0
- package/types/src/wait-until.d.ts +20 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fabrika/platform-node",
|
|
3
|
-
"version": "0.9.0-dev.
|
|
3
|
+
"version": "0.9.0-dev.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -13,22 +13,23 @@
|
|
|
13
13
|
"files": [
|
|
14
14
|
"src",
|
|
15
15
|
"migrations",
|
|
16
|
-
"!src/**/__tests__/**"
|
|
16
|
+
"!src/**/__tests__/**",
|
|
17
|
+
"types"
|
|
17
18
|
],
|
|
18
19
|
"exports": {
|
|
19
20
|
".": {
|
|
20
21
|
"bun": "./src/index.ts",
|
|
21
|
-
"types": "./src/index.ts",
|
|
22
|
+
"types": "./types/src/index.d.ts",
|
|
22
23
|
"default": "./src/index.ts"
|
|
23
24
|
},
|
|
24
25
|
"./postgres-migrations": {
|
|
25
26
|
"bun": "./src/postgres-migrations.ts",
|
|
26
|
-
"types": "./src/postgres-migrations.ts",
|
|
27
|
+
"types": "./types/src/postgres-migrations.d.ts",
|
|
27
28
|
"default": "./src/postgres-migrations.ts"
|
|
28
29
|
},
|
|
29
30
|
"./sql": {
|
|
30
31
|
"bun": "./src/sql.ts",
|
|
31
|
-
"types": "./src/sql.ts",
|
|
32
|
+
"types": "./types/src/sql.d.ts",
|
|
32
33
|
"default": "./src/sql.ts"
|
|
33
34
|
}
|
|
34
35
|
},
|
|
@@ -37,6 +38,6 @@
|
|
|
37
38
|
"test": "bun test"
|
|
38
39
|
},
|
|
39
40
|
"dependencies": {
|
|
40
|
-
"@fabrika/platform": "0.9.0-dev.
|
|
41
|
+
"@fabrika/platform": "0.9.0-dev.2"
|
|
41
42
|
}
|
|
42
43
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { SqlDatabase } from '@fabrika/platform';
|
|
2
|
+
/**
|
|
3
|
+
* Takes the migration's session-level advisory lock. Any role may take any advisory lock, the application's own
|
|
4
|
+
* runtime included, so an application's wait is bounded by `timeoutMs`; a service plan waits as long as it takes.
|
|
5
|
+
*/
|
|
6
|
+
export declare function acquireMigrationLock(db: SqlDatabase, lockKey: number, timeoutMs: number | undefined, label: string): Promise<void>;
|
|
7
|
+
/**
|
|
8
|
+
* Clears PUBLIC from the default privileges of the session's role (the migration role), globally and in its schema,
|
|
9
|
+
* before the plan creates anything. A default the provisioning credential or a superuser set would otherwise hand
|
|
10
|
+
* every table a later migration creates to the runtime role through PUBLIC.
|
|
11
|
+
*/
|
|
12
|
+
export declare function normalizeDefaultPrivileges(db: SqlDatabase, schema: string): Promise<void>;
|
|
13
|
+
/**
|
|
14
|
+
* Resets the schema's privileges to what the runtime role needs: PUBLIC and the runtime role lose everything on the
|
|
15
|
+
* schema and its objects, then the runtime role gets USAGE, DML on every table but the ledger, EXECUTE on the schema's
|
|
16
|
+
* routines and USAGE on its types. One transaction, so a live runtime never sees a gap. Then the effective privileges
|
|
17
|
+
* are verified, PUBLIC included, and anything beyond that stops the deploy.
|
|
18
|
+
*/
|
|
19
|
+
export declare function grantRuntimeRole(db: SqlDatabase, schema: string, ledgerTable: string, role: string): Promise<void>;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { SqlDatabase } from '@fabrika/platform';
|
|
2
|
+
import { type ApplicationDatabaseRoles } from './application-postgres-roles';
|
|
3
|
+
import { type DatabaseObject } from './migration-classify';
|
|
4
|
+
import { type PostgresMigration, type PostgresMigrationPlan } from './postgres-migrations';
|
|
5
|
+
/** Migration identity of every application (ADR-0017): bundle `app`, ledger `app_<id>.fabrika_migrations`. */
|
|
6
|
+
export declare const APPLICATION_MIGRATION_BUNDLE = "app";
|
|
7
|
+
export declare const APPLICATION_MIGRATION_LEDGER = "fabrika_migrations";
|
|
8
|
+
/** Fails a migration that waits this long for a lock, so a blocked `ALTER` does not queue the live version's queries. */
|
|
9
|
+
export declare const DEFAULT_MIGRATION_LOCK_TIMEOUT_MS = 5000;
|
|
10
|
+
/** Bounds a statement such as a backfill or a non-concurrent index build, which holds its lock while it runs. */
|
|
11
|
+
export declare const DEFAULT_MIGRATION_STATEMENT_TIMEOUT_MS = 60000;
|
|
12
|
+
/** How long `migrate` waits for the application's migration lock: another container's migrations, or a stray holder. */
|
|
13
|
+
export declare const DEFAULT_MIGRATION_ADVISORY_LOCK_TIMEOUT_MS = 300000;
|
|
14
|
+
export interface PostgresApplicationMigrationsConfig {
|
|
15
|
+
readonly appId: string;
|
|
16
|
+
/** The application's `db/migrations/` directory. */
|
|
17
|
+
readonly directory: string;
|
|
18
|
+
readonly lockTimeoutMs?: number;
|
|
19
|
+
readonly statementTimeoutMs?: number;
|
|
20
|
+
readonly advisoryLockTimeoutMs?: number;
|
|
21
|
+
/**
|
|
22
|
+
* The framework's own tables (`fabrika_*`, `@fabrika/db/migrations`), applied as their own bundle before the
|
|
23
|
+
* application's, in the same ledger and under the same checksum rule.
|
|
24
|
+
*/
|
|
25
|
+
readonly framework?: {
|
|
26
|
+
readonly bundle: string;
|
|
27
|
+
readonly migrations: readonly PostgresMigration[];
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export interface PostgresApplicationMigrations {
|
|
31
|
+
/** `app_<id>`: the application's schema, which the data layer sets as its `search_path`. */
|
|
32
|
+
readonly schema: string;
|
|
33
|
+
readonly roles: ApplicationDatabaseRoles;
|
|
34
|
+
read(): PostgresMigration[];
|
|
35
|
+
plan(migrations?: readonly PostgresMigration[]): PostgresMigrationPlan;
|
|
36
|
+
/**
|
|
37
|
+
* The manifest of the framework's and the application's migrations: the database objects they declare, which the
|
|
38
|
+
* catalog check admits at every start (`fabrika db codegen`) as the runner does before each commit.
|
|
39
|
+
*/
|
|
40
|
+
objects(migrations?: readonly PostgresMigration[]): DatabaseObject[];
|
|
41
|
+
/** Applies the plan over `db` as whatever role it connects as, and grants no runtime role: the runner's own tests. */
|
|
42
|
+
apply(db: SqlDatabase, migrations?: readonly PostgresMigration[]): Promise<string[]>;
|
|
43
|
+
/**
|
|
44
|
+
* What `fabrika db migrate` runs. With the provisioning credential it creates or checks the role family and the
|
|
45
|
+
* schema; then it applies the plan as the migration role and grants the runtime role. No migration runs as the
|
|
46
|
+
* provisioning credential, and a credential that cannot create roles is refused.
|
|
47
|
+
*/
|
|
48
|
+
migrate(provisioningUrl: string, migrations?: readonly PostgresMigration[]): Promise<string[]>;
|
|
49
|
+
}
|
|
50
|
+
/** The first 48 bits of SHA-256(`fabrika-db:<appId>`), plus one: applications sharing a database do not serialize each other. */
|
|
51
|
+
export declare function applicationMigrationLockKey(appId: string): number;
|
|
52
|
+
/** The application's schema, its migration role (named like the schema) and its runtime role `run_<id>`. */
|
|
53
|
+
export declare function applicationDatabaseRoles(appId: string): ApplicationDatabaseRoles;
|
|
54
|
+
/** The runtime role's connection URL, derived from the provisioning credential the platform hands the application. */
|
|
55
|
+
export declare function applicationRuntimeUrl(provisioningUrl: string, appId: string): string;
|
|
56
|
+
/** Drops an application's schema and both of its roles, as the provisioning credential: for throwaway test databases. */
|
|
57
|
+
export declare function dropApplicationDatabase(provisioningUrl: string, appId: string): Promise<void>;
|
|
58
|
+
/** An application's migration wrapper: its schema, ledger, lock and timeouts are derived, never configured. */
|
|
59
|
+
export declare function definePostgresApplicationMigrations(config: PostgresApplicationMigrationsConfig): PostgresApplicationMigrations;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { SqlDatabase } from '@fabrika/platform';
|
|
2
|
+
/** An application's schema and the two roles that reach it (ADR-0017). */
|
|
3
|
+
export interface ApplicationDatabaseRoles {
|
|
4
|
+
/** `app_<id>`. */
|
|
5
|
+
readonly schema: string;
|
|
6
|
+
/** Owns the schema and every object in it, so it is the only role that runs DDL there. Named like the schema. */
|
|
7
|
+
readonly migration: string;
|
|
8
|
+
/** `run_<id>`: DML on the schema's tables except the migration ledger, and nothing else. */
|
|
9
|
+
readonly runtime: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function provisioningCredential(provisioningUrl: string): {
|
|
12
|
+
readonly url: URL;
|
|
13
|
+
readonly password: string;
|
|
14
|
+
};
|
|
15
|
+
/** Postgres' SCRAM-SHA-256 verifier (RFC 7677): the password itself never reaches the server, so no statement log holds it. */
|
|
16
|
+
export declare function scramVerifier(password: string): string;
|
|
17
|
+
/** The URL `role` connects with: the provisioning URL's host, database and options, the role's name and derived password. */
|
|
18
|
+
export declare function postgresRoleUrl(provisioningUrl: string, role: string): string;
|
|
19
|
+
/**
|
|
20
|
+
* Creates the role family and the schema when absent, resets both roles' passwords to their derived values, and refuses
|
|
21
|
+
* a family that holds more than it should. Runs over one connection of the provisioning credential, under the
|
|
22
|
+
* application's migration lock, so two containers starting together do not race on the same catalog rows.
|
|
23
|
+
*/
|
|
24
|
+
export declare function provisionPostgresRoles(admin: SqlDatabase, provisioningUrl: string, roles: ApplicationDatabaseRoles, lock: {
|
|
25
|
+
readonly key: number;
|
|
26
|
+
readonly timeoutMs: number;
|
|
27
|
+
}): Promise<void>;
|
|
28
|
+
export declare const quotedIdentifier: (name: string) => string;
|
|
29
|
+
/**
|
|
30
|
+
* Drops the schema and both roles: for throwaway test databases only. The schema is dropped as its owner, which a
|
|
31
|
+
* CREATEROLE provisioning credential may become (`provisionPostgresRoles` granted it the migration role).
|
|
32
|
+
*/
|
|
33
|
+
export declare function dropPostgresRoles(admin: SqlDatabase, roles: ApplicationDatabaseRoles): Promise<void>;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { AssetServer } from '@fabrika/platform';
|
|
2
|
+
export interface FileSystemAssetServerOptions {
|
|
3
|
+
/** File served for a directory and for the SPA fallback. */
|
|
4
|
+
indexFile?: string;
|
|
5
|
+
/** Serve `indexFile` (200) for an unmatched document request. Off = everything unmatched 404s. */
|
|
6
|
+
spaFallback?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare class FileSystemAssetServer implements AssetServer {
|
|
9
|
+
private readonly root;
|
|
10
|
+
private readonly indexFile;
|
|
11
|
+
private readonly spaFallback;
|
|
12
|
+
constructor(root: string, options?: FileSystemAssetServerOptions);
|
|
13
|
+
fetch(request: Request): Promise<Response>;
|
|
14
|
+
/** The file backing `pathname`, or null when there is none inside the root. */
|
|
15
|
+
private locate;
|
|
16
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type BlobStore, type FileObjectStore, type ObjectMetadata, type ObjectWriteOptions, type StoredObject } from '@fabrika/platform';
|
|
2
|
+
/** Connection details for an S3-compatible bucket. `endpoint` is what makes it R2, MinIO, or AWS. */
|
|
3
|
+
export interface S3BlobStoreOptions {
|
|
4
|
+
bucket: string;
|
|
5
|
+
accessKeyId: string;
|
|
6
|
+
secretAccessKey: string;
|
|
7
|
+
/** e.g. `https://<account>.r2.cloudflarestorage.com` (R2) or `http://storage:9000` (Zerops/MinIO). */
|
|
8
|
+
endpoint?: string;
|
|
9
|
+
/** Required by AWS; R2 wants `auto`; MinIO ignores it. */
|
|
10
|
+
region?: string;
|
|
11
|
+
/** Temporary-credential session token, when the deployment uses one. */
|
|
12
|
+
sessionToken?: string;
|
|
13
|
+
/** `https://<bucket>.<host>` addressing. MinIO normally wants this OFF (path-style). */
|
|
14
|
+
virtualHostedStyle?: boolean;
|
|
15
|
+
}
|
|
16
|
+
/** The blob body handed back by `get()` — a stream, opened lazily and only once, plus a text shortcut. */
|
|
17
|
+
interface BlobBody {
|
|
18
|
+
body: ReadableStream;
|
|
19
|
+
text(): Promise<string>;
|
|
20
|
+
}
|
|
21
|
+
export declare class S3BlobStore implements BlobStore, FileObjectStore {
|
|
22
|
+
#private;
|
|
23
|
+
private readonly client;
|
|
24
|
+
constructor(client: Bun.S3Client);
|
|
25
|
+
/** Build a store from plain connection options. Never logs them. */
|
|
26
|
+
static connect(options: S3BlobStoreOptions): S3BlobStore;
|
|
27
|
+
put(key: string, value: string | ArrayBuffer | ReadableStream): Promise<void>;
|
|
28
|
+
get(key: string): Promise<BlobBody | null>;
|
|
29
|
+
delete(key: string): Promise<void>;
|
|
30
|
+
write(key: string, body: ReadableStream<Uint8Array>, options: ObjectWriteOptions): Promise<ObjectMetadata>;
|
|
31
|
+
read(key: string, signal?: AbortSignal): Promise<StoredObject | null>;
|
|
32
|
+
readVersion(key: string, version: string, signal?: AbortSignal): Promise<StoredObject | null>;
|
|
33
|
+
}
|
|
34
|
+
export {};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQLSTATE 53300 (`too_many_connections`): no slot the role may take is free, whether the server's, the database's or
|
|
3
|
+
* the role's limit is reached. It refuses a connection and never a statement on one that is open.
|
|
4
|
+
*/
|
|
5
|
+
export declare const TOO_MANY_CONNECTIONS = "53300";
|
|
6
|
+
/** How every report of an exhausted connection limit starts, so a caller that reads only output can recognise one. */
|
|
7
|
+
export declare const CONNECTION_CAPACITY_MESSAGE = "the database had no free connection slot";
|
|
8
|
+
/** How long a caller waits for a free connection slot by default before it gives up. */
|
|
9
|
+
export declare const DEFAULT_CONNECTION_WAIT_MS = 60000;
|
|
10
|
+
/** Whether `error` is the server refusing a connection because every slot it may take is in use. */
|
|
11
|
+
export declare function isConnectionCapacityError(error: unknown): boolean;
|
|
12
|
+
/** The database kept refusing connections for lack of a free slot until the wait ran out. */
|
|
13
|
+
export declare class ConnectionCapacityError extends Error {
|
|
14
|
+
readonly waitedMs: number;
|
|
15
|
+
constructor(waitedMs: number);
|
|
16
|
+
}
|
|
17
|
+
export interface ConnectionCapacityWait {
|
|
18
|
+
/** How long to keep retrying; `DEFAULT_CONNECTION_WAIT_MS` by default. */
|
|
19
|
+
readonly waitMs?: number;
|
|
20
|
+
readonly sleep?: (ms: number) => Promise<void>;
|
|
21
|
+
readonly now?: () => number;
|
|
22
|
+
readonly random?: () => number;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Runs `attempt` until it does not fail for lack of a connection slot, waiting with jittered exponential backoff between
|
|
26
|
+
* tries, and throws `ConnectionCapacityError` once `waitMs` has passed. Any other failure is thrown at once. `attempt`
|
|
27
|
+
* must release whatever it opened before it throws, so that a waiting caller holds no slot another needs.
|
|
28
|
+
*/
|
|
29
|
+
export declare function withConnectionCapacity<T>(attempt: () => Promise<T>, options?: ConnectionCapacityWait): Promise<T>;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { DatabaseObject } from './migration-classify';
|
|
2
|
+
/**
|
|
3
|
+
* One row (`violation`) per escape and per undeclared object in `schema`. Escapes: a function that is `SECURITY
|
|
4
|
+
* DEFINER`, has a `SET` clause or a language other than `sql` and `plpgsql`; an event trigger on a function of the
|
|
5
|
+
* schema; and any dependency of an object of the schema on a type, function, relation, operator, operator class or
|
|
6
|
+
* collation outside the schema and `pg_catalog`; a routine that shadows a `pg_catalog` name or has more than one
|
|
7
|
+
* signature; a sequence other than an identity column's. Undeclared: a function, procedure or aggregate, operator,
|
|
8
|
+
* trigger (a foreign key's excepted), rule, view, policy or domain that `declared` does not name. Every name is
|
|
9
|
+
* qualified with `pg_catalog`, the query has no backslash, and the manifest is
|
|
10
|
+
* hex text, so the session's `search_path` and `standard_conforming_strings` cannot change what it finds.
|
|
11
|
+
*/
|
|
12
|
+
export declare function databaseCodeQuery(schema: string, declared: readonly DatabaseObject[]): string;
|
|
13
|
+
/** The message a `databaseCodeCheck` failure carries before its list of violations. */
|
|
14
|
+
export declare const DATABASE_CODE_FAILURE = "fabrika-migration-database-code: ";
|
|
15
|
+
/** Fails the transaction it runs in when `schema` escapes or holds an object `declared` does not name. */
|
|
16
|
+
export declare function databaseCodeCheck(schema: string, declared: readonly DatabaseObject[]): string;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type SqlToken } from './migration-sql';
|
|
2
|
+
/**
|
|
3
|
+
* A file row's states. `pending`: its object is still being written. `quarantined`: written to a scanning collection
|
|
4
|
+
* and not yet given a clean verdict. `live`: served. `rejected`: refused by the scan and its object is gone.
|
|
5
|
+
*/
|
|
6
|
+
export declare const FILE_STATUSES: readonly ["pending", "live", "quarantined", "rejected"];
|
|
7
|
+
export declare const FILE_TABLE_PREFIX = "fabrika_files_";
|
|
8
|
+
/** Every stored file (`@fabrika/db`'s `0008_files.sql`): an application's file column is a foreign key to its `id`. */
|
|
9
|
+
export declare const STORED_FILES_TABLE = "fabrika_files";
|
|
10
|
+
/** What an application's last adoption migration of a file collection calls, with the collection's name. */
|
|
11
|
+
export declare const FILES_RELEASE_FUNCTION = "fabrika_files_release";
|
|
12
|
+
/**
|
|
13
|
+
* What an adoption migration calls right before it reads a collection's table: it copies the rows an older instance
|
|
14
|
+
* wrote after `0008_files.sql` ran, so every row the migration adopts has its `fabrika_files` row.
|
|
15
|
+
*/
|
|
16
|
+
export declare const FILES_COPY_FUNCTION = "fabrika_files_copy_legacy";
|
|
17
|
+
/** The columns the scan job keeps: the scanned object's version, and the retry count and time of a file without a verdict. */
|
|
18
|
+
export declare const FILE_SCAN_COLUMNS: readonly ["object_version TEXT", "scan_attempts INTEGER NOT NULL DEFAULT 0", "scan_after TIMESTAMPTZ"];
|
|
19
|
+
/** The named status check of every state. */
|
|
20
|
+
export declare function fileStatusCheck(table: string): string;
|
|
21
|
+
/** The index the scan job selects its batch by. */
|
|
22
|
+
export declare function fileScanIndex(table: string): string;
|
|
23
|
+
/**
|
|
24
|
+
* The name Postgres gave the unnamed status check of a table an older release's `fileTableSql` created:
|
|
25
|
+
* `<table>_status_check`, the table's name cut to 50 characters first (`makeObjectName`).
|
|
26
|
+
*/
|
|
27
|
+
export declare function legacyStatusCheckName(table: string): string;
|
|
28
|
+
/** The statement of an older release's `fileScanSql` that widens its collection table: its check and its columns. */
|
|
29
|
+
export declare function fileScanAlter(table: string): string;
|
|
30
|
+
/** The file table a statement is exactly `fileScanAlter` of, or `undefined`. */
|
|
31
|
+
export declare function fileScanAlterTable(tokens: readonly SqlToken[]): string | undefined;
|
|
32
|
+
/**
|
|
33
|
+
* Whether the migrations, in order, leave `table` able to hold the scan states: created with the check of every state
|
|
34
|
+
* and the scan columns, or widened by `fileScanAlter`. `undefined` when none of them creates it.
|
|
35
|
+
*/
|
|
36
|
+
export declare function fileTableScanReady(migrations: readonly string[], table: string): boolean | undefined;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export { APPLICATION_MIGRATION_BUNDLE, APPLICATION_MIGRATION_LEDGER, applicationDatabaseRoles, applicationMigrationLockKey, applicationRuntimeUrl, DEFAULT_MIGRATION_LOCK_TIMEOUT_MS, DEFAULT_MIGRATION_STATEMENT_TIMEOUT_MS, definePostgresApplicationMigrations, dropApplicationDatabase, type PostgresApplicationMigrations, type PostgresApplicationMigrationsConfig, } from './application-postgres-migrations';
|
|
2
|
+
export type { ApplicationDatabaseRoles } from './application-postgres-roles';
|
|
3
|
+
export { applicationDatabaseSchema } from './application-schema';
|
|
4
|
+
export { FileSystemAssetServer, type FileSystemAssetServerOptions } from './asset-server-fs';
|
|
5
|
+
export { S3BlobStore, type S3BlobStoreOptions } from './blob-s3';
|
|
6
|
+
export { CONNECTION_CAPACITY_MESSAGE, ConnectionCapacityError, type ConnectionCapacityWait, DEFAULT_CONNECTION_WAIT_MS, isConnectionCapacityError, TOO_MANY_CONNECTIONS, withConnectionCapacity, } from './connection-capacity';
|
|
7
|
+
export { databaseCodeQuery } from './database-code';
|
|
8
|
+
export { decodeJob, type Job, PostgresJobConsumer, type PostgresJobConsumerOptions, PostgresJobQueue, type PostgresJobQueueOptions, } from './job-queue-postgres';
|
|
9
|
+
export type { DatabaseObject, DatabaseObjectKind } from './migration-classify';
|
|
10
|
+
export { rewritePlaceholders, type RewrittenSql } from './placeholders';
|
|
11
|
+
export { applyPostgresMigrations, definePostgresMigrationPlan, migrationChecksum, platformNodePostgresMigrationBundle, type PostgresApplicationMigrationSettings, type PostgresMigration, type PostgresMigrationBundle, PostgresMigrationError, postgresMigrationFilenames, postgresMigrationIdentity, type PostgresMigrationPlan, readPostgresMigrationBundle, type ReadPostgresMigrationBundleOptions, } from './postgres-migrations';
|
|
12
|
+
export { createTestProvisioner, type TestProvisioner } from './postgres-test-provisioner';
|
|
13
|
+
export { definePostgresServiceMigrations, postgresMigrationResultMessage, type PostgresServiceMigrations, type PostgresServiceMigrationsConfig, } from './service-postgres-migrations';
|
|
14
|
+
export { PostgresDatabase, PostgresStatement, type SqlBindValue } from './sql-postgres';
|
|
15
|
+
export { type BackgroundTasks, type BackgroundTasksOptions, createBackgroundTasks } from './wait-until';
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { type JobQueue, type SqlDatabase } from '@fabrika/platform';
|
|
2
|
+
/** A claimed delivery. Raw claims carry stored JSON text; typed claims carry the caller's decoded payload. */
|
|
3
|
+
export interface Job<T> {
|
|
4
|
+
id: string;
|
|
5
|
+
payload: T;
|
|
6
|
+
/** How many times this job has been claimed, including now. First delivery is 1. */
|
|
7
|
+
attempts: number;
|
|
8
|
+
/** Claims allowed before the job is abandoned. */
|
|
9
|
+
maxAttempts: number;
|
|
10
|
+
}
|
|
11
|
+
/** Parse and narrow one stored delivery without losing its settlement metadata. */
|
|
12
|
+
export declare function decodeJob<T>(job: Job<string>, decode: (payload: unknown) => T): Job<T>;
|
|
13
|
+
export interface PostgresJobQueueOptions {
|
|
14
|
+
/** Table holding the jobs. Must already exist; see migrations/0001_jobs.sql. */
|
|
15
|
+
table?: string;
|
|
16
|
+
/** Queue name, so several queues can share one table. */
|
|
17
|
+
queue?: string;
|
|
18
|
+
/** Claims allowed per job before it is abandoned. */
|
|
19
|
+
maxAttempts?: number;
|
|
20
|
+
/** Injectable clock (unix millis) so delays and timeouts are testable without sleeping. */
|
|
21
|
+
now?: () => number;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Producer + claim/ack primitives over one jobs table. Implements the `JobQueue` port (`send`); the
|
|
25
|
+
* rest is the consumer-side surface `PostgresJobConsumer` is built from and tests drive directly.
|
|
26
|
+
*/
|
|
27
|
+
export declare class PostgresJobQueue<T> implements JobQueue<T> {
|
|
28
|
+
private readonly db;
|
|
29
|
+
private readonly queue;
|
|
30
|
+
private readonly maxAttempts;
|
|
31
|
+
private readonly now;
|
|
32
|
+
private readonly sendSql;
|
|
33
|
+
private readonly claimSql;
|
|
34
|
+
private readonly ackSql;
|
|
35
|
+
private readonly deferSql;
|
|
36
|
+
constructor(db: SqlDatabase, options?: PostgresJobQueueOptions);
|
|
37
|
+
/** Enqueue a message. `delaySeconds` becomes a visible-after deadline, exactly as on Cloudflare Queues. */
|
|
38
|
+
send(message: T, options?: {
|
|
39
|
+
delaySeconds?: number;
|
|
40
|
+
}): Promise<void>;
|
|
41
|
+
/**
|
|
42
|
+
* Take up to `limit` due jobs and hide them for `visibilityTimeoutMs`. `decode` narrows the stored
|
|
43
|
+
* JSON at the deserialization boundary — a row this consumer cannot understand throws here rather
|
|
44
|
+
* than reaching a handler that assumes a shape.
|
|
45
|
+
*/
|
|
46
|
+
claim(options: {
|
|
47
|
+
limit: number;
|
|
48
|
+
visibilityTimeoutMs: number;
|
|
49
|
+
decode: (payload: unknown) => T;
|
|
50
|
+
}): Promise<Job<T>[]>;
|
|
51
|
+
/** Claim stored JSON text so consumers can decode and settle each delivery independently. */
|
|
52
|
+
claimRaw(options: {
|
|
53
|
+
limit: number;
|
|
54
|
+
visibilityTimeoutMs: number;
|
|
55
|
+
}): Promise<Job<string>[]>;
|
|
56
|
+
/** The job is done (or permanently abandoned) — drop the row. Idempotent. */
|
|
57
|
+
ack(id: string): Promise<void>;
|
|
58
|
+
/** Make the job claimable again after `delayMs`. Used for retries and for a deferred (locked-out) run. */
|
|
59
|
+
defer(id: string, delayMs: number): Promise<void>;
|
|
60
|
+
}
|
|
61
|
+
export interface PostgresJobConsumerOptions<T> {
|
|
62
|
+
/** Narrow the stored JSON to the message type. Invalid deliveries retry until their budget is spent. */
|
|
63
|
+
decode: (payload: unknown) => T;
|
|
64
|
+
/** Handle one job. A throw schedules a retry until `maxAttempts` is spent. */
|
|
65
|
+
handler: (job: Job<T>) => Promise<void>;
|
|
66
|
+
/** Jobs claimed per pass. Handlers still run one at a time. */
|
|
67
|
+
batchSize?: number;
|
|
68
|
+
/** Sleep between passes that found nothing. A pass that found work polls again immediately. */
|
|
69
|
+
pollIntervalMs?: number;
|
|
70
|
+
/** How long a claimed job stays hidden. MUST exceed the slowest handler, or the job is redelivered mid-flight. */
|
|
71
|
+
visibilityTimeoutMs?: number;
|
|
72
|
+
/** Backoff before the next attempt, from the attempt count just consumed. */
|
|
73
|
+
retryDelayMs?: (attempts: number) => number;
|
|
74
|
+
/** A handler threw and the job will be retried. Never log the error object (it may carry a token). */
|
|
75
|
+
onError?: (error: unknown, job: Job<T>) => void;
|
|
76
|
+
/** A job spent its last attempt and was dropped. */
|
|
77
|
+
onAbandoned?: (job: Job<T>) => void;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The in-process poller. One pass = claim a batch, run the handlers in order, ack or reschedule each.
|
|
81
|
+
* Handlers run SEQUENTIALLY: a deploy consumer is one run per message and ordering per queue is the
|
|
82
|
+
* cheapest correct default; raise `batchSize` only for genuinely independent work.
|
|
83
|
+
*
|
|
84
|
+
* `start()` is fire-and-forget and never throws; `stop()` waits for the pass in flight, so a shutdown
|
|
85
|
+
* does not orphan a job it has already made invisible.
|
|
86
|
+
*/
|
|
87
|
+
export declare class PostgresJobConsumer<T> {
|
|
88
|
+
private readonly queue;
|
|
89
|
+
private readonly options;
|
|
90
|
+
private readonly onError;
|
|
91
|
+
private readonly onAbandoned;
|
|
92
|
+
private running;
|
|
93
|
+
private timer;
|
|
94
|
+
private pass;
|
|
95
|
+
constructor(queue: PostgresJobQueue<T>, options: PostgresJobConsumerOptions<T>);
|
|
96
|
+
/** Run one pass. Returns the raw claimed count, including invalid deliveries. Exposed for tests. */
|
|
97
|
+
poll(): Promise<number>;
|
|
98
|
+
/** Start polling in the background. Idempotent. */
|
|
99
|
+
start(): void;
|
|
100
|
+
/** Stop polling and wait for the pass in flight. Idempotent. */
|
|
101
|
+
stop(): Promise<void>;
|
|
102
|
+
private schedule;
|
|
103
|
+
private runPass;
|
|
104
|
+
private handleRaw;
|
|
105
|
+
private handle;
|
|
106
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/** A database object a migration declares, named as the catalog check names it (`trigger deals.audit`). */
|
|
2
|
+
export type DatabaseObjectKind = 'function' | 'procedure' | 'trigger' | 'view' | 'materialized view' | 'rule' | 'policy';
|
|
3
|
+
export interface DatabaseObject {
|
|
4
|
+
readonly kind: DatabaseObjectKind;
|
|
5
|
+
/** `<name>`, or `<table>.<name>` for a trigger, a rule and a policy. A routine has one signature, so its name is enough. */
|
|
6
|
+
readonly name: string;
|
|
7
|
+
}
|
|
8
|
+
/** Database code and unusual expressions: allowed, and shown to the approver as needing closer review. */
|
|
9
|
+
export type DeviationKind = DatabaseObjectKind | 'do' | 'expression';
|
|
10
|
+
/** One deviation. Its approval key is `kind` + `object`: a line moves when the file is edited, the key does not. */
|
|
11
|
+
export interface MigrationDeviation {
|
|
12
|
+
readonly kind: DeviationKind;
|
|
13
|
+
/**
|
|
14
|
+
* The object's name; `block <n>` for the n-th `DO` of the file;
|
|
15
|
+
* `<where> calls <function>` for an expression, where `<where>` is a column, a constraint, a table or an index.
|
|
16
|
+
*/
|
|
17
|
+
readonly object: string;
|
|
18
|
+
readonly file: string;
|
|
19
|
+
readonly line: number;
|
|
20
|
+
}
|
|
21
|
+
export interface MigrationRefusal {
|
|
22
|
+
/** What is refused, phrased to precede "is not allowed". */
|
|
23
|
+
readonly reason: string;
|
|
24
|
+
readonly file: string;
|
|
25
|
+
readonly line: number;
|
|
26
|
+
}
|
|
27
|
+
/** How one statement changes the set of declared objects, in file order. */
|
|
28
|
+
export type ManifestChange = {
|
|
29
|
+
readonly change: 'create';
|
|
30
|
+
readonly object: DatabaseObject;
|
|
31
|
+
} | {
|
|
32
|
+
readonly change: 'drop';
|
|
33
|
+
readonly object: DatabaseObject;
|
|
34
|
+
} | {
|
|
35
|
+
readonly change: 'drop table';
|
|
36
|
+
readonly table: string;
|
|
37
|
+
} | {
|
|
38
|
+
readonly change: 'rename table';
|
|
39
|
+
readonly from: string;
|
|
40
|
+
readonly to: string;
|
|
41
|
+
} | {
|
|
42
|
+
readonly change: 'rename';
|
|
43
|
+
readonly from: DatabaseObject;
|
|
44
|
+
readonly to: DatabaseObject;
|
|
45
|
+
}
|
|
46
|
+
/** A routine's create also carries its argument list, as written, so a second signature for the name is refused. */
|
|
47
|
+
| {
|
|
48
|
+
readonly change: 'create routine';
|
|
49
|
+
readonly object: DatabaseObject;
|
|
50
|
+
readonly args: string;
|
|
51
|
+
readonly line: number;
|
|
52
|
+
};
|
|
53
|
+
export interface MigrationClassification {
|
|
54
|
+
readonly file: string;
|
|
55
|
+
/** One per refused statement, in order. A file with a refusal does not run. */
|
|
56
|
+
readonly refused: readonly MigrationRefusal[];
|
|
57
|
+
readonly deviations: readonly MigrationDeviation[];
|
|
58
|
+
/** The objects the file creates. */
|
|
59
|
+
readonly objects: readonly DatabaseObject[];
|
|
60
|
+
readonly changes: readonly ManifestChange[];
|
|
61
|
+
}
|
|
62
|
+
export interface ClassifyMigrationOptions {
|
|
63
|
+
readonly file: string;
|
|
64
|
+
/** The functions and procedures the whole migration set declares in the schema: a body or an expression may call them. */
|
|
65
|
+
readonly functions?: ReadonlySet<string>;
|
|
66
|
+
/** The framework's own bundle, which alone may name the framework's `fabrika_*` tables. */
|
|
67
|
+
readonly framework?: boolean;
|
|
68
|
+
}
|
|
69
|
+
export interface MigrationFile {
|
|
70
|
+
readonly name: string;
|
|
71
|
+
readonly sql: string;
|
|
72
|
+
readonly framework?: boolean;
|
|
73
|
+
}
|
|
74
|
+
export interface MigrationSetClassification {
|
|
75
|
+
readonly files: readonly MigrationClassification[];
|
|
76
|
+
/** The manifest: every object the files declare and do not drop, in order. The catalog check admits exactly these. */
|
|
77
|
+
readonly objects: readonly DatabaseObject[];
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Classifies one application migration. Paved: `CREATE TABLE` (a column list and nothing after it), `ALTER TABLE`
|
|
81
|
+
* column, constraint and rename clauses, `CREATE [UNIQUE] INDEX`, `ALTER INDEX … RENAME TO`, `CREATE TYPE … AS ENUM`,
|
|
82
|
+
* `ALTER TYPE … ADD|RENAME VALUE`, `DROP`, `TRUNCATE`, `COMMENT ON` and `INSERT`/`UPDATE`/`DELETE` backfills.
|
|
83
|
+
* Deviations: functions and procedures (one signature per name), triggers, views, rules, policies, their
|
|
84
|
+
* `ALTER … RENAME TO`, `DO`, and a stored expression that calls anything but a paved built-in. Refused: a schema
|
|
85
|
+
* other than `schema` (`pg_catalog` relations included), a framework `fabrika_*` name, `SECURITY DEFINER`, a function
|
|
86
|
+
* `SET` clause, a language other than `sql` and `plpgsql`, a body that is not literal or that holds dynamic `EXECUTE`
|
|
87
|
+
* or a statement outside its allowlist, a call outside the function allowlist, and every form not named here:
|
|
88
|
+
* `COPY`, `GRANT`, `REVOKE`, roles, extensions, event triggers, domains (codegen cannot bind one), `SELECT`, `WITH`
|
|
89
|
+
* and `MERGE` among them. Transaction control and
|
|
90
|
+
* `SET`/`RESET` are refused for every plan before this runs (`refusedStatement`).
|
|
91
|
+
*/
|
|
92
|
+
export declare function classifyMigration(sql: string, schema: string, options: ClassifyMigrationOptions): MigrationClassification;
|
|
93
|
+
/** The names of the functions and procedures a migration creates or renames to in `schema`, read without classifying them. */
|
|
94
|
+
export declare function declaredFunctions(sql: string, schema: string): string[];
|
|
95
|
+
/**
|
|
96
|
+
* The manifest of a migration set, folded in file order: a create adds an object (a replace keeps it), a drop removes
|
|
97
|
+
* it, dropping a table removes its triggers, rules and policies, and renaming a table renames them. A `CASCADE` may
|
|
98
|
+
* drop more than the manifest removes; a stale entry only admits an object that no longer exists.
|
|
99
|
+
*/
|
|
100
|
+
export declare function migrationManifest(classifications: readonly MigrationClassification[]): DatabaseObject[];
|
|
101
|
+
/**
|
|
102
|
+
* Classifies a whole migration set in order. A body or an expression may call a function any file of the set
|
|
103
|
+
* declares; the manifest is what the catalog check admits.
|
|
104
|
+
*/
|
|
105
|
+
export declare function classifyMigrations(files: readonly MigrationFile[], schema: string): MigrationSetClassification;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
export interface StatementStart {
|
|
2
|
+
/** The first two words of a top-level statement, upper-cased; fewer when the statement has fewer. */
|
|
3
|
+
readonly words: readonly string[];
|
|
4
|
+
/** 1-based line of the statement's first word. */
|
|
5
|
+
readonly line: number;
|
|
6
|
+
}
|
|
7
|
+
/** One lexical token of SQL: comments and whitespace are dropped; a literal or a quoted body keeps its contents. */
|
|
8
|
+
export type SqlToken = {
|
|
9
|
+
readonly kind: 'word';
|
|
10
|
+
readonly text: string;
|
|
11
|
+
readonly line: number;
|
|
12
|
+
} | {
|
|
13
|
+
readonly kind: 'quoted';
|
|
14
|
+
readonly text: string;
|
|
15
|
+
readonly line: number;
|
|
16
|
+
}
|
|
17
|
+
/** `text` is the literal's value for a plain `'…'` string; an `E'…'` string, whose escapes are not decoded, has none. */
|
|
18
|
+
| {
|
|
19
|
+
readonly kind: 'string';
|
|
20
|
+
readonly text: string | undefined;
|
|
21
|
+
readonly line: number;
|
|
22
|
+
} | {
|
|
23
|
+
readonly kind: 'dollar';
|
|
24
|
+
readonly body: string;
|
|
25
|
+
readonly line: number;
|
|
26
|
+
} | {
|
|
27
|
+
readonly kind: 'punct';
|
|
28
|
+
readonly char: string;
|
|
29
|
+
readonly line: number;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* The tokens of `sql`. Comments (`--`, nested `/* *\/`), string literals (`''`, `E''` with backslash escapes), quoted
|
|
33
|
+
* identifiers (`""` unescaped) and dollar-quoted bodies are each one token, so a keyword inside them is no word.
|
|
34
|
+
*/
|
|
35
|
+
export declare function sqlTokens(sql: string): SqlToken[];
|
|
36
|
+
/**
|
|
37
|
+
* The tokens of `sql`, and the line of the first string, quoted identifier, dollar-quoted body or block comment that
|
|
38
|
+
* never closes. Text after it is one token, which a check of names cannot read.
|
|
39
|
+
*/
|
|
40
|
+
export declare function lexSql(sql: string): {
|
|
41
|
+
readonly tokens: SqlToken[];
|
|
42
|
+
readonly unterminated: number | undefined;
|
|
43
|
+
};
|
|
44
|
+
export interface Statement {
|
|
45
|
+
/** The statement's tokens, without the `;` that ends it. */
|
|
46
|
+
readonly tokens: readonly SqlToken[];
|
|
47
|
+
/** 1-based line of the statement's first token. */
|
|
48
|
+
readonly line: number;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Splits SQL into top-level statements at every `;` outside a comment, a literal, a quoted identifier or a
|
|
52
|
+
* dollar-quoted body. Postgres admits a `;` inside parentheses only in a rule's actions, so splitting there too only
|
|
53
|
+
* makes a check stricter.
|
|
54
|
+
*/
|
|
55
|
+
export declare function topLevelStatements(sql: string): Statement[];
|
|
56
|
+
export declare const upperWord: (token: SqlToken | undefined) => string | undefined;
|
|
57
|
+
/** Reports how each top-level statement begins. A keyword inside a comment, a literal or a quoted body is no statement. */
|
|
58
|
+
export declare function statementStarts(sql: string): StatementStart[];
|
|
59
|
+
/** The first refused top-level statement of a migration file, or `undefined`. */
|
|
60
|
+
export declare function refusedStatement(sql: string): {
|
|
61
|
+
readonly statement: string;
|
|
62
|
+
readonly line: number;
|
|
63
|
+
} | undefined;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** The Postgres major versions the sets below are generated from. */
|
|
2
|
+
export declare const PG_CATALOG_MAJORS: readonly number[];
|
|
3
|
+
/** Every function name in `pg_catalog`: an application function may not take one, or a call could resolve to the built-in. */
|
|
4
|
+
export declare const PG_CATALOG_FUNCTIONS: ReadonlySet<string>;
|
|
5
|
+
/** Every SQL keyword, of every category: an application function may not take one (`operator`, `position`). */
|
|
6
|
+
export declare const SQL_KEYWORDS: ReadonlySet<string>;
|
|
7
|
+
/** Per major, the names of the sets above that its own catalog lacks. */
|
|
8
|
+
export declare const PG_CATALOG_ABSENT: ReadonlyMap<number, {
|
|
9
|
+
functions: ReadonlySet<string>;
|
|
10
|
+
keywords: ReadonlySet<string>;
|
|
11
|
+
}>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** One rewritten statement. `count` is exactly how many values `bind()` must supply. */
|
|
2
|
+
export interface RewrittenSql {
|
|
3
|
+
/** Postgres-dialect text: every positional `?` replaced by `$1..$n`, left to right. */
|
|
4
|
+
text: string;
|
|
5
|
+
/** How many placeholders were rewritten. */
|
|
6
|
+
count: number;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Rewrite a `?`-placeholder statement into Postgres `$n` form. Throws on an unterminated string,
|
|
10
|
+
* identifier, dollar-quote or block comment — the statement is malformed and no rewriting of it can
|
|
11
|
+
* be trusted.
|
|
12
|
+
*/
|
|
13
|
+
export declare function rewritePlaceholders(sql: string): RewrittenSql;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { SqlDatabase } from '@fabrika/platform';
|
|
2
|
+
import { PostgresMigrationError } from './postgres-migration-error';
|
|
3
|
+
export interface PostgresMigration {
|
|
4
|
+
name: string;
|
|
5
|
+
sql: string;
|
|
6
|
+
}
|
|
7
|
+
export interface PostgresMigrationBundle {
|
|
8
|
+
name: string;
|
|
9
|
+
migrations: readonly PostgresMigration[];
|
|
10
|
+
}
|
|
11
|
+
export interface PostgresMigrationPlan {
|
|
12
|
+
/** Service-scoped ledger table, such as `iam_schema_migrations`; an application's is qualified by its schema. */
|
|
13
|
+
ledgerTable: string;
|
|
14
|
+
/** Service-specific session advisory lock. Keep this stable across runner upgrades. */
|
|
15
|
+
lockKey: number;
|
|
16
|
+
bundles: readonly PostgresMigrationBundle[];
|
|
17
|
+
/** Present only in an application's plan (ADR-0017): its schema, a checksum per migration, and timeouts. */
|
|
18
|
+
application?: PostgresApplicationMigrationSettings;
|
|
19
|
+
}
|
|
20
|
+
export interface PostgresApplicationMigrationSettings {
|
|
21
|
+
/** Created when absent. Each migration runs with it as its only `search_path`. */
|
|
22
|
+
schema: string;
|
|
23
|
+
/** `lock_timeout` of each migration, so a blocked `ALTER` fails the deploy instead of stalling the live version. */
|
|
24
|
+
lockTimeoutMs: number;
|
|
25
|
+
statementTimeoutMs: number;
|
|
26
|
+
/** The framework's own bundle (`fabrika`), which alone may name the framework's `fabrika_*` tables. */
|
|
27
|
+
frameworkBundle?: string;
|
|
28
|
+
/**
|
|
29
|
+
* The application's runtime role. Before the plan runs, the migration role's default privileges are cleared of
|
|
30
|
+
* PUBLIC; after it ran, the runtime role's and PUBLIC's privileges in the schema are reset to the runtime role's DML
|
|
31
|
+
* on every table but the ledger and verified, in one transaction under the migration lock; it never gets DDL.
|
|
32
|
+
*/
|
|
33
|
+
runtimeRole?: string;
|
|
34
|
+
/** How long the runner waits for the migration lock; any session may take an advisory lock, so the wait is bounded. */
|
|
35
|
+
advisoryLockTimeoutMs?: number;
|
|
36
|
+
}
|
|
37
|
+
export { PostgresMigrationError };
|
|
38
|
+
export interface ReadPostgresMigrationBundleOptions {
|
|
39
|
+
name: string;
|
|
40
|
+
directory: string;
|
|
41
|
+
files?: readonly string[];
|
|
42
|
+
}
|
|
43
|
+
/** Read one ordered migration bundle from disk. An explicit file list fails closed when a file is absent. */
|
|
44
|
+
export declare function readPostgresMigrationBundle(options: ReadPostgresMigrationBundleOptions): PostgresMigrationBundle;
|
|
45
|
+
/** The generic migrations owned by the long-running Bun runtime. */
|
|
46
|
+
export declare function platformNodePostgresMigrationBundle(files?: readonly string[]): PostgresMigrationBundle;
|
|
47
|
+
/** Validate a complete plan at composition time, before any SQL is executed. */
|
|
48
|
+
export declare function definePostgresMigrationPlan(plan: PostgresMigrationPlan): PostgresMigrationPlan;
|
|
49
|
+
export declare function postgresMigrationIdentity(bundle: string, name: string): string;
|
|
50
|
+
/** Preserve a service wrapper's historical filename-only result while the core uses qualified ids. */
|
|
51
|
+
export declare function postgresMigrationFilenames(identities: readonly string[], bundle: string): string[];
|
|
52
|
+
/**
|
|
53
|
+
* Apply a plan over one Postgres session.
|
|
54
|
+
*
|
|
55
|
+
* The caller must supply a database backed by a single-connection pool. Advisory locks are session
|
|
56
|
+
* state; allowing acquire and release to land on different pooled connections would make the runner
|
|
57
|
+
* appear serialized while providing no serialization at all.
|
|
58
|
+
*/
|
|
59
|
+
export declare function applyPostgresMigrations(db: SqlDatabase, input: PostgresMigrationPlan, options?: {
|
|
60
|
+
now?: () => number;
|
|
61
|
+
}): Promise<string[]>;
|
|
62
|
+
/** SHA-256 of the file's text, hex. */
|
|
63
|
+
export declare function migrationChecksum(sql: string): string;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { SqlDatabase } from '@fabrika/platform';
|
|
2
|
+
/** A provisioning credential that exists for one test run only, and what it created. */
|
|
3
|
+
export interface TestProvisioner {
|
|
4
|
+
/**
|
|
5
|
+
* Connects as `fabrika_tests_<hex>`: CREATEROLE and CREATE on the database, nothing of any application, and a login
|
|
6
|
+
* for `TEST_PROVISIONER_LIFETIME_MINUTES` only.
|
|
7
|
+
*/
|
|
8
|
+
readonly url: string;
|
|
9
|
+
/** The provisioners of earlier runs that creating this one dropped. Role names, never a credential. */
|
|
10
|
+
readonly swept: readonly string[];
|
|
11
|
+
/** Drops every role the provisioner created, with everything those roles own, then the provisioner itself. */
|
|
12
|
+
drop(): Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Every grant or revoke on a database changes its one `pg_database` row, and two concurrent changes of that row fail with
|
|
16
|
+
* `tuple concurrently updated`, so each change waits for this transaction-level lock first.
|
|
17
|
+
*/
|
|
18
|
+
export declare const DATABASE_GRANT_LOCK_KEY: number;
|
|
19
|
+
/**
|
|
20
|
+
* Grants (or revokes) CREATE on `database` to `role`, serialized with every other run that does so on the same server.
|
|
21
|
+
* Verified afterwards: a grantor that neither owns the database, is a superuser nor holds CREATE WITH GRANT OPTION only
|
|
22
|
+
* gets a WARNING, and the statement changes nothing.
|
|
23
|
+
*/
|
|
24
|
+
export declare function changeCreateOnDatabase(admin: SqlDatabase, change: 'grant' | 'revoke', role: string, database: string): Promise<void>;
|
|
25
|
+
/**
|
|
26
|
+
* How long a provisioner can log in (`VALID UNTIL`). A run that a kill stopped before `drop()` leaves its provisioner
|
|
27
|
+
* behind, and nobody knows its password; after this time it also cannot log in, and the next run's sweep drops it. A
|
|
28
|
+
* run makes new connections until it ends, so this is far beyond the longest run.
|
|
29
|
+
*/
|
|
30
|
+
export declare const TEST_PROVISIONER_LIFETIME_MINUTES = 15;
|
|
31
|
+
/**
|
|
32
|
+
* Creates a throwaway provisioning credential for code the platform runs but did not write, such as an application's
|
|
33
|
+
* generated tests: it can create the test kit's role families and schemas, and reaches no application's data, because
|
|
34
|
+
* a CREATEROLE role administers only the roles it created. The provisioning credential itself never leaves this process.
|
|
35
|
+
* First it drops the provisioners earlier runs left behind (`sweepTestProvisioners`).
|
|
36
|
+
*/
|
|
37
|
+
export declare function createTestProvisioner(provisioningUrl: string): Promise<TestProvisioner>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { SqlDatabase } from '@fabrika/platform';
|
|
2
|
+
import { type PostgresMigration, type PostgresMigrationBundle, type PostgresMigrationPlan } from './postgres-migrations';
|
|
3
|
+
export interface PostgresServiceMigrationsConfig {
|
|
4
|
+
/** Stable service-owned bundle name. */
|
|
5
|
+
readonly name: string;
|
|
6
|
+
/** Stable service-owned ledger table. */
|
|
7
|
+
readonly ledgerTable: string;
|
|
8
|
+
/** Stable service advisory lock. */
|
|
9
|
+
readonly lockKey: number;
|
|
10
|
+
readonly directory: string;
|
|
11
|
+
/** Runtime-owned bundles applied before the service schema. */
|
|
12
|
+
readonly dependencies?: readonly PostgresMigrationBundle[];
|
|
13
|
+
}
|
|
14
|
+
export interface PostgresServiceMigrations {
|
|
15
|
+
read(directory?: string): PostgresMigration[];
|
|
16
|
+
plan(migrations?: readonly PostgresMigration[]): PostgresMigrationPlan;
|
|
17
|
+
apply(db: SqlDatabase, migrations?: readonly PostgresMigration[]): Promise<string[]>;
|
|
18
|
+
applyQualified(db: SqlDatabase, migrations?: readonly PostgresMigration[]): Promise<string[]>;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Define the repeated service migration wrapper while keeping service identity and lifecycle local.
|
|
22
|
+
* Bundle names, ledgers, lock ids, and dependency order remain explicit inputs.
|
|
23
|
+
*/
|
|
24
|
+
export declare function definePostgresServiceMigrations(config: PostgresServiceMigrationsConfig): PostgresServiceMigrations;
|
|
25
|
+
export declare function postgresMigrationResultMessage(applied: readonly string[]): string;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { type SqlToken } from './migration-sql';
|
|
2
|
+
export declare const isName: (token: SqlToken | undefined) => boolean;
|
|
3
|
+
export declare const isPunct: (token: SqlToken | undefined, char: string) => boolean;
|
|
4
|
+
/** A name as Postgres stores it: an unquoted one with only ASCII folded to lower case, either truncated to 63 bytes. */
|
|
5
|
+
export declare function nameText(token: SqlToken | undefined): string | undefined;
|
|
6
|
+
/** The index past a name at `index`, schema-qualified or not, or `undefined` when no name is there. */
|
|
7
|
+
export declare function afterName(tokens: readonly SqlToken[], index: number): number | undefined;
|
|
8
|
+
/** A possibly schema-qualified object name: `qualifier` is set only when the statement wrote one. */
|
|
9
|
+
export interface ObjectName {
|
|
10
|
+
readonly qualifier: string | undefined;
|
|
11
|
+
readonly name: string;
|
|
12
|
+
}
|
|
13
|
+
/** The name at `index` (`name` or `schema.name`), or `undefined`. */
|
|
14
|
+
export declare function objectNameAt(tokens: readonly SqlToken[], index: number): ObjectName | undefined;
|
|
15
|
+
/** The index past the words `words` at `index`, or `index` itself when they are not all there. */
|
|
16
|
+
export declare function skipWords(tokens: readonly SqlToken[], index: number, ...words: readonly string[]): number;
|
|
17
|
+
/** The token lists between the commas at parenthesis depth 0 of `tokens`. */
|
|
18
|
+
export declare function splitAtCommas(tokens: readonly SqlToken[]): SqlToken[][];
|
|
19
|
+
/** The index of the `)` that closes the `(` at `open`, or `undefined`. */
|
|
20
|
+
export declare function closingParenthesis(tokens: readonly SqlToken[], open: number): number | undefined;
|
|
21
|
+
/** The index of the first word `word` at parenthesis depth 0 at or after `from`, or `undefined`. */
|
|
22
|
+
export declare function wordAtDepthZero(tokens: readonly SqlToken[], word: string, from?: number): number | undefined;
|
|
23
|
+
/**
|
|
24
|
+
* What a statement is, for a refusal to name: its first word, and after `CREATE`, `ALTER` or `DROP` (past `OR
|
|
25
|
+
* REPLACE`) the kind of object. A statement that does not begin with a word is named by its first token.
|
|
26
|
+
*/
|
|
27
|
+
export declare function statementKind(tokens: readonly SqlToken[]): string;
|
|
28
|
+
/**
|
|
29
|
+
* The first schema a DDL statement names other than `schema`, `pg_catalog` included: every qualified name counts,
|
|
30
|
+
* since a DDL statement has no table alias to qualify a column with, and so does any unqualified `pg_…` name, which
|
|
31
|
+
* resolves to the catalog (`LIKE pg_authid`, `REFERENCES pg_class`).
|
|
32
|
+
*/
|
|
33
|
+
export declare function ddlForeignSchema(tokens: readonly SqlToken[], schema: string): string | undefined;
|
|
34
|
+
/** How a name is used where it stands; `operator` is the operator `OPERATOR(schema.op)` names. */
|
|
35
|
+
export type NameRole = 'relation' | 'call' | 'type' | 'operator' | 'other';
|
|
36
|
+
/** One name of a statement: `qualifier` is the first part of a two- or three-part name. */
|
|
37
|
+
export interface NameReference {
|
|
38
|
+
readonly qualifier: string | undefined;
|
|
39
|
+
readonly name: string;
|
|
40
|
+
readonly parts: number;
|
|
41
|
+
readonly role: NameRole;
|
|
42
|
+
readonly line: number;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Every name of a DML statement, a query or an expression, with its role. A name in a relation position (after
|
|
46
|
+
* `FROM`, `JOIN`, `USING`, `INTO`, `UPDATE`, `TABLE`, `REFERENCES`, or a `,` of a `FROM` list) is a relation; a name
|
|
47
|
+
* that `(` follows is a call, unless it is syntax or a type with a modifier; a name after `::` or `AS` is a type. A
|
|
48
|
+
* two-part name anywhere else is a column of a table or an alias (`d.note`, `NEW.stage`).
|
|
49
|
+
*/
|
|
50
|
+
export declare function nameReferences(tokens: readonly SqlToken[]): NameReference[];
|
|
51
|
+
/**
|
|
52
|
+
* The first schema a DML statement, a query or a function body reaches other than `schema`, `pg_catalog` included: a
|
|
53
|
+
* qualified relation, call or type, or a three-part name. An unqualified relation whose name starts with `pg_`
|
|
54
|
+
* resolves to the catalog and counts as `pg_catalog`.
|
|
55
|
+
*/
|
|
56
|
+
export declare function dmlForeignSchema(tokens: readonly SqlToken[], schema: string): string | undefined;
|
|
57
|
+
/**
|
|
58
|
+
* The first nested statement a nested position may not hold: in parentheses (a CTE body, a subquery) and after a
|
|
59
|
+
* CTE list, anything but a query or `INSERT`/`UPDATE`/`DELETE`, whose names the same checks read. `MERGE` among them.
|
|
60
|
+
*/
|
|
61
|
+
export declare function nestedStatement(tokens: readonly SqlToken[]): {
|
|
62
|
+
readonly statement: string;
|
|
63
|
+
readonly line: number;
|
|
64
|
+
} | undefined;
|
|
65
|
+
/**
|
|
66
|
+
* The first data-changing statement whose target is in parentheses (`UPDATE ONLY (t)`, `DELETE FROM (t)`): the
|
|
67
|
+
* classifier reads a target as the name after the keyword, so it refuses the form rather than read through a group.
|
|
68
|
+
*/
|
|
69
|
+
export declare function parenthesizedTarget(tokens: readonly SqlToken[]): {
|
|
70
|
+
readonly statement: string;
|
|
71
|
+
readonly line: number;
|
|
72
|
+
} | undefined;
|
|
73
|
+
/** The relations a DML statement or a query names, in order: what its static text reads or writes. */
|
|
74
|
+
export declare function dmlRelations(tokens: readonly SqlToken[]): NameReference[];
|
|
75
|
+
/**
|
|
76
|
+
* Whether a name is the framework's (`fabrika_…`): its tables share the application's schema and hold its secrets,
|
|
77
|
+
* leases and deliveries. Case-insensitive, so a quoted `"FABRIKA_x"` counts too.
|
|
78
|
+
*/
|
|
79
|
+
export declare function isFrameworkName(name: string): boolean;
|
|
80
|
+
/** The first framework name among `tokens` that `allowed` does not hold, with its line. */
|
|
81
|
+
export declare function frameworkReference(tokens: readonly SqlToken[], allowed?: ReadonlySet<string>): {
|
|
82
|
+
readonly name: string;
|
|
83
|
+
readonly line: number;
|
|
84
|
+
} | undefined;
|
|
85
|
+
/** `U&"…"`: an identifier whose escapes the lexer does not decode, so a name rule cannot read it. */
|
|
86
|
+
export declare function unicodeIdentifier(tokens: readonly SqlToken[]): {
|
|
87
|
+
readonly line: number;
|
|
88
|
+
} | undefined;
|
|
89
|
+
/** How a call is admitted: `paved` and `reviewed` built-ins, `expression`-only ones, and the migrations' own functions. */
|
|
90
|
+
export type FunctionUse = 'paved' | 'reviewed' | 'declared';
|
|
91
|
+
/**
|
|
92
|
+
* How a call of `call` is admitted, or `undefined` when it is not. A function the migrations declare in `schema` may be
|
|
93
|
+
* called unqualified or qualified with `schema`; a built-in only unqualified, since a qualified one names `pg_catalog`.
|
|
94
|
+
*/
|
|
95
|
+
export declare function functionUse(call: NameReference, schema: string, functions: ReadonlySet<string>): FunctionUse | undefined;
|
|
96
|
+
/**
|
|
97
|
+
* The first call among `tokens` that a body, a query or a backfill may not make: anything but a paved or reviewed
|
|
98
|
+
* built-in, or a function the migrations declare in `schema`. An allowlist, because a built-in such as
|
|
99
|
+
* `query_to_xml`, `dblink` or `lo_import` runs SQL from a string or reaches past the schema, which no name rule sees.
|
|
100
|
+
*/
|
|
101
|
+
export declare function refusedFunctionCall(tokens: readonly SqlToken[], schema: string, functions: ReadonlySet<string>): NameReference | undefined;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { SqlDatabase, SqlQueryResult, SqlRunResult, SqlStatement } from '@fabrika/platform';
|
|
2
|
+
/** What a bound value may be. Anything else is a programming error and throws at bind time, as D1 does. */
|
|
3
|
+
export type SqlBindValue = string | number | bigint | boolean | Date | Uint8Array | null;
|
|
4
|
+
/**
|
|
5
|
+
* One prepared statement. IMMUTABLE: `bind()` returns a NEW statement carrying the values and leaves
|
|
6
|
+
* the receiver untouched, which is D1's contract and what call sites rely on when they prepare once
|
|
7
|
+
* and bind in a loop.
|
|
8
|
+
*/
|
|
9
|
+
export declare class PostgresStatement implements SqlStatement {
|
|
10
|
+
private readonly sql;
|
|
11
|
+
private readonly text;
|
|
12
|
+
private readonly placeholders;
|
|
13
|
+
private readonly values;
|
|
14
|
+
constructor(sql: Bun.SQL, text: string, placeholders: number, values: readonly SqlBindValue[]);
|
|
15
|
+
bind(...values: unknown[]): SqlStatement;
|
|
16
|
+
first<T = Record<string, unknown>>(): Promise<T | null>;
|
|
17
|
+
all<T = Record<string, unknown>>(): Promise<SqlQueryResult<T>>;
|
|
18
|
+
run(): Promise<SqlRunResult>;
|
|
19
|
+
/** This statement, once every placeholder has its value: a wrong count fails before any connection is taken. */
|
|
20
|
+
bound(): PostgresStatement;
|
|
21
|
+
/**
|
|
22
|
+
* Run this statement on a specific handle — a reserved connection of the pool, or the one `batch()` holds its
|
|
23
|
+
* transaction on. Internal to the package; the port only ever sees `first`/`all`/`run`.
|
|
24
|
+
*/
|
|
25
|
+
rows<T>(handle: Bun.SQL): Promise<T[]>;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* A Postgres-backed `SqlDatabase`. Rewritten statement text is memoised per SQL string, so the
|
|
29
|
+
* prepare-once/bind-many idiom tokenises each statement exactly once.
|
|
30
|
+
*/
|
|
31
|
+
export declare class PostgresDatabase implements SqlDatabase {
|
|
32
|
+
private readonly sql;
|
|
33
|
+
private readonly rewrites;
|
|
34
|
+
constructor(sql: Bun.SQL);
|
|
35
|
+
/**
|
|
36
|
+
* Open a pooled client. The URL is a CREDENTIAL — it is never logged, and never included in an
|
|
37
|
+
* error message raised here.
|
|
38
|
+
*/
|
|
39
|
+
static connect(url: string, options?: Omit<Bun.SQL.PostgresOrMySQLOptions, 'url' | 'adapter'>): PostgresDatabase;
|
|
40
|
+
prepare(sql: string): SqlStatement;
|
|
41
|
+
/**
|
|
42
|
+
* Run every statement in ONE transaction, in order. A throw rolls the whole batch back. The transaction is sent by
|
|
43
|
+
* hand on the reserved connection: Bun 1.3.14's `begin()` on a reserved connection rejects a failed statement twice,
|
|
44
|
+
* once unhandled.
|
|
45
|
+
*/
|
|
46
|
+
batch<T = Record<string, unknown>>(statements: SqlStatement[]): Promise<SqlQueryResult<T>[]>;
|
|
47
|
+
/** Close the pool. Waits for in-flight queries unless a timeout is given. */
|
|
48
|
+
close(options?: {
|
|
49
|
+
timeout?: number;
|
|
50
|
+
}): Promise<void>;
|
|
51
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { applicationDatabaseSchema } from './application-schema';
|
|
2
|
+
export { FILE_SCAN_COLUMNS, FILE_STATUSES, FILE_TABLE_PREFIX, fileScanAlter, fileScanAlterTable, fileScanIndex, fileStatusCheck, fileTableScanReady, legacyStatusCheckName, } from './file-table-ddl';
|
|
3
|
+
export { classifyMigration, type ClassifyMigrationOptions, classifyMigrations, type DatabaseObject, type DatabaseObjectKind, declaredFunctions, type DeviationKind, type ManifestChange, type MigrationClassification, type MigrationDeviation, type MigrationFile, migrationManifest, type MigrationRefusal, type MigrationSetClassification, } from './migration-classify';
|
|
4
|
+
export { lexSql, type SqlToken, sqlTokens, type Statement, topLevelStatements, upperWord } from './migration-sql';
|
|
5
|
+
export { PG_CATALOG_FUNCTIONS, SQL_KEYWORDS } from './pg-catalog-names';
|
|
6
|
+
export { afterName, closingParenthesis, ddlForeignSchema, dmlForeignSchema, dmlRelations, frameworkReference, type FunctionUse, functionUse, isFrameworkName, isPunct, type NameReference, nameReferences, type NameRole, nameText, nestedStatement, objectNameAt, parenthesizedTarget, refusedFunctionCall, skipWords, splitAtCommas, statementKind, unicodeIdentifier, } from './sql-names';
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { WaitUntil } from '@fabrika/platform';
|
|
2
|
+
export interface BackgroundTasks {
|
|
3
|
+
/** The port itself. Hand it a promise and forget it; it never throws and never rejects. */
|
|
4
|
+
readonly waitUntil: WaitUntil;
|
|
5
|
+
/** How many supervised promises are still in flight. */
|
|
6
|
+
readonly pending: number;
|
|
7
|
+
/** Settle once every supervised promise has finished, including ones added while draining. */
|
|
8
|
+
drain(): Promise<void>;
|
|
9
|
+
}
|
|
10
|
+
export interface BackgroundTasksOptions {
|
|
11
|
+
/**
|
|
12
|
+
* Called for every rejection instead of the default log. Keep it cheap and non-throwing — a throw
|
|
13
|
+
* here is itself swallowed. NEVER log the error object: it may quote a clone URL with an embedded
|
|
14
|
+
* token.
|
|
15
|
+
*/
|
|
16
|
+
onError?: (error: unknown) => void;
|
|
17
|
+
/** Prefix for the default log line, so a process running several of these can tell them apart. */
|
|
18
|
+
label?: string;
|
|
19
|
+
}
|
|
20
|
+
export declare function createBackgroundTasks(options?: BackgroundTasksOptions): BackgroundTasks;
|