@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.
Files changed (121) hide show
  1. package/dist/takeout/better-auth.d.ts +19 -0
  2. package/dist/takeout/better-auth.d.ts.map +1 -0
  3. package/dist/takeout/better-auth.js +59 -0
  4. package/dist/takeout/better-auth.js.map +1 -0
  5. package/dist/takeout/build.d.ts +2 -0
  6. package/dist/takeout/build.d.ts.map +1 -0
  7. package/dist/takeout/build.js +3 -0
  8. package/dist/takeout/build.js.map +1 -0
  9. package/dist/takeout/chunkedQuery.d.ts +17 -0
  10. package/dist/takeout/chunkedQuery.d.ts.map +1 -0
  11. package/dist/takeout/chunkedQuery.js +55 -0
  12. package/dist/takeout/chunkedQuery.js.map +1 -0
  13. package/dist/takeout/cli.d.ts +3 -0
  14. package/dist/takeout/cli.d.ts.map +1 -0
  15. package/dist/takeout/cli.js +114 -0
  16. package/dist/takeout/cli.js.map +1 -0
  17. package/dist/takeout/createDatabase.d.ts +49 -0
  18. package/dist/takeout/createDatabase.d.ts.map +1 -0
  19. package/dist/takeout/createDatabase.js +100 -0
  20. package/dist/takeout/createDatabase.js.map +1 -0
  21. package/dist/takeout/createDb.d.ts +3 -0
  22. package/dist/takeout/createDb.d.ts.map +1 -0
  23. package/dist/takeout/createDb.js +11 -0
  24. package/dist/takeout/createDb.js.map +1 -0
  25. package/dist/takeout/createDrizzle.d.ts +38 -0
  26. package/dist/takeout/createDrizzle.d.ts.map +1 -0
  27. package/dist/takeout/createDrizzle.js +60 -0
  28. package/dist/takeout/createDrizzle.js.map +1 -0
  29. package/dist/takeout/createPool.d.ts +11 -0
  30. package/dist/takeout/createPool.d.ts.map +1 -0
  31. package/dist/takeout/createPool.js +36 -0
  32. package/dist/takeout/createPool.js.map +1 -0
  33. package/dist/takeout/createServerHelpers.d.ts +9 -0
  34. package/dist/takeout/createServerHelpers.d.ts.map +1 -0
  35. package/dist/takeout/createServerHelpers.js +11 -0
  36. package/dist/takeout/createServerHelpers.js.map +1 -0
  37. package/dist/takeout/getDBClient.d.ts +11 -0
  38. package/dist/takeout/getDBClient.d.ts.map +1 -0
  39. package/dist/takeout/getDBClient.js +169 -0
  40. package/dist/takeout/getDBClient.js.map +1 -0
  41. package/dist/takeout/index.d.ts +13 -0
  42. package/dist/takeout/index.d.ts.map +1 -0
  43. package/dist/takeout/index.js +13 -0
  44. package/dist/takeout/index.js.map +1 -0
  45. package/dist/takeout/migrate.d.ts +33 -0
  46. package/dist/takeout/migrate.d.ts.map +1 -0
  47. package/dist/takeout/migrate.js +214 -0
  48. package/dist/takeout/migrate.js.map +1 -0
  49. package/dist/takeout/scripts/build-migrations.d.ts +8 -0
  50. package/dist/takeout/scripts/build-migrations.d.ts.map +1 -0
  51. package/dist/takeout/scripts/build-migrations.js +51 -0
  52. package/dist/takeout/scripts/build-migrations.js.map +1 -0
  53. package/dist/takeout/scripts/drizzle-migrations-sync.d.ts +17 -0
  54. package/dist/takeout/scripts/drizzle-migrations-sync.d.ts.map +1 -0
  55. package/dist/takeout/scripts/drizzle-migrations-sync.js +69 -0
  56. package/dist/takeout/scripts/drizzle-migrations-sync.js.map +1 -0
  57. package/dist/takeout/scripts/dump-database.d.ts +22 -0
  58. package/dist/takeout/scripts/dump-database.d.ts.map +1 -0
  59. package/dist/takeout/scripts/dump-database.js +64 -0
  60. package/dist/takeout/scripts/dump-database.js.map +1 -0
  61. package/dist/takeout/scripts/migration-add.d.ts +6 -0
  62. package/dist/takeout/scripts/migration-add.d.ts.map +1 -0
  63. package/dist/takeout/scripts/migration-add.js +39 -0
  64. package/dist/takeout/scripts/migration-add.js.map +1 -0
  65. package/dist/takeout/scripts/pg_dump.d.ts +11 -0
  66. package/dist/takeout/scripts/pg_dump.d.ts.map +1 -0
  67. package/dist/takeout/scripts/pg_dump.js +37 -0
  68. package/dist/takeout/scripts/pg_dump.js.map +1 -0
  69. package/dist/takeout/scripts/prune-backups.d.ts +8 -0
  70. package/dist/takeout/scripts/prune-backups.d.ts.map +1 -0
  71. package/dist/takeout/scripts/prune-backups.js +166 -0
  72. package/dist/takeout/scripts/prune-backups.js.map +1 -0
  73. package/dist/takeout/scripts/psql.d.ts +11 -0
  74. package/dist/takeout/scripts/psql.d.ts.map +1 -0
  75. package/dist/takeout/scripts/psql.js +41 -0
  76. package/dist/takeout/scripts/psql.js.map +1 -0
  77. package/dist/takeout/seed.d.ts +10 -0
  78. package/dist/takeout/seed.d.ts.map +1 -0
  79. package/dist/takeout/seed.js +22 -0
  80. package/dist/takeout/seed.js.map +1 -0
  81. package/dist/takeout/sql.d.ts +9 -0
  82. package/dist/takeout/sql.d.ts.map +1 -0
  83. package/dist/takeout/sql.js +24 -0
  84. package/dist/takeout/sql.js.map +1 -0
  85. package/dist/takeout/sqlite.d.ts +66 -0
  86. package/dist/takeout/sqlite.d.ts.map +1 -0
  87. package/dist/takeout/sqlite.js +110 -0
  88. package/dist/takeout/sqlite.js.map +1 -0
  89. package/dist/takeout/waitForDatabase.d.ts +2 -0
  90. package/dist/takeout/waitForDatabase.d.ts.map +1 -0
  91. package/dist/takeout/waitForDatabase.js +26 -0
  92. package/dist/takeout/waitForDatabase.js.map +1 -0
  93. package/dist/takeout/zero.d.ts +44 -0
  94. package/dist/takeout/zero.d.ts.map +1 -0
  95. package/dist/takeout/zero.js +177 -0
  96. package/dist/takeout/zero.js.map +1 -0
  97. package/package.json +54 -4
  98. package/src/takeout/better-auth.ts +110 -0
  99. package/src/takeout/build.ts +2 -0
  100. package/src/takeout/chunkedQuery.ts +91 -0
  101. package/src/takeout/cli.ts +126 -0
  102. package/src/takeout/createDatabase.ts +164 -0
  103. package/src/takeout/createDb.ts +15 -0
  104. package/src/takeout/createDrizzle.ts +127 -0
  105. package/src/takeout/createPool.ts +58 -0
  106. package/src/takeout/createServerHelpers.ts +21 -0
  107. package/src/takeout/getDBClient.ts +254 -0
  108. package/src/takeout/index.ts +38 -0
  109. package/src/takeout/migrate.ts +272 -0
  110. package/src/takeout/scripts/build-migrations.ts +66 -0
  111. package/src/takeout/scripts/drizzle-migrations-sync.ts +81 -0
  112. package/src/takeout/scripts/dump-database.ts +103 -0
  113. package/src/takeout/scripts/migration-add.ts +54 -0
  114. package/src/takeout/scripts/pg_dump.ts +46 -0
  115. package/src/takeout/scripts/prune-backups.ts +211 -0
  116. package/src/takeout/scripts/psql.ts +51 -0
  117. package/src/takeout/seed.ts +45 -0
  118. package/src/takeout/sql.ts +39 -0
  119. package/src/takeout/sqlite.ts +237 -0
  120. package/src/takeout/waitForDatabase.ts +27 -0
  121. 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,2 @@
1
+ // build utilities that depend on vite
2
+ export { buildMigrations } from './scripts/build-migrations.js'
@@ -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
+ }