mirror of
https://github.com/HeyPuter/puter.git
synced 2026-10-26 08:40:30 +00:00
fix(subdomains): emit subdomain.delete for sites deleted with an app (#4273)
Sites and workers tied to an app go with it by FK cascade, on app delete and on deleting the app owner's account. Only caches were dropped for them, so the subdomain.delete listeners (CDN purge, worker script teardown, worker sessions, site cleanup) never ran.
This commit is contained in:
1 parent
2a49689fad
commit
41e4b582d3
2 files changed
+81
-12
No files matched your search
@@ -535,8 +535,16 @@ describe('SubdomainStore app_owner filtering', () => {
|
||||
rows.map((row) => row.subdomain).sort();
|
||||
expect(await store.listAppCascade({ appId: app.id })).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ subdomain: owned.subdomain, root_dir_id: null },
|
||||
{ subdomain: associated.subdomain, root_dir_id: 7 },
|
||||
{
|
||||
uuid: owned.uuid,
|
||||
subdomain: owned.subdomain,
|
||||
root_dir_id: null,
|
||||
},
|
||||
{
|
||||
uuid: associated.uuid,
|
||||
subdomain: associated.subdomain,
|
||||
root_dir_id: 7,
|
||||
},
|
||||
]),
|
||||
);
|
||||
expect(names(await store.listAppCascade({ appId: app.id }))).toEqual(
|
||||
@@ -547,6 +555,51 @@ describe('SubdomainStore app_owner filtering', () => {
|
||||
).toEqual(names([owned, associated, siblings]));
|
||||
});
|
||||
|
||||
it('emits subdomain.delete for each row an app delete cascades to', async () => {
|
||||
const developer = await makeUser();
|
||||
const siteOwner = await makeUser();
|
||||
const app = await makeApp(developer);
|
||||
const name = (tag: string) =>
|
||||
`sds-cascade-ev-${tag}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
const owned = await store.create({
|
||||
userId: siteOwner,
|
||||
subdomain: name('owned'),
|
||||
appOwner: app.id,
|
||||
});
|
||||
const associated = await store.create({
|
||||
userId: siteOwner,
|
||||
subdomain: name('associated'),
|
||||
associatedAppId: app.id,
|
||||
});
|
||||
|
||||
const events: Array<{ subdomain: string; uid?: string }> = [];
|
||||
const listener = (
|
||||
_key: string,
|
||||
data: { subdomain: string; uid?: string },
|
||||
) => {
|
||||
events.push(data);
|
||||
};
|
||||
server.clients.event.on('subdomain.delete', listener);
|
||||
try {
|
||||
expect(await server.stores.app.delete(app.id)).toBe(true);
|
||||
await vi.waitFor(() => expect(events).toHaveLength(2));
|
||||
} finally {
|
||||
server.clients.event.off('subdomain.delete', listener);
|
||||
}
|
||||
|
||||
expect(events).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ subdomain: owned.subdomain, uid: owned.uuid },
|
||||
{ subdomain: associated.subdomain, uid: associated.uuid },
|
||||
]),
|
||||
);
|
||||
// SQLite reuses the deleted app's id; drop anything it left behind.
|
||||
await server.clients.db.write(
|
||||
'DELETE FROM `subdomains` WHERE `uuid` IN (?, ?)',
|
||||
[owned.uuid, associated.uuid],
|
||||
);
|
||||
});
|
||||
|
||||
it('lists an app cascade past one page', async () => {
|
||||
const userId = await makeUser();
|
||||
const app = await makeApp(userId);
|
||||
|
||||
@@ -45,6 +45,12 @@ export interface SubdomainRow {
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/** A row deleted by the database along with an app. */
|
||||
export type CascadeRow = Pick<
|
||||
SubdomainRow,
|
||||
'uuid' | 'subdomain' | 'root_dir_id'
|
||||
>;
|
||||
|
||||
// Columns that may not be set through an `update` patch map. Defence-in-depth
|
||||
// against future callers (admin routes, extensions, new REST handlers) that
|
||||
// might forward `req.body` straight into the store: the driver's update
|
||||
@@ -759,7 +765,7 @@ export class SubdomainStore extends PuterStore {
|
||||
*/
|
||||
async listAppCascade(
|
||||
apps: { appId: number } | { ownerUserId: number },
|
||||
): Promise<Array<Pick<SubdomainRow, 'subdomain' | 'root_dir_id'>>> {
|
||||
): Promise<CascadeRow[]> {
|
||||
const [match, param] =
|
||||
'appId' in apps
|
||||
? ['= ?', apps.appId]
|
||||
@@ -767,17 +773,17 @@ export class SubdomainStore extends PuterStore {
|
||||
'IN (SELECT `id` FROM `apps` WHERE `owner_user_id` = ?)',
|
||||
apps.ownerUserId,
|
||||
];
|
||||
const out: Array<Pick<SubdomainRow, 'subdomain' | 'root_dir_id'>> = [];
|
||||
const out: CascadeRow[] = [];
|
||||
let afterId = 0;
|
||||
for (;;) {
|
||||
const rows = (await this.clients.db.pread(
|
||||
`SELECT \`id\`, \`subdomain\`, \`root_dir_id\` FROM \`subdomains\`
|
||||
`SELECT \`id\`, \`uuid\`, \`subdomain\`, \`root_dir_id\` FROM \`subdomains\`
|
||||
WHERE (\`app_owner\` ${match} OR \`associated_app_id\` ${match})
|
||||
AND \`id\` > ? ORDER BY \`id\` LIMIT ?`,
|
||||
[param, param, afterId, BULK_QUERY_CHUNK_SIZE],
|
||||
)) as Array<Pick<SubdomainRow, 'id' | 'subdomain' | 'root_dir_id'>>;
|
||||
for (const { subdomain, root_dir_id } of rows) {
|
||||
out.push({ subdomain, root_dir_id });
|
||||
)) as Array<Pick<SubdomainRow, 'id'> & CascadeRow>;
|
||||
for (const { uuid, subdomain, root_dir_id } of rows) {
|
||||
out.push({ uuid, subdomain, root_dir_id });
|
||||
}
|
||||
if (rows.length < BULK_QUERY_CHUNK_SIZE) return out;
|
||||
afterId = Number(rows[rows.length - 1].id);
|
||||
@@ -786,11 +792,10 @@ export class SubdomainStore extends PuterStore {
|
||||
|
||||
/**
|
||||
* Drop cached state, here and on peers, for rows deleted outside this
|
||||
* store.
|
||||
* store, and emit `subdomain.delete` for each as the driver does, so CDN
|
||||
* purges, deployed-worker teardown and other site cleanup still run.
|
||||
*/
|
||||
async forgetCascadeDeleted(
|
||||
rows: Array<Pick<SubdomainRow, 'subdomain' | 'root_dir_id'>>,
|
||||
): Promise<void> {
|
||||
async forgetCascadeDeleted(rows: CascadeRow[]): Promise<void> {
|
||||
if (rows.length === 0) return;
|
||||
await this.#publishChanged(
|
||||
[],
|
||||
@@ -804,6 +809,17 @@ export class SubdomainStore extends PuterStore {
|
||||
await runWithConcurrencyLimit(rootDirIds, 8, (id) =>
|
||||
this.#invalidateRootDirEntry(id),
|
||||
);
|
||||
for (const row of rows) {
|
||||
try {
|
||||
this.clients.event.emit(
|
||||
'subdomain.delete',
|
||||
{ subdomain: row.subdomain, uid: String(row.uuid) },
|
||||
{},
|
||||
);
|
||||
} catch {
|
||||
// Non-critical.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -- Internals ----------------------------------------------------
|
||||
|
||||
Reference in new issue
Block a user