test: verify atomic share creation

This commit is contained in:
2026-08-16 01:18:15 +07:00 Unverified
parent a097dfb994
commit c9f01231c6
3 changed files with 164 additions and 56 deletions
+76
View File
@@ -0,0 +1,76 @@
import { describe, expect, test } from "bun:test";
import {
createShareAtomically,
type CreateShareInput,
type CreateShareStore,
} from "@/lib/share/create-core";
const input: CreateShareInput = {
content: "exact content\n",
description: "description",
image: {
bytes: Buffer.from("image"),
name: "image.png",
type: "image/png",
size: 5,
},
source: "bot",
author: {
userId: "user-1",
discordId: "123456789012345678",
displayName: "Example user",
avatarUrl: null,
},
};
function memoryStore(failShareInsert = false) {
const images: string[] = [];
const shares: string[] = [];
const store: CreateShareStore = {
async transaction(callback) {
const nextImages = [...images];
const nextShares = [...shares];
const result = await callback({
async insertImage() {
nextImages.push("image-1");
return "image-1";
},
async insertShare(_input, imageCdnId) {
if (failShareInsert) throw new Error("share insert failed");
nextShares.push("share-1");
return {
id: "a1b2c3d4",
imageCdnId,
createdAt: new Date("2026-08-16T00:00:00.000Z"),
};
},
});
images.splice(0, images.length, ...nextImages);
shares.splice(0, shares.length, ...nextShares);
return result;
},
};
return { images, shares, store };
}
describe("atomic share creation", () => {
test("stores the image and share in one transaction", async () => {
const memory = memoryStore();
await expect(createShareAtomically(input, memory.store)).resolves.toEqual({
id: "a1b2c3d4",
imageCdnId: "image-1",
createdAt: new Date("2026-08-16T00:00:00.000Z"),
});
expect(memory.images).toEqual(["image-1"]);
expect(memory.shares).toEqual(["share-1"]);
});
test("rolls back the image when inserting the share fails", async () => {
const memory = memoryStore(true);
await expect(createShareAtomically(input, memory.store)).rejects.toThrow(
"share insert failed",
);
expect(memory.images).toEqual([]);
expect(memory.shares).toEqual([]);
});
});
+46
View File
@@ -0,0 +1,46 @@
import type { ValidatedImage } from "@/lib/share/validation";
export type ShareAuthor = {
userId: string;
discordId: string;
displayName: string;
avatarUrl: string | null;
};
export type CreateShareInput = {
content: string;
description: string | null;
image: ValidatedImage | null;
source: "web" | "bot";
author: ShareAuthor;
};
export type CreatedShare = {
id: string;
imageCdnId: string | null;
createdAt: Date;
};
export type CreateShareTransaction = {
insertImage(image: ValidatedImage, ownerDiscordId: string): Promise<string>;
insertShare(
input: CreateShareInput,
imageCdnId: string | null,
): Promise<CreatedShare>;
};
export type CreateShareStore = {
transaction<T>(callback: (tx: CreateShareTransaction) => Promise<T>): Promise<T>;
};
export function createShareAtomically(
input: CreateShareInput,
store: CreateShareStore,
) {
return store.transaction(async (tx) => {
const imageCdnId = input.image
? await tx.insertImage(input.image, input.author.discordId)
: null;
return tx.insertShare(input, imageCdnId);
});
}
+42 -56
View File
@@ -2,63 +2,49 @@ import "server-only";
import { db } from "@/db";
import { cdn, shareTexts } from "@/db/schema";
import type { ShareSource } from "@/db/schema/share";
import type { ValidatedImage } from "@/lib/share/validation";
import {
createShareAtomically,
type CreateShareInput,
} from "@/lib/share/create-core";
export type ShareAuthor = {
userId: string;
discordId: string;
displayName: string;
avatarUrl: string | null;
};
export type { ShareAuthor } from "@/lib/share/create-core";
export async function createShare({
content,
description,
image,
source,
author,
}: {
content: string;
description: string | null;
image: ValidatedImage | null;
source: ShareSource;
author: ShareAuthor;
}) {
return db.transaction(async (tx) => {
let imageCdnId: string | null = null;
if (image) {
const [record] = await tx
.insert(cdn)
.values({
data: image.bytes,
name: image.name,
type: image.type,
size: image.size,
ownerDiscordId: author.discordId,
})
.returning({ id: cdn.id });
imageCdnId = record.id;
}
const [share] = await tx
.insert(shareTexts)
.values({
content,
description,
imageCdnId,
source,
authorId: author.userId,
authorDiscordId: author.discordId,
authorName: author.displayName,
authorAvatarUrl: author.avatarUrl,
})
.returning({
id: shareTexts.id,
imageCdnId: shareTexts.imageCdnId,
createdAt: shareTexts.createdAt,
});
return share;
export async function createShare(input: CreateShareInput) {
return createShareAtomically(input, {
transaction: (callback) => db.transaction(async (tx) => callback({
async insertImage(image, ownerDiscordId) {
const [record] = await tx
.insert(cdn)
.values({
data: image.bytes,
name: image.name,
type: image.type,
size: image.size,
ownerDiscordId,
})
.returning({ id: cdn.id });
return record.id;
},
async insertShare(share, imageCdnId) {
const [record] = await tx
.insert(shareTexts)
.values({
content: share.content,
description: share.description,
imageCdnId,
source: share.source,
authorId: share.author.userId,
authorDiscordId: share.author.discordId,
authorName: share.author.displayName,
authorAvatarUrl: share.author.avatarUrl,
})
.returning({
id: shareTexts.id,
imageCdnId: shareTexts.imageCdnId,
createdAt: shareTexts.createdAt,
});
return record;
},
})),
});
}