Changes
5 changed files (+434/-1)
-
-
@@ -21,8 +21,9 @@ }export async function idbBackend(): Promise<IDBBackend> { return new IDBBackend({ db: await openDB("yamori", 2, { db: await openDB("yamori", 3, { async upgrade(db, oldVersion, _newVersion, transaction, _event) { // v1 if (oldVersion < 1) { const workspaces = db.createObjectStore("workspaces", { keyPath: "id",
-
@@ -33,6 +34,7 @@ unique: false,}); } // v2 if (oldVersion < 2) { const store = transaction.objectStore("workspaces");
-
@@ -55,6 +57,21 @@ workerAddKey,}, }); } } // v3 if (oldVersion < 3) { const workers = db.createObjectStore("workers", { keyPath: "id", }); workers.createIndex("workspaceId", "workspaceId", { unique: false, }); workers.createIndex("updatedAt", "updatedAt", { unique: false, }); } }, }),
-
-
-
@@ -20,6 +20,21 @@ updatedAt: number;}; indexes: { updatedAt: number }; }; workers: { key: string; value: { id: string; workspaceId: string; displayName: string; createdAt: number; updatedAt: number; }; indexes: { workspaceId: string; updatedAt: number; }; }; } export interface Context {
-
-
-
@@ -0,0 +1,235 @@// 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 } from "@bufbuild/protobuf"; 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 { 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 { IDBFactory } from "fake-indexeddb"; import { openDB } from "idb"; import { idbBackend } from "../../../../lib"; import { CONTEXT } from "../../../../symbols"; import { type Context } from "../../../../types"; import { create as method } from "./create"; import { create as createWorkspace } from "../../../workspace/v1/key_value_storage_based_workspace_service/create"; beforeEach(() => { indexedDB = new IDBFactory(); }); test("Should return an error on DB error", async () => { const resp = fromBinary( CreateResponseSchema, await method( toBinary( CreateRequestSchema, create(CreateRequestSchema, { workspaceId: { value: "ws-foo", }, displayName: "Alice", workerAddKey: { key: new Uint8Array([]) }, }), ), { db: await openDB("test"), }, ), ); if (resp.result.case !== "systemError") { expect.unreachable(); } 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 if display_name is empty", async () => { const { [CONTEXT]: ctx } = await idbBackend(); const workspace = await createTestWorkspace(ctx); const resp = fromBinary( CreateResponseSchema, await method( toBinary( CreateRequestSchema, create(CreateRequestSchema, { workspaceId: workspace.id!, workerAddKey: workspace.workerAddKey!, }), ), ctx, ), ); if (resp.result.case !== "missingField") { expect.unreachable(`Expected "missing_field", found ${resp.result.case}`); } expect(resp.result.value.path).toBe("display_name"); }); test("Should return an error if workspace_id is empty", async () => { const { [CONTEXT]: ctx } = await idbBackend(); const workspace = await createTestWorkspace(ctx); const resp = fromBinary( CreateResponseSchema, await method( toBinary( CreateRequestSchema, create(CreateRequestSchema, { workerAddKey: workspace.workerAddKey!, displayName: "Alice", }), ), ctx, ), ); if (resp.result.case !== "missingField") { expect.unreachable(`Expected "missing_field", found ${resp.result.case}`); } expect(resp.result.value.path).toBe("workspace_id"); }); test("Should return an error if worker_add_key is empty", async () => { const { [CONTEXT]: ctx } = await idbBackend(); const workspace = await createTestWorkspace(ctx); const resp = fromBinary( CreateResponseSchema, await method( toBinary( CreateRequestSchema, create(CreateRequestSchema, { workspaceId: workspace.id!, displayName: "Alice", }), ), ctx, ), ); if (resp.result.case !== "missingField") { expect.unreachable(`Expected "missing_field", found ${resp.result.case}`); } expect(resp.result.value.path).toBe("worker_add_key"); }); test("Should return an error if target workspace does not exist", async () => { const { [CONTEXT]: ctx } = await idbBackend(); const resp = fromBinary( CreateResponseSchema, await method( toBinary( CreateRequestSchema, create(CreateRequestSchema, { workspaceId: { value: "ws-foo" }, workerAddKey: { key: new Uint8Array([]) }, displayName: "Alice", }), ), ctx, ), ); if (resp.result.case !== "workspaceAccessError") { expect.unreachable(`Expected "workspace_access_error", found ${resp.result.case}`); } expect(resp.result.value.workspaceId?.value).toBe("ws-foo"); }); test("Should return an error for capability mismatch", async () => { const { [CONTEXT]: ctx } = await idbBackend(); const workspace = await createTestWorkspace(ctx); const resp = fromBinary( CreateResponseSchema, await method( toBinary( CreateRequestSchema, create(CreateRequestSchema, { workspaceId: workspace.id!, workerAddKey: { key: new Uint8Array([]) }, displayName: "Alice", }), ), ctx, ), ); if (resp.result.case !== "capabilityError") { expect.unreachable(`Expected "capability_error", found ${resp.result.case}`); } expect(resp.result.value.path).toBe("worker_add_key"); }); test("Should add a worker entry", async () => { const { [CONTEXT]: ctx } = await idbBackend(); const workspace = await createTestWorkspace(ctx); const resp = fromBinary( CreateResponseSchema, await method( toBinary( CreateRequestSchema, create(CreateRequestSchema, { workspaceId: workspace.id!, workerAddKey: workspace.workerAddKey!, displayName: "Alice", }), ), ctx, ), ); if (resp.result.case !== "ok") { expect.unreachable(`Expected "ok", found ${resp.result.case}`); } expect(resp.result.value.worker?.id?.value).not.toBeEmpty(); expect(resp.result.value.worker?.displayName).toBe("Alice"); });
-
-
-
@@ -0,0 +1,148 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only import { create as createMessage, type MessageInitShape, fromBinary, toBinary, } from "@bufbuild/protobuf"; import { CreateRequestSchema, type CreateRequest, } from "@yamori/proto/yamori/worker/v1/create_request_pb.js"; import { CreateResponseSchema } from "@yamori/proto/yamori/worker/v1/create_response_pb.js"; import { isSameBytes } from "../../../../helpers"; import { type Context } from "../../../../types"; export function respond( result: Exclude< NonNullable<MessageInitShape<typeof CreateResponseSchema>["result"]>, { case: undefined } >, ): Uint8Array { return toBinary(CreateResponseSchema, createMessage(CreateResponseSchema, { result })); } export async function create(data: Uint8Array, { db }: Context): Promise<Uint8Array> { let req: CreateRequest; try { req = fromBinary(CreateRequestSchema, 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.displayName) { return respond({ case: "missingField", value: { path: "display_name", }, }); } if (!req.workspaceId) { return respond({ case: "missingField", value: { path: "workspace_id", }, }); } if (!req.workerAddKey) { return respond({ case: "missingField", value: { path: "worker_add_key", }, }); } // TODO: Handle idempotency_key (how?) try { const found = await db.get("workspaces", req.workspaceId.value); if (!found) { return respond({ case: "workspaceAccessError", value: { workspaceId: req.workspaceId, }, }); } if (!isSameBytes(req.workerAddKey.key, found.capabilities.workerAddKey)) { return respond({ case: "capabilityError", value: { path: "worker_add_key", }, }); } } 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}`, }, }); } const id = "wr-" + crypto.randomUUID(); try { const tx = db.transaction("workers", "readwrite"); const key = await tx.store.add({ id, workspaceId: req.workspaceId.value, displayName: req.displayName, createdAt: Date.now(), updatedAt: Date.now(), }); const added = await tx.store.get(key); if (!added) { throw "Failed to query an added entry"; } await tx.done; return respond({ case: "ok", value: { worker: { id: { value: added.id }, displayName: added.displayName, }, }, }); } catch (error) { return respond({ case: "systemError", value: { code: "IDB_ERROR", message: import.meta.env.NODE_ENV === "production" ? "Failed to create a new worker record" : `Exception thrown during inserting worker: ${error}`, }, }); } }
-
-
-
@@ -0,0 +1,18 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only import { type Context, type RPCMessage } from "../../../../types"; import { create } from "./create"; export async function WorkerService( request: RPCMessage, ctx: Context, ): Promise<Uint8Array> { switch (request.method) { case "Create": return create(request.data, ctx); default: throw new Error(`Unknown method "${request.method}" for ${request.service}`); } }
-