feat: native PB migrations

This commit is contained in:
Ben Allfree 2023-06-07 17:06:34 -07:00
parent 1a23f5b2fd
commit 17ab6c5fa7
15 changed files with 542 additions and 481 deletions

View File

@ -3,6 +3,7 @@ PUBLIC_APP_DOMAIN=pockethost.test
PUBLIC_APP_DB=pockethost-central
DAEMON_PB_BIN_DIR=`pwd`/packages/pocketbase/dist
DAEMON_PB_DATA_DIR=`pwd`/.data
DAEMON_PB_MIGRATIONS_DIR=`pwd`/packages/daemon/migrations
DAEMON_PB_USERNAME=admin@pockethost.test
DAEMON_PB_PASSWORD=admin@pockethost.test
DAEMON_PB_PORT=8090

View File

@ -21,8 +21,6 @@
"pm2:proxy": "cd packages/proxy && yarn pm2",
"pm2:www": "cd packages/pockethost.io && yarn pm2",
"pm2:daemon": "cd packages/daemon && yarn pm2",
"migrate": "yarn migrate:daemon",
"migrate:daemon": "cd packages/daemon && yarn migrate",
"postinstall": "patch-package"
},
"workspaces": {
@ -50,4 +48,4 @@
"dependencies": {
"postinstall-postinstall": "^2.1.0"
}
}
}

View File

@ -0,0 +1,492 @@
migrate((db) => {
const snapshot = [
{
"id": "etae8tuiaxl6xfv",
"created": "2022-10-20 08:51:44.195Z",
"updated": "2023-06-07 22:41:11.725Z",
"name": "instances",
"type": "base",
"system": false,
"schema": [
{
"system": false,
"id": "qdtuuld1",
"name": "subdomain",
"type": "text",
"required": true,
"unique": true,
"options": {
"min": null,
"max": 50,
"pattern": "^[a-z][\\-a-z]+$"
}
},
{
"system": false,
"id": "rbj14krn",
"name": "uid",
"type": "relation",
"required": true,
"unique": false,
"options": {
"collectionId": "systemprofiles0",
"cascadeDelete": false,
"minSelect": null,
"maxSelect": 1,
"displayFields": null
}
},
{
"system": false,
"id": "c2y74d7h",
"name": "status",
"type": "text",
"required": true,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "yxby5r6b",
"name": "platform",
"type": "text",
"required": true,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "4ydffkv3",
"name": "version",
"type": "text",
"required": true,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "1arlklqq",
"name": "secondsThisMonth",
"type": "number",
"required": false,
"unique": false,
"options": {
"min": null,
"max": null
}
},
{
"system": false,
"id": "66vjgzcg",
"name": "isBackupAllowed",
"type": "bool",
"required": false,
"unique": false,
"options": {}
},
{
"system": false,
"id": "qew2o2d6",
"name": "currentWorkerBundleId",
"type": "text",
"required": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "3yu1db4p",
"name": "secrets",
"type": "json",
"required": false,
"unique": false,
"options": {}
}
],
"indexes": [
"CREATE UNIQUE INDEX \"idx_unique_qdtuuld1\" on \"instances\" (\"subdomain\")"
],
"listRule": "uid=@request.auth.id",
"viewRule": "uid = @request.auth.id",
"createRule": null,
"updateRule": null,
"deleteRule": null,
"options": {}
},
{
"id": "systemprofiles0",
"created": "2022-10-31 21:31:52.175Z",
"updated": "2023-06-07 22:41:11.723Z",
"name": "users",
"type": "auth",
"system": false,
"schema": [
{
"system": false,
"id": "pbfieldname",
"name": "name",
"type": "text",
"required": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "pbfieldavatar",
"name": "avatar",
"type": "file",
"required": false,
"unique": false,
"options": {
"maxSelect": 1,
"maxSize": 5242880,
"mimeTypes": [
"image/jpg",
"image/jpeg",
"image/png",
"image/svg+xml",
"image/gif"
],
"thumbs": null,
"protected": false
}
}
],
"indexes": [],
"listRule": "id = @request.auth.id",
"viewRule": "id = @request.auth.id",
"createRule": "",
"updateRule": "id = @request.auth.id",
"deleteRule": null,
"options": {
"allowEmailAuth": true,
"allowOAuth2Auth": true,
"allowUsernameAuth": false,
"exceptEmailDomains": null,
"manageRule": null,
"minPasswordLength": 8,
"onlyEmailDomains": null,
"requireEmail": true
}
},
{
"id": "aiw8te7y7atklwn",
"created": "2022-11-04 13:54:23.745Z",
"updated": "2023-06-07 22:41:11.723Z",
"name": "invocations",
"type": "base",
"system": false,
"schema": [
{
"system": false,
"id": "st9ydrbo",
"name": "instanceId",
"type": "relation",
"required": true,
"unique": false,
"options": {
"collectionId": "etae8tuiaxl6xfv",
"cascadeDelete": false,
"minSelect": null,
"maxSelect": 1,
"displayFields": null
}
},
{
"system": false,
"id": "av4mpuyh",
"name": "startedAt",
"type": "date",
"required": true,
"unique": false,
"options": {
"min": "",
"max": ""
}
},
{
"system": false,
"id": "fnwatixg",
"name": "endedAt",
"type": "date",
"required": false,
"unique": false,
"options": {
"min": "",
"max": ""
}
},
{
"system": false,
"id": "awjozhbn",
"name": "pid",
"type": "number",
"required": false,
"unique": false,
"options": {
"min": null,
"max": null
}
},
{
"system": false,
"id": "vdkfqege",
"name": "totalSeconds",
"type": "number",
"required": false,
"unique": false,
"options": {
"min": null,
"max": null
}
}
],
"indexes": [],
"listRule": null,
"viewRule": null,
"createRule": null,
"updateRule": null,
"deleteRule": null,
"options": {}
},
{
"id": "v7s41iokt1vizxd",
"created": "2022-11-06 17:23:25.947Z",
"updated": "2023-06-07 22:41:11.723Z",
"name": "rpc",
"type": "base",
"system": false,
"schema": [
{
"system": false,
"id": "yv38czcf",
"name": "userId",
"type": "relation",
"required": true,
"unique": false,
"options": {
"collectionId": "systemprofiles0",
"cascadeDelete": false,
"minSelect": null,
"maxSelect": 1,
"displayFields": null
}
},
{
"system": false,
"id": "tgvaxwfv",
"name": "payload",
"type": "json",
"required": true,
"unique": false,
"options": {}
},
{
"system": false,
"id": "zede8pci",
"name": "status",
"type": "text",
"required": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "nd7cwqmn",
"name": "result",
"type": "json",
"required": false,
"unique": false,
"options": {}
},
{
"system": false,
"id": "2hlrcx5j",
"name": "cmd",
"type": "text",
"required": true,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
}
],
"indexes": [],
"listRule": "userId = @request.auth.id",
"viewRule": "userId = @request.auth.id",
"createRule": "userId = @request.auth.id && status='' && result='' && cmd ?= @collection.rpc_cmds.name",
"updateRule": null,
"deleteRule": null,
"options": {}
},
{
"id": "72clb6v41bzsay9",
"created": "2022-11-09 15:23:20.313Z",
"updated": "2023-06-07 22:41:11.723Z",
"name": "backups",
"type": "base",
"system": false,
"schema": [
{
"system": false,
"id": "someqtjw",
"name": "message",
"type": "text",
"required": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "jk4zwiaj",
"name": "instanceId",
"type": "relation",
"required": true,
"unique": false,
"options": {
"collectionId": "etae8tuiaxl6xfv",
"cascadeDelete": false,
"minSelect": null,
"maxSelect": 1,
"displayFields": null
}
},
{
"system": false,
"id": "wsy3l5gm",
"name": "status",
"type": "text",
"required": true,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "gmkrc5d9",
"name": "bytes",
"type": "number",
"required": false,
"unique": false,
"options": {
"min": null,
"max": null
}
},
{
"system": false,
"id": "4lmammjz",
"name": "platform",
"type": "text",
"required": true,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "fheqxmbj",
"name": "version",
"type": "text",
"required": true,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"system": false,
"id": "cinbmdwe",
"name": "progress",
"type": "json",
"required": false,
"unique": false,
"options": {}
}
],
"indexes": [],
"listRule": "@request.auth.id = instanceId.uid",
"viewRule": null,
"createRule": null,
"updateRule": null,
"deleteRule": null,
"options": {}
},
{
"id": "enp8mrv5ewtrltj",
"created": "2023-01-06 10:21:51.659Z",
"updated": "2023-06-07 22:41:11.725Z",
"name": "rpc_cmds",
"type": "base",
"system": false,
"schema": [
{
"system": false,
"id": "jbostfhp",
"name": "name",
"type": "text",
"required": true,
"unique": true,
"options": {
"min": null,
"max": null,
"pattern": ""
}
}
],
"indexes": [
"CREATE UNIQUE INDEX \"idx_unique_jbostfhp\" on \"rpc_cmds\" (\"name\")"
],
"listRule": null,
"viewRule": null,
"createRule": null,
"updateRule": null,
"deleteRule": null,
"options": {}
}
];
const collections = snapshot.map((item) => new Collection(item));
return Dao(db).importCollections(collections, true, null);
}, (db) => {
return null;
})

View File

@ -21,6 +21,19 @@ export const DAEMON_PB_PASSWORD = (() => {
})()
export const DAEMON_PB_PORT_BASE = envi('DAEMON_PB_PORT_BASE', 8090)
export const DAEMON_PB_IDLE_TTL = envi('DAEMON_PB_IDLE_TTL', 5000)
export const DAEMON_PB_MIGRATIONS_DIR = (() => {
const v = env('DAEMON_PB_MIGRATIONS_DIR')
if (!v) {
throw new Error(
`DAEMON_PB_MIGRATIONS_DIR (${v}) environment variable must be specified`
)
}
if (!existsSync(v)) {
throw new Error(`DAEMON_PB_MIGRATIONS_DIR (${v}) path must exist`)
}
return v
})()
export const DAEMON_PB_DATA_DIR = (() => {
const v = env('DAEMON_PB_DATA_DIR')
if (!v) {
@ -68,6 +81,7 @@ console.log({
PUBLIC_APP_DB,
DAEMON_PB_USERNAME,
DAEMON_PB_PASSWORD,
DAEMON_PB_MIGRATIONS_DIR,
DAEMON_PB_SEMVER,
DENO_PATH,
PH_FTP_PASV_IP,

View File

@ -1,49 +0,0 @@
import { DEBUG, PH_BIN_CACHE, PUBLIC_APP_DB } from '$constants'
import { pocketbase } from '$services'
import { InstanceStatus, logger, safeCatch } from '@pockethost/common'
import { schema } from './schema'
import { withInstance } from './withInstance'
;(async () => {
const { info } = logger({ debug: DEBUG }).create('migrate')
const pbService = await pocketbase({
cachePath: PH_BIN_CACHE,
checkIntervalMs: 5 * 60 * 1000,
})
safeCatch(`root`, async () => {
// await backupInstance(
// PUBLIC_PB_SUBDOMAIN,
// `${+new Date()}`,
// async (progress) => {
// dbg(progress)
// }
// )
info(`Upgrading`)
const upgradeProcess = await pbService.spawn({
command: 'upgrade',
slug: PUBLIC_APP_DB,
})
await upgradeProcess.exited
await withInstance(async (client) => {
await client.applySchema(schema)
await client.updateInstances((instance) => {
const version = (() => {
if (instance.platform === 'ermine') return '~0.7.0'
if (instance.platform === 'lollipop') return '~0.10.0'
return pbService.getLatestVersion()
})()
console.log(`Updating instance ${instance.id} to ${version}`)
return {
status: InstanceStatus.Idle,
version,
}
})
})
console.log(`All instances updated`)
})()
pbService.shutdown()
})()

View File

@ -1,357 +0,0 @@
import { Collection, SchemaField } from 'pocketbase'
export type Collection_Serialized = Omit<Partial<Collection>, 'schema'> & {
schema: Array<Partial<SchemaField>>
}
export const schema: Collection_Serialized[] = [
{
id: 'etae8tuiaxl6xfv',
name: 'instances',
type: 'base',
system: false,
schema: [
{
id: 'qdtuuld1',
name: 'subdomain',
type: 'text',
system: false,
required: true,
unique: true,
options: {
min: null,
max: 50,
pattern: '^[a-z][\\-a-z]+$',
},
},
{
id: 'rbj14krn',
name: 'uid',
type: 'relation',
system: false,
required: true,
unique: false,
options: {
maxSelect: 1,
collectionId: '_pb_users_auth_',
cascadeDelete: false,
},
},
{
id: 'c2y74d7h',
name: 'status',
type: 'text',
system: false,
required: true,
unique: false,
options: {
min: null,
max: null,
pattern: '',
},
},
{
id: 'yxby5r6b',
name: 'platform',
type: 'text',
system: false,
required: true,
unique: false,
options: {
min: null,
max: null,
pattern: '',
},
},
{
id: '4ydffkv3',
name: 'version',
type: 'text',
system: false,
required: true,
unique: false,
options: {
min: null,
max: null,
pattern: '',
},
},
{
id: '1arlklqq',
name: 'secondsThisMonth',
type: 'number',
system: false,
required: false,
unique: false,
options: {
min: null,
max: null,
},
},
{
id: '66vjgzcg',
name: 'isBackupAllowed',
type: 'bool',
system: false,
required: false,
unique: false,
options: {},
},
],
listRule: 'uid=@request.auth.id',
viewRule: 'uid = @request.auth.id',
createRule: null,
updateRule: null,
deleteRule: null,
options: {},
},
{
id: 'aiw8te7y7atklwn',
name: 'invocations',
type: 'base',
system: false,
schema: [
{
id: 'st9ydrbo',
name: 'instanceId',
type: 'relation',
system: false,
required: true,
unique: false,
options: {
maxSelect: 1,
collectionId: 'etae8tuiaxl6xfv',
cascadeDelete: false,
},
},
{
id: 'av4mpuyh',
name: 'startedAt',
type: 'date',
system: false,
required: true,
unique: false,
options: {
min: '',
max: '',
},
},
{
id: 'fnwatixg',
name: 'endedAt',
type: 'date',
system: false,
required: false,
unique: false,
options: {
min: '',
max: '',
},
},
{
id: 'awjozhbn',
name: 'pid',
type: 'number',
system: false,
required: false,
unique: false,
options: {
min: null,
max: null,
},
},
{
id: 'vdkfqege',
name: 'totalSeconds',
type: 'number',
system: false,
required: false,
unique: false,
options: {
min: null,
max: null,
},
},
],
listRule: null,
viewRule: null,
createRule: null,
updateRule: null,
deleteRule: null,
options: {},
},
{
id: 'v7s41iokt1vizxd',
name: 'rpc',
type: 'base',
system: false,
schema: [
{
id: 'yv38czcf',
name: 'userId',
type: 'relation',
system: false,
required: true,
unique: false,
options: {
maxSelect: 1,
collectionId: '_pb_users_auth_',
cascadeDelete: false,
},
},
{
id: 'tgvaxwfv',
name: 'payload',
type: 'json',
system: false,
required: true,
unique: false,
options: {},
},
{
id: 'zede8pci',
name: 'status',
type: 'text',
system: false,
required: false,
unique: false,
options: {
min: null,
max: null,
pattern: '',
},
},
{
id: 'nd7cwqmn',
name: 'result',
type: 'json',
system: false,
required: false,
unique: false,
options: {},
},
{
id: '2hlrcx5j',
name: 'cmd',
type: 'text',
system: false,
required: true,
unique: false,
options: {
min: null,
max: null,
pattern: '',
},
},
],
listRule: 'userId = @request.auth.id',
viewRule: 'userId = @request.auth.id',
createRule:
"userId = @request.auth.id && status='' && (cmd='backup-instance' || cmd='restore-instance' || cmd='create-instance') && payload!='' && result=''",
updateRule: null,
deleteRule: null,
options: {},
},
{
id: '72clb6v41bzsay9',
name: 'backups',
type: 'base',
system: false,
schema: [
{
id: 'someqtjw',
name: 'message',
type: 'text',
system: false,
required: false,
unique: false,
options: {
min: null,
max: null,
pattern: '',
},
},
{
id: 'jk4zwiaj',
name: 'instanceId',
type: 'relation',
system: false,
required: true,
unique: false,
options: {
maxSelect: 1,
collectionId: 'etae8tuiaxl6xfv',
cascadeDelete: false,
},
},
{
id: 'wsy3l5gm',
name: 'status',
type: 'text',
system: false,
required: true,
unique: false,
options: {
min: null,
max: null,
pattern: '',
},
},
{
id: 'gmkrc5d9',
name: 'bytes',
type: 'number',
system: false,
required: false,
unique: false,
options: {
min: null,
max: null,
},
},
{
id: '4lmammjz',
name: 'platform',
type: 'text',
system: false,
required: true,
unique: false,
options: {
min: null,
max: null,
pattern: '',
},
},
{
id: 'fheqxmbj',
name: 'version',
type: 'text',
system: false,
required: true,
unique: false,
options: {
min: null,
max: null,
pattern: '',
},
},
{
id: 'cinbmdwe',
name: 'progress',
type: 'json',
system: false,
required: false,
unique: false,
options: {},
},
],
listRule: '@request.auth.id = instanceId.uid',
viewRule: null,
createRule: null,
updateRule: null,
deleteRule: null,
options: {},
},
]
console.log(JSON.stringify(schema))

View File

@ -1,45 +0,0 @@
import {
DAEMON_PB_PASSWORD,
DAEMON_PB_USERNAME,
PUBLIC_APP_DB,
PUBLIC_APP_DOMAIN,
PUBLIC_APP_PROTOCOL,
} from '$constants'
import { createPbClient, pocketbase, PocketbaseClientApi } from '$services'
import { logger, safeCatch } from '@pockethost/common'
export const withInstance = safeCatch(
`withInstance`,
async (cb: (client: PocketbaseClientApi) => Promise<void>) => {
const { info, error } = logger().create('withInstance')
// Add `platform` and `bin` required columns (migrate db json)
try {
const mainProcess = await (
await pocketbase()
).spawn({
command: 'serve',
slug: PUBLIC_APP_DB,
})
try {
const { url } = mainProcess
const client = createPbClient(url)
await client.adminAuthViaEmail(DAEMON_PB_USERNAME, DAEMON_PB_PASSWORD)
await cb(client)
} catch (e) {
error(
`***WARNING*** CANNOT AUTHENTICATE TO ${PUBLIC_APP_PROTOCOL}://${PUBLIC_APP_DB}.${PUBLIC_APP_DOMAIN}/_/`
)
error(`***WARNING*** LOG IN MANUALLY, ADJUST .env, AND RESTART DOCKER`)
process.exit(-1)
} finally {
info(`Exiting process`)
mainProcess.kill()
await mainProcess.exited
}
} catch (e) {
error(`${e}`)
}
}
)

View File

@ -50,8 +50,22 @@ global.EventSource = require('eventsource')
/**
* Launch central database
*/
{
info(`Migrating mothership`)
await (
await pbService.spawn({
command: 'migrate',
isMothership: true,
version: DAEMON_PB_SEMVER,
slug: PUBLIC_APP_DB,
})
).exited
info(`Migrating done`)
}
info(`Serving`)
const { url } = await pbService.spawn({
command: 'serve',
isMothership: true,
version: DAEMON_PB_SEMVER,
slug: PUBLIC_APP_DB,
})

View File

@ -1,4 +1,4 @@
import { DAEMON_PB_DATA_DIR } from '$constants'
import { DAEMON_PB_DATA_DIR, DAEMON_PB_MIGRATIONS_DIR } from '$constants'
import {
downloadAndExtract,
mkInternalAddress,
@ -22,12 +22,13 @@ import { join } from 'path'
import { maxSatisfying, rsort } from 'semver'
import { AsyncReturnType } from 'type-fest'
export type PocketbaseCommand = 'serve' | 'upgrade'
export type PocketbaseCommand = 'serve' | 'migrate'
export type SpawnConfig = {
command: PocketbaseCommand
slug: string
version?: string
port?: number
isMothership?: boolean
onUnexpectedStop?: (code: number | null) => void
}
export type PocketbaseServiceApi = AsyncReturnType<
@ -138,11 +139,14 @@ export const createPocketbaseService = async (
const _cfg: Required<SpawnConfig> = {
version: maxVersion,
port: await getPort(),
isMothership: false,
onUnexpectedStop: (code) => {
dbg(`Unexpected stop default handler. Exit code: ${code}`)
},
...cfg,
}
const { version, command, slug, port, onUnexpectedStop, isMothership } =
_cfg
const _version = version || maxVersion // If _version is blank, we use the max version available
const bin = (await getVersion(_version)).binPath
if (!existsSync(bin)) {
@ -157,6 +161,10 @@ export const createPocketbaseService = async (
`${DAEMON_PB_DATA_DIR}/${slug}/pb_data`,
`--publicDir`,
`${DAEMON_PB_DATA_DIR}/${slug}/pb_static`,
`--migrationsDir`,
isMothership
? DAEMON_PB_MIGRATIONS_DIR
: `${DAEMON_PB_DATA_DIR}/${slug}/pb_migrations`,
]
if (command === 'serve') {
args.push(`--http`)

View File

@ -1,12 +1,7 @@
import { DAEMON_PB_DATA_DIR, PUBLIC_APP_DB } from '$constants'
import { logger, safeCatch } from '@pockethost/common'
import { Knex } from 'knex'
import {
Collection,
default as PocketBase,
default as pocketbaseEs,
} from 'pocketbase'
import { Collection_Serialized } from '../../migrate/schema'
import { default as PocketBase, default as pocketbaseEs } from 'pocketbase'
import { createBackupMixin } from './BackupMixin'
import { createInstanceMixin } from './InstanceMIxin'
import { createInvocationMixin } from './InvocationMixin'
@ -45,13 +40,6 @@ export const createPbClient = (url: string) => {
})
)
const applySchema = safeCatch(
`applySchema`,
async (collections: Collection_Serialized[]) => {
await client.collections.import(collections as Collection[], false)
}
)
const context: MixinContext = { client, rawDb }
const rpcApi = createRpcHelper(context)
const instanceApi = createInstanceMixin(context)
@ -64,7 +52,6 @@ export const createPbClient = (url: string) => {
knex: rawDb,
createFirstAdmin,
adminAuthViaEmail,
applySchema,
...rpcApi,
...instanceApi,
...invocationApi,

View File

@ -5,7 +5,6 @@ import {
PUBLIC_APP_DOMAIN,
PUBLIC_APP_PROTOCOL,
} from '$constants'
import { schema } from '$src/migrate/schema'
import { logger, mkSingleton } from '@pockethost/common'
import { createPbClient } from './PbClient'
@ -31,15 +30,6 @@ export const clientService = mkSingleton(async (url: string) => {
}
}
try {
dbg(`Applying schema`)
await client.applySchema(schema)
dbg(`Schema applied`)
} catch (e) {
error(`Failed to apply base migration schema`)
process.exit(-1)
}
return {
client,
shutdown() {

View File

@ -10,8 +10,8 @@ import { basename, resolve } from 'path'
import { chdir, cwd } from 'process'
import { Database } from 'sqlite3'
import tmp from 'tmp'
import { pexec } from '../migrate/pexec'
import { ensureDirExists } from './ensureDirExists'
import { pexec } from './pexec'
export type BackupProgress = {
current: number

View File

@ -128,6 +128,11 @@ By default, PocketHost will download and run the latest version of PocketBase. I
# Release History
**0.7.0**
- PocketHost will now always select and run the latest version of PocketBase for new instances and for the PocketHost central database. This was previously restricted until PocketBase matured more, but we think it is safe now.
- Now using native PocketBase migrations to manage PocketHost central database migrations. Roadmap has been updated with task to allow end users to put their PocketBase instance in maintenance mode and run migrations.
**0.6.1**
- Fixed semver locking error

View File

@ -61,6 +61,9 @@ Ideas, in no particular order...
- Version upgrades/downgrades
- Allow instance to be placed into maintenance mode
- Allow maintenance mode instance to run migrations (check automigrate first?)
- Allow instance to be shut down and restarted
- [Allow user to move to platform that requires migrations](https://github.com/benallfree/pockethost/issues/72)
- [Allow user to move between platforms that don't require migrations](https://github.com/benallfree/pockethost/issues/60)
- [Allow user to move between versions on a given pocketbase platform](https://github.com/benallfree/pockethost/issues/59)