@fabrika/platform-node 0.3.3 → 0.4.0
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 -2
- package/src/application-postgres-grants.ts +117 -0
- package/src/application-postgres-migrations.ts +152 -0
- package/src/application-postgres-roles.ts +239 -0
- package/src/application-schema.ts +21 -0
- package/src/blob-s3.ts +88 -2
- package/src/database-code.ts +164 -0
- package/src/file-table-ddl.ts +96 -0
- package/src/index.ts +21 -0
- package/src/migration-classify.ts +1044 -0
- package/src/migration-sql.ts +204 -0
- package/src/pg-catalog-names.ts +359 -0
- package/src/postgres-migration-error.ts +4 -0
- package/src/postgres-migrations.ts +270 -25
- package/src/postgres-test-provisioner.ts +96 -0
- package/src/sql-names.ts +740 -0
- package/src/sql-postgres.ts +56 -22
- package/src/sql.ts +52 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// The catalog half of ADR-0017's migration rules: the runtime witness. Postgres has already resolved every name the
|
|
2
|
+
// schema stores, so this reads the catalog instead of the SQL text: the runner runs it inside each migration's
|
|
3
|
+
// transaction, before its commit, and `@fabrika/db`'s codegen runs it at every start. It refuses what escapes the
|
|
4
|
+
// schema and any database object that no migration declares, such as one created at runtime.
|
|
5
|
+
|
|
6
|
+
import type { DatabaseObject } from './migration-classify'
|
|
7
|
+
|
|
8
|
+
const IDENTIFIER = /^[a-z_][a-z0-9_]*$/
|
|
9
|
+
const encoder = new TextEncoder()
|
|
10
|
+
|
|
11
|
+
/** A text literal with no quote and no backslash in it, whatever the session's `standard_conforming_strings`. */
|
|
12
|
+
function hexText(value: string): string {
|
|
13
|
+
const hex = Array.from(encoder.encode(value), (byte) => byte.toString(16).padStart(2, '0')).join('')
|
|
14
|
+
return `pg_catalog.convert_from(pg_catalog.decode('${hex}', 'hex'), 'UTF8')`
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** The manifest as a `text[]` of `<kind> <name>`, the names `objects` gives the schema's objects. */
|
|
18
|
+
function declaredArray(declared: readonly DatabaseObject[]): string {
|
|
19
|
+
if (declared.length === 0) return 'ARRAY[]::pg_catalog.text[]'
|
|
20
|
+
return `ARRAY[${declared.map((object) => hexText(`${object.kind} ${object.name}`)).join(', ')}]::pg_catalog.text[]`
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** A catalog's `regclass`, qualified, since the query may run with the application's schema first on the path. */
|
|
24
|
+
const catalog = (table: string): string => `'pg_catalog.${table}'::pg_catalog.regclass`
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* One row (`violation`) per escape and per undeclared object in `schema`. Escapes: a function that is `SECURITY
|
|
28
|
+
* DEFINER`, has a `SET` clause or a language other than `sql` and `plpgsql`; an event trigger on a function of the
|
|
29
|
+
* schema; and any dependency of an object of the schema on a type, function, relation, operator, operator class or
|
|
30
|
+
* collation outside the schema and `pg_catalog`; a routine that shadows a `pg_catalog` name or has more than one
|
|
31
|
+
* signature; a sequence other than an identity column's. Undeclared: a function, procedure or aggregate, operator,
|
|
32
|
+
* trigger (a foreign key's excepted), rule, view, policy or domain that `declared` does not name. Every name is
|
|
33
|
+
* qualified with `pg_catalog`, the query has no backslash, and the manifest is
|
|
34
|
+
* hex text, so the session's `search_path` and `standard_conforming_strings` cannot change what it finds.
|
|
35
|
+
*/
|
|
36
|
+
export function databaseCodeQuery(schema: string, declared: readonly DatabaseObject[]): string {
|
|
37
|
+
if (!IDENTIFIER.test(schema)) throw new Error(`Invalid application schema: ${schema}`)
|
|
38
|
+
// A constant, not a subquery, so the planner sizes each scan of the schema and does not reach for JIT.
|
|
39
|
+
const namespace = `'${schema}'::pg_catalog.regnamespace::pg_catalog.oid`
|
|
40
|
+
const trusted = `(SELECT oid FROM pg_catalog.pg_namespace WHERE nspname IN ('${schema}', 'pg_catalog'))`
|
|
41
|
+
const outside = (kind: string, table: string, name: string, namespaceColumn: string): string =>
|
|
42
|
+
`SELECT d.object || ' depends on the ${kind} ' || s.nspname || '.' || r.${name}
|
|
43
|
+
FROM dependencies d
|
|
44
|
+
JOIN pg_catalog.${table} r ON d.refclassid = ${catalog(table)} AND r.oid = d.refobjid
|
|
45
|
+
JOIN pg_catalog.pg_namespace s ON s.oid = r.${namespaceColumn}
|
|
46
|
+
WHERE r.${namespaceColumn} NOT IN ${trusted}`
|
|
47
|
+
return `
|
|
48
|
+
WITH objects AS (
|
|
49
|
+
SELECT CASE p.prokind WHEN 'a' THEN 'aggregate ' WHEN 'p' THEN 'procedure ' ELSE 'function ' END || p.proname AS object
|
|
50
|
+
FROM pg_catalog.pg_proc p
|
|
51
|
+
WHERE p.pronamespace = ${namespace} AND NOT EXISTS (
|
|
52
|
+
SELECT 1 FROM pg_catalog.pg_depend d WHERE d.classid = ${catalog('pg_proc')} AND d.objid = p.oid AND d.deptype = 'i'
|
|
53
|
+
)
|
|
54
|
+
UNION ALL
|
|
55
|
+
SELECT 'operator ' || o.oprname FROM pg_catalog.pg_operator o WHERE o.oprnamespace = ${namespace}
|
|
56
|
+
UNION ALL
|
|
57
|
+
SELECT 'trigger ' || c.relname || '.' || t.tgname
|
|
58
|
+
FROM pg_catalog.pg_trigger t JOIN pg_catalog.pg_class c ON c.oid = t.tgrelid
|
|
59
|
+
WHERE c.relnamespace = ${namespace} AND NOT t.tgisinternal
|
|
60
|
+
UNION ALL
|
|
61
|
+
SELECT 'rule ' || c.relname || '.' || r.rulename
|
|
62
|
+
FROM pg_catalog.pg_rewrite r JOIN pg_catalog.pg_class c ON c.oid = r.ev_class
|
|
63
|
+
WHERE c.relnamespace = ${namespace} AND r.rulename <> '_RETURN'
|
|
64
|
+
UNION ALL
|
|
65
|
+
SELECT CASE c.relkind WHEN 'm' THEN 'materialized view ' ELSE 'view ' END || c.relname
|
|
66
|
+
FROM pg_catalog.pg_class c WHERE c.relnamespace = ${namespace} AND c.relkind IN ('v', 'm')
|
|
67
|
+
UNION ALL
|
|
68
|
+
SELECT 'policy ' || c.relname || '.' || p.polname
|
|
69
|
+
FROM pg_catalog.pg_policy p JOIN pg_catalog.pg_class c ON c.oid = p.polrelid
|
|
70
|
+
WHERE c.relnamespace = ${namespace}
|
|
71
|
+
UNION ALL
|
|
72
|
+
SELECT 'domain ' || t.typname FROM pg_catalog.pg_type t WHERE t.typnamespace = ${namespace} AND t.typtype = 'd'
|
|
73
|
+
),
|
|
74
|
+
dependents AS (
|
|
75
|
+
SELECT ${catalog('pg_class')} AS classid, c.oid AS objid,
|
|
76
|
+
CASE c.relkind WHEN 'i' THEN 'index ' WHEN 'v' THEN 'view ' WHEN 'm' THEN 'materialized view ' WHEN 'S' THEN 'sequence ' ELSE 'table ' END
|
|
77
|
+
|| c.relname AS object
|
|
78
|
+
FROM pg_catalog.pg_class c WHERE c.relnamespace = ${namespace}
|
|
79
|
+
UNION ALL
|
|
80
|
+
SELECT ${catalog('pg_proc')}, p.oid, 'function ' || p.proname FROM pg_catalog.pg_proc p WHERE p.pronamespace = ${namespace}
|
|
81
|
+
UNION ALL
|
|
82
|
+
SELECT ${catalog('pg_type')}, t.oid, 'type ' || t.typname FROM pg_catalog.pg_type t WHERE t.typnamespace = ${namespace}
|
|
83
|
+
UNION ALL
|
|
84
|
+
SELECT ${catalog('pg_operator')}, o.oid, 'operator ' || o.oprname FROM pg_catalog.pg_operator o WHERE o.oprnamespace = ${namespace}
|
|
85
|
+
UNION ALL
|
|
86
|
+
SELECT ${catalog('pg_constraint')}, k.oid, 'constraint ' || k.conname FROM pg_catalog.pg_constraint k WHERE k.connamespace = ${namespace}
|
|
87
|
+
UNION ALL
|
|
88
|
+
SELECT ${catalog('pg_attrdef')}, d.oid, 'default ' || c.relname || '.' || a.attname
|
|
89
|
+
FROM pg_catalog.pg_attrdef d
|
|
90
|
+
JOIN pg_catalog.pg_attribute a ON a.attrelid = d.adrelid AND a.attnum = d.adnum
|
|
91
|
+
JOIN pg_catalog.pg_class c ON c.oid = d.adrelid
|
|
92
|
+
WHERE c.relnamespace = ${namespace}
|
|
93
|
+
UNION ALL
|
|
94
|
+
SELECT ${catalog('pg_trigger')}, t.oid, 'trigger ' || c.relname || '.' || t.tgname
|
|
95
|
+
FROM pg_catalog.pg_trigger t JOIN pg_catalog.pg_class c ON c.oid = t.tgrelid WHERE c.relnamespace = ${namespace}
|
|
96
|
+
UNION ALL
|
|
97
|
+
SELECT ${catalog('pg_rewrite')}, r.oid, CASE WHEN r.rulename = '_RETURN' THEN 'view ' || c.relname ELSE 'rule ' || c.relname || '.' || r.rulename END
|
|
98
|
+
FROM pg_catalog.pg_rewrite r JOIN pg_catalog.pg_class c ON c.oid = r.ev_class WHERE c.relnamespace = ${namespace}
|
|
99
|
+
UNION ALL
|
|
100
|
+
SELECT ${catalog('pg_policy')}, p.oid, 'policy ' || c.relname || '.' || p.polname
|
|
101
|
+
FROM pg_catalog.pg_policy p JOIN pg_catalog.pg_class c ON c.oid = p.polrelid WHERE c.relnamespace = ${namespace}
|
|
102
|
+
),
|
|
103
|
+
dependencies AS (
|
|
104
|
+
SELECT DISTINCT o.object, d.refclassid, d.refobjid
|
|
105
|
+
FROM pg_catalog.pg_depend d JOIN dependents o ON o.classid = d.classid AND o.objid = d.objid
|
|
106
|
+
),
|
|
107
|
+
violations AS (
|
|
108
|
+
SELECT 'function ' || p.proname || ' is SECURITY DEFINER' AS violation
|
|
109
|
+
FROM pg_catalog.pg_proc p WHERE p.pronamespace = ${namespace} AND p.prosecdef
|
|
110
|
+
UNION ALL
|
|
111
|
+
SELECT 'function ' || p.proname || ' has a SET clause' FROM pg_catalog.pg_proc p WHERE p.pronamespace = ${namespace} AND p.proconfig IS NOT NULL
|
|
112
|
+
UNION ALL
|
|
113
|
+
SELECT 'function ' || p.proname || ' is in the language ' || l.lanname
|
|
114
|
+
FROM pg_catalog.pg_proc p JOIN pg_catalog.pg_language l ON l.oid = p.prolang
|
|
115
|
+
WHERE p.pronamespace = ${namespace} AND l.lanname NOT IN ('sql', 'plpgsql')
|
|
116
|
+
UNION ALL
|
|
117
|
+
SELECT DISTINCT 'function ' || p.proname || ' shadows a pg_catalog function'
|
|
118
|
+
FROM pg_catalog.pg_proc p
|
|
119
|
+
JOIN pg_catalog.pg_proc b ON b.proname = p.proname
|
|
120
|
+
JOIN pg_catalog.pg_namespace n ON n.oid = b.pronamespace AND n.nspname = 'pg_catalog'
|
|
121
|
+
WHERE p.pronamespace = ${namespace}
|
|
122
|
+
UNION ALL
|
|
123
|
+
SELECT 'routine ' || p.proname || ' has more than one signature'
|
|
124
|
+
FROM pg_catalog.pg_proc p WHERE p.pronamespace = ${namespace} GROUP BY p.proname HAVING pg_catalog.count(*) > 1
|
|
125
|
+
UNION ALL
|
|
126
|
+
SELECT 'sequence ' || c.relname || ' (only an identity column''s; use GENERATED ALWAYS AS IDENTITY)'
|
|
127
|
+
FROM pg_catalog.pg_class c
|
|
128
|
+
WHERE c.relnamespace = ${namespace} AND c.relkind = 'S' AND NOT EXISTS (
|
|
129
|
+
SELECT 1 FROM pg_catalog.pg_depend d
|
|
130
|
+
WHERE d.classid = ${catalog('pg_class')} AND d.objid = c.oid AND d.refclassid = ${catalog('pg_class')} AND d.refobjsubid > 0 AND d.deptype = 'i'
|
|
131
|
+
)
|
|
132
|
+
UNION ALL
|
|
133
|
+
SELECT 'event trigger ' || e.evtname FROM pg_catalog.pg_event_trigger e JOIN pg_catalog.pg_proc p ON p.oid = e.evtfoid
|
|
134
|
+
WHERE p.pronamespace = ${namespace}
|
|
135
|
+
UNION ALL
|
|
136
|
+
${outside('type', 'pg_type', 'typname', 'typnamespace')}
|
|
137
|
+
UNION ALL
|
|
138
|
+
${outside('function', 'pg_proc', 'proname', 'pronamespace')}
|
|
139
|
+
UNION ALL
|
|
140
|
+
${outside('relation', 'pg_class', 'relname', 'relnamespace')}
|
|
141
|
+
UNION ALL
|
|
142
|
+
${outside('operator', 'pg_operator', 'oprname', 'oprnamespace')}
|
|
143
|
+
UNION ALL
|
|
144
|
+
${outside('operator class', 'pg_opclass', 'opcname', 'opcnamespace')}
|
|
145
|
+
UNION ALL
|
|
146
|
+
${outside('collation', 'pg_collation', 'collname', 'collnamespace')}
|
|
147
|
+
UNION ALL
|
|
148
|
+
SELECT o.object || ' (no migration declares it)' FROM objects o WHERE o.object <> ALL (${declaredArray(declared)})
|
|
149
|
+
)
|
|
150
|
+
SELECT DISTINCT violation FROM violations ORDER BY violation`
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** The message a `databaseCodeCheck` failure carries before its list of violations. */
|
|
154
|
+
export const DATABASE_CODE_FAILURE = 'fabrika-migration-database-code: '
|
|
155
|
+
|
|
156
|
+
/** Fails the transaction it runs in when `schema` escapes or holds an object `declared` does not name. */
|
|
157
|
+
export function databaseCodeCheck(schema: string, declared: readonly DatabaseObject[]): string {
|
|
158
|
+
return `DO $check$
|
|
159
|
+
DECLARE found text;
|
|
160
|
+
BEGIN
|
|
161
|
+
SELECT pg_catalog.string_agg(violation, ', ' ORDER BY violation) INTO found FROM (${databaseCodeQuery(schema, declared)}) AS code;
|
|
162
|
+
IF found IS NOT NULL THEN RAISE EXCEPTION '${DATABASE_CODE_FAILURE}%', found; END IF;
|
|
163
|
+
END $check$`
|
|
164
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// The SQL of a file collection's table that three readers must agree on: `@fabrika/db`'s `fileTableSql` and
|
|
2
|
+
// `fileScanSql` write it, the migration classifier admits exactly the scan migration, and `@fabrika/check` recognizes
|
|
3
|
+
// a table that can hold the scan states. Pure: tokens in, answers out.
|
|
4
|
+
|
|
5
|
+
import { type SqlToken, sqlTokens, topLevelStatements, upperWord } from './migration-sql'
|
|
6
|
+
import { nameText } from './sql-names'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A file row's states. `pending`: its object is still being written. `quarantined`: written to a scanning collection
|
|
10
|
+
* and not yet given a clean verdict. `live`: served. `rejected`: refused by the scan and its object is gone.
|
|
11
|
+
*/
|
|
12
|
+
export const FILE_STATUSES = ['pending', 'live', 'quarantined', 'rejected'] as const
|
|
13
|
+
|
|
14
|
+
export const FILE_TABLE_PREFIX = 'fabrika_files_'
|
|
15
|
+
|
|
16
|
+
/** The columns the scan job keeps: the scanned object's version, and the retry count and time of a file without a verdict. */
|
|
17
|
+
export const FILE_SCAN_COLUMNS = ['object_version TEXT', 'scan_attempts INTEGER NOT NULL DEFAULT 0', 'scan_after TIMESTAMPTZ'] as const
|
|
18
|
+
|
|
19
|
+
const STATUS_CHECK = `CHECK (status IN (${FILE_STATUSES.map((status) => `'${status}'`).join(', ')}))`
|
|
20
|
+
|
|
21
|
+
/** The named status check of every state. */
|
|
22
|
+
export function fileStatusCheck(table: string): string {
|
|
23
|
+
return `CONSTRAINT ${table}_status ${STATUS_CHECK}`
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** The index the scan job selects its batch by. */
|
|
27
|
+
export function fileScanIndex(table: string): string {
|
|
28
|
+
return `CREATE INDEX ${table}_scan ON ${table} (scan_after) WHERE status = 'quarantined'`
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The name Postgres gave the unnamed status check of a table an earlier `fileTableSql` created:
|
|
33
|
+
* `<table>_status_check`, the table's name cut to 50 characters first (`makeObjectName`).
|
|
34
|
+
*/
|
|
35
|
+
export function legacyStatusCheckName(table: string): string {
|
|
36
|
+
return `${table.slice(0, 63 - '_status_check'.length)}_status_check`
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The statement of `fileScanSql` that widens a table an earlier `fileTableSql` created: its check and its columns. */
|
|
40
|
+
export function fileScanAlter(table: string): string {
|
|
41
|
+
const columns = FILE_SCAN_COLUMNS.map((column) => `ADD COLUMN ${column}`).join(', ')
|
|
42
|
+
return `ALTER TABLE ${table} DROP CONSTRAINT IF EXISTS ${legacyStatusCheckName(table)}, ADD ${fileStatusCheck(table)}, ${columns}`
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const tokenKey = (token: SqlToken): string =>
|
|
46
|
+
token.kind === 'word'
|
|
47
|
+
? `w:${token.text.toLowerCase()}`
|
|
48
|
+
: token.kind === 'punct'
|
|
49
|
+
? `p:${token.char}`
|
|
50
|
+
: token.kind === 'string'
|
|
51
|
+
? `s:${token.text ?? ''}`
|
|
52
|
+
: `${token.kind}:`
|
|
53
|
+
|
|
54
|
+
const keys = (tokens: readonly SqlToken[]): string[] => tokens.map(tokenKey)
|
|
55
|
+
|
|
56
|
+
const equalKeys = (actual: readonly string[], expected: readonly string[]): boolean =>
|
|
57
|
+
actual.length === expected.length && expected.every((key, index) => actual[index] === key)
|
|
58
|
+
|
|
59
|
+
/** Whether `needle`'s tokens appear in `haystack`, in a row. */
|
|
60
|
+
function containsRun(haystack: readonly string[], needle: readonly string[]): boolean {
|
|
61
|
+
for (let start = 0; start + needle.length <= haystack.length; start++) {
|
|
62
|
+
if (needle.every((key, offset) => haystack[start + offset] === key)) return true
|
|
63
|
+
}
|
|
64
|
+
return false
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The file table a statement is exactly `fileScanAlter` of, or `undefined`. */
|
|
68
|
+
export function fileScanAlterTable(tokens: readonly SqlToken[]): string | undefined {
|
|
69
|
+
const table = nameText(tokens[2])
|
|
70
|
+
if (upperWord(tokens[0]) !== 'ALTER' || table?.startsWith(FILE_TABLE_PREFIX) !== true || tokens[2]?.kind !== 'word') return undefined
|
|
71
|
+
return equalKeys(keys(tokens), keys(sqlTokens(fileScanAlter(table)))) ? table : undefined
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Whether a `CREATE TABLE` of `table` declares the check of every state and the scan columns. */
|
|
75
|
+
function createsScanReady(tokens: readonly SqlToken[], table: string): boolean {
|
|
76
|
+
const statement = keys(tokens)
|
|
77
|
+
return containsRun(statement, keys(sqlTokens(fileStatusCheck(table))))
|
|
78
|
+
&& FILE_SCAN_COLUMNS.every((column) => containsRun(statement, keys(sqlTokens(column))))
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Whether the migrations, in order, leave `table` able to hold the scan states: created with the check of every state
|
|
83
|
+
* and the scan columns, or widened by `fileScanAlter`. `undefined` when none of them creates it.
|
|
84
|
+
*/
|
|
85
|
+
export function fileTableScanReady(migrations: readonly string[], table: string): boolean | undefined {
|
|
86
|
+
let state: boolean | undefined
|
|
87
|
+
for (const sql of migrations) {
|
|
88
|
+
for (const { tokens } of topLevelStatements(sql)) {
|
|
89
|
+
const create = upperWord(tokens[0]) === 'CREATE' && upperWord(tokens[1]) === 'TABLE'
|
|
90
|
+
const named = upperWord(tokens[2]) === 'IF' ? tokens[5] : tokens[2]
|
|
91
|
+
if (create && nameText(named) === table) state = createsScanReady(tokens, table)
|
|
92
|
+
else if (state !== undefined && fileScanAlterTable(tokens) === table) state = true
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return state
|
|
96
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -18,8 +18,24 @@
|
|
|
18
18
|
// Migrations for what this package owns (the jobs table, the Postgres lock table) live in
|
|
19
19
|
// `migrations/` — DDL is per-dialect even when the statement over it is portable.
|
|
20
20
|
|
|
21
|
+
export {
|
|
22
|
+
APPLICATION_MIGRATION_BUNDLE,
|
|
23
|
+
APPLICATION_MIGRATION_LEDGER,
|
|
24
|
+
applicationDatabaseRoles,
|
|
25
|
+
applicationMigrationLockKey,
|
|
26
|
+
applicationRuntimeUrl,
|
|
27
|
+
DEFAULT_MIGRATION_LOCK_TIMEOUT_MS,
|
|
28
|
+
DEFAULT_MIGRATION_STATEMENT_TIMEOUT_MS,
|
|
29
|
+
definePostgresApplicationMigrations,
|
|
30
|
+
dropApplicationDatabase,
|
|
31
|
+
type PostgresApplicationMigrations,
|
|
32
|
+
type PostgresApplicationMigrationsConfig,
|
|
33
|
+
} from './application-postgres-migrations'
|
|
34
|
+
export type { ApplicationDatabaseRoles } from './application-postgres-roles'
|
|
35
|
+
export { applicationDatabaseSchema } from './application-schema'
|
|
21
36
|
export { FileSystemAssetServer, type FileSystemAssetServerOptions } from './asset-server-fs'
|
|
22
37
|
export { S3BlobStore, type S3BlobStoreOptions } from './blob-s3'
|
|
38
|
+
export { databaseCodeQuery } from './database-code'
|
|
23
39
|
export {
|
|
24
40
|
decodeJob,
|
|
25
41
|
type Job,
|
|
@@ -28,19 +44,24 @@ export {
|
|
|
28
44
|
PostgresJobQueue,
|
|
29
45
|
type PostgresJobQueueOptions,
|
|
30
46
|
} from './job-queue-postgres'
|
|
47
|
+
export type { DatabaseObject, DatabaseObjectKind } from './migration-classify'
|
|
31
48
|
export { rewritePlaceholders, type RewrittenSql } from './placeholders'
|
|
32
49
|
export {
|
|
33
50
|
applyPostgresMigrations,
|
|
34
51
|
definePostgresMigrationPlan,
|
|
52
|
+
migrationChecksum,
|
|
35
53
|
platformNodePostgresMigrationBundle,
|
|
54
|
+
type PostgresApplicationMigrationSettings,
|
|
36
55
|
type PostgresMigration,
|
|
37
56
|
type PostgresMigrationBundle,
|
|
57
|
+
PostgresMigrationError,
|
|
38
58
|
postgresMigrationFilenames,
|
|
39
59
|
postgresMigrationIdentity,
|
|
40
60
|
type PostgresMigrationPlan,
|
|
41
61
|
readPostgresMigrationBundle,
|
|
42
62
|
type ReadPostgresMigrationBundleOptions,
|
|
43
63
|
} from './postgres-migrations'
|
|
64
|
+
export { createTestProvisioner, type TestProvisioner } from './postgres-test-provisioner'
|
|
44
65
|
export {
|
|
45
66
|
definePostgresServiceMigrations,
|
|
46
67
|
postgresMigrationResultMessage,
|