@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,110 @@
|
|
|
1
|
+
import { drizzleAdapter, type DrizzleAdapterConfig } from 'better-auth/adapters/drizzle'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
createSQLiteDrizzle,
|
|
5
|
+
type CreateSQLiteDatabaseOptions,
|
|
6
|
+
type SQLiteSchema,
|
|
7
|
+
type SQLiteTransactionExecutor,
|
|
8
|
+
type SQLiteTransactionProvider,
|
|
9
|
+
} from './sqlite.js'
|
|
10
|
+
|
|
11
|
+
import type { AnyRelations, EmptyRelations } from 'drizzle-orm'
|
|
12
|
+
|
|
13
|
+
export type CreateBetterAuthSQLiteAdapterOptions<
|
|
14
|
+
TSchema extends SQLiteSchema,
|
|
15
|
+
TRelations extends AnyRelations = EmptyRelations,
|
|
16
|
+
> = Pick<
|
|
17
|
+
CreateSQLiteDatabaseOptions<TSchema, TRelations>,
|
|
18
|
+
'relations' | 'schema' | 'transactionProvider'
|
|
19
|
+
> & {
|
|
20
|
+
/**
|
|
21
|
+
* runs the adapter's read-only operations (`findOne`, `findMany`, `count`).
|
|
22
|
+
* the SQLite host decides what a read session means; on a host that admits
|
|
23
|
+
* one writer at a time and readers together, a session lookup no longer
|
|
24
|
+
* waits for the writer turn. defaults to `transactionProvider`.
|
|
25
|
+
*/
|
|
26
|
+
readTransactionProvider?: SQLiteTransactionProvider
|
|
27
|
+
} & Omit<DrizzleAdapterConfig, 'provider' | 'schema' | 'transaction'>
|
|
28
|
+
|
|
29
|
+
// the adapter operations that only SELECT. every other operation, and the
|
|
30
|
+
// explicit `transaction`, keeps the write provider.
|
|
31
|
+
const READ_OPERATIONS: ReadonlySet<string> = new Set(['findOne', 'findMany', 'count'])
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* gives Better Auth its official Drizzle adapter inside the transaction
|
|
35
|
+
* callback owned by the SQLite host. the Drizzle adapter's transaction mode
|
|
36
|
+
* stays disabled because it must never emit its own BEGIN or COMMIT.
|
|
37
|
+
*/
|
|
38
|
+
export function createBetterAuthSQLiteAdapter<
|
|
39
|
+
TSchema extends SQLiteSchema,
|
|
40
|
+
TRelations extends AnyRelations = EmptyRelations,
|
|
41
|
+
>(
|
|
42
|
+
options: CreateBetterAuthSQLiteAdapterOptions<TSchema, TRelations>
|
|
43
|
+
): ReturnType<typeof drizzleAdapter> {
|
|
44
|
+
const {
|
|
45
|
+
relations,
|
|
46
|
+
schema,
|
|
47
|
+
transactionProvider,
|
|
48
|
+
readTransactionProvider = transactionProvider,
|
|
49
|
+
...drizzleOptions
|
|
50
|
+
} = options
|
|
51
|
+
const adapterConfig = {
|
|
52
|
+
...drizzleOptions,
|
|
53
|
+
provider: 'sqlite',
|
|
54
|
+
schema,
|
|
55
|
+
transaction: false,
|
|
56
|
+
} satisfies DrizzleAdapterConfig
|
|
57
|
+
const createAdapter = (
|
|
58
|
+
executor: SQLiteTransactionExecutor,
|
|
59
|
+
authOptions: Parameters<ReturnType<typeof drizzleAdapter>>[0]
|
|
60
|
+
) =>
|
|
61
|
+
drizzleAdapter(
|
|
62
|
+
createSQLiteDrizzle(executor, { relations, schema }),
|
|
63
|
+
adapterConfig
|
|
64
|
+
)(authOptions)
|
|
65
|
+
|
|
66
|
+
return (authOptions) => {
|
|
67
|
+
const unavailableExecutor: SQLiteTransactionExecutor = {
|
|
68
|
+
exec() {
|
|
69
|
+
throw new Error('Better Auth SQLite operations require a transaction callback')
|
|
70
|
+
},
|
|
71
|
+
execMany() {
|
|
72
|
+
throw new Error('Better Auth SQLite operations require a transaction callback')
|
|
73
|
+
},
|
|
74
|
+
query() {
|
|
75
|
+
throw new Error('Better Auth SQLite operations require a transaction callback')
|
|
76
|
+
},
|
|
77
|
+
queryAst() {
|
|
78
|
+
throw new Error('Better Auth SQLite operations require a transaction callback')
|
|
79
|
+
},
|
|
80
|
+
}
|
|
81
|
+
const target = createAdapter(unavailableExecutor, authOptions)
|
|
82
|
+
const transaction: typeof target.transaction = (work) =>
|
|
83
|
+
transactionProvider(async (executor) => work(createAdapter(executor, authOptions)))
|
|
84
|
+
|
|
85
|
+
return new Proxy(target, {
|
|
86
|
+
get(_target, property, receiver) {
|
|
87
|
+
if (property === 'transaction') return transaction
|
|
88
|
+
|
|
89
|
+
const value = Reflect.get(target, property, receiver)
|
|
90
|
+
if (typeof value !== 'function') return value
|
|
91
|
+
|
|
92
|
+
const provider =
|
|
93
|
+
typeof property === 'string' && READ_OPERATIONS.has(property)
|
|
94
|
+
? readTransactionProvider
|
|
95
|
+
: transactionProvider
|
|
96
|
+
return (...args: unknown[]) =>
|
|
97
|
+
provider(async (executor) => {
|
|
98
|
+
const adapter = createAdapter(executor, authOptions)
|
|
99
|
+
const operation = Reflect.get(adapter, property)
|
|
100
|
+
if (typeof operation !== 'function') {
|
|
101
|
+
throw new TypeError(
|
|
102
|
+
`Better Auth adapter operation ${String(property)} is unavailable`
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
return Reflect.apply(operation, adapter, args)
|
|
106
|
+
})
|
|
107
|
+
},
|
|
108
|
+
})
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { PoolClient, QueryResultRow } from 'pg'
|
|
2
|
+
|
|
3
|
+
interface ChunkedQueryOptions {
|
|
4
|
+
chunkSize?: number
|
|
5
|
+
onProgress?: (processed: number, total: number) => void
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Process database records in chunks to avoid memory issues with large datasets
|
|
10
|
+
*/
|
|
11
|
+
export async function processInChunks<T extends QueryResultRow = QueryResultRow>(
|
|
12
|
+
client: PoolClient,
|
|
13
|
+
query: string,
|
|
14
|
+
processor: (rows: T[]) => Promise<void>,
|
|
15
|
+
options: ChunkedQueryOptions = {}
|
|
16
|
+
): Promise<void> {
|
|
17
|
+
const { chunkSize = 1000, onProgress } = options
|
|
18
|
+
let offset = 0
|
|
19
|
+
let hasMore = true
|
|
20
|
+
let totalProcessed = 0
|
|
21
|
+
|
|
22
|
+
// first get total count for progress reporting
|
|
23
|
+
const countQuery = query
|
|
24
|
+
.replace(/SELECT .+ FROM/, 'SELECT COUNT(*) FROM')
|
|
25
|
+
.replace(/ORDER BY .+/, '')
|
|
26
|
+
const countResult = await client.query(countQuery)
|
|
27
|
+
const totalCount = Number.parseInt(countResult.rows[0].count, 10)
|
|
28
|
+
|
|
29
|
+
while (hasMore) {
|
|
30
|
+
const paginatedQuery = `${query} LIMIT ${chunkSize} OFFSET ${offset}`
|
|
31
|
+
const result = await client.query<T>(paginatedQuery)
|
|
32
|
+
|
|
33
|
+
if (result.rows.length === 0) {
|
|
34
|
+
hasMore = false
|
|
35
|
+
break
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
await processor(result.rows)
|
|
39
|
+
|
|
40
|
+
totalProcessed += result.rows.length
|
|
41
|
+
offset += chunkSize
|
|
42
|
+
|
|
43
|
+
if (onProgress) {
|
|
44
|
+
onProgress(totalProcessed, totalCount)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// check if we've processed all records
|
|
48
|
+
if (result.rows.length < chunkSize) {
|
|
49
|
+
hasMore = false
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Update records in chunks with a transformer function
|
|
56
|
+
*/
|
|
57
|
+
export async function updateInChunks<T extends QueryResultRow & { id: string }>(
|
|
58
|
+
client: PoolClient,
|
|
59
|
+
tableName: string,
|
|
60
|
+
selectQuery: string,
|
|
61
|
+
transformer: (row: T) => Promise<Partial<T> | null>,
|
|
62
|
+
options: ChunkedQueryOptions = {}
|
|
63
|
+
): Promise<number> {
|
|
64
|
+
let totalUpdated = 0
|
|
65
|
+
|
|
66
|
+
await processInChunks<T>(
|
|
67
|
+
client,
|
|
68
|
+
selectQuery,
|
|
69
|
+
async (rows) => {
|
|
70
|
+
for (const row of rows) {
|
|
71
|
+
const updates = await transformer(row)
|
|
72
|
+
|
|
73
|
+
if (updates && Object.keys(updates).length > 0) {
|
|
74
|
+
// build update query dynamically
|
|
75
|
+
const setClause = Object.keys(updates)
|
|
76
|
+
.map((key, index) => `${key} = $${index + 2}`)
|
|
77
|
+
.join(', ')
|
|
78
|
+
|
|
79
|
+
const values = [row.id, ...Object.values(updates)]
|
|
80
|
+
|
|
81
|
+
await client.query(`UPDATE ${tableName} SET ${setClause} WHERE id = $1`, values)
|
|
82
|
+
|
|
83
|
+
totalUpdated++
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
options
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
return totalUpdated
|
|
91
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
|
|
4
|
+
import { defineCommand, runMain } from 'citty'
|
|
5
|
+
|
|
6
|
+
import { buildMigrations as buildMigrationsRun } from './scripts/build-migrations.js'
|
|
7
|
+
import { syncDrizzleMigrations } from './scripts/drizzle-migrations-sync.js'
|
|
8
|
+
import { addMigration } from './scripts/migration-add.js'
|
|
9
|
+
import { runPgDump } from './scripts/pg_dump.js'
|
|
10
|
+
import { runPsql } from './scripts/psql.js'
|
|
11
|
+
|
|
12
|
+
const syncDrizzle = defineCommand({
|
|
13
|
+
meta: {
|
|
14
|
+
name: 'sync-drizzle',
|
|
15
|
+
description: 'Sync Drizzle SQL migrations to TypeScript wrappers',
|
|
16
|
+
},
|
|
17
|
+
args: {
|
|
18
|
+
dir: {
|
|
19
|
+
type: 'string',
|
|
20
|
+
description: 'Migrations directory',
|
|
21
|
+
required: false,
|
|
22
|
+
default: './src/database/migrations',
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
async run({ args }) {
|
|
26
|
+
const migrationsDir = join(process.cwd(), args.dir)
|
|
27
|
+
console.info(`Syncing migrations in ${migrationsDir}`)
|
|
28
|
+
await syncDrizzleMigrations({ migrationsDir })
|
|
29
|
+
},
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
const migrationAdd = defineCommand({
|
|
33
|
+
meta: {
|
|
34
|
+
name: 'migrate:add',
|
|
35
|
+
description: 'Create a new custom TypeScript migration',
|
|
36
|
+
},
|
|
37
|
+
args: {
|
|
38
|
+
name: {
|
|
39
|
+
type: 'positional',
|
|
40
|
+
description: 'Migration name',
|
|
41
|
+
required: false,
|
|
42
|
+
},
|
|
43
|
+
dir: {
|
|
44
|
+
type: 'string',
|
|
45
|
+
description: 'Migrations directory',
|
|
46
|
+
required: false,
|
|
47
|
+
default: './src/database/migrations',
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
async run({ args }) {
|
|
51
|
+
const migrationsDir = join(process.cwd(), args.dir)
|
|
52
|
+
addMigration({ migrationsDir, name: args.name })
|
|
53
|
+
},
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
const psql = defineCommand({
|
|
57
|
+
meta: {
|
|
58
|
+
name: 'psql',
|
|
59
|
+
description: 'Connect to PostgreSQL database with psql',
|
|
60
|
+
},
|
|
61
|
+
args: {
|
|
62
|
+
connectionString: {
|
|
63
|
+
type: 'string',
|
|
64
|
+
description: 'PostgreSQL connection string',
|
|
65
|
+
required: false,
|
|
66
|
+
},
|
|
67
|
+
query: {
|
|
68
|
+
type: 'string',
|
|
69
|
+
description: 'Query to execute',
|
|
70
|
+
required: false,
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
async run({ args }) {
|
|
74
|
+
const connectionString = args.connectionString || process.env.ZERO_UPSTREAM_DB
|
|
75
|
+
if (!connectionString) {
|
|
76
|
+
console.error(
|
|
77
|
+
'No connection string provided. Set ZERO_UPSTREAM_DB or pass --connectionString'
|
|
78
|
+
)
|
|
79
|
+
process.exit(1)
|
|
80
|
+
}
|
|
81
|
+
const exitCode = runPsql({ connectionString, query: args.query })
|
|
82
|
+
process.exit(exitCode || 0)
|
|
83
|
+
},
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
const pgDump = defineCommand({
|
|
87
|
+
meta: {
|
|
88
|
+
name: 'pg_dump',
|
|
89
|
+
description: 'Dump PostgreSQL database using pg_dump',
|
|
90
|
+
},
|
|
91
|
+
args: {
|
|
92
|
+
connectionString: {
|
|
93
|
+
type: 'string',
|
|
94
|
+
description: 'PostgreSQL connection string',
|
|
95
|
+
required: false,
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
async run({ args }) {
|
|
99
|
+
const connectionString = args.connectionString || process.env.ZERO_UPSTREAM_DB
|
|
100
|
+
if (!connectionString) {
|
|
101
|
+
console.error(
|
|
102
|
+
'No connection string provided. Set ZERO_UPSTREAM_DB or pass --connectionString'
|
|
103
|
+
)
|
|
104
|
+
process.exit(1)
|
|
105
|
+
}
|
|
106
|
+
const cliArgs = process.argv.slice(3) // get args after command name
|
|
107
|
+
const exitCode = runPgDump({ connectionString, args: cliArgs })
|
|
108
|
+
process.exit(exitCode || 0)
|
|
109
|
+
},
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
const main = defineCommand({
|
|
113
|
+
meta: {
|
|
114
|
+
name: 'database',
|
|
115
|
+
description: 'Database utilities and migration tools',
|
|
116
|
+
version: '0.0.1',
|
|
117
|
+
},
|
|
118
|
+
subCommands: {
|
|
119
|
+
'sync-drizzle': syncDrizzle,
|
|
120
|
+
'migrate:add': migrationAdd,
|
|
121
|
+
psql,
|
|
122
|
+
pg_dump: pgDump,
|
|
123
|
+
},
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
runMain(main)
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { getTableName } from 'drizzle-orm'
|
|
2
|
+
import { PgTable } from 'drizzle-orm/pg-core'
|
|
3
|
+
|
|
4
|
+
import { createDrizzle } from './createDrizzle.js'
|
|
5
|
+
import { createPool, type CreatePoolOptions } from './createPool.js'
|
|
6
|
+
import { createServerHelpers } from './createServerHelpers.js'
|
|
7
|
+
import { getDBClient } from './getDBClient.js'
|
|
8
|
+
import { migrate } from './migrate.js'
|
|
9
|
+
import { waitForDatabase } from './waitForDatabase.js'
|
|
10
|
+
import {
|
|
11
|
+
ensureZeroDatabases,
|
|
12
|
+
ensureZeroPublication,
|
|
13
|
+
ensureZeroSeeds,
|
|
14
|
+
ensureZeroTriggers,
|
|
15
|
+
type SeedDefinition,
|
|
16
|
+
type TriggerDefinition,
|
|
17
|
+
} from './zero.js'
|
|
18
|
+
|
|
19
|
+
import type { AnyRelations, EmptyRelations } from 'drizzle-orm'
|
|
20
|
+
import type { Pool } from 'pg'
|
|
21
|
+
|
|
22
|
+
export type DatabaseConfig<
|
|
23
|
+
TSchema extends Record<string, unknown>,
|
|
24
|
+
TRelations extends AnyRelations | Record<string, unknown> = EmptyRelations,
|
|
25
|
+
> = {
|
|
26
|
+
/**
|
|
27
|
+
* The PG connection string. Required unless a pre-built `pool` is provided
|
|
28
|
+
* (it is still used by `migrate()` / `waitForDatabase()`, which need a string).
|
|
29
|
+
*/
|
|
30
|
+
connectionString?: string
|
|
31
|
+
schema: TSchema
|
|
32
|
+
relations?: TRelations
|
|
33
|
+
/**
|
|
34
|
+
* Optional pre-built Pool-like client (e.g. a DoBackend-backed Pool for the
|
|
35
|
+
* Cloudflare Durable Object runtime, where there is no external Postgres).
|
|
36
|
+
* When provided, `createPool` is skipped and this pool is used directly.
|
|
37
|
+
* `connectionString` is then only needed for the migration helpers.
|
|
38
|
+
*/
|
|
39
|
+
pool?: Pool
|
|
40
|
+
/** Options forwarded to the default `createPool` (ignored when `pool` is set). */
|
|
41
|
+
poolOptions?: Omit<CreatePoolOptions, 'connectionString'>
|
|
42
|
+
zero?: {
|
|
43
|
+
publicationName: string
|
|
44
|
+
publicTables: string[]
|
|
45
|
+
triggers?: TriggerDefinition[]
|
|
46
|
+
seeds?: SeedDefinition[]
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type MigrateOptions = {
|
|
51
|
+
migrations: Record<string, () => Promise<unknown>>
|
|
52
|
+
cvrDb?: string
|
|
53
|
+
changeDb?: string
|
|
54
|
+
createDatabases?: string[]
|
|
55
|
+
gitSha?: string
|
|
56
|
+
defaultTimeout?: number
|
|
57
|
+
onMigrationComplete?: () => Promise<void>
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function stripQueryParams(connStr: string | undefined): string | undefined {
|
|
61
|
+
if (!connStr) return connStr
|
|
62
|
+
return connStr.split('?')[0]
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function createDatabase<
|
|
66
|
+
TSchema extends Record<string, unknown>,
|
|
67
|
+
TRelations extends AnyRelations = EmptyRelations,
|
|
68
|
+
>(config: DatabaseConfig<TSchema, TRelations>) {
|
|
69
|
+
const { connectionString, schema, relations, zero } = config
|
|
70
|
+
|
|
71
|
+
// use a caller-provided pool (e.g. the Cloudflare DO-backed pool) when given;
|
|
72
|
+
// otherwise build the default real-pg pool from the connection string.
|
|
73
|
+
const pool =
|
|
74
|
+
config.pool ??
|
|
75
|
+
createPool({
|
|
76
|
+
connectionString: connectionString ?? '',
|
|
77
|
+
...config.poolOptions,
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
const drizzle = createDrizzle({
|
|
81
|
+
pool,
|
|
82
|
+
schema,
|
|
83
|
+
relations: relations as TRelations & AnyRelations,
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
const { sql, getDBClient: getBoundDBClient } = createServerHelpers(pool)
|
|
87
|
+
|
|
88
|
+
const allTableNames = Object.values(schema)
|
|
89
|
+
.filter((v): v is PgTable => v instanceof PgTable)
|
|
90
|
+
.map((t) => getTableName(t))
|
|
91
|
+
|
|
92
|
+
const publicTableSet = new Set(zero?.publicTables || [])
|
|
93
|
+
const privateTableNames = allTableNames.filter((n) => !publicTableSet.has(n))
|
|
94
|
+
|
|
95
|
+
async function close() {
|
|
96
|
+
try {
|
|
97
|
+
await pool.end()
|
|
98
|
+
} catch (e) {
|
|
99
|
+
console.error('[database] error closing:', e)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function runMigrate(options: MigrateOptions) {
|
|
104
|
+
if (!connectionString) {
|
|
105
|
+
throw new Error(
|
|
106
|
+
'createDatabase: migrate() requires a `connectionString` (it is not derivable from a pre-built `pool`)'
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
console.info('[database] waiting for database to be ready...')
|
|
110
|
+
await waitForDatabase(connectionString)
|
|
111
|
+
|
|
112
|
+
console.info('[database] running migrations...')
|
|
113
|
+
await migrate({
|
|
114
|
+
connectionString,
|
|
115
|
+
migrations: options.migrations,
|
|
116
|
+
cvrDb: stripQueryParams(options.cvrDb),
|
|
117
|
+
changeDb: stripQueryParams(options.changeDb),
|
|
118
|
+
createDatabases: options.createDatabases,
|
|
119
|
+
gitSha: options.gitSha,
|
|
120
|
+
defaultTimeout: options.defaultTimeout,
|
|
121
|
+
onMigrationComplete: async () => {
|
|
122
|
+
if (zero) {
|
|
123
|
+
await ensureZeroPublication({
|
|
124
|
+
connectionString,
|
|
125
|
+
publicationName: zero.publicationName,
|
|
126
|
+
privateTableNames,
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
if (zero.triggers?.length || zero.seeds?.length) {
|
|
130
|
+
const client = await getDBClient({ connectionString })
|
|
131
|
+
try {
|
|
132
|
+
if (zero.triggers?.length) {
|
|
133
|
+
await ensureZeroTriggers(client, zero.triggers)
|
|
134
|
+
}
|
|
135
|
+
if (zero.seeds?.length) {
|
|
136
|
+
await ensureZeroSeeds(client, zero.seeds)
|
|
137
|
+
}
|
|
138
|
+
} finally {
|
|
139
|
+
client.release()
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (options.onMigrationComplete) {
|
|
145
|
+
await options.onMigrationComplete()
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
})
|
|
149
|
+
console.info('[database] migrations complete')
|
|
150
|
+
await close()
|
|
151
|
+
process.exit(0)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
pool,
|
|
156
|
+
drizzle,
|
|
157
|
+
sql,
|
|
158
|
+
getDBClient: getBoundDBClient,
|
|
159
|
+
close,
|
|
160
|
+
migrate: runMigrate,
|
|
161
|
+
privateTableNames,
|
|
162
|
+
config,
|
|
163
|
+
}
|
|
164
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type NodePgDatabase, drizzle } from 'drizzle-orm/node-postgres'
|
|
2
|
+
|
|
3
|
+
import { createPool } from './createPool.js'
|
|
4
|
+
|
|
5
|
+
export const createDb = <TSchema extends Record<string, unknown>>(
|
|
6
|
+
connectionString: string,
|
|
7
|
+
schema: TSchema
|
|
8
|
+
): NodePgDatabase<TSchema> => {
|
|
9
|
+
const pool = createPool({ connectionString })
|
|
10
|
+
return drizzle({
|
|
11
|
+
client: pool,
|
|
12
|
+
schema: schema as Record<string, unknown>,
|
|
13
|
+
logger: false,
|
|
14
|
+
} as any) as unknown as NodePgDatabase<TSchema>
|
|
15
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { and, eq, gt, gte, lt, lte, ne } from 'drizzle-orm'
|
|
2
|
+
import { type NodePgDatabase, drizzle } from 'drizzle-orm/node-postgres'
|
|
3
|
+
import { PgTimestampString } from 'drizzle-orm/pg-core'
|
|
4
|
+
|
|
5
|
+
import type { AnyRelations, EmptyRelations, InferInsertModel, SQL } from 'drizzle-orm'
|
|
6
|
+
import type { PgTable } from 'drizzle-orm/pg-core'
|
|
7
|
+
import type { Pool } from 'pg'
|
|
8
|
+
|
|
9
|
+
let patched = false
|
|
10
|
+
function patchTimestampForZero() {
|
|
11
|
+
if (patched) return
|
|
12
|
+
patched = true
|
|
13
|
+
const orig = PgTimestampString.prototype.mapToDriverValue
|
|
14
|
+
PgTimestampString.prototype.mapToDriverValue = function (value: any) {
|
|
15
|
+
if (typeof value === 'number') return new Date(value).toISOString()
|
|
16
|
+
return orig.call(this, value)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const operators = { eq, ne, lt, lte, gt, gte } as const
|
|
21
|
+
type Op = keyof typeof operators
|
|
22
|
+
type WhereOp = Partial<Record<Op, any>>
|
|
23
|
+
type WhereValue = string | number | boolean | null | WhereOp
|
|
24
|
+
type WithSQL<T> = { [K in keyof T]: T[K] | SQL }
|
|
25
|
+
|
|
26
|
+
type TableName<TSchema> = {
|
|
27
|
+
[K in keyof TSchema]: TSchema[K] extends PgTable ? K : never
|
|
28
|
+
}[keyof TSchema]
|
|
29
|
+
|
|
30
|
+
type WhereClause<TSchema, T extends TableName<TSchema>> = Partial<
|
|
31
|
+
Record<
|
|
32
|
+
keyof InferInsertModel<TSchema[T] extends PgTable ? TSchema[T] : never> & string,
|
|
33
|
+
WhereValue
|
|
34
|
+
>
|
|
35
|
+
>
|
|
36
|
+
|
|
37
|
+
type InsertMap<TSchema> = {
|
|
38
|
+
[K in TableName<TSchema>]: Partial<
|
|
39
|
+
WithSQL<InferInsertModel<TSchema[K] extends PgTable ? TSchema[K] : never>>
|
|
40
|
+
>
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function resolveWhere(table: any, where: Record<string, WhereValue>) {
|
|
44
|
+
const conditions = Object.entries(where).map(([key, value]) => {
|
|
45
|
+
if (value !== null && typeof value === 'object') {
|
|
46
|
+
const entries = Object.entries(value) as [Op, any][]
|
|
47
|
+
if (entries.length === 1) {
|
|
48
|
+
return operators[entries[0]![0]](table[key], entries[0]![1])
|
|
49
|
+
}
|
|
50
|
+
return and(...entries.map(([op, v]) => operators[op](table[key], v)))!
|
|
51
|
+
}
|
|
52
|
+
return eq(table[key], value)
|
|
53
|
+
})
|
|
54
|
+
return conditions.length === 1 ? conditions[0]! : and(...conditions)!
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export type CreateDrizzleOptions<
|
|
58
|
+
TSchema extends Record<string, unknown>,
|
|
59
|
+
TRelations extends AnyRelations = EmptyRelations,
|
|
60
|
+
> = {
|
|
61
|
+
pool: Pool
|
|
62
|
+
schema: TSchema
|
|
63
|
+
relations?: TRelations
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
type ExtractTable<TSchema, T extends TableName<TSchema>> = TSchema[T] extends PgTable
|
|
67
|
+
? TSchema[T]
|
|
68
|
+
: never
|
|
69
|
+
|
|
70
|
+
export type DrizzleHelpers<
|
|
71
|
+
TSchema extends Record<string, unknown> = Record<string, unknown>,
|
|
72
|
+
TDb extends NodePgDatabase<any, any> = NodePgDatabase<TSchema>,
|
|
73
|
+
> = {
|
|
74
|
+
insertRow<T extends TableName<TSchema>>(
|
|
75
|
+
tableName: T,
|
|
76
|
+
values: InsertMap<TSchema>[T]
|
|
77
|
+
): ReturnType<ReturnType<TDb['insert']>['values']>
|
|
78
|
+
updateRow<T extends TableName<TSchema>>(
|
|
79
|
+
tableName: T,
|
|
80
|
+
where: WhereClause<TSchema, T>,
|
|
81
|
+
set: InsertMap<TSchema>[T]
|
|
82
|
+
): ReturnType<ReturnType<ReturnType<TDb['update']>['set']>['where']>
|
|
83
|
+
deleteRow<T extends TableName<TSchema>>(
|
|
84
|
+
tableName: T,
|
|
85
|
+
where: WhereClause<TSchema, T>
|
|
86
|
+
): ReturnType<ReturnType<TDb['delete']>['where']>
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function createDrizzle<
|
|
90
|
+
TSchema extends Record<string, unknown>,
|
|
91
|
+
TRelations extends AnyRelations = EmptyRelations,
|
|
92
|
+
>(
|
|
93
|
+
options: CreateDrizzleOptions<TSchema, TRelations>
|
|
94
|
+
): NodePgDatabase<TSchema, TRelations> &
|
|
95
|
+
DrizzleHelpers<TSchema, NodePgDatabase<TSchema, TRelations>> {
|
|
96
|
+
patchTimestampForZero()
|
|
97
|
+
|
|
98
|
+
// drizzle v1 overloads don't support generic schema+relations together at the call site
|
|
99
|
+
const instance = drizzle({
|
|
100
|
+
client: options.pool,
|
|
101
|
+
schema: options.schema as Record<string, unknown>,
|
|
102
|
+
relations: options.relations,
|
|
103
|
+
logger: process.env.DEBUG_SQL === '1',
|
|
104
|
+
} as any) as unknown as NodePgDatabase<TSchema, TRelations>
|
|
105
|
+
|
|
106
|
+
const helpers: DrizzleHelpers<TSchema, NodePgDatabase<TSchema, TRelations>> = {
|
|
107
|
+
insertRow(tableName, values) {
|
|
108
|
+
const table = options.schema[tableName] as any
|
|
109
|
+
return instance.insert(table).values(values as Record<string, unknown>)
|
|
110
|
+
},
|
|
111
|
+
updateRow(tableName, where, set) {
|
|
112
|
+
const table = options.schema[tableName] as any
|
|
113
|
+
return instance
|
|
114
|
+
.update(table)
|
|
115
|
+
.set(set as Record<string, unknown>)
|
|
116
|
+
.where(resolveWhere(table, where as Record<string, WhereValue>))
|
|
117
|
+
},
|
|
118
|
+
deleteRow(tableName, where) {
|
|
119
|
+
const table = options.schema[tableName] as any
|
|
120
|
+
return instance
|
|
121
|
+
.delete(table)
|
|
122
|
+
.where(resolveWhere(table, where as Record<string, WhereValue>))
|
|
123
|
+
},
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return Object.assign(instance, helpers)
|
|
127
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import pg, { type Pool } from 'pg'
|
|
2
|
+
|
|
3
|
+
export type CreatePoolOptions = {
|
|
4
|
+
connectionString: string
|
|
5
|
+
max?: number
|
|
6
|
+
idleTimeoutMillis?: number
|
|
7
|
+
connectionTimeoutMillis?: number
|
|
8
|
+
keepAlive?: boolean
|
|
9
|
+
keepAliveInitialDelayMillis?: number
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function createPool(options: CreatePoolOptions): Pool {
|
|
13
|
+
const {
|
|
14
|
+
connectionString,
|
|
15
|
+
max = 20,
|
|
16
|
+
idleTimeoutMillis = 30_000,
|
|
17
|
+
connectionTimeoutMillis = 5_000,
|
|
18
|
+
keepAlive = true,
|
|
19
|
+
keepAliveInitialDelayMillis = 10_000,
|
|
20
|
+
} = options
|
|
21
|
+
|
|
22
|
+
const ssl = connectionString.includes('sslmode=require')
|
|
23
|
+
? { rejectUnauthorized: false }
|
|
24
|
+
: undefined
|
|
25
|
+
|
|
26
|
+
const pool = new pg.Pool({
|
|
27
|
+
connectionString,
|
|
28
|
+
max,
|
|
29
|
+
idleTimeoutMillis,
|
|
30
|
+
connectionTimeoutMillis,
|
|
31
|
+
keepAlive,
|
|
32
|
+
keepAliveInitialDelayMillis,
|
|
33
|
+
ssl,
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
pool.on('error', (error) => {
|
|
37
|
+
console.error(`[database] pool error`, error.message, error.stack)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
pool.on('connect', (client) => {
|
|
41
|
+
client.on('error', (error) => {
|
|
42
|
+
console.error(`[database] client error`, error.message, error.stack)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
// pg-protocol's parser can throw synchronously inside the stream 'data' handler
|
|
46
|
+
// (e.g. RangeError when reading corrupted/stale buffer data from a zombie connection).
|
|
47
|
+
// wrapping the stream with its own error handler catches them at the source.
|
|
48
|
+
const stream = (client as any).connection?.stream
|
|
49
|
+
if (stream && !stream.__pgErrorPatched) {
|
|
50
|
+
stream.__pgErrorPatched = true
|
|
51
|
+
stream.on('error', (error: Error) => {
|
|
52
|
+
console.error(`[database] stream error`, error.message, error.stack)
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
return pool
|
|
58
|
+
}
|