@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,81 @@
1
+ #!/usr/bin/env bun
2
+
3
+ /**
4
+ * drizzle-kit v1 beta generates migrations as directories:
5
+ * YYYYMMDDHHMMSS_name/migration.sql + snapshot.json
6
+ *
7
+ * our migration runner uses loadMigrationsFromDir which only
8
+ * finds flat .ts files. this script creates a flat .ts wrapper for each
9
+ * directory migration so the runner can pick them up.
10
+ *
11
+ * the wrapper inlines the SQL as a string constant (no ?raw import needed)
12
+ * which makes it work with both vite and bun.
13
+ */
14
+
15
+ import { existsSync } from 'node:fs'
16
+ import { readdir, readFile, writeFile, stat } from 'node:fs/promises'
17
+ import { join } from 'node:path'
18
+
19
+ export type DrizzleSyncOptions = {
20
+ migrationsDir: string
21
+ }
22
+
23
+ export async function syncDrizzleMigrations(options: DrizzleSyncOptions) {
24
+ const { migrationsDir } = options
25
+ const entries = await readdir(migrationsDir, { withFileTypes: true })
26
+
27
+ // find directory-format migrations (contain migration.sql)
28
+ const migrationDirs = entries.filter(
29
+ (e) => e.isDirectory() && e.name !== 'meta' && e.name !== 'node_modules'
30
+ )
31
+
32
+ let created = 0
33
+ let updated = 0
34
+
35
+ for (const dir of migrationDirs) {
36
+ const sqlPath = join(migrationsDir, dir.name, 'migration.sql')
37
+ if (!existsSync(sqlPath)) continue
38
+
39
+ const sql = await readFile(sqlPath, 'utf-8')
40
+ const trimmed = sql.trim()
41
+
42
+ if (!trimmed || trimmed.startsWith('-- seed')) {
43
+ continue
44
+ }
45
+
46
+ const wrapperPath = join(migrationsDir, `${dir.name}.ts`)
47
+
48
+ // check if wrapper already exists and is up to date
49
+ if (existsSync(wrapperPath)) {
50
+ const sqlMtime = (await stat(sqlPath)).mtimeMs
51
+ const tsMtime = (await stat(wrapperPath)).mtimeMs
52
+ if (tsMtime > sqlMtime) continue
53
+ updated++
54
+ } else {
55
+ created++
56
+ }
57
+
58
+ // generate wrapper with inlined SQL
59
+ const escaped = trimmed
60
+ .replace(/\\/g, '\\\\')
61
+ .replace(/`/g, '\\`')
62
+ .replace(/\$/g, '\\$')
63
+ const content = `import type { PoolClient } from 'pg'
64
+
65
+ const sql = \`${escaped}\`
66
+
67
+ export async function up(client: PoolClient) {
68
+ for (const stmt of sql.split('--> statement-breakpoint')) {
69
+ const trimmed = stmt.trim()
70
+ if (trimmed) await client.query(trimmed)
71
+ }
72
+ }
73
+ `
74
+
75
+ await writeFile(wrapperPath, content)
76
+ }
77
+
78
+ if (created > 0) console.info(`created ${created} migration wrapper(s)`)
79
+ if (updated > 0) console.info(`updated ${updated} migration wrapper(s)`)
80
+ if (created === 0 && updated === 0) console.info('migration wrappers up to date')
81
+ }
@@ -0,0 +1,103 @@
1
+ import { spawn } from 'node:child_process'
2
+
3
+ export type DumpDatabaseOptions = {
4
+ host: string
5
+ database: string
6
+ username: string
7
+ password?: string
8
+ /** output file path */
9
+ outputPath: string
10
+ /** timeout in ms (default: 5 minutes) */
11
+ timeout?: number
12
+ /** pg_dump format (default: 'custom') */
13
+ format?: 'custom' | 'plain' | 'directory' | 'tar'
14
+ /** compression level 0-9 (default: 9) */
15
+ compress?: number
16
+ }
17
+
18
+ export async function dumpDatabase(options: DumpDatabaseOptions): Promise<boolean> {
19
+ const {
20
+ host,
21
+ database,
22
+ username,
23
+ password,
24
+ outputPath,
25
+ timeout = 5 * 60 * 1000,
26
+ format = 'custom',
27
+ compress = 9,
28
+ } = options
29
+
30
+ return new Promise<boolean>((resolve) => {
31
+ const proc = spawn(
32
+ 'pg_dump',
33
+ [
34
+ '--clean',
35
+ '--if-exists',
36
+ '--no-owner',
37
+ '--no-privileges',
38
+ '--verbose',
39
+ `--format=${format}`,
40
+ `--compress=${compress}`,
41
+ '--file',
42
+ outputPath,
43
+ ],
44
+ {
45
+ env: {
46
+ ...process.env,
47
+ PGPASSWORD: password,
48
+ PGUSER: username,
49
+ PGHOST: host,
50
+ PGDATABASE: database,
51
+ PGCONNECT_TIMEOUT: '10',
52
+ } as any,
53
+ stdio: 'inherit',
54
+ }
55
+ )
56
+
57
+ let timeoutId: ReturnType<typeof setTimeout>
58
+
59
+ proc.on('error', (err) => {
60
+ console.error('[database] pg_dump process error:', err)
61
+ clearTimeout(timeoutId)
62
+ resolve(false)
63
+ })
64
+
65
+ proc.on('exit', (code) => {
66
+ clearTimeout(timeoutId)
67
+ if (code === 0) {
68
+ resolve(true)
69
+ } else {
70
+ console.error(`[database] pg_dump exited with code ${code}`)
71
+ resolve(false)
72
+ }
73
+ })
74
+
75
+ timeoutId = setTimeout(() => {
76
+ console.error(`[database] pg_dump timed out after ${timeout}ms`)
77
+ proc.kill('SIGTERM')
78
+ setTimeout(() => {
79
+ if (!proc.killed) proc.kill('SIGKILL')
80
+ }, 5000)
81
+ resolve(false)
82
+ }, timeout)
83
+ })
84
+ }
85
+
86
+ export async function checkDatabaseConnection(options: {
87
+ host: string
88
+ database: string
89
+ username: string
90
+ timeout?: number
91
+ }): Promise<boolean> {
92
+ const { host, database, username, timeout = 5000 } = options
93
+
94
+ return new Promise<boolean>((resolve) => {
95
+ const proc = spawn('pg_isready', ['-d', database, '-h', host, '-U', username], {
96
+ stdio: 'inherit',
97
+ timeout,
98
+ })
99
+
100
+ proc.on('error', () => resolve(false))
101
+ proc.on('exit', (code) => resolve(code === 0))
102
+ })
103
+ }
@@ -0,0 +1,54 @@
1
+ import { readdirSync, writeFileSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+
4
+ export type MigrationAddOptions = {
5
+ migrationsDir: string
6
+ name?: string
7
+ }
8
+
9
+ const commonWords = ['sapphire', 'emerald', 'ruby', 'amber', 'topaz', 'onyx', 'pearl']
10
+
11
+ export function addMigration(options: MigrationAddOptions): string {
12
+ const { migrationsDir, name } = options
13
+
14
+ // if no name is provided, pick a random one from common words
15
+ const migrationName =
16
+ name || commonWords[Math.floor(Math.random() * commonWords.length)]!
17
+
18
+ // read all files in the migrations directory
19
+ const files = readdirSync(migrationsDir)
20
+
21
+ // find the highest migration number from both .ts and .sql files
22
+ const migrationRegex = /^(\d+)[-_].*\.(ts|sql)$/
23
+ let maxNumber = 0
24
+ for (const file of files) {
25
+ const match = file.match(migrationRegex)
26
+ if (match?.[1]) {
27
+ const num = Number.parseInt(match[1], 10)
28
+ if (!Number.isNaN(num) && num > maxNumber) {
29
+ maxNumber = num
30
+ }
31
+ }
32
+ }
33
+
34
+ // calculate the next migration number and pad to 4 digits
35
+ const nextNumber = (maxNumber + 1).toString().padStart(4, '0')
36
+ const newFilename = `${nextNumber}-${migrationName}.ts`
37
+ const newFilePath = join(migrationsDir, newFilename)
38
+
39
+ // create a template for custom TypeScript migrations
40
+ const templateContent = `import type { PoolClient } from 'pg'
41
+
42
+ export async function up(client: PoolClient) {
43
+ // implementation for applying this migration
44
+ }
45
+ `
46
+
47
+ // write the template to the new file
48
+ writeFileSync(newFilePath, templateContent)
49
+
50
+ console.info(`Created custom migration: ${newFilePath}`)
51
+ console.info(`For Drizzle schema migrations, run 'drizzle-kit generate' instead`)
52
+
53
+ return newFilePath
54
+ }
@@ -0,0 +1,46 @@
1
+ import { spawnSync } from 'node:child_process'
2
+
3
+ export type PgDumpOptions = {
4
+ connectionString?: string
5
+ host?: string
6
+ port?: number
7
+ database?: string
8
+ user?: string
9
+ password?: string
10
+ args?: string[]
11
+ }
12
+
13
+ export function runPgDump(options: PgDumpOptions = {}) {
14
+ const { connectionString, host, port, database, user, password, args = [] } = options
15
+
16
+ const env: Record<string, string> = {
17
+ ...process.env,
18
+ } as any
19
+
20
+ if (connectionString) {
21
+ // parse connection string and set individual env vars
22
+ const url = new URL(connectionString)
23
+ env.PGHOST = url.hostname
24
+ env.PGPORT = url.port || '5432'
25
+ env.PGDATABASE = url.pathname.slice(1)
26
+ env.PGUSER = url.username
27
+ if (url.password) {
28
+ env.PGPASSWORD = url.password
29
+ }
30
+ } else {
31
+ if (host) env.PGHOST = host
32
+ if (port) env.PGPORT = port.toString()
33
+ if (database) env.PGDATABASE = database
34
+ if (user) env.PGUSER = user
35
+ if (password) env.PGPASSWORD = password
36
+ }
37
+
38
+ console.info(`Running pg_dump on postgres ${env.PGHOST}/${env.PGDATABASE}`)
39
+
40
+ const result = spawnSync('pg_dump', args, {
41
+ stdio: ['ignore', 'inherit', 'inherit'],
42
+ env,
43
+ })
44
+
45
+ return result.status
46
+ }
@@ -0,0 +1,211 @@
1
+ import { DeleteObjectsCommand, ListObjectsV2Command, S3Client } from '@aws-sdk/client-s3'
2
+
3
+ interface BackupInfo {
4
+ key: string
5
+ date: Date
6
+ size: number
7
+ }
8
+
9
+ export async function pruneBackups(
10
+ bucketName: string,
11
+ prefix: string,
12
+ options?: {
13
+ region?: string
14
+ dryRun?: boolean
15
+ }
16
+ ): Promise<{ kept: string[]; deleted: string[] }> {
17
+ const region = options?.region ?? 'us-west-1'
18
+ const dryRun = options?.dryRun ?? false
19
+
20
+ const s3Client = new S3Client({
21
+ region,
22
+ credentials: {
23
+ accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
24
+ secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
25
+ },
26
+ })
27
+
28
+ console.info('🧹 Pruning old backups...')
29
+
30
+ // list all backups
31
+ const allBackups: BackupInfo[] = []
32
+ let continuationToken: string | undefined
33
+
34
+ do {
35
+ const response = await s3Client.send(
36
+ new ListObjectsV2Command({
37
+ Bucket: bucketName,
38
+ Prefix: prefix,
39
+ ContinuationToken: continuationToken,
40
+ })
41
+ )
42
+
43
+ if (response.Contents) {
44
+ for (const obj of response.Contents) {
45
+ if (obj.Key && obj.LastModified && obj.Size) {
46
+ // extract date from filename (format: db-backup-2025-08-21T15-50-48-103Z.sql)
47
+ const match = obj.Key.match(/(\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}Z)/)
48
+ if (match) {
49
+ const dateStr = match[1]!.replace(/-(\d{2})-(\d{2})-(\d{3}Z)/, ':$1:$2.$3')
50
+ allBackups.push({
51
+ key: obj.Key,
52
+ date: new Date(dateStr),
53
+ size: obj.Size,
54
+ })
55
+ }
56
+ }
57
+ }
58
+ }
59
+
60
+ continuationToken = response.NextContinuationToken
61
+ } while (continuationToken)
62
+
63
+ if (allBackups.length === 0) {
64
+ console.info('No backups found to prune')
65
+ return { kept: [], deleted: [] }
66
+ }
67
+
68
+ // sort by date (newest first)
69
+ allBackups.sort((a, b) => b.date.getTime() - a.date.getTime())
70
+
71
+ const now = new Date()
72
+ const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000)
73
+
74
+ const toKeep = new Set<string>()
75
+ const toDelete: string[] = []
76
+
77
+ // group backups by day and month
78
+ const dailyBackups = new Map<string, BackupInfo[]>()
79
+ const monthlyBackups = new Map<string, BackupInfo[]>()
80
+
81
+ for (const backup of allBackups) {
82
+ const dayKey = backup.date.toISOString().split('T')[0]! // YYYY-MM-DD
83
+ const monthKey = dayKey.substring(0, 7) // YYYY-MM
84
+
85
+ if (!dailyBackups.has(dayKey)) {
86
+ dailyBackups.set(dayKey, [])
87
+ }
88
+ const dayBackupsList = dailyBackups.get(dayKey)
89
+ if (dayBackupsList) {
90
+ dayBackupsList.push(backup)
91
+ }
92
+
93
+ if (!monthlyBackups.has(monthKey)) {
94
+ monthlyBackups.set(monthKey, [])
95
+ }
96
+ const monthBackupsList = monthlyBackups.get(monthKey)
97
+ if (monthBackupsList) {
98
+ monthBackupsList.push(backup)
99
+ }
100
+ }
101
+
102
+ // keep daily backups for last 30 days
103
+ for (const [dayKey, backups] of dailyBackups) {
104
+ const dayDate = new Date(dayKey)
105
+ if (dayDate >= thirtyDaysAgo) {
106
+ // keep the most recent backup for each day
107
+ const latestBackup = backups[0]
108
+ if (latestBackup) {
109
+ toKeep.add(latestBackup.key)
110
+ }
111
+ }
112
+ }
113
+
114
+ // keep monthly backups for older than 30 days
115
+ for (const [monthKey, backups] of monthlyBackups) {
116
+ const monthDate = new Date(monthKey + '-01')
117
+ if (monthDate < thirtyDaysAgo) {
118
+ // keep the most recent backup for each month
119
+ const latestBackup = backups[0]
120
+ if (latestBackup) {
121
+ toKeep.add(latestBackup.key)
122
+ }
123
+ }
124
+ }
125
+
126
+ // always keep the very latest backup regardless of rules
127
+ if (allBackups.length > 0) {
128
+ const latestBackup = allBackups[0]
129
+ if (latestBackup) {
130
+ toKeep.add(latestBackup.key)
131
+ }
132
+ }
133
+
134
+ // determine which backups to delete
135
+ for (const backup of allBackups) {
136
+ if (!toKeep.has(backup.key)) {
137
+ toDelete.push(backup.key)
138
+ }
139
+ }
140
+
141
+ console.info(`📊 Backup retention summary:`)
142
+ console.info(` Total backups: ${allBackups.length}`)
143
+ console.info(` Keeping: ${toKeep.size}`)
144
+ console.info(` Deleting: ${toDelete.length}`)
145
+
146
+ if (toDelete.length > 0) {
147
+ if (dryRun) {
148
+ console.info('\n🔍 DRY RUN - Would delete:')
149
+ for (const key of toDelete.slice(0, 10)) {
150
+ console.info(` - ${key}`)
151
+ }
152
+ if (toDelete.length > 10) {
153
+ console.info(` ... and ${toDelete.length - 10} more`)
154
+ }
155
+ } else {
156
+ console.info('\n🗑️ Deleting old backups...')
157
+
158
+ // delete in batches of 1000 (S3 limit)
159
+ for (let i = 0; i < toDelete.length; i += 1000) {
160
+ const batch = toDelete.slice(i, i + 1000)
161
+
162
+ await s3Client.send(
163
+ new DeleteObjectsCommand({
164
+ Bucket: bucketName,
165
+ Delete: {
166
+ Objects: batch.map((key) => ({ Key: key })),
167
+ Quiet: true,
168
+ },
169
+ })
170
+ )
171
+
172
+ console.info(
173
+ ` Deleted batch ${Math.floor(i / 1000) + 1}/${Math.ceil(toDelete.length / 1000)}`
174
+ )
175
+ }
176
+
177
+ console.info(`✅ Successfully deleted ${toDelete.length} old backup(s)`)
178
+ }
179
+ } else {
180
+ console.info('✅ No backups need to be deleted')
181
+ }
182
+
183
+ // log retention details
184
+ console.info('\n📅 Retention details:')
185
+ const keptList = Array.from(toKeep).sort()
186
+ const recentBackups = keptList.filter((key) => {
187
+ const match = key.match(/(\d{4}-\d{2}-\d{2})/)
188
+ if (match && match[1]) {
189
+ const date = new Date(match[1])
190
+ return date >= thirtyDaysAgo
191
+ }
192
+ return false
193
+ })
194
+
195
+ const oldBackups = keptList.filter((key) => {
196
+ const match = key.match(/(\d{4}-\d{2}-\d{2})/)
197
+ if (match && match[1]) {
198
+ const date = new Date(match[1])
199
+ return date < thirtyDaysAgo
200
+ }
201
+ return false
202
+ })
203
+
204
+ console.info(` Daily backups (last 30 days): ${recentBackups.length}`)
205
+ console.info(` Monthly backups (older): ${oldBackups.length}`)
206
+
207
+ return {
208
+ kept: Array.from(toKeep),
209
+ deleted: toDelete,
210
+ }
211
+ }
@@ -0,0 +1,51 @@
1
+ import { spawnSync } from 'node:child_process'
2
+
3
+ export type PsqlOptions = {
4
+ connectionString?: string
5
+ host?: string
6
+ port?: number
7
+ database?: string
8
+ user?: string
9
+ password?: string
10
+ query?: string
11
+ }
12
+
13
+ export function runPsql(options: PsqlOptions = {}) {
14
+ const { connectionString, host, port, database, user, password, query } = options
15
+
16
+ const env: Record<string, string> = {
17
+ ...process.env,
18
+ } as any
19
+
20
+ if (connectionString) {
21
+ // parse connection string and set individual env vars
22
+ const url = new URL(connectionString)
23
+ env.PGHOST = url.hostname
24
+ env.PGPORT = url.port || '5432'
25
+ env.PGDATABASE = url.pathname.slice(1)
26
+ env.PGUSER = url.username
27
+ if (url.password) {
28
+ env.PGPASSWORD = url.password
29
+ }
30
+ } else {
31
+ if (host) env.PGHOST = host
32
+ if (port) env.PGPORT = port.toString()
33
+ if (database) env.PGDATABASE = database
34
+ if (user) env.PGUSER = user
35
+ if (password) env.PGPASSWORD = password
36
+ }
37
+
38
+ const args: string[] = []
39
+ if (query) {
40
+ args.push('-c', query)
41
+ }
42
+
43
+ console.info(`Connecting to postgres ${env.PGHOST}/${env.PGDATABASE}`)
44
+
45
+ const result = spawnSync('psql', args, {
46
+ stdio: ['ignore', 'inherit', 'inherit'],
47
+ env,
48
+ })
49
+
50
+ return result.status
51
+ }
@@ -0,0 +1,45 @@
1
+ // standardized demo/world seed applier — the one mechanism every runtime uses
2
+ // to apply a project's database/seed.ts rows after migrations: pg via the
3
+ // --seed flag on database/migrate.ts, and the browser preview worker (PGlite)
4
+ // at cold boot. same rows, same insert semantics, only the client differs.
5
+
6
+ import type { InferInsertModel, Table } from 'drizzle-orm'
7
+
8
+ // plain rows keyed by table name, in insert order (FK parents before children)
9
+ export type SeedRows = Record<string, Array<Record<string, unknown>>>
10
+
11
+ // typed view of SeedRows against a drizzle schema module: table exports map to
12
+ // their insert models so seed rows typecheck against real columns, while
13
+ // `import type` keeps the seed file drizzle-free at runtime
14
+ export type SeedData<TSchema> = {
15
+ [K in keyof TSchema as TSchema[K] extends Table ? K : never]?: Array<
16
+ InferInsertModel<Extract<TSchema[K], Table>>
17
+ >
18
+ }
19
+
20
+ // the minimal client shape every runtime already has: node-pg PoolClient,
21
+ // PGlite, and the preview worker pool all expose query(sql, params)
22
+ export interface SeedClient {
23
+ query(sql: string, params?: unknown[]): Promise<unknown>
24
+ }
25
+
26
+ const quote = (id: string) => `"${id.replaceAll('"', '""')}"`
27
+
28
+ // INSERT ... ON CONFLICT DO NOTHING per row, tables in key order. idempotent
29
+ // by construction: deterministic seed ids make re-application a no-op and a
30
+ // grown seed set back-fills only the missing rows. undefined columns are
31
+ // omitted so they fall to the DB default.
32
+ export async function applySeed(client: SeedClient, rows: SeedRows): Promise<void> {
33
+ for (const [table, tableRows] of Object.entries(rows)) {
34
+ for (const row of tableRows) {
35
+ const columns = Object.keys(row).filter((key) => row[key] !== undefined)
36
+ if (columns.length === 0) continue
37
+ const values = columns.map((key) => row[key])
38
+ const placeholders = columns.map((_, i) => `$${i + 1}`).join(', ')
39
+ await client.query(
40
+ `INSERT INTO ${quote(table)} (${columns.map(quote).join(', ')}) VALUES (${placeholders}) ON CONFLICT DO NOTHING`,
41
+ values
42
+ )
43
+ }
44
+ }
45
+ }
@@ -0,0 +1,39 @@
1
+ import { ellipsis } from '@o/helpers'
2
+
3
+ import type { Pool, QueryResult } from 'pg'
4
+
5
+ export type SqlQuery = {
6
+ text: string
7
+ values: any[]
8
+ }
9
+
10
+ export const createSql = (pool: Pool) => {
11
+ return (strings: TemplateStringsArray, ...values: any[]): Promise<QueryResult> => {
12
+ const text = strings.reduce((result, str, i) => {
13
+ return result + str + (i < values.length ? `$${i + 1}` : '')
14
+ }, '')
15
+
16
+ if (process.env.DEBUG) {
17
+ console.info(`sql: ${ellipsis(text, 80)}`)
18
+ }
19
+
20
+ return pool.query(text.trim(), values)
21
+ }
22
+ }
23
+
24
+ // default export for backward compatibility
25
+ let defaultPool: Pool | null = null
26
+
27
+ export const setDefaultPool = (pool: Pool) => {
28
+ defaultPool = pool
29
+ }
30
+
31
+ export const sql = (
32
+ strings: TemplateStringsArray,
33
+ ...values: any[]
34
+ ): Promise<QueryResult> => {
35
+ if (!defaultPool) {
36
+ throw new Error('No default pool set. Call setDefaultPool() first or use createSql()')
37
+ }
38
+ return createSql(defaultPool)(strings, ...values)
39
+ }