Skip to content

Commit 996ef46

Browse files
committed
feat: implement migration API with CRUD operations and update migration data structure
1 parent 887b0b7 commit 996ef46

4 files changed

Lines changed: 188 additions & 73 deletions

File tree

client/components/ModuleMigrations.vue

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,11 @@ const props = defineProps({
2121
})
2222
2323
interface Migration {
24-
id: string
2524
name: string
26-
created_at: string
25+
module: string
26+
filename: string
2727
status: string
28+
executedAt: string | null
2829
}
2930
3031
const migrations = ref<Migration[]>([])
@@ -43,12 +44,12 @@ const columns = defineColumns<Migration>([
4344
4445
async function load() {
4546
loading.value = true
46-
const data = await $fetch(`/api/modules/${props.module.id}/migrations`)
47-
47+
const data = await $fetch('/api/migrations', { query: { module: props.module.id } })
48+
4849
if (Array.isArray(data)) {
4950
migrations.value = data as Migration[]
5051
}
51-
52+
5253
if (!Array.isArray(data)) {
5354
migrations.value = []
5455
}
@@ -63,7 +64,12 @@ const operation = ref<'migrate' | 'rollback' | 'fresh'>()
6364
async function migrate() {
6465
operation.value = 'migrate'
6566
66-
const [error] = await tryCatch(() => $fetch(`/api/modules/${props.module.id}/migrate`, { method: 'POST' }))
67+
const [error] = await $fetch.try('/api/migrations/migrate', {
68+
method: 'POST',
69+
data: {
70+
module: props.module.id
71+
}
72+
})
6773
6874
if (error) {
6975
operation.value = undefined
@@ -81,7 +87,12 @@ async function migrate() {
8187
async function rollback() {
8288
operation.value = 'rollback'
8389
84-
const [error] = await tryCatch(() => $fetch(`/api/modules/${props.module.id}/rollback`, { method: 'POST' }))
90+
const [error] = await $fetch.try('/api/migrations/rollback', {
91+
method: 'POST',
92+
data: {
93+
module: props.module.id
94+
}
95+
})
8596
8697
if (error) {
8798
operation.value = undefined
@@ -99,7 +110,12 @@ async function rollback() {
99110
async function fresh() {
100111
operation.value = 'fresh'
101112
102-
const [error] = await tryCatch(() => $fetch(`/api/modules/${props.module.id}/fresh`, { method: 'POST' }))
113+
const [error] = await $fetch.try('/api/migrations/fresh', {
114+
method: 'POST',
115+
data: {
116+
module: props.module.id
117+
}
118+
})
103119
104120
if (error) {
105121
operation.value = undefined

server/routes/migrations.route.ts

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
import path from 'path'
2+
import migrator from '#server/facades/migrator.facade.ts'
3+
import root from '#server/facades/router.facade.ts'
4+
import authMiddleware from '#server/middlewares/auth.middleware.ts'
5+
import BaseException from '#server/exceptions/base.ts'
6+
import validator from '#shared/services/validator.service.ts'
7+
import { basePath } from '#server/utils/paths.ts'
8+
9+
const router = root.use(authMiddleware)
10+
.prefix('/api/migrations')
11+
.group()
12+
13+
router.get('/', async ({ acl, query }) => {
14+
acl.authorize('list', 'Migration')
15+
16+
const payload = validator.validate(query, v => v.object({
17+
module: v.optional(v.string()),
18+
root: v.optional(v.pipe(v.string(), v.transform(v => v === 'true'))),
19+
}))
20+
21+
const migrations = await migrator.list({
22+
module: payload.module,
23+
root: payload.root,
24+
})
25+
26+
return migrations.map(m => ({
27+
name: m.name,
28+
module: m.module,
29+
filename: path.relative(basePath(), m.filePath),
30+
status: m.executedAt ? 'executed' : 'pending',
31+
executedAt: m.executedAt,
32+
}))
33+
})
34+
35+
router.post('/migrate', async ({ acl, body }) => {
36+
acl.authorize('migrate', 'Migration')
37+
38+
const payload = validator.validate(body, v => v.object({
39+
module: v.optional(v.string()),
40+
root: v.optional(v.boolean()),
41+
steps: v.optional(v.pipe(v.number(), v.integer())),
42+
}))
43+
44+
const results = await migrator.migrate(payload)
45+
46+
const failed = results.find(r => r.result === 'failed')
47+
48+
if (failed) {
49+
throw new BaseException(failed.errorMessage ?? 'Migration failed')
50+
}
51+
52+
return results
53+
})
54+
55+
router.post('/rollback', async ({ acl, body }) => {
56+
acl.authorize('rollback', 'Migration')
57+
58+
const payload = validator.validate(body, v => v.object({
59+
module: v.optional(v.string()),
60+
root: v.optional(v.boolean()),
61+
steps: v.optional(v.pipe(v.number(), v.integer())),
62+
}))
63+
64+
const results = await migrator.rollback(payload)
65+
66+
const failed = results.find(r => r.result === 'failed')
67+
68+
if (failed) {
69+
throw new BaseException(failed.errorMessage ?? 'Rollback failed')
70+
}
71+
72+
return results
73+
})
74+
75+
router.post('/up', async ({ acl, body }) => {
76+
acl.authorize('migrate', 'Migration')
77+
78+
const payload = validator.validate(body, v => v.object({
79+
steps: v.optional(v.pipe(v.number(), v.integer()), 1),
80+
module: v.optional(v.string()),
81+
root: v.optional(v.boolean()),
82+
}))
83+
84+
const { steps, ...filters } = payload
85+
86+
const results = await migrator.up(steps, filters)
87+
88+
const failed = results.find(r => r.result === 'failed')
89+
90+
if (failed) {
91+
throw new BaseException(failed.errorMessage ?? 'Migration failed')
92+
}
93+
94+
return results
95+
})
96+
97+
router.post('/down', async ({ acl, body }) => {
98+
acl.authorize('rollback', 'Migration')
99+
100+
const payload = validator.validate(body, v => v.object({
101+
steps: v.optional(v.pipe(v.number(), v.integer()), 1),
102+
module: v.optional(v.string()),
103+
root: v.optional(v.boolean()),
104+
}))
105+
106+
const { steps, ...filters } = payload
107+
108+
const results = await migrator.down(steps, filters)
109+
110+
const failed = results.find(r => r.result === 'failed')
111+
112+
if (failed) {
113+
throw new BaseException(failed.errorMessage ?? 'Rollback failed')
114+
}
115+
116+
return results
117+
})
118+
119+
router.post('/fresh', async ({ acl, body }) => {
120+
acl.authorize('fresh', 'Migration')
121+
122+
const payload = validator.validate(body, v => v.object({
123+
module: v.optional(v.string()),
124+
root: v.optional(v.boolean()),
125+
steps: v.optional(v.pipe(v.number(), v.integer())),
126+
}))
127+
128+
const results = await migrator.fresh(payload)
129+
130+
const failed = results.find(r => r.result === 'failed')
131+
132+
if (failed) {
133+
throw new BaseException(failed.errorMessage ?? 'Fresh migration failed')
134+
}
135+
136+
return results
137+
})
138+
139+
router.post('/:name/migrate', async ({ params, acl }) => {
140+
acl.authorize('migrate', 'Migration')
141+
142+
const result = await migrator.migrateFile(params.name)
143+
144+
if (result.result === 'failed') {
145+
throw new BaseException(result.errorMessage ?? 'Migration failed')
146+
}
147+
148+
return result
149+
})
150+
151+
router.post('/:name/rollback', async ({ params, acl }) => {
152+
acl.authorize('rollback', 'Migration')
153+
154+
const result = await migrator.rollbackFile(params.name)
155+
156+
if (result.result === 'failed') {
157+
throw new BaseException(result.errorMessage ?? 'Rollback failed')
158+
}
159+
160+
return result
161+
})

server/routes/modules.route.ts

Lines changed: 1 addition & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import migrator from '#server/facades/migrator.facade.ts'
55
import root from '#server/facades/router.facade.ts'
66
import authMiddleware from '#server/middlewares/auth.middleware.ts'
77
import modules from '#server/facades/modules.facade.ts'
8-
import { basePath, tmpPath } from '#server/utils/paths.ts'
8+
import { tmpPath } from '#server/utils/paths.ts'
99
import { tryCatch } from '#shared/utils/tryCatch.ts'
1010
import BaseException from '#server/exceptions/base.ts'
1111
import validator from '#shared/services/validator.service.ts'
@@ -49,22 +49,6 @@ router.get('/:id', async ({ params, acl, query }) => {
4949
})
5050

5151

52-
router.get('/:id/migrations', async ({ params, acl }) => {
53-
acl.authorize('read', 'Module')
54-
55-
const all = await migrator.list()
56-
57-
const migrations = all
58-
.filter(m => m.module === params.id)
59-
.map(m => ({
60-
name: m.name,
61-
filename: path.relative(basePath(), m.filePath),
62-
status: m.executedAt ? 'executed' : 'pending',
63-
}))
64-
65-
return migrations
66-
})
67-
6852
router.post('/:id/toggle', async ({ params, acl }) => {
6953
acl.authorize('update', 'Module')
7054

@@ -73,53 +57,7 @@ router.post('/:id/toggle', async ({ params, acl }) => {
7357
await server.reload()
7458
})
7559

76-
router.post('/:id/migrate', async ({ params, acl }) => {
77-
acl.authorize('update', 'Module')
78-
79-
const items = await migrator.migrate({
80-
module: params.id,
81-
})
82-
83-
const itemWithError = items.find(i => i.error)
84-
85-
if (itemWithError?.error) {
86-
throw new BaseException(itemWithError.error)
87-
}
88-
89-
return { success: true, }
90-
})
91-
92-
router.post('/:id/rollback', async ({ params, acl }) => {
93-
acl.authorize('update', 'Module')
9460

95-
const items = await migrator.rollback({
96-
module: params.id,
97-
})
98-
99-
const itemWithError = items.find(i => i.error)
100-
101-
if (itemWithError?.error) {
102-
throw new BaseException(itemWithError.error)
103-
}
104-
105-
return { success: true, }
106-
})
107-
108-
router.post('/:id/fresh', async ({ params, acl }) => {
109-
acl.authorize('update', 'Module')
110-
111-
const items = await migrator.fresh({
112-
module: params.id,
113-
})
114-
115-
const itemWithError = items.find(i => i.error)
116-
117-
if (itemWithError?.error) {
118-
throw new BaseException(itemWithError.error)
119-
}
120-
121-
return items
122-
})
12361

12462
router.post('/:id/install-dependencies', async ({ params, acl }) => {
12563
acl.authorize('update', 'Module')

server/services/migrator.service.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ export default class MigratorService {
7979
const mods = await modules.list()
8080

8181
for (const mod of mods) {
82-
const migrationPath = basePath('modules', mod.name, 'server', 'migrations')
82+
const migrationPath = mod.makePath('server', 'migrations')
8383

8484
if (!fs.existsSync(migrationPath)) continue
8585

@@ -100,7 +100,7 @@ export default class MigratorService {
100100

101101
allMigrations.push({
102102
name: filename,
103-
module: mod.name,
103+
module: mod.id,
104104
filePath: fullPath,
105105
executedAt: null,
106106
up: migration.up,

0 commit comments

Comments
 (0)