Skip to content

Commit dfa5313

Browse files
authored
feat(migrations): support new .data/hub/migrations (#42)
1 parent 6af6f41 commit dfa5313

File tree

10 files changed

+75
-30
lines changed

10 files changed

+75
-30
lines changed

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
"nuxt-hub": "./src/index.mjs"
1616
},
1717
"exports": {
18-
".": "./src/index.mjs",
1918
"./internal": "./src/internal.mjs"
2019
},
2120
"files": [

src/commands/database/migrations.mjs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,16 @@ import { defineCommand } from 'citty'
22
import create from './migrations/create.mjs'
33
import list from './migrations/list.mjs'
44
import markAllApplied from './migrations/mark-all-applied.mjs'
5+
import { consola } from 'consola'
56

67
export default defineCommand({
78
meta: {
89
name: 'migrations',
910
description: 'Database migrations commands.',
1011
},
12+
async setup() {
13+
consola.info('Make sure to run `npx nuxi prepare` before running this command if some migrations are missing.')
14+
},
1115
subCommands: {
1216
create,
1317
list,

src/commands/database/migrations/create.mjs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { defineCommand } from 'citty'
22
import { consola } from 'consola'
3-
import { useMigrationsStorage, getNextMigrationNumber } from '../../../utils/database.mjs'
3+
import { writeFile, mkdir } from 'node:fs/promises'
4+
import { join } from 'pathe'
5+
import { getNextMigrationNumber } from '../../../utils/database.mjs'
46

57
export default defineCommand({
68
meta: {
@@ -25,8 +27,10 @@ export default defineCommand({
2527
.replace(/-+/g, '-') // replace multiple dashes with a single dash
2628
|| 'migration'
2729
const migrationName = `${nextMigrationNumber}_${name}.sql`
28-
await useMigrationsStorage().set(migrationName, `-- Migration number: ${nextMigrationNumber} \t ${new Date().toISOString()}\n`)
30+
const userMigrationsDir = join(process.cwd(), 'server/database/migrations')
31+
await mkdir(userMigrationsDir, { recursive: true })
32+
await writeFile(join(userMigrationsDir, migrationName), `-- Migration number: ${nextMigrationNumber} \t ${new Date().toISOString()}\n`)
2933

30-
consola.success(`Created migration file \`server/migrations/${migrationName}\``)
34+
consola.success(`Created migration file \`server/database/migrations/${migrationName}\``)
3135
}
3236
});

src/commands/database/migrations/list.mjs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import ora from 'ora'
22
import { defineCommand, runCommand } from 'citty'
3+
import { join, relative } from 'pathe'
34
import { consola } from 'consola'
45
import { colors } from 'consola/utils'
5-
import { fetchUser, fetchProject, projectPath, getProjectEnv, fetchRemoteMigrations, getMigrationFiles } from '../../../utils/index.mjs'
6+
import { fetchUser, fetchProject, projectPath, getProjectEnv, fetchRemoteMigrations, getMigrationsDir, getMigrationFiles } from '../../../utils/index.mjs'
67
import link from '../../link.mjs'
78
import login from '../../login.mjs'
89

@@ -37,7 +38,7 @@ export default defineCommand({
3738
const total = localMigrations.length
3839

3940
if (total === 0) {
40-
consola.info('No migrations found in `./server/database/migrations`, please create one first.')
41+
consola.info('No migrations found, please create one first with `nuxthub database migrations create <name>`.')
4142
return process.exit(0)
4243
}
4344

@@ -103,10 +104,11 @@ export default defineCommand({
103104
const formattedPendingMigrations = pendingMigrations.map(fileName => ({ id: null, name: fileName, applied_at: null }))
104105
const migrations = remoteMigrations.concat(formattedPendingMigrations)
105106

107+
const migrationsDir = relative(process.cwd(), getMigrationsDir())
106108
for (const { name, applied_at } of migrations) {
107109
const appliedAt = applied_at ? new Date(applied_at).toLocaleString() : 'Pending'
108110
const color = applied_at ? colors.green : colors.yellow
109-
consola.log(`${color(applied_at ? '✅' : '🕒')} \`./server/database/migrations/${name}.sql\` ${colors.gray(appliedAt)}`)
111+
consola.log(`${color(applied_at ? '✅' : '🕒')} \`${join(migrationsDir, name)}.sql\` ${colors.gray(appliedAt)}`)
110112
}
111113

112114
process.exit(0)

src/commands/database/migrations/mark-all-applied.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export default defineCommand({
3939
const total = localMigrations.length
4040

4141
if (total === 0) {
42-
consola.info('No migrations found in `./server/database/migrations`, please create one first.')
42+
consola.info('No migrations found, please create one first with `nuxthub database migrations create <name>`.')
4343
return process.exit(0)
4444
}
4545

src/commands/deploy.mjs

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -221,18 +221,18 @@ export default defineCommand({
221221

222222
spinner.succeed(`Deployed ${colors.blueBright(linkedProject.slug)} to ${deployEnvColored}...`)
223223

224-
// #region Database migrations
225224
if (config.database) {
226-
const remoteMigrationsSpinner = ora(`Retrieving migrations on ${deployEnvColored} for ${colors.blueBright(linkedProject.slug)}...`).start()
225+
// #region Database migrations
226+
const remoteMigrationsSpinner = ora(`Retrieving database migrations on ${deployEnvColored} for ${colors.blueBright(linkedProject.slug)}...`).start()
227227

228228
await createMigrationsTable({ env: deployEnv })
229229

230230
const remoteMigrations = await fetchRemoteMigrations({ env: deployEnv }).catch((error) => {
231-
remoteMigrationsSpinner.fail(`Could not retrieve migrations on ${deployEnvColored} for ${colors.blueBright(linkedProject.slug)}.`)
231+
remoteMigrationsSpinner.fail(`Could not retrieve database migrations on ${deployEnvColored} for ${colors.blueBright(linkedProject.slug)}.`)
232232
consola.error(error.message)
233233
process.exit(1)
234234
})
235-
remoteMigrationsSpinner.succeed(`Found ${remoteMigrations.length} migration${remoteMigrations.length === 1 ? '' : 's'} on ${colors.blueBright(linkedProject.slug)}`)
235+
remoteMigrationsSpinner.succeed(`Found ${remoteMigrations.length} database migration${remoteMigrations.length === 1 ? '' : 's'} on ${colors.blueBright(linkedProject.slug)}`)
236236

237237
const localMigrations = fileKeys
238238
.filter(fileKey => {
@@ -246,10 +246,10 @@ export default defineCommand({
246246
.replace('.sql', '')
247247
})
248248
const pendingMigrations = localMigrations.filter(localName => !remoteMigrations.find(({ name }) => name === localName))
249-
if (!pendingMigrations.length) consola.info('No pending migrations to apply.')
249+
if (!pendingMigrations.length) consola.info('No pending database migrations to apply.')
250250

251251
for (const migration of pendingMigrations) {
252-
const migrationSpinner = ora(`Applying migration ${colors.blueBright(migration)}...`).start()
252+
const migrationSpinner = ora(`Applying database migration ${colors.blueBright(migration)}...`).start()
253253

254254
let query = await storage.getItem(`database/migrations/${migration}.sql`)
255255

@@ -261,14 +261,41 @@ export default defineCommand({
261261
try {
262262
await queryDatabase({ env: deployEnv, query })
263263
} catch (error) {
264-
migrationSpinner.fail(`Failed to apply migration ${colors.blueBright(migration)}.`)
264+
migrationSpinner.fail(`Failed to apply database migration ${colors.blueBright(migration)}.`)
265265

266266
if (error) consola.error(error.response?._data?.message || error.message)
267267
break
268268
}
269269

270-
migrationSpinner.succeed(`Applied migration ${colors.blueBright(migration)}.`)
270+
migrationSpinner.succeed(`Applied database migration ${colors.blueBright(migration)}.`)
271271
}
272+
// #endregion
273+
// #region Database queries
274+
const localQueries = fileKeys
275+
.filter(fileKey => fileKey.startsWith('database:queries:') && fileKey.endsWith('.sql'))
276+
.map(fileKey => fileKey.replace('database:queries:', '').replace('.sql', ''))
277+
278+
279+
if (localQueries.length) {
280+
const querySpinner = ora(`Applying ${colors.blueBright(formatNumber(localQueries.length))} database queries...`).start()
281+
for (const queryName of localQueries) {
282+
const query = await storage.getItem(`database/queries/${queryName}.sql`)
283+
284+
try {
285+
await queryDatabase({ env: deployEnv, query })
286+
} catch (error) {
287+
querySpinner.fail(`Failed to apply database query ${colors.blueBright(queryName)}.`)
288+
289+
if (error) consola.error(error.response?._data?.message || error.message)
290+
break
291+
}
292+
293+
}
294+
querySpinner.succeed(`Applied ${colors.blueBright(formatNumber(localQueries.length))} database queries.`)
295+
} else {
296+
consola.info('No pending database queries to apply.')
297+
}
298+
// #endregion
272299
}
273300

274301
// Check DNS & ready url for first deployment

src/commands/preview.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export default defineCommand({
4242
fileSideEffects.push(devVarsPath)
4343
}
4444

45-
const wrangler = generateWrangler(hubConfig, nitroConfig)
45+
const wrangler = generateWrangler(hubConfig, { preset: nitroConfig.preset })
4646
const wranglerPath = join(distDir, 'wrangler.toml')
4747
consola.info(`Generating \`${relative(process.cwd(), wranglerPath)}\`...`)
4848
fileSideEffects.push(wranglerPath)

src/internal.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
export { getStorage, getPathsToDeploy, getPublicFiles, uploadAssetsToCloudflare, isMetaPath, getFile, isServerPath } from './utils/deploy.mjs';
2+
export { CreateDatabaseMigrationsTableQuery, ListDatabaseMigrationsQuery } from './utils/database.mjs';
3+
export { generateWrangler } from './utils/wrangler.mjs';

src/utils/database.mjs

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { consola } from 'consola'
22
import { join } from 'pathe'
3+
import { existsSync } from 'node:fs'
34
import { createStorage } from 'unstorage'
45
import fsDriver from 'unstorage/drivers/fs'
56
import { $api } from './data.mjs'
67
import { $fetch } from 'ofetch'
78

8-
99
export async function queryDatabase({ env, url, token, query, params }) {
1010
if (url) {
1111
return queryRemoteDatabase({ url, token, query, params })
@@ -40,20 +40,26 @@ export async function queryRemoteDatabase({ url, token, query, params }) {
4040
})
4141
}
4242

43+
let _migrationsDir
44+
export function getMigrationsDir() {
45+
if (!_migrationsDir) {
46+
const cwd = process.cwd()
47+
_migrationsDir = existsSync(join(cwd, '.data/hub/database/migrations')) ? join(cwd, '.data/hub/database/migrations') : join(cwd, 'server/database/migrations')
48+
}
49+
return _migrationsDir
50+
}
4351

4452
/**
4553
* @type {import('unstorage').Storage}
4654
*/
4755
let _storage
4856
export function useMigrationsStorage() {
4957
if (!_storage) {
50-
const cwd = process.cwd()
51-
const migrationsDir = join(cwd, 'server/database/migrations')
5258
_storage = createStorage({
5359
driver: fsDriver({
54-
base: migrationsDir,
60+
base: getMigrationsDir(),
5561
ignore: ['.DS_Store']
56-
}),
62+
})
5763
})
5864
}
5965
return _storage
@@ -72,24 +78,25 @@ export async function getNextMigrationNumber() {
7278
.sort((a, b) => a - b)
7379
.pop() ?? 0
7480

75-
return (lastSequentialMigrationNumber + 1).toString().padStart(4, '0')
81+
return (lastSequentialMigrationNumber + 1).toString().padStart(4, '0')
7682
}
7783

78-
const CreateMigrationsTableQuery = `CREATE TABLE IF NOT EXISTS _hub_migrations (
84+
export const CreateDatabaseMigrationsTableQuery = `CREATE TABLE IF NOT EXISTS _hub_migrations (
7985
id INTEGER PRIMARY KEY AUTOINCREMENT,
8086
name TEXT UNIQUE,
8187
applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
8288
);`
89+
export const ListDatabaseMigrationsQuery = 'select "id", "name", "applied_at" from "_hub_migrations" order by "_hub_migrations"."id"'
90+
8391
export async function createMigrationsTable({ env, url, token }) {
84-
await queryDatabase({ env, url, token, query: CreateMigrationsTableQuery })
92+
await queryDatabase({ env, url, token, query: CreateDatabaseMigrationsTableQuery })
8593
}
8694

8795
/**
8896
* @type {Promise<Array<{ id: number, name: string, applied_at: string }>>}
8997
*/
9098
export async function fetchRemoteMigrations({ env, url, token }) {
91-
const query = 'select "id", "name", "applied_at" from "_hub_migrations" order by "_hub_migrations"."id"'
92-
const res = await queryDatabase({ env, url, token, query }).catch((error) => {
99+
const res = await queryDatabase({ env, url, token, query: ListDatabaseMigrationsQuery }).catch((error) => {
93100
if (error.response?._data?.message.includes('no such table')) {
94101
return []
95102
}

src/utils/wrangler.mjs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@ import { stringifyTOML } from 'confbox'
22

33
// Taken from https://github.com/nuxt-hub/core/blob/main/src/utils/wrangler.ts
44
// With some modifications to fit the needs of this project
5-
export function generateWrangler(hub, nitro) {
5+
export function generateWrangler(hub, { preset } = {}) {
66
const wrangler = {}
77

88
// Workers specific settings
9-
if (nitro.preset === 'cloudflare-module' || nitro.preset === 'cloudflare-durable') {
9+
if (preset === 'cloudflare-module' || preset === 'cloudflare-durable') {
1010
wrangler.name = 'nuxthub-local-preview'
1111
wrangler.main = './server/index.mjs'
1212
wrangler.assets = { directory: './public/', binding: 'ASSETS' }
13-
if (nitro.preset === 'cloudflare-durable') {
13+
if (preset === 'cloudflare-durable') {
1414
wrangler.durable_objects ||= {}
1515
wrangler.durable_objects.bindings = [{ name: '$DurableObject', class_name: '$DurableObject' }]
1616
wrangler.migrations = [{ tag: 'v1', new_classes: ['$DurableObject'] }]

0 commit comments

Comments
 (0)