Changes
3 changed files (+307/-0)
-
packages/pwa/src/worker/yamori/workspace/v1/key_value_storage_based_workspace_service/delete.test.ts (new)
-
@@ -0,0 +1,201 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only import "fake-indexeddb/auto"; import { beforeEach, expect, test } from "bun:test"; import { create, fromBinary, toBinary, type MessageInitShape, type DescMessage, type MessageShape, } from "@bufbuild/protobuf"; import { CreateRequestSchema } from "@yamori/proto/yamori/workspace/v1/create_request_pb.js"; import { CreateResponseSchema } from "@yamori/proto/yamori/workspace/v1/create_response_pb.js"; import { DeleteRequestSchema } from "@yamori/proto/yamori/workspace/v1/delete_request_pb.js"; import { DeleteResponseSchema } from "@yamori/proto/yamori/workspace/v1/delete_response_pb.js"; import { ListRequestSchema } from "@yamori/proto/yamori/workspace/v1/list_request_pb.js"; import { ListResponseSchema } from "@yamori/proto/yamori/workspace/v1/list_response_pb.js"; import { IDBFactory } from "fake-indexeddb"; import { openDB } from "idb"; import { setup } from "../../../../setup.ts"; import { type ServiceContext } from "../../../../context.ts"; import { create as createHandler } from "./create.ts"; import { deleteWorkspace as deleteHandler } from "./delete.ts"; import { list } from "./list.ts"; function bind<RequestSchema extends DescMessage, ResponseSchema extends DescMessage>( requestSchema: RequestSchema, responseSchema: ResponseSchema, handler: (req: Uint8Array, ctx: ServiceContext) => Promise<Uint8Array>, ): ( req: MessageInitShape<RequestSchema>, ctx: ServiceContext, ) => Promise<MessageShape<ResponseSchema>> { return async (request, ctx) => fromBinary( responseSchema, await handler(toBinary(requestSchema, create(requestSchema, request)), ctx), ); } const createWorkspace = bind(CreateRequestSchema, CreateResponseSchema, createHandler); const listWorkspaces = bind(ListRequestSchema, ListResponseSchema, list); const deleteWorkspace = bind(DeleteRequestSchema, DeleteResponseSchema, deleteHandler); beforeEach(() => { indexedDB = new IDBFactory(); }); test("Should return an error on IndexedDB exceptions", async () => { const resp = await deleteWorkspace( { id: { value: "foo-bar", }, }, { db: await openDB("test"), }, ); if (resp.result.case !== "systemError") { expect.unreachable(`Expected "systemError", found "${resp.result.case}"`); } expect(resp.result.value.code).toBe("IDB_ERROR"); }); test("Should return a missing field error if ID is nonexistent", async () => { const ctx = await setup(); const created = await createWorkspace( { displayName: "Foo", }, ctx, ); if (created.result.case !== "ok") { expect.unreachable(); } if (!created.result.value.workspace?.id?.value) { expect.unreachable("Create handler assigned an empty ID"); } const deleted = await deleteWorkspace( { id: { value: created.result.value.workspace.id.value + "-1111", }, }, ctx, ); if (deleted.result.case !== "notFound") { expect.unreachable(`Expected "not_found", found "${deleted.result.case}"`); } const workspaces = await listWorkspaces({}, ctx); if (workspaces.result.case !== "ok") { expect.unreachable(); } expect(workspaces.result.value.workspaces).toHaveLength(1); }); test("Should return an error if ID field is absent", async () => { const ctx = await setup(); const created = await createWorkspace( { displayName: "Foo", }, ctx, ); if (created.result.case !== "ok") { expect.unreachable(); } if (!created.result.value.workspace?.id?.value) { expect.unreachable("Create handler assigned an empty ID"); } const deleted = await deleteWorkspace( { id: {}, }, ctx, ); if (deleted.result.case !== "missingField") { expect.unreachable(`Expected "missing_field", found "${deleted.result.case}"`); } expect(deleted.result.value.path).toBe("id.value"); const workspaces = await listWorkspaces({}, ctx); if (workspaces.result.case !== "ok") { expect.unreachable(); } expect(workspaces.result.value.workspaces).toHaveLength(1); expect(workspaces.result.value.workspaces[0]?.displayName).toBe("Foo"); }); test("Should delete a workspace", async () => { const ctx = await setup(); const foo = await createWorkspace( { displayName: "Foo", }, ctx, ); if (foo.result.case !== "ok") { expect.unreachable(); } if (!foo.result.value.workspace?.id?.value) { expect.unreachable("Create handler assigned an empty ID"); } const bar = await createWorkspace( { displayName: "Bar", }, ctx, ); if (bar.result.case !== "ok") { expect.unreachable(); } if (!bar.result.value.workspace?.id?.value) { expect.unreachable("Create handler assigned an empty ID"); } const deleted = await deleteWorkspace( { id: foo.result.value.workspace.id, }, ctx, ); if (deleted.result.case !== "ok") { expect.unreachable(`Expected "ok", found "${deleted.result.case}"`); } expect(deleted.result.value.workspace?.id).toEqual(foo.result.value.workspace.id); const workspaces = await listWorkspaces({}, ctx); if (workspaces.result.case !== "ok") { expect.unreachable(); } expect(workspaces.result.value.workspaces).toHaveLength(1); expect(workspaces.result.value.workspaces[0]?.displayName).toBe("Bar"); expect(workspaces.result.value.workspaces[0]?.id).toEqual( bar.result.value.workspace.id, ); });
-
-
packages/pwa/src/worker/yamori/workspace/v1/key_value_storage_based_workspace_service/delete.ts (new)
-
@@ -0,0 +1,100 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only import { create, fromBinary, toBinary, type MessageInitShape } from "@bufbuild/protobuf"; import { type DeleteRequest, DeleteRequestSchema, } from "@yamori/proto/yamori/workspace/v1/delete_request_pb.js"; import { DeleteResponseSchema } from "@yamori/proto/yamori/workspace/v1/delete_response_pb.js"; import { type ServiceContext } from "../../../../context.ts"; function respond(payload: MessageInitShape<typeof DeleteResponseSchema>): Uint8Array { return toBinary(DeleteResponseSchema, create(DeleteResponseSchema, payload)); } /** * `delete` は予約後だからこれだけ目的語がついている。 */ export async function deleteWorkspace( data: Uint8Array, { db }: ServiceContext, ): Promise<Uint8Array> { let req: DeleteRequest; try { req = fromBinary(DeleteRequestSchema, data); } catch (error) { return respond({ result: { case: "systemError", value: { code: "INVALID_MESSAGE", message: import.meta.env.NODE_ENV === "production" ? "Failed to parse request message" : `Request message does not conform schema: ${error}`, }, }, }); } if (!req.id?.value) { return respond({ result: { case: "missingField", value: { path: req.id ? "id.value" : "id", }, }, }); } try { const tx = db.transaction("workspaces", "readwrite"); const store = tx.objectStore("workspaces"); const found = await store.get(req.id.value); if (!found) { await tx.done; return respond({ result: { case: "notFound", value: { typeName: "yamori.workspace.v1.Workspace", }, }, }); } await store.delete(req.id.value); await tx.done; return respond({ result: { case: "ok", value: { workspace: { id: { value: found.id, }, displayName: found.displayName, }, }, }, }); } catch (error) { return respond({ result: { case: "systemError", value: { code: "IDB_ERROR", message: import.meta.env.NODE_ENV === "production" ? "Failed to delete a workspace" : `Exception thrown during delete transaction: ${error}`, }, }, }); } }
-
-
-
@@ -5,6 +5,7 @@ import { type ServiceContext } from "../../../../context.ts";import { type Message } from "../../../../message.ts"; import { create } from "./create.ts"; import { deleteWorkspace } from "./delete.ts"; import { list } from "./list.ts"; import { update } from "./update.ts";
-
@@ -27,6 +28,11 @@ case "Update":return { ...request, data: await update(request.data, ctx), }; case "Delete": return { ...request, data: await deleteWorkspace(request.data, ctx), }; default: throw new Error(`Unknown method "${request.method}" for ${request.service}`);
-