diff --git a/src/ingress/bindings-port.test.ts b/src/ingress/bindings-port.test.ts new file mode 100644 index 0000000..fcfdde0 --- /dev/null +++ b/src/ingress/bindings-port.test.ts @@ -0,0 +1,42 @@ +import { describe, expect, test } from "bun:test"; +import type { GranolaBindingsPort } from "./bindings-port.js"; + +type FakeBinding = { folderId: string }; + +function fakePort(): { + port: GranolaBindingsPort; + saved: { tenantId: string; principalId: string; bindings: FakeBinding[] }[]; +} { + const saved: { tenantId: string; principalId: string; bindings: FakeBinding[] }[] = []; + let stored: { bindings: FakeBinding[]; version: number } | undefined; + + const port: GranolaBindingsPort = { + async load({ tenantId: _tenantId }) { + return stored; + }, + async save({ tenantId, principalId, bindings }) { + saved.push({ tenantId, principalId, bindings }); + stored = { bindings, version: (stored?.version ?? 0) + 1 }; + }, + }; + + return { port, saved }; +} + +describe("GranolaBindingsPort", () => { + test("load returns undefined when nothing has ever been saved", async () => { + const { port } = fakePort(); + expect(await port.load({ tenantId: "t1" })).toBeUndefined(); + }); + + test("save then load round-trips the bindings and bumps the version", async () => { + const { port, saved } = fakePort(); + const bindings: FakeBinding[] = [{ folderId: "fol_1" }]; + + await port.save({ tenantId: "t1", principalId: "p1", bindings }); + const loaded = await port.load({ tenantId: "t1" }); + + expect(loaded).toEqual({ bindings, version: 1 }); + expect(saved).toEqual([{ tenantId: "t1", principalId: "p1", bindings }]); + }); +}); diff --git a/src/ingress/bindings-port.ts b/src/ingress/bindings-port.ts new file mode 100644 index 0000000..1194448 --- /dev/null +++ b/src/ingress/bindings-port.ts @@ -0,0 +1,19 @@ +// The persistence seam for durable Granola bucket bindings (folder id -> +// workflow type -> chat channel). This package never persists anything +// itself — a host composing @corbits/granola/ingress supplies a concrete +// GranolaBindingsPort backed by whatever storage it already has (a +// database row, a config artifact, a file — this package doesn't care). +// Generic over the binding shape so this file has zero dependency on any +// host-specific or product-specific type. + +export interface GranolaBindingsLoadResult { + bindings: Binding[]; + version: number; +} + +export interface GranolaBindingsPort { + /** Current persisted bindings for a tenant, or `undefined` if none have ever been saved. */ + load(args: { tenantId: string }): Promise | undefined>; + /** Persists a whole new binding set as the tenant's current bindings. */ + save(args: { tenantId: string; principalId: string; bindings: Binding[] }): Promise; +} diff --git a/src/ingress/index.ts b/src/ingress/index.ts index cab33e3..fa47930 100644 --- a/src/ingress/index.ts +++ b/src/ingress/index.ts @@ -5,3 +5,4 @@ // structurally enforced. export { verifyGranolaSignature } from "./webhook.js"; export type { GranolaIngressOptions, GranolaWebhookHandler } from "./webhook.js"; +export type { GranolaBindingsPort, GranolaBindingsLoadResult } from "./bindings-port.js";