@meith/db 0.33.1 → 0.33.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +31 -31
- package/src/index.ts +1 -1
- package/src/migrate.ts +138 -24
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meith/db",
|
|
3
|
-
"version": "0.33.
|
|
3
|
+
"version": "0.33.2",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -22,36 +22,36 @@
|
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"drizzle-orm": "^0.45.2",
|
|
24
24
|
"postgres": "^3.4.7",
|
|
25
|
-
"@meith/accounts": "0.33.
|
|
26
|
-
"@meith/admin": "0.33.
|
|
27
|
-
"@meith/antispam": "0.33.
|
|
28
|
-
"@meith/api": "0.33.
|
|
29
|
-
"@meith/attachments": "0.33.
|
|
30
|
-
"@meith/authorization": "0.33.
|
|
31
|
-
"@meith/avatars": "0.33.
|
|
32
|
-
"@meith/board-digest": "0.33.
|
|
33
|
-
"@meith/core": "0.33.
|
|
34
|
-
"@meith/drafts": "0.33.
|
|
35
|
-
"@meith/events": "0.33.
|
|
36
|
-
"@meith/forums": "0.33.
|
|
37
|
-
"@meith/groups": "0.33.
|
|
38
|
-
"@meith/i18n": "0.33.
|
|
39
|
-
"@meith/
|
|
40
|
-
"@meith/
|
|
41
|
-
"@meith/
|
|
42
|
-
"@meith/moderation": "0.33.
|
|
43
|
-
"@meith/notifications": "0.33.
|
|
44
|
-
"@meith/plugin-kit": "0.33.
|
|
45
|
-
"@meith/polls": "0.33.
|
|
46
|
-
"@meith/profile-fields": "0.33.
|
|
47
|
-
"@meith/relations": "0.33.
|
|
48
|
-
"@meith/reputation": "0.33.
|
|
49
|
-
"@meith/search": "0.33.
|
|
50
|
-
"@meith/settings": "0.33.
|
|
51
|
-
"@meith/signatures": "0.33.
|
|
52
|
-
"@meith/subscriptions": "0.33.
|
|
53
|
-
"@meith/tasks": "0.33.
|
|
54
|
-
"@meith/threads": "0.33.
|
|
25
|
+
"@meith/accounts": "0.33.2",
|
|
26
|
+
"@meith/admin": "0.33.2",
|
|
27
|
+
"@meith/antispam": "0.33.2",
|
|
28
|
+
"@meith/api": "0.33.2",
|
|
29
|
+
"@meith/attachments": "0.33.2",
|
|
30
|
+
"@meith/authorization": "0.33.2",
|
|
31
|
+
"@meith/avatars": "0.33.2",
|
|
32
|
+
"@meith/board-digest": "0.33.2",
|
|
33
|
+
"@meith/core": "0.33.2",
|
|
34
|
+
"@meith/drafts": "0.33.2",
|
|
35
|
+
"@meith/events": "0.33.2",
|
|
36
|
+
"@meith/forums": "0.33.2",
|
|
37
|
+
"@meith/groups": "0.33.2",
|
|
38
|
+
"@meith/i18n": "0.33.2",
|
|
39
|
+
"@meith/marketplace": "0.33.2",
|
|
40
|
+
"@meith/messages": "0.33.2",
|
|
41
|
+
"@meith/markdown": "0.33.2",
|
|
42
|
+
"@meith/moderation": "0.33.2",
|
|
43
|
+
"@meith/notifications": "0.33.2",
|
|
44
|
+
"@meith/plugin-kit": "0.33.2",
|
|
45
|
+
"@meith/polls": "0.33.2",
|
|
46
|
+
"@meith/profile-fields": "0.33.2",
|
|
47
|
+
"@meith/relations": "0.33.2",
|
|
48
|
+
"@meith/reputation": "0.33.2",
|
|
49
|
+
"@meith/search": "0.33.2",
|
|
50
|
+
"@meith/settings": "0.33.2",
|
|
51
|
+
"@meith/signatures": "0.33.2",
|
|
52
|
+
"@meith/subscriptions": "0.33.2",
|
|
53
|
+
"@meith/tasks": "0.33.2",
|
|
54
|
+
"@meith/threads": "0.33.2"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
57
|
"drizzle-kit": "^0.31.6",
|
package/src/index.ts
CHANGED
|
@@ -172,7 +172,7 @@ export {
|
|
|
172
172
|
export { PostgresMemberProfileRepository } from './member-profile-repo'
|
|
173
173
|
export { PostgresMemberSettingsRepository } from './member-settings-repo'
|
|
174
174
|
export { PostgresMessageRepository } from './message-repo'
|
|
175
|
-
export { migrationUrl, runMigrations } from './migrate'
|
|
175
|
+
export { migrationUrl, pendingCoreMigrations, runMigrations } from './migrate'
|
|
176
176
|
export { PostgresModCpRepository } from './modcp-repo'
|
|
177
177
|
export { PostgresModerationQueueRepository } from './moderation-queue'
|
|
178
178
|
export {
|
package/src/migrate.ts
CHANGED
|
@@ -1,17 +1,37 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
2
3
|
import path from 'node:path'
|
|
3
4
|
import { fileURLToPath } from 'node:url'
|
|
4
5
|
|
|
5
|
-
import {
|
|
6
|
-
import { migrate } from 'drizzle-orm/postgres-js/migrator'
|
|
6
|
+
import { sql } from 'drizzle-orm'
|
|
7
7
|
import postgres from 'postgres'
|
|
8
8
|
|
|
9
9
|
import { ConfigurationError, env, logger } from '@meith/core'
|
|
10
10
|
|
|
11
|
+
import type { Database } from './client'
|
|
12
|
+
import { resultRows } from './result-rows'
|
|
13
|
+
|
|
11
14
|
const JOURNAL = path.join('meta', '_journal.json')
|
|
12
15
|
|
|
13
16
|
const IMAGE_MIGRATIONS = '/app/migrations'
|
|
14
17
|
|
|
18
|
+
const STATEMENT_BREAKPOINT = '--> statement-breakpoint'
|
|
19
|
+
|
|
20
|
+
export const MIGRATIONS_TABLE = 'drizzle.__drizzle_migrations'
|
|
21
|
+
|
|
22
|
+
export interface Migration {
|
|
23
|
+
readonly tag: string
|
|
24
|
+
readonly hash: string
|
|
25
|
+
readonly when: number
|
|
26
|
+
readonly statements: readonly string[]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface MigrationExecutor {
|
|
30
|
+
exec(statement: string): Promise<void>
|
|
31
|
+
query<T>(statement: string): Promise<readonly T[]>
|
|
32
|
+
transaction(work: (exec: (statement: string) => Promise<void>) => Promise<void>): Promise<void>
|
|
33
|
+
}
|
|
34
|
+
|
|
15
35
|
export function migrationFolderCandidates(input: {
|
|
16
36
|
readonly explicit?: string | undefined
|
|
17
37
|
readonly moduleDir?: string | undefined
|
|
@@ -31,6 +51,7 @@ export function migrationFolderCandidates(input: {
|
|
|
31
51
|
for (;;) {
|
|
32
52
|
candidates.push(path.join(dir, 'migrations'))
|
|
33
53
|
candidates.push(path.join(dir, 'packages', 'db', 'migrations'))
|
|
54
|
+
candidates.push(path.join(dir, 'node_modules', '@meith', 'db', 'migrations'))
|
|
34
55
|
const parent = path.dirname(dir)
|
|
35
56
|
if (parent === dir) break
|
|
36
57
|
dir = parent
|
|
@@ -63,6 +84,95 @@ function migrationsFolder(): string {
|
|
|
63
84
|
)
|
|
64
85
|
}
|
|
65
86
|
|
|
87
|
+
export function readMigrations(folder: string): readonly Migration[] {
|
|
88
|
+
const journal = JSON.parse(readFileSync(path.join(folder, JOURNAL), 'utf8')) as {
|
|
89
|
+
readonly entries: readonly { readonly tag: string; readonly when: number }[]
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return journal.entries.map((entry) => {
|
|
93
|
+
if (!Number.isSafeInteger(entry.when)) {
|
|
94
|
+
throw new ConfigurationError(`Migration ${entry.tag} has no usable timestamp in the journal.`)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const source = readFileSync(path.join(folder, `${entry.tag}.sql`), 'utf8')
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
tag: entry.tag,
|
|
101
|
+
when: entry.when,
|
|
102
|
+
hash: createHash('sha256').update(source).digest('hex'),
|
|
103
|
+
statements: source.split(STATEMENT_BREAKPOINT).filter((statement) => statement.trim() !== ''),
|
|
104
|
+
}
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function appliedHashes(client: MigrationExecutor): Promise<ReadonlySet<string>> {
|
|
109
|
+
const [table] = await client.query<{ found: string | null }>(
|
|
110
|
+
`select to_regclass('${MIGRATIONS_TABLE}')::text as found`,
|
|
111
|
+
)
|
|
112
|
+
if (table === undefined || table.found === null) return new Set()
|
|
113
|
+
|
|
114
|
+
const rows = await client.query<{ hash: string }>(`select hash from ${MIGRATIONS_TABLE}`)
|
|
115
|
+
return new Set(rows.map((row) => row.hash))
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export async function pendingMigrations(
|
|
119
|
+
client: MigrationExecutor,
|
|
120
|
+
migrations: readonly Migration[],
|
|
121
|
+
): Promise<readonly Migration[]> {
|
|
122
|
+
const applied = await appliedHashes(client)
|
|
123
|
+
return migrations.filter((migration) => !applied.has(migration.hash))
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export async function applyMigrations(
|
|
127
|
+
client: MigrationExecutor,
|
|
128
|
+
migrations: readonly Migration[],
|
|
129
|
+
): Promise<readonly Migration[]> {
|
|
130
|
+
const pending = await pendingMigrations(client, migrations)
|
|
131
|
+
if (pending.length === 0) return pending
|
|
132
|
+
|
|
133
|
+
await client.exec('create schema if not exists drizzle')
|
|
134
|
+
await client.exec(
|
|
135
|
+
`create table if not exists ${MIGRATIONS_TABLE} (id serial primary key, ` +
|
|
136
|
+
'hash text not null, created_at bigint)',
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
await client.transaction(async (exec) => {
|
|
140
|
+
for (const migration of pending) {
|
|
141
|
+
for (const statement of migration.statements) await exec(statement)
|
|
142
|
+
await exec(
|
|
143
|
+
`insert into ${MIGRATIONS_TABLE} ("hash", "created_at") ` +
|
|
144
|
+
`values ('${migration.hash}', ${migration.when})`,
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
return pending
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function databaseExecutor(db: Database): MigrationExecutor {
|
|
153
|
+
return {
|
|
154
|
+
async exec(statement) {
|
|
155
|
+
await db.execute(sql.raw(statement))
|
|
156
|
+
},
|
|
157
|
+
async query<T>(statement: string) {
|
|
158
|
+
return resultRows<T>(await db.execute(sql.raw(statement)))
|
|
159
|
+
},
|
|
160
|
+
async transaction(work) {
|
|
161
|
+
await db.transaction(async (tx) => {
|
|
162
|
+
await work(async (statement) => {
|
|
163
|
+
await tx.execute(sql.raw(statement))
|
|
164
|
+
})
|
|
165
|
+
})
|
|
166
|
+
},
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export async function pendingCoreMigrations(db: Database): Promise<readonly string[]> {
|
|
171
|
+
const migrations = readMigrations(migrationsFolder())
|
|
172
|
+
const pending = await pendingMigrations(databaseExecutor(db), migrations)
|
|
173
|
+
return pending.map((migration) => migration.tag)
|
|
174
|
+
}
|
|
175
|
+
|
|
66
176
|
export const MIGRATION_LOCK_KEY = '-2943916371013839929'
|
|
67
177
|
|
|
68
178
|
export function migrationUrl(source: {
|
|
@@ -91,39 +201,43 @@ export async function withMigrationLock<T>(
|
|
|
91
201
|
}
|
|
92
202
|
}
|
|
93
203
|
|
|
204
|
+
function postgresExecutor(sql: ReturnType<typeof postgres>): MigrationExecutor {
|
|
205
|
+
return {
|
|
206
|
+
async exec(statement) {
|
|
207
|
+
await sql.unsafe(statement)
|
|
208
|
+
},
|
|
209
|
+
async query<T>(statement: string) {
|
|
210
|
+
return (await sql.unsafe(statement)) as unknown as readonly T[]
|
|
211
|
+
},
|
|
212
|
+
async transaction(work) {
|
|
213
|
+
await sql.begin(async (tx) => {
|
|
214
|
+
await work(async (statement) => {
|
|
215
|
+
await tx.unsafe(statement)
|
|
216
|
+
})
|
|
217
|
+
})
|
|
218
|
+
},
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
94
222
|
export async function runMigrations(
|
|
95
223
|
options: { readonly folder?: string; readonly url?: string } = {},
|
|
96
224
|
): Promise<number> {
|
|
97
225
|
const url = options.url ?? migrationUrl(env)
|
|
226
|
+
const migrations = readMigrations(options.folder ?? migrationsFolder())
|
|
98
227
|
|
|
99
228
|
const sql = postgres(url, { max: 1, prepare: false, onnotice: () => {} })
|
|
100
229
|
|
|
101
230
|
try {
|
|
102
231
|
return await withMigrationLock(sql, async () => {
|
|
103
|
-
const
|
|
104
|
-
await migrate(drizzle(sql), { migrationsFolder: options.folder ?? migrationsFolder() })
|
|
105
|
-
const after = await appliedCount(sql)
|
|
232
|
+
const applied = await applyMigrations(postgresExecutor(sql), migrations)
|
|
106
233
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
234
|
+
logger({ component: 'migrate' }).info(
|
|
235
|
+
{ applied: applied.length, tags: applied.map((migration) => migration.tag) },
|
|
236
|
+
'migrations applied',
|
|
237
|
+
)
|
|
238
|
+
return applied.length
|
|
110
239
|
})
|
|
111
240
|
} finally {
|
|
112
241
|
await sql.end({ timeout: 5 })
|
|
113
242
|
}
|
|
114
243
|
}
|
|
115
|
-
|
|
116
|
-
async function appliedCount(sql: ReturnType<typeof postgres>): Promise<number> {
|
|
117
|
-
const rows = await sql<{ count: string }[]>`
|
|
118
|
-
SELECT COUNT(*)::text AS count
|
|
119
|
-
FROM information_schema.tables
|
|
120
|
-
WHERE table_schema = 'drizzle' AND table_name = '__drizzle_migrations'
|
|
121
|
-
`
|
|
122
|
-
|
|
123
|
-
if (rows[0]?.count === '0') return 0
|
|
124
|
-
|
|
125
|
-
const applied = await sql<{ count: string }[]>`
|
|
126
|
-
SELECT COUNT(*)::text AS count FROM drizzle.__drizzle_migrations
|
|
127
|
-
`
|
|
128
|
-
return Number(applied[0]?.count ?? '0')
|
|
129
|
-
}
|