@o/database 0.16.18 → 0.16.19-canary.1790191946359
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/dist/takeout/better-auth.d.ts +19 -0
- package/dist/takeout/better-auth.d.ts.map +1 -0
- package/dist/takeout/better-auth.js +59 -0
- package/dist/takeout/better-auth.js.map +1 -0
- package/dist/takeout/build.d.ts +2 -0
- package/dist/takeout/build.d.ts.map +1 -0
- package/dist/takeout/build.js +3 -0
- package/dist/takeout/build.js.map +1 -0
- package/dist/takeout/chunkedQuery.d.ts +17 -0
- package/dist/takeout/chunkedQuery.d.ts.map +1 -0
- package/dist/takeout/chunkedQuery.js +55 -0
- package/dist/takeout/chunkedQuery.js.map +1 -0
- package/dist/takeout/cli.d.ts +3 -0
- package/dist/takeout/cli.d.ts.map +1 -0
- package/dist/takeout/cli.js +114 -0
- package/dist/takeout/cli.js.map +1 -0
- package/dist/takeout/createDatabase.d.ts +49 -0
- package/dist/takeout/createDatabase.d.ts.map +1 -0
- package/dist/takeout/createDatabase.js +100 -0
- package/dist/takeout/createDatabase.js.map +1 -0
- package/dist/takeout/createDb.d.ts +3 -0
- package/dist/takeout/createDb.d.ts.map +1 -0
- package/dist/takeout/createDb.js +11 -0
- package/dist/takeout/createDb.js.map +1 -0
- package/dist/takeout/createDrizzle.d.ts +38 -0
- package/dist/takeout/createDrizzle.d.ts.map +1 -0
- package/dist/takeout/createDrizzle.js +60 -0
- package/dist/takeout/createDrizzle.js.map +1 -0
- package/dist/takeout/createPool.d.ts +11 -0
- package/dist/takeout/createPool.d.ts.map +1 -0
- package/dist/takeout/createPool.js +36 -0
- package/dist/takeout/createPool.js.map +1 -0
- package/dist/takeout/createServerHelpers.d.ts +9 -0
- package/dist/takeout/createServerHelpers.d.ts.map +1 -0
- package/dist/takeout/createServerHelpers.js +11 -0
- package/dist/takeout/createServerHelpers.js.map +1 -0
- package/dist/takeout/getDBClient.d.ts +11 -0
- package/dist/takeout/getDBClient.d.ts.map +1 -0
- package/dist/takeout/getDBClient.js +169 -0
- package/dist/takeout/getDBClient.js.map +1 -0
- package/dist/takeout/index.d.ts +13 -0
- package/dist/takeout/index.d.ts.map +1 -0
- package/dist/takeout/index.js +13 -0
- package/dist/takeout/index.js.map +1 -0
- package/dist/takeout/migrate.d.ts +33 -0
- package/dist/takeout/migrate.d.ts.map +1 -0
- package/dist/takeout/migrate.js +214 -0
- package/dist/takeout/migrate.js.map +1 -0
- package/dist/takeout/scripts/build-migrations.d.ts +8 -0
- package/dist/takeout/scripts/build-migrations.d.ts.map +1 -0
- package/dist/takeout/scripts/build-migrations.js +51 -0
- package/dist/takeout/scripts/build-migrations.js.map +1 -0
- package/dist/takeout/scripts/drizzle-migrations-sync.d.ts +17 -0
- package/dist/takeout/scripts/drizzle-migrations-sync.d.ts.map +1 -0
- package/dist/takeout/scripts/drizzle-migrations-sync.js +69 -0
- package/dist/takeout/scripts/drizzle-migrations-sync.js.map +1 -0
- package/dist/takeout/scripts/dump-database.d.ts +22 -0
- package/dist/takeout/scripts/dump-database.d.ts.map +1 -0
- package/dist/takeout/scripts/dump-database.js +64 -0
- package/dist/takeout/scripts/dump-database.js.map +1 -0
- package/dist/takeout/scripts/migration-add.d.ts +6 -0
- package/dist/takeout/scripts/migration-add.d.ts.map +1 -0
- package/dist/takeout/scripts/migration-add.js +39 -0
- package/dist/takeout/scripts/migration-add.js.map +1 -0
- package/dist/takeout/scripts/pg_dump.d.ts +11 -0
- package/dist/takeout/scripts/pg_dump.d.ts.map +1 -0
- package/dist/takeout/scripts/pg_dump.js +37 -0
- package/dist/takeout/scripts/pg_dump.js.map +1 -0
- package/dist/takeout/scripts/prune-backups.d.ts +8 -0
- package/dist/takeout/scripts/prune-backups.d.ts.map +1 -0
- package/dist/takeout/scripts/prune-backups.js +166 -0
- package/dist/takeout/scripts/prune-backups.js.map +1 -0
- package/dist/takeout/scripts/psql.d.ts +11 -0
- package/dist/takeout/scripts/psql.d.ts.map +1 -0
- package/dist/takeout/scripts/psql.js +41 -0
- package/dist/takeout/scripts/psql.js.map +1 -0
- package/dist/takeout/seed.d.ts +10 -0
- package/dist/takeout/seed.d.ts.map +1 -0
- package/dist/takeout/seed.js +22 -0
- package/dist/takeout/seed.js.map +1 -0
- package/dist/takeout/sql.d.ts +9 -0
- package/dist/takeout/sql.d.ts.map +1 -0
- package/dist/takeout/sql.js +24 -0
- package/dist/takeout/sql.js.map +1 -0
- package/dist/takeout/sqlite.d.ts +66 -0
- package/dist/takeout/sqlite.d.ts.map +1 -0
- package/dist/takeout/sqlite.js +110 -0
- package/dist/takeout/sqlite.js.map +1 -0
- package/dist/takeout/waitForDatabase.d.ts +2 -0
- package/dist/takeout/waitForDatabase.d.ts.map +1 -0
- package/dist/takeout/waitForDatabase.js +26 -0
- package/dist/takeout/waitForDatabase.js.map +1 -0
- package/dist/takeout/zero.d.ts +44 -0
- package/dist/takeout/zero.d.ts.map +1 -0
- package/dist/takeout/zero.js +177 -0
- package/dist/takeout/zero.js.map +1 -0
- package/package.json +54 -4
- package/src/takeout/better-auth.ts +110 -0
- package/src/takeout/build.ts +2 -0
- package/src/takeout/chunkedQuery.ts +91 -0
- package/src/takeout/cli.ts +126 -0
- package/src/takeout/createDatabase.ts +164 -0
- package/src/takeout/createDb.ts +15 -0
- package/src/takeout/createDrizzle.ts +127 -0
- package/src/takeout/createPool.ts +58 -0
- package/src/takeout/createServerHelpers.ts +21 -0
- package/src/takeout/getDBClient.ts +254 -0
- package/src/takeout/index.ts +38 -0
- package/src/takeout/migrate.ts +272 -0
- package/src/takeout/scripts/build-migrations.ts +66 -0
- package/src/takeout/scripts/drizzle-migrations-sync.ts +81 -0
- package/src/takeout/scripts/dump-database.ts +103 -0
- package/src/takeout/scripts/migration-add.ts +54 -0
- package/src/takeout/scripts/pg_dump.ts +46 -0
- package/src/takeout/scripts/prune-backups.ts +211 -0
- package/src/takeout/scripts/psql.ts +51 -0
- package/src/takeout/seed.ts +45 -0
- package/src/takeout/sql.ts +39 -0
- package/src/takeout/sqlite.ts +237 -0
- package/src/takeout/waitForDatabase.ts +27 -0
- package/src/takeout/zero.ts +281 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks'
|
|
2
|
+
|
|
3
|
+
import { drizzle, type SqliteRemoteDatabase } from 'drizzle-orm/sqlite-proxy'
|
|
4
|
+
|
|
5
|
+
import type { Database, SQLQueryBindings } from 'bun:sqlite'
|
|
6
|
+
import type { AnyRelations, EmptyRelations } from 'drizzle-orm'
|
|
7
|
+
import type { SQLiteTable } from 'drizzle-orm/sqlite-core'
|
|
8
|
+
|
|
9
|
+
export type SqlStatementMetadata = {
|
|
10
|
+
table: string
|
|
11
|
+
publicTable: string
|
|
12
|
+
kind: 'delete' | 'insert' | 'update' | 'upsert'
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type SQLiteQueryFormat = {
|
|
16
|
+
relationships: Record<string, SQLiteQueryFormat>
|
|
17
|
+
singular: boolean
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type SQLiteExecResult = {
|
|
21
|
+
changes: number
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type SQLiteStatement = {
|
|
25
|
+
sql: string
|
|
26
|
+
params?: readonly unknown[]
|
|
27
|
+
metadata?: SqlStatementMetadata
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type SQLiteTransactionExecutor = {
|
|
31
|
+
exec(
|
|
32
|
+
sql: string,
|
|
33
|
+
params?: readonly unknown[],
|
|
34
|
+
metadata?: SqlStatementMetadata
|
|
35
|
+
): SQLiteExecResult | Promise<SQLiteExecResult>
|
|
36
|
+
/**
|
|
37
|
+
* every statement in one call to the storage owner, in order, as one
|
|
38
|
+
* atomic step. a remote owner holds its writer across each round trip, so a
|
|
39
|
+
* list sent one statement at a time holds it for the whole exchange.
|
|
40
|
+
*/
|
|
41
|
+
execMany(
|
|
42
|
+
statements: readonly SQLiteStatement[]
|
|
43
|
+
): SQLiteExecResult[] | Promise<SQLiteExecResult[]>
|
|
44
|
+
query<Row extends Record<string, unknown> = Record<string, unknown>>(
|
|
45
|
+
sql: string,
|
|
46
|
+
params?: readonly unknown[]
|
|
47
|
+
): Row[] | Promise<Row[]>
|
|
48
|
+
queryAst<Result = unknown>(
|
|
49
|
+
ast: unknown,
|
|
50
|
+
format: SQLiteQueryFormat,
|
|
51
|
+
queryName?: string
|
|
52
|
+
): Result | Promise<Result>
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export type SQLiteTransactionProvider = <Value>(
|
|
56
|
+
work: (executor: SQLiteTransactionExecutor) => Value | Promise<Value>
|
|
57
|
+
) => Promise<Value>
|
|
58
|
+
|
|
59
|
+
export type SQLiteSchema = Record<string, SQLiteTable>
|
|
60
|
+
|
|
61
|
+
export type CreateSQLiteDatabaseOptions<
|
|
62
|
+
TSchema extends SQLiteSchema,
|
|
63
|
+
TRelations extends AnyRelations = EmptyRelations,
|
|
64
|
+
> = {
|
|
65
|
+
schema: TSchema
|
|
66
|
+
relations?: TRelations
|
|
67
|
+
transactionProvider: SQLiteTransactionProvider
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export type SQLiteDatabaseTransaction<
|
|
71
|
+
TSchema extends SQLiteSchema,
|
|
72
|
+
TRelations extends AnyRelations,
|
|
73
|
+
> = {
|
|
74
|
+
executor: SQLiteTransactionExecutor
|
|
75
|
+
drizzle: SqliteRemoteDatabase<TSchema, TRelations>
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export type SQLiteDatabase<
|
|
79
|
+
TSchema extends SQLiteSchema,
|
|
80
|
+
TRelations extends AnyRelations,
|
|
81
|
+
> = {
|
|
82
|
+
transaction<Value>(
|
|
83
|
+
work: (
|
|
84
|
+
transaction: SQLiteDatabaseTransaction<TSchema, TRelations>
|
|
85
|
+
) => Value | Promise<Value>
|
|
86
|
+
): Promise<Value>
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Creates Drizzle only after the storage owner has started its transaction.
|
|
91
|
+
* The sqlite-proxy driver is used solely to turn its query callbacks into
|
|
92
|
+
* typed Drizzle operations; it never owns BEGIN or COMMIT.
|
|
93
|
+
*/
|
|
94
|
+
export function createSQLiteDatabase<
|
|
95
|
+
TSchema extends SQLiteSchema,
|
|
96
|
+
TRelations extends AnyRelations = EmptyRelations,
|
|
97
|
+
>(
|
|
98
|
+
options: CreateSQLiteDatabaseOptions<TSchema, TRelations>
|
|
99
|
+
): SQLiteDatabase<TSchema, TRelations> {
|
|
100
|
+
return {
|
|
101
|
+
transaction(work) {
|
|
102
|
+
return options.transactionProvider(async (executor) =>
|
|
103
|
+
work({
|
|
104
|
+
executor,
|
|
105
|
+
drizzle: createSQLiteDrizzle(executor, options),
|
|
106
|
+
})
|
|
107
|
+
)
|
|
108
|
+
},
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function createSQLiteDrizzle<
|
|
113
|
+
TSchema extends SQLiteSchema,
|
|
114
|
+
TRelations extends AnyRelations = EmptyRelations,
|
|
115
|
+
>(
|
|
116
|
+
executor: SQLiteTransactionExecutor,
|
|
117
|
+
options: Pick<CreateSQLiteDatabaseOptions<TSchema, TRelations>, 'relations' | 'schema'>
|
|
118
|
+
): SqliteRemoteDatabase<TSchema, TRelations> {
|
|
119
|
+
return drizzle<TSchema, TRelations>(
|
|
120
|
+
async (sql, params, method) => {
|
|
121
|
+
if (method === 'run') {
|
|
122
|
+
const result = await executor.exec(sql, params)
|
|
123
|
+
return { changes: result.changes, rows: [] }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const rows = await executor.query(sql, params)
|
|
127
|
+
if (method === 'get') {
|
|
128
|
+
const row = rows[0]
|
|
129
|
+
return remoteRows(row ? rowValues(row) : undefined)
|
|
130
|
+
}
|
|
131
|
+
return remoteRows(rows.map(rowValues))
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
schema: options.schema,
|
|
135
|
+
relations: options.relations,
|
|
136
|
+
}
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export type CreateBunSQLiteExecutorOptions = {
|
|
141
|
+
database: Database
|
|
142
|
+
queryAst<Result = unknown>(
|
|
143
|
+
ast: unknown,
|
|
144
|
+
format: SQLiteQueryFormat,
|
|
145
|
+
queryName?: string
|
|
146
|
+
): Result | Promise<Result>
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function createBunSQLiteExecutor({
|
|
150
|
+
database,
|
|
151
|
+
queryAst,
|
|
152
|
+
}: CreateBunSQLiteExecutorOptions): SQLiteTransactionExecutor {
|
|
153
|
+
return {
|
|
154
|
+
exec(sql, params = []) {
|
|
155
|
+
const result = database.prepare(sql).run(...bunBindings(params))
|
|
156
|
+
return { changes: result.changes }
|
|
157
|
+
},
|
|
158
|
+
execMany(statements) {
|
|
159
|
+
return statements.map(({ sql, params = [] }) => ({
|
|
160
|
+
changes: database.prepare(sql).run(...bunBindings(params)).changes,
|
|
161
|
+
}))
|
|
162
|
+
},
|
|
163
|
+
query<Row extends Record<string, unknown>>(
|
|
164
|
+
sql: string,
|
|
165
|
+
params: readonly unknown[] = []
|
|
166
|
+
): Row[] {
|
|
167
|
+
return database.prepare<Row, SQLQueryBindings[]>(sql).all(...bunBindings(params))
|
|
168
|
+
},
|
|
169
|
+
queryAst,
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export type CreateBunSQLiteTransactionProviderOptions = CreateBunSQLiteExecutorOptions
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Bun's synchronous driver needs its own native transaction boundary. Remote
|
|
177
|
+
* and Durable Object hosts pass their protected callback directly to
|
|
178
|
+
* createSQLiteDatabase instead.
|
|
179
|
+
*/
|
|
180
|
+
export function createBunSQLiteTransactionProvider({
|
|
181
|
+
database,
|
|
182
|
+
queryAst,
|
|
183
|
+
}: CreateBunSQLiteTransactionProviderOptions): SQLiteTransactionProvider {
|
|
184
|
+
const executor = createBunSQLiteExecutor({ database, queryAst })
|
|
185
|
+
const transactionContext = new AsyncLocalStorage<boolean>()
|
|
186
|
+
let tail = Promise.resolve()
|
|
187
|
+
|
|
188
|
+
return async (work) => {
|
|
189
|
+
if (transactionContext.getStore()) {
|
|
190
|
+
throw new Error('nested SQLite transactions are not supported')
|
|
191
|
+
}
|
|
192
|
+
// keep the native connection's transaction state exclusive across awaits.
|
|
193
|
+
const previous = tail
|
|
194
|
+
let release!: () => void
|
|
195
|
+
tail = new Promise<void>((resolve) => {
|
|
196
|
+
release = resolve
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
await previous
|
|
200
|
+
try {
|
|
201
|
+
return await transactionContext.run(true, async () => {
|
|
202
|
+
let began = false
|
|
203
|
+
try {
|
|
204
|
+
database.exec('BEGIN')
|
|
205
|
+
began = true
|
|
206
|
+
const value = await work(executor)
|
|
207
|
+
database.exec('COMMIT')
|
|
208
|
+
began = false
|
|
209
|
+
return value
|
|
210
|
+
} catch (error) {
|
|
211
|
+
if (began) database.exec('ROLLBACK')
|
|
212
|
+
throw error
|
|
213
|
+
}
|
|
214
|
+
})
|
|
215
|
+
} finally {
|
|
216
|
+
release()
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function rowValues(row: Record<string, unknown>): unknown[] {
|
|
222
|
+
return Object.values(row)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function remoteRows(rows: unknown[] | undefined): { rows: any[] } {
|
|
226
|
+
// Drizzle's sqlite-proxy declaration requires an array even though its get()
|
|
227
|
+
// implementation recognizes an absent row. Keep that transport mismatch at
|
|
228
|
+
// this one adapter boundary rather than turning a missing row into one full
|
|
229
|
+
// of undefined column values.
|
|
230
|
+
return { rows: rows as any[] }
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function bunBindings(params: readonly unknown[]): SQLQueryBindings[] {
|
|
234
|
+
// on-zero accepts unknown bindings so every native provider can expose one
|
|
235
|
+
// structural contract. Bun validates these at its typed boundary.
|
|
236
|
+
return params as SQLQueryBindings[]
|
|
237
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import pg from 'pg'
|
|
2
|
+
|
|
3
|
+
export async function waitForDatabase(connectionString: string, maxRetries = 30) {
|
|
4
|
+
for (let i = 0; i < maxRetries; i++) {
|
|
5
|
+
try {
|
|
6
|
+
const client = new pg.Client({
|
|
7
|
+
connectionString,
|
|
8
|
+
connectionTimeoutMillis: 5_000,
|
|
9
|
+
ssl: connectionString.includes('sslmode=require')
|
|
10
|
+
? { rejectUnauthorized: false }
|
|
11
|
+
: undefined,
|
|
12
|
+
})
|
|
13
|
+
await client.connect()
|
|
14
|
+
await client.query('SELECT 1')
|
|
15
|
+
await client.end()
|
|
16
|
+
console.info('[database] connection successful')
|
|
17
|
+
return
|
|
18
|
+
} catch {
|
|
19
|
+
const delay = Math.min(1000 * 1.5 ** i, 10000)
|
|
20
|
+
console.info(
|
|
21
|
+
`[database] waiting... attempt ${i + 1}/${maxRetries} (retry in ${delay}ms)`
|
|
22
|
+
)
|
|
23
|
+
await new Promise((resolve) => setTimeout(resolve, delay))
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
throw new Error('database connection timeout after ' + maxRetries + ' attempts')
|
|
27
|
+
}
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import { basename } from 'node:path'
|
|
2
|
+
|
|
3
|
+
import { getTableName } from 'drizzle-orm'
|
|
4
|
+
import { PgTable } from 'drizzle-orm/pg-core'
|
|
5
|
+
|
|
6
|
+
import { getDBClient } from './getDBClient.js'
|
|
7
|
+
|
|
8
|
+
import type { PoolClient } from 'pg'
|
|
9
|
+
|
|
10
|
+
// --- zero publication management ---
|
|
11
|
+
|
|
12
|
+
export type ZeroPublicationOptions = {
|
|
13
|
+
connectionString: string
|
|
14
|
+
/** zero publication name, e.g. 'zero_takeout' or 'zero_chat' */
|
|
15
|
+
publicationName: string
|
|
16
|
+
/** table names excluded from publication (private/auth tables) */
|
|
17
|
+
privateTableNames: string[]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function ensureZeroPublication(options: ZeroPublicationOptions) {
|
|
21
|
+
const { connectionString, publicationName, privateTableNames } = options
|
|
22
|
+
const client = await getDBClient({ connectionString })
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const { rows: wanted } = await client.query(
|
|
26
|
+
`SELECT tablename FROM pg_tables
|
|
27
|
+
WHERE schemaname = 'public'
|
|
28
|
+
AND tablename != ALL($1)
|
|
29
|
+
AND tablename NOT LIKE '_zero_%'
|
|
30
|
+
AND tablename != 'migrations'`,
|
|
31
|
+
[privateTableNames]
|
|
32
|
+
)
|
|
33
|
+
const wantedSet = new Set(wanted.map((r: any) => r.tablename))
|
|
34
|
+
|
|
35
|
+
if (!wantedSet.size) {
|
|
36
|
+
console.info(`[zero] no public tables found for ${publicationName} publication`)
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const { rows: pub } = await client.query(
|
|
41
|
+
`SELECT 1 FROM pg_publication WHERE pubname = $1`,
|
|
42
|
+
[publicationName]
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
if (!pub.length) {
|
|
46
|
+
try {
|
|
47
|
+
const tableList = [...wantedSet].map((t) => `"${t}"`).join(', ')
|
|
48
|
+
await client.query(`CREATE PUBLICATION ${publicationName} FOR TABLE ${tableList}`)
|
|
49
|
+
console.info(
|
|
50
|
+
`[zero] created publication ${publicationName} with ${wantedSet.size} tables`
|
|
51
|
+
)
|
|
52
|
+
} catch (e: any) {
|
|
53
|
+
if (e.code === '42710') {
|
|
54
|
+
console.info(
|
|
55
|
+
`[zero] ${publicationName} publication was created concurrently, will sync`
|
|
56
|
+
)
|
|
57
|
+
} else {
|
|
58
|
+
console.warn(
|
|
59
|
+
`[zero] could not create ${publicationName} publication: ${e.message}`
|
|
60
|
+
)
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const { rows: current } = await client.query(
|
|
67
|
+
`SELECT tablename FROM pg_publication_tables
|
|
68
|
+
WHERE pubname = $1 AND schemaname = 'public'`,
|
|
69
|
+
[publicationName]
|
|
70
|
+
)
|
|
71
|
+
const currentSet = new Set(current.map((r: any) => r.tablename))
|
|
72
|
+
|
|
73
|
+
const toRemove = [...currentSet].filter((t) => !wantedSet.has(t))
|
|
74
|
+
if (toRemove.length) {
|
|
75
|
+
const dropList = toRemove.map((t) => `"${t}"`).join(', ')
|
|
76
|
+
await client.query(`ALTER PUBLICATION ${publicationName} DROP TABLE ${dropList}`)
|
|
77
|
+
console.info(`[zero] removed from ${publicationName}: ${dropList}`)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const toAdd = [...wantedSet].filter((t) => !currentSet.has(t))
|
|
81
|
+
const added: string[] = []
|
|
82
|
+
for (const table of toAdd) {
|
|
83
|
+
try {
|
|
84
|
+
await client.query(`ALTER PUBLICATION ${publicationName} ADD TABLE "${table}"`)
|
|
85
|
+
added.push(table)
|
|
86
|
+
} catch (e: any) {
|
|
87
|
+
if (e.code !== '42710') throw e
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (added.length) {
|
|
92
|
+
console.info(
|
|
93
|
+
`[zero] added to ${publicationName}: ${added.map((t) => `"${t}"`).join(', ')}`
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (!toRemove.length && !added.length) {
|
|
98
|
+
console.info(
|
|
99
|
+
`[zero] ${publicationName} publication is up to date (${currentSet.size} tables)`
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
} finally {
|
|
103
|
+
client.release()
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// --- zero database creation ---
|
|
108
|
+
|
|
109
|
+
export type ZeroDatabaseOptions = {
|
|
110
|
+
connectionString: string
|
|
111
|
+
cvrDb?: string
|
|
112
|
+
changeDb?: string
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export async function ensureZeroDatabases(options: ZeroDatabaseOptions) {
|
|
116
|
+
const { connectionString, cvrDb, changeDb } = options
|
|
117
|
+
if (!cvrDb && !changeDb) return
|
|
118
|
+
|
|
119
|
+
const client = await getDBClient({ connectionString })
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
const dbNames = [basename(cvrDb || ''), basename(changeDb || '')].filter(Boolean)
|
|
123
|
+
|
|
124
|
+
for (const name of dbNames) {
|
|
125
|
+
const { rows } = await client.query(
|
|
126
|
+
`SELECT 1 FROM pg_database WHERE datname = '${name}'`
|
|
127
|
+
)
|
|
128
|
+
if (!rows.length) {
|
|
129
|
+
await client.query(`CREATE DATABASE ${name};`)
|
|
130
|
+
console.info(`[zero] created database ${name}`)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
} finally {
|
|
134
|
+
client.release()
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// --- zero triggers ---
|
|
139
|
+
|
|
140
|
+
export type TriggerDefinition = {
|
|
141
|
+
name: string
|
|
142
|
+
sql: string
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function defineTrigger(name: string, sql: string): TriggerDefinition {
|
|
146
|
+
return { name, sql }
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** load trigger definitions from a glob result (import.meta.glob or loadTriggersFromDir) */
|
|
150
|
+
export async function loadTriggers(
|
|
151
|
+
glob: Record<string, () => Promise<unknown>>
|
|
152
|
+
): Promise<TriggerDefinition[]> {
|
|
153
|
+
const triggers: TriggerDefinition[] = []
|
|
154
|
+
for (const [path, load] of Object.entries(glob).sort(([a], [b]) =>
|
|
155
|
+
a.localeCompare(b)
|
|
156
|
+
)) {
|
|
157
|
+
const mod = (await load()) as {
|
|
158
|
+
default?: TriggerDefinition
|
|
159
|
+
name?: string
|
|
160
|
+
sql?: string
|
|
161
|
+
}
|
|
162
|
+
if (mod.default) {
|
|
163
|
+
triggers.push(mod.default)
|
|
164
|
+
} else if (mod.sql) {
|
|
165
|
+
triggers.push({ name: mod.name || basename(path, '.ts'), sql: mod.sql })
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return triggers
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export async function ensureZeroTriggers(
|
|
172
|
+
client: PoolClient,
|
|
173
|
+
triggers: TriggerDefinition[]
|
|
174
|
+
) {
|
|
175
|
+
for (const trigger of triggers) {
|
|
176
|
+
try {
|
|
177
|
+
await client.query(trigger.sql)
|
|
178
|
+
} catch (err) {
|
|
179
|
+
console.error(`[zero] failed to apply trigger ${trigger.name}:`, err)
|
|
180
|
+
throw err
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
console.info(`[zero] applied ${triggers.length} trigger definitions`)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// --- zero seeds ---
|
|
187
|
+
|
|
188
|
+
export type SeedDefinition = {
|
|
189
|
+
name: string
|
|
190
|
+
sql: string
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function defineSeed(name: string, sql: string): SeedDefinition {
|
|
194
|
+
return { name, sql }
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export async function loadSeeds(
|
|
198
|
+
glob: Record<string, () => Promise<unknown>>
|
|
199
|
+
): Promise<SeedDefinition[]> {
|
|
200
|
+
const seeds: SeedDefinition[] = []
|
|
201
|
+
for (const [path, load] of Object.entries(glob).sort(([a], [b]) =>
|
|
202
|
+
a.localeCompare(b)
|
|
203
|
+
)) {
|
|
204
|
+
const mod = (await load()) as {
|
|
205
|
+
default?: SeedDefinition
|
|
206
|
+
name?: string
|
|
207
|
+
sql?: string
|
|
208
|
+
}
|
|
209
|
+
if (mod.default) {
|
|
210
|
+
seeds.push(mod.default)
|
|
211
|
+
} else if (mod.sql) {
|
|
212
|
+
seeds.push({ name: mod.name || basename(path, '.ts'), sql: mod.sql })
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return seeds
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export async function ensureZeroSeeds(client: PoolClient, seeds: SeedDefinition[]) {
|
|
219
|
+
for (const seed of seeds) {
|
|
220
|
+
try {
|
|
221
|
+
await client.query(seed.sql)
|
|
222
|
+
} catch (err) {
|
|
223
|
+
console.error(`[zero] failed to apply seed ${seed.name}:`, err)
|
|
224
|
+
throw err
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
console.info(`[zero] applied ${seeds.length} seed definitions`)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// --- init sql for pglite/browser ---
|
|
231
|
+
// todo: drizzleSchemaToSQL(schema) → string for browser pglite init (currently ddl is passed as string)
|
|
232
|
+
|
|
233
|
+
export type InitSqlOptions = {
|
|
234
|
+
/** DDL statements (CREATE TABLE, etc.) */
|
|
235
|
+
ddl?: string
|
|
236
|
+
triggers?: TriggerDefinition[]
|
|
237
|
+
seeds?: SeedDefinition[]
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function buildInitSql(options: InitSqlOptions): string {
|
|
241
|
+
const parts: string[] = []
|
|
242
|
+
|
|
243
|
+
if (options.ddl) {
|
|
244
|
+
parts.push(options.ddl)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (options.triggers?.length) {
|
|
248
|
+
parts.push(...options.triggers.map((t) => `-- trigger: ${t.name}\n${t.sql}`))
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (options.seeds?.length) {
|
|
252
|
+
parts.push(...options.seeds.map((s) => `-- seed: ${s.name}\n${s.sql}`))
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return parts.join('\n\n')
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// --- schema helpers ---
|
|
259
|
+
|
|
260
|
+
const PRIVATE = Symbol.for('take-out/database/private')
|
|
261
|
+
|
|
262
|
+
/** wrap pgTable to mark tables as private (excluded from zero replication) */
|
|
263
|
+
export function privateTable<T extends (...args: any[]) => any>(createTable: T): T {
|
|
264
|
+
return ((...args: any[]) => {
|
|
265
|
+
const table = createTable(...args)
|
|
266
|
+
;(table as any)[PRIVATE] = true
|
|
267
|
+
return table
|
|
268
|
+
}) as unknown as T
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** check if a table is marked as private */
|
|
272
|
+
export function isPrivateTable(table: unknown): boolean {
|
|
273
|
+
return !!(table && typeof table === 'object' && PRIVATE in table)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** derive public table names from a schema (all tables not marked private) */
|
|
277
|
+
export function derivePublicTables(schema: Record<string, unknown>): string[] {
|
|
278
|
+
return Object.values(schema)
|
|
279
|
+
.filter((v) => v instanceof PgTable && !isPrivateTable(v))
|
|
280
|
+
.map((t) => getTableName(t as PgTable))
|
|
281
|
+
}
|