Changes
3 changed files (+384/-0)
-
-
@@ -0,0 +1,274 @@// 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 } from "@bufbuild/protobuf"; import { CreateRequestSchema as WorkspaceCreateRequestSchema } from "@yamori/proto/yamori/workspace/v1/create_request_pb.js"; import { CreateResponseSchema as WorkspaceCreateResponseSchema } from "@yamori/proto/yamori/workspace/v1/create_response_pb.js"; import { type Workspace } from "@yamori/proto/yamori/workspace/v1/workspace_pb.js"; import { CreateRequestSchema } from "@yamori/proto/yamori/worker/v1/create_request_pb.js"; import { CreateResponseSchema } from "@yamori/proto/yamori/worker/v1/create_response_pb.js"; import { ListRequestSchema } from "@yamori/proto/yamori/worker/v1/list_request_pb.js"; import { ListResponseSchema } from "@yamori/proto/yamori/worker/v1/list_response_pb.js"; import { type Worker } from "@yamori/proto/yamori/worker/v1/worker_pb.js"; import { openDB } from "idb"; import { idbBackend } from "../../../../lib"; import { CONTEXT } from "../../../../symbols"; import { type Context } from "../../../../types"; import { create as createWorkspace } from "../../../workspace/v1/key_value_storage_based_workspace_service/create"; import { create as createWorker } from "./create"; import { list } from "./list"; beforeEach(() => { indexedDB = new IDBFactory(); }); test("Should return an error on DB error", async () => { const resp = fromBinary( ListResponseSchema, await list( toBinary( ListRequestSchema, create(ListRequestSchema, { workspaceId: { value: "ws-foo" }, }), ), { db: await openDB("test") }, ), ); if (resp.result.case !== "systemError") { expect.unreachable( `Expected "system_error", found ${JSON.stringify(resp.result.case)}`, ); } expect(resp.result.value.code).toBe("IDB_ERROR"); }); async function createTestWorkspace(ctx: Context): Promise<Workspace> { const created = fromBinary( WorkspaceCreateResponseSchema, await createWorkspace( toBinary( WorkspaceCreateRequestSchema, create(WorkspaceCreateRequestSchema, { displayName: "Test Workspace", }), ), ctx, ), ); if (created.result.case !== "ok") { expect.unreachable(`Failed to create workspace: result.case=${created.result.case}`); } if (!created.result.value.workspace) { expect.unreachable("Failed to create workspace: workspace is empty."); } return created.result.value.workspace; } test("Should return an error when workspace_id is empty", async () => { const { [CONTEXT]: ctx } = await idbBackend(); // 既存のワークスペースを参照しないことをテスト await createTestWorkspace(ctx); const resp = fromBinary( ListResponseSchema, await list(toBinary(ListRequestSchema, create(ListRequestSchema, {})), ctx), ); if (resp.result.case !== "missingField") { expect.unreachable( `Expected "missing_field", found ${JSON.stringify(resp.result.case)}`, ); } expect(resp.result.value.path).toBe("workspace_id"); }); test("Should return an error when the workspace does not exist", async () => { const { [CONTEXT]: ctx } = await idbBackend(); const workspace = await createTestWorkspace(ctx); const resp = fromBinary( ListResponseSchema, await list( toBinary( ListRequestSchema, create(ListRequestSchema, { workspaceId: { value: workspace.id!.value + ".000", }, }), ), ctx, ), ); if (resp.result.case !== "workspaceAccessError") { expect.unreachable( `Expected "workspace_access_error", found ${JSON.stringify(resp.result.case)}`, ); } expect(resp.result.value.workspaceId?.value).toBe(workspace.id!.value + ".000"); }); test("Should return an empty list", async () => { const { [CONTEXT]: ctx } = await idbBackend(); const workspace = await createTestWorkspace(ctx); const resp = fromBinary( ListResponseSchema, await list( toBinary( ListRequestSchema, create(ListRequestSchema, { workspaceId: workspace.id!, }), ), ctx, ), ); if (resp.result.case !== "ok") { expect.unreachable(`Expected "ok", found ${JSON.stringify(resp.result.case)}`); } expect(resp.result.value.workspaceId).toEqual(workspace.id!); expect(resp.result.value.workers).toEqual([]); }); async function createTestWorker( workspace: Workspace, { displayName }: Pick<MessageInitShape<typeof CreateRequestSchema>, "displayName">, ctx: Context, ): Promise<Worker> { const created = fromBinary( CreateResponseSchema, await createWorker( toBinary( CreateRequestSchema, create(CreateRequestSchema, { workspaceId: workspace.id!, workerAddKey: workspace.workerAddKey!, displayName: displayName!, }), ), ctx, ), ); if (created.result.case !== "ok") { expect.unreachable(`Failed to create worker: result.case=${created.result.case}`); } if (!created.result.value.worker) { expect.unreachable("Failed to create worker: worker is empty."); } return created.result.value.worker; } test("Should return a worker", async () => { const { [CONTEXT]: ctx } = await idbBackend(); const workspace = await createTestWorkspace(ctx); const alice = await createTestWorker(workspace, { displayName: "Alice" }, ctx); const resp = fromBinary( ListResponseSchema, await list( toBinary( ListRequestSchema, create(ListRequestSchema, { workspaceId: workspace.id!, }), ), ctx, ), ); if (resp.result.case !== "ok") { expect.unreachable(`Expected "ok", found ${JSON.stringify(resp.result.case)}`); } expect(resp.result.value.workspaceId).toEqual(workspace.id!); expect(resp.result.value.workers).toEqual([alice]); }); test("Should return workers in descending order", async () => { const { [CONTEXT]: ctx } = await idbBackend(); const workspace = await createTestWorkspace(ctx); const alice = await createTestWorker(workspace, { displayName: "Alice" }, ctx); await new Promise<void>((resolve) => { setTimeout(() => { resolve(); }, 100); }); const bob = await createTestWorker(workspace, { displayName: "Bob" }, ctx); const resp = fromBinary( ListResponseSchema, await list( toBinary( ListRequestSchema, create(ListRequestSchema, { workspaceId: workspace.id!, }), ), ctx, ), ); if (resp.result.case !== "ok") { expect.unreachable(`Expected "ok", found ${JSON.stringify(resp.result.case)}`); } expect(resp.result.value.workspaceId).toEqual(workspace.id!); expect(resp.result.value.workers).toEqual([bob, alice]); }); test("Should not return workers from other workspaces", async () => { const { [CONTEXT]: ctx } = await idbBackend(); const foo = await createTestWorkspace(ctx); const alice = await createTestWorker(foo, { displayName: "Alice" }, ctx); const bar = await createTestWorkspace(ctx); await createTestWorker(bar, { displayName: "Bob" }, ctx); const resp = fromBinary( ListResponseSchema, await list( toBinary( ListRequestSchema, create(ListRequestSchema, { workspaceId: foo.id!, }), ), ctx, ), ); if (resp.result.case !== "ok") { expect.unreachable(`Expected "ok", found ${JSON.stringify(resp.result.case)}`); } expect(resp.result.value.workspaceId).toEqual(foo.id!); expect(resp.result.value.workers).toEqual([alice]); });
-
-
-
@@ -0,0 +1,107 @@// 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 { ListRequestSchema, type ListRequest, } from "@yamori/proto/yamori/worker/v1/list_request_pb.js"; import { ListResponseSchema } from "@yamori/proto/yamori/worker/v1/list_response_pb.js"; import { WorkerSchema, type Worker } from "@yamori/proto/yamori/worker/v1/worker_pb.js"; import { type Context, type YamoriDB } from "../../../../types"; export function respond( result: Exclude< NonNullable<MessageInitShape<typeof ListResponseSchema>["result"]>, { case: undefined } >, ): Uint8Array { return toBinary(ListResponseSchema, create(ListResponseSchema, { result })); } export async function list(data: Uint8Array, { db }: Context): Promise<Uint8Array> { let req: ListRequest; try { req = fromBinary(ListRequestSchema, data); } catch (error) { return respond({ 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.workspaceId) { return respond({ case: "missingField", value: { path: "workspace_id", }, }); } let workspace: YamoriDB["workspaces"]["value"] | undefined; try { workspace = await db.get("workspaces", req.workspaceId.value); if (!workspace) { return respond({ case: "workspaceAccessError", value: { workspaceId: req.workspaceId, }, }); } } catch (error) { return respond({ case: "systemError", value: { code: "IDB_ERROR", message: import.meta.env.NODE_ENV === "production" ? "An error occurred during workspace lookup" : `Exception thrown during workspace lookup: ${error}`, }, }); } try { const tx = db.transaction("workers", "readonly"); const index = tx.store.index("workspaceId"); const workers: Worker[] = (await index.getAll(IDBKeyRange.only(workspace.id))) .sort((a, b) => b.updatedAt - a.updatedAt) .map((worker) => create(WorkerSchema, { id: { value: worker.id }, displayName: worker.displayName, }), ); await tx.done; return respond({ case: "ok", value: { workspaceId: { value: workspace.id }, workers, }, }); } catch (error) { return respond({ case: "systemError", value: { code: "IDB_ERROR", message: import.meta.env.NODE_ENV === "production" ? "An error occurred during listing workers" : `Exception thrown during listing workers: ${error}`, }, }); } }
-
-
-
@@ -4,6 +4,7 @@import { type Context, type RPCMessage } from "../../../../types"; import { create } from "./create"; import { list } from "./list"; export async function WorkerService( request: RPCMessage,
-
@@ -12,6 +13,8 @@ ): Promise<Uint8Array> {switch (request.method) { case "Create": return create(request.data, ctx); case "List": return list(request.data, ctx); default: throw new Error(`Unknown method "${request.method}" for ${request.service}`); }
-