Files
TREK/server/tests/helpers/storage-release.ts
T
Maurice d17457de56 fix(storage): delete what a delete or a replace left unnamed right away again
Leaving every replaced cover, place image and deleted journey photo to the
nightly pass kept them publicly fetchable by URL for a week at least, and
for good while deletion was off. Owners now hand the values a delete or a
replace left behind to StorageReleaseService once their write committed.
It asks every registered reference source, with the same parser as the
nightly pass, and deletes at once what no row names any more: an object a
trip copy, a journey or a saved place still shows stays, which the old
inline deletes got wrong.

The release holds the pass's guards (reclaimable categories at their root
only, never an owner marker, never while a restore runs, never with a
required source missing or an overlapping location), never fails the
request and logs at debug. It runs for trip covers (update, upload, trip
delete), collection covers and saved-place images, place images (update,
bulk update, delete), journey covers and journey or gallery photos
(their thumbnail goes with a file that went), avatars (change, delete,
account deletion), trip files (permanent delete, empty trash) and collab
note and chat attachments. A trip file row now goes before its bytes, so a
failing backend no longer fails the delete. The objects of a deleted trip's
places and files are left to the nightly pass: their rows are other
domains'.

PlacesService drops the collection places repository nothing read any
more. Size, warning and boundary baselines follow the shrunk files.
2026-11-01 16:18:21 +01:00

60 lines
2.6 KiB
TypeScript

/**
* Stand-ins for StorageReleaseService in suites that hand-build an owner
* service. The release itself (asking every reference source, the ledger's
* guards) has its own suites: storage-release.service.test.ts and the
* integration suite over the full app.
*
* - `releaseNothing()` releases nothing and records the calls;
* - `releaseDirect(storage)` deletes every root-level object the values name,
* as a suite with no second row naming them would see the real one do.
*/
import { inReclaimScope } from '../../src/nest/storage/storage-reclaim.ledger';
import {
storageKeysIn,
type StorageKeyOptions,
type StorageKeyRef,
} from '../../src/nest/storage/storage-reclaim.types';
import type { StorageReleaseService } from '../../src/nest/storage/storage-release.service';
import type { StorageService } from '../../src/nest/storage/storage.service';
import type { ServedCategory } from '../../src/nest/storage/storage.types';
import { vi } from 'vitest';
export function releaseNothing() {
const releaseValues = vi.fn(async (): Promise<StorageKeyRef[]> => []);
const releaseIfUnreferenced = vi.fn(async (): Promise<string[]> => []);
return { releaseValues, releaseIfUnreferenced } as unknown as StorageReleaseService & {
releaseValues: typeof releaseValues;
releaseIfUnreferenced: typeof releaseIfUnreferenced;
};
}
export function releaseDirect(storage: StorageService): StorageReleaseService {
const releaseIfUnreferenced = async (category: ServedCategory, keys: readonly string[]): Promise<string[]> => {
const present = await Promise.all(
[...new Set(keys)].map(async (key) =>
inReclaimScope(key) && (await storage.stat(category, key).catch(() => null)) !== null ? key : null,
),
);
const gone = present.filter((key): key is string => key !== null);
await Promise.all(gone.map((key) => storage.delete(category, key)));
return gone;
};
const releaseValues = async (
values: ReadonlyArray<string | null | undefined>,
options: StorageKeyOptions = {},
): Promise<StorageKeyRef[]> => {
const byCategory = new Map<string, Set<string>>();
for (const ref of values.flatMap((value) => storageKeysIn(value, options))) {
byCategory.set(ref.category, (byCategory.get(ref.category) ?? new Set<string>()).add(ref.key));
}
const released = await Promise.all(
[...byCategory].map(async ([category, keys]) =>
(await releaseIfUnreferenced(category as ServedCategory, [...keys])).map((key) => ({ category, key })),
),
);
return released.flat();
};
return { releaseValues, releaseIfUnreferenced } as unknown as StorageReleaseService;
}