Deleting products: by their creators, and removal by moderation
CI / test (push) Successful in 1m4s

A creator deletes a product: it's no longer listed and nobody new can get it, while people who have it
keep it (its page, files and library entries stay, and rooms keep showing it), as old revisions stay
available. Moderation removes one everywhere, with a reason: its page and files answer 410, and it
leaves libraries and rooms. Both are on the product's page and in the audit log.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
dhkatzandClaude Opus 5.5 committed 2026-10-30 08:10:24 -07:00
1 parent 07bbc11565
commit 97ce2be569
12 files changed
+2435 -14

No files matched your search

+25 -3
View File
@@ -296,6 +296,25 @@ export class Products {
const [p] = await this.db.select().from(product).where(eq(product.id, productId));
if (!p) throw new ProductError(404, "not_found", "No such product.");
if (p.creatorId !== personId) throw new ProductError(403, "forbidden", "That product isn't yours.");
if (p.removedAt) throw new ProductError(410, "removed", "This product was removed by moderation.");
if (p.deletedAt) throw new ProductError(410, "deleted", "You deleted this product.");
return p;
}
/** Its creator deletes a product: it's no longer listed or added to libraries (those who have it keep it). */
async delete(personId: string, id: string) {
await this.own(personId, id);
await this.db.update(product).set({ deletedAt: new Date() }).where(eq(product.id, id));
}
/** Moderation removes a product: gone everywhere. */
async removeByModeration(id: string, reason: string) {
const [p] = await this.db
.update(product)
.set({ removedAt: new Date(), removedReason: reason })
.where(and(eq(product.id, id), isNull(product.removedAt)))
.returning();
if (!p) throw new ProductError(404, "not_found", "No such product, or it's removed already.");
return p;
}
@@ -304,7 +323,7 @@ export class Products {
* that IMVU category (anywhere in its path). Each comes with its latest revision's parent and parent info.
*/
async list(opts: { q?: string; creator?: string; category?: number; adult: boolean; limit?: number }) {
const conds = [ne(product.latest, 0)];
const conds = [ne(product.latest, 0), isNull(product.deletedAt), isNull(product.removedAt)];
if (!opts.adult) conds.push(eq(product.rating, "general"));
if (opts.q) conds.push(ilike(product.name, `%${opts.q.replace(/[%_\\]/g, (c) => "\\" + c)}%`));
if (opts.creator) conds.push(eq(user.username, opts.creator.toLowerCase()));
@@ -343,6 +362,7 @@ export class Products {
.innerJoin(user, eq(user.id, product.creatorId))
.where(eq(product.id, id));
if (!row || row.product.latest === 0) throw new ProductError(404, "not_found", "No such product.");
if (row.product.removedAt) throw new ProductError(410, "removed", "This product was removed by moderation.");
if (row.product.rating === "adult" && !adult) throw new ProductError(404, "not_found", "No such product.");
const revisions = await this.db
.select()
@@ -407,7 +427,7 @@ export class Products {
.innerJoin(product, eq(product.id, library.productId))
.innerJoin(user, eq(user.id, product.creatorId))
.leftJoin(productRevision, latestJoin)
.where(eq(library.userId, personId))
.where(and(eq(library.userId, personId), isNull(product.removedAt)))
.orderBy(desc(library.addedAt));
}
@@ -420,7 +440,9 @@ export class Products {
}
async add(personId: string, productId: string, adult: boolean) {
await this.get(productId, adult);
const p = await this.get(productId, adult);
if (p.product.deletedAt && !(await this.inLibrary(personId, productId)))
throw new ProductError(410, "deleted", "Its creator deleted this product.");
await this.db.insert(library).values({ userId: personId, productId }).onConflictDoNothing();
}