feat: add db migration support for multiple custom domains per instance

This commit is contained in:
Ben Allfree 2025-07-17 23:01:11 -07:00
parent 81fd2818cc
commit 60307e6ca7
7 changed files with 3229 additions and 3255 deletions

View File

@ -0,0 +1,5 @@
---
'pockethost': patch
---
Add db migration support for multiple custom domains per instance

File diff suppressed because it is too large Load Diff

View File

@ -1,128 +1,111 @@
// src/lib/handlers/instance/hooks.ts
routerAdd( //#region src/lib/handlers/instance/hooks.ts
"PUT", routerAdd("PUT", "/api/instance/:id", (c) => {
"/api/instance/:id", return require(`${__hooks}/mothership`).HandleInstanceUpdate(c);
(c) => { }, $apis.requireRecordAuth());
return require(`${__hooks}/mothership`).HandleInstanceUpdate(c); routerAdd("POST", "/api/instance", (c) => {
}, return require(`${__hooks}/mothership`).HandleInstanceCreate(c);
$apis.requireRecordAuth() }, $apis.requireRecordAuth());
); routerAdd("DELETE", "/api/instance/:id", (c) => {
routerAdd( return require(`${__hooks}/mothership`).HandleInstanceDelete(c);
"POST", }, $apis.requireRecordAuth());
"/api/instance", routerAdd("GET", "/api/instance/resolve", (c) => {
(c) => { return require(`${__hooks}/mothership`).HandleInstanceResolve(c);
return require(`${__hooks}/mothership`).HandleInstanceCreate(c); }, $apis.requireAdminAuth());
}, /** Validate instance version */
$apis.requireRecordAuth()
);
routerAdd(
"DELETE",
"/api/instance/:id",
(c) => {
return require(`${__hooks}/mothership`).HandleInstanceDelete(c);
},
$apis.requireRecordAuth()
);
routerAdd(
"GET",
"/api/instance/resolve",
(c) => {
return require(`${__hooks}/mothership`).HandleInstanceResolve(c);
},
$apis.requireAdminAuth()
);
onModelBeforeCreate((e) => { onModelBeforeCreate((e) => {
return require(`${__hooks}/mothership`).HandleInstanceVersionValidation(e); return require(`${__hooks}/mothership`).HandleInstanceVersionValidation(e);
}, "instances");
onModelAfterCreate((e) => {
}, "instances"); }, "instances");
onModelAfterCreate((e) => {}, "instances");
onAfterBootstrap((e) => {});
onAfterBootstrap((e) => {});
/** Reset instance status to idle on start */
onAfterBootstrap((e) => { onAfterBootstrap((e) => {
return require(`${__hooks}/mothership`).HandleInstancesResetIdle(e);
}); });
/** Migrate existing cnames to domains table */
onAfterBootstrap((e) => { onAfterBootstrap((e) => {
return require(`${__hooks}/mothership`).HandleMigrateCnamesToDomains(e);
}); });
onAfterBootstrap((e) => { /** Validate instance version */
return require(`${__hooks}/mothership`).HandleInstancesResetIdle(e);
});
onModelBeforeUpdate((e) => { onModelBeforeUpdate((e) => {
return require(`${__hooks}/mothership`).HandleInstanceBeforeUpdate(e); return require(`${__hooks}/mothership`).HandleInstanceBeforeUpdate(e);
}, "instances"); }, "instances");
// src/lib/handlers/lemon/hooks.ts //#endregion
//#region src/lib/handlers/lemon/hooks.ts
routerAdd("POST", "/api/ls", (c) => { routerAdd("POST", "/api/ls", (c) => {
return require(`${__hooks}/mothership`).HandleLemonSqueezySale(c); return require(`${__hooks}/mothership`).HandleLemonSqueezySale(c);
}); });
// src/lib/handlers/mail/hooks.ts //#endregion
routerAdd( //#region src/lib/handlers/mail/hooks.ts
"POST", routerAdd("POST", "/api/mail", (c) => {
"/api/mail", return require(`${__hooks}/mothership`).HandleMailSend(c);
(c) => { }, $apis.requireAdminAuth());
return require(`${__hooks}/mothership`).HandleMailSend(c);
},
$apis.requireAdminAuth()
);
// src/lib/handlers/meta/hooks.ts //#endregion
//#region src/lib/handlers/meta/hooks.ts
onAfterBootstrap((e) => { onAfterBootstrap((e) => {
return require(`${__hooks}/mothership`).HandleMetaUpdateAtBoot(e); return require(`${__hooks}/mothership`).HandleMetaUpdateAtBoot(e);
}); });
// src/lib/handlers/mirror/hooks.ts //#endregion
routerAdd( //#region src/lib/handlers/mirror/hooks.ts
"GET", routerAdd("GET", "/api/mirror", (c) => {
"/api/mirror", return require(`${__hooks}/mothership`).HandleMirrorData(c);
(c) => { }, $apis.gzip(), $apis.requireAdminAuth());
return require(`${__hooks}/mothership`).HandleMirrorData(c);
},
$apis.gzip(),
$apis.requireAdminAuth()
);
// src/lib/handlers/notify/hooks.ts //#endregion
//#region src/lib/handlers/notify/hooks.ts
routerAdd(`GET`, `api/process_single_notification`, (c) => { routerAdd(`GET`, `api/process_single_notification`, (c) => {
return require(`${__hooks}/mothership`).HandleProcessSingleNotification(c); return require(`${__hooks}/mothership`).HandleProcessSingleNotification(c);
}); });
onModelAfterCreate((e) => { onModelAfterCreate((e) => {
return require(`${__hooks}/mothership`).HandleProcessNotification(e); return require(`${__hooks}/mothership`).HandleProcessNotification(e);
}, `notifications`); }, `notifications`);
onModelBeforeUpdate((e) => { onModelBeforeUpdate((e) => {
return require(`${__hooks}/mothership`).HandleUserWelcomeMessage(e); return require(`${__hooks}/mothership`).HandleUserWelcomeMessage(e);
}, "users"); }, "users");
// src/lib/handlers/outpost/hooks.ts //#endregion
//#region src/lib/handlers/outpost/hooks.ts
routerAdd("GET", "/api/unsubscribe", (c) => { routerAdd("GET", "/api/unsubscribe", (c) => {
return require(`${__hooks}/mothership`).HandleOutpostUnsubscribe(c); return require(`${__hooks}/mothership`).HandleOutpostUnsubscribe(c);
}); });
// src/lib/handlers/signup/hooks.ts //#endregion
//#region src/lib/handlers/signup/hooks.ts
routerAdd("GET", "/api/signup", (c) => { routerAdd("GET", "/api/signup", (c) => {
return require(`${__hooks}/mothership`).HandleSignupCheck(c); return require(`${__hooks}/mothership`).HandleSignupCheck(c);
}); });
routerAdd("POST", "/api/signup", (c) => { routerAdd("POST", "/api/signup", (c) => {
return require(`${__hooks}/mothership`).HandleSignupConfirm(c); return require(`${__hooks}/mothership`).HandleSignupConfirm(c);
}); });
// src/lib/handlers/sns/hooks.ts //#endregion
//#region src/lib/handlers/sns/hooks.ts
routerAdd("POST", "/api/sns", (c) => { routerAdd("POST", "/api/sns", (c) => {
return require(`${__hooks}/mothership`).HandleSesError(c); return require(`${__hooks}/mothership`).HandleSesError(c);
}); });
// src/lib/handlers/stats/hooks.ts //#endregion
//#region src/lib/handlers/stats/hooks.ts
routerAdd("GET", "/api/stats", (c) => { routerAdd("GET", "/api/stats", (c) => {
return require(`${__hooks}/mothership`).HandleStatsRequest(c); return require(`${__hooks}/mothership`).HandleStatsRequest(c);
}); });
// src/lib/handlers/user/hooks.ts //#endregion
routerAdd( //#region src/lib/handlers/user/hooks.ts
"GET", routerAdd("GET", "/api/userToken/:id", (c) => {
"/api/userToken/:id", return require(`${__hooks}/mothership`).HandleUserTokenRequest(c);
(c) => { }, $apis.requireAdminAuth());
return require(`${__hooks}/mothership`).HandleUserTokenRequest(c);
},
$apis.requireAdminAuth()
);
// src/lib/handlers/versions/hooks.ts //#endregion
//#region src/lib/handlers/versions/hooks.ts
/** Return a list of available PocketBase versions */
routerAdd("GET", "/api/versions", (c) => { routerAdd("GET", "/api/versions", (c) => {
return require(`${__hooks}/mothership`).HandleVersionsRequest(c); return require(`${__hooks}/mothership`).HandleVersionsRequest(c);
}); });
//#endregion

View File

@ -0,0 +1,76 @@
/// <reference path="../src/types/types.d.ts" />
migrate(
(db) => {
const collection = new Collection({
id: 'jw1wz9nmkvnhcm6',
created: '2025-07-18 05:18:37.698Z',
updated: '2025-07-18 05:18:37.698Z',
name: 'domains',
type: 'base',
system: false,
schema: [
{
system: false,
id: 'fhie5snn',
name: 'instance',
type: 'relation',
required: false,
presentable: false,
unique: false,
options: {
collectionId: 'etae8tuiaxl6xfv',
cascadeDelete: false,
minSelect: null,
maxSelect: 1,
displayFields: null,
},
},
{
system: false,
id: 'wn3oncif',
name: 'domain',
type: 'text',
required: false,
presentable: false,
unique: false,
options: {
min: null,
max: null,
pattern: '',
},
},
{
system: false,
id: 'vzkcvhhg',
name: 'active',
type: 'bool',
required: false,
presentable: false,
unique: false,
options: {},
},
],
indexes: [
'CREATE UNIQUE INDEX `idx_gtGcwf2` ON `domains` (`domain`)',
'CREATE INDEX `idx_OtPOwXe` ON `domains` (`instance`)',
'CREATE INDEX `idx_0omsTdi` ON `domains` (`created`)',
'CREATE INDEX `idx_uMaIOVQ` ON `domains` (`updated`)',
'CREATE UNIQUE INDEX `idx_VLBOSap` ON `domains` (\n `instance`,\n `domain`\n)',
],
listRule: null,
viewRule: null,
createRule: null,
updateRule: null,
deleteRule: null,
options: {},
})
return Dao(db).saveCollection(collection)
},
(db) => {
const dao = new Dao(db)
const collection = dao.findCollectionByNameOrId('jw1wz9nmkvnhcm6')
return dao.deleteCollection(collection)
},
)

View File

@ -0,0 +1,76 @@
import { mkLog } from '$util/Logger'
export const HandleMigrateCnamesToDomains = (e: core.BootstrapEvent) => {
const dao = $app.dao()
const log = mkLog(`bootstrap:migrate-cnames`)
log(`Starting cname to domains migration`)
try {
// Check if domains table exists
const domainsCollection = dao.findCollectionByNameOrId('domains')
if (!domainsCollection) {
log(`Domains collection not found, skipping migration`)
return
}
// Check if there are any instances with cnames that haven't been migrated
log(`Checking for instances with cnames`)
const instancesWithCnames = dao.findRecordsByFilter(
'instances',
"cname != NULL && cname != ''",
)
if (instancesWithCnames.length === 0) {
log(`No cnames to migrate`)
return
}
log(`Found ${instancesWithCnames.length} instances with cnames`)
// Check which ones are already migrated
const unmigrated = instancesWithCnames.filter((instance) => {
if (!instance) return false
try {
const existingDomain = dao.findFirstRecordByFilter(
'domains',
`instance = "${instance.getId()}"`,
)
return !existingDomain
} catch (e) {
// Not found means not migrated
return true
}
})
if (unmigrated.length === 0) {
log(`All cnames already migrated`)
return
}
log(`Found ${unmigrated.length} cnames to migrate`)
// Migrate cnames to domains table
let migrated = 0
unmigrated.forEach((instance) => {
if (!instance) return
try {
const domainsCollection = dao.findCollectionByNameOrId('domains')
const domainRecord = new Record(domainsCollection)
domainRecord.set('instance', instance.getId())
domainRecord.set('domain', instance.getString('cname'))
domainRecord.set('active', instance.getBool('cname_active'))
dao.saveRecord(domainRecord)
migrated++
} catch (error) {
log(`Failed to migrate cname for instance ${instance.getId()}:`, error)
}
})
log(`Successfully migrated ${migrated} cnames to domains table`)
} catch (error) {
log(`Error migrating cnames: ${error}`)
}
}

View File

@ -52,6 +52,11 @@ onAfterBootstrap((e) => {
return require(`${__hooks}/mothership`).HandleInstancesResetIdle(e) return require(`${__hooks}/mothership`).HandleInstancesResetIdle(e)
}) })
/** Migrate existing cnames to domains table */
onAfterBootstrap((e) => {
return require(`${__hooks}/mothership`).HandleMigrateCnamesToDomains(e)
})
/** Validate instance version */ /** Validate instance version */
onModelBeforeUpdate((e) => { onModelBeforeUpdate((e) => {
return require(`${__hooks}/mothership`).HandleInstanceBeforeUpdate(e) return require(`${__hooks}/mothership`).HandleInstanceBeforeUpdate(e)

View File

@ -3,6 +3,7 @@ export * from './api/HandleInstanceDelete'
export * from './api/HandleInstanceResolve' export * from './api/HandleInstanceResolve'
export * from './api/HandleInstanceUpdate' export * from './api/HandleInstanceUpdate'
export * from './bootstrap/HandleInstancesResetIdle' export * from './bootstrap/HandleInstancesResetIdle'
export * from './bootstrap/HandleMigrateCnamesToDomains'
export * from './bootstrap/HandleMigrateInstanceVersions' export * from './bootstrap/HandleMigrateInstanceVersions'
export * from './bootstrap/HandleMigrateRegions' export * from './bootstrap/HandleMigrateRegions'
export * from './model/HandleInstanceBeforeUpdate' export * from './model/HandleInstanceBeforeUpdate'