206 lines · 6.8 KB · TypeScript
RawDownload
import { Data, Effect } from "effect";import {  definePluginStorageCollection,  sha256Hex,  type Owner,  type PluginBlobStore,  type PluginStorageFacade,  type StorageFailure,} from "@executor-js/sdk"; import type { AppDescriptor } from "../pipeline/descriptor"; export class AppPublishConflictError extends Data.TaggedError("AppPublishConflictError")<{  readonly app: string;  readonly expectedSourceRef: string | null;  readonly actualSourceRef: string | null;}> {} export const descriptorCollection = definePluginStorageCollection("apps_descriptors", {  Type: {} as {    readonly app: string;    readonly sourceRef: string;    readonly descriptorKey: string;    readonly publishedAt: number;    readonly toolchain: AppDescriptor["toolchain"];  },}); export const toolCollection = definePluginStorageCollection(  "apps_tools",  {    Type: {} as {      readonly app: string;      readonly name: string;      readonly sourceRef: string;      readonly descriptorKey: string;      readonly bundleKey: string;      readonly description: string;      readonly inputSchema?: unknown;      readonly outputSchema?: unknown;      readonly integrations: AppDescriptor["tools"][number]["integrations"];      readonly annotations?: AppDescriptor["tools"][number]["annotations"];      readonly tombstoned: boolean;      readonly updatedAt: number;    },  },  { indexes: ["app", "name", "tombstoned"] },); export interface AppsStore {  readonly putBlob: (body: string, owner: Owner) => Effect.Effect<string, StorageFailure>;  readonly getBlob: (key: string) => Effect.Effect<string | null, StorageFailure>;  readonly getDescriptorRecord: (app: string) => Effect.Effect<    {      readonly sourceRef: string;      readonly descriptorKey: string;    } | null,    StorageFailure  >;  readonly putPublished: (    descriptor: AppDescriptor,    descriptorKey: string,    owner: Owner,    expectedSourceRef: string | null,  ) => Effect.Effect<void, StorageFailure | AppPublishConflictError>;  readonly listActiveTools: () => Effect.Effect<    readonly AppDescriptor["tools"][number][],    StorageFailure  >;  readonly getTool: (name: string) => Effect.Effect<    {      readonly app: string;      readonly name: string;      readonly bundleKey: string;      readonly description: string;      readonly inputSchema?: unknown;      readonly outputSchema?: unknown;      readonly integrations: AppDescriptor["tools"][number]["integrations"];      readonly annotations?: AppDescriptor["tools"][number]["annotations"];    } | null,    StorageFailure  >;} interface PutManyEntry {  readonly collection: string;  readonly key: string;  readonly data: unknown;} export const makeAppsStore = (input: {  readonly blobs: PluginBlobStore;  readonly pluginStorage: PluginStorageFacade;}): AppsStore => {  const descriptors = input.pluginStorage.collection(descriptorCollection);  const tools = input.pluginStorage.collection(toolCollection);  const keyForTool = (app: string, name: string) => `${app}:${name}`;  return {    putBlob: (body, owner) =>      sha256Hex(body).pipe(        Effect.flatMap((hash) =>          input.blobs.put(`apps/${hash}`, body, { owner }).pipe(Effect.as(`apps/${hash}`)),        ),      ),    getBlob: (key) => input.blobs.get(key),    getDescriptorRecord: (app) =>      descriptors.get({ key: app }).pipe(        Effect.map((entry) =>          entry            ? {                sourceRef: entry.data.sourceRef,                descriptorKey: entry.data.descriptorKey,              }            : null,        ),      ),    putPublished: (descriptor, descriptorKey, owner, expectedSourceRef) =>      Effect.gen(function* () {        const current = yield* descriptors.get({ key: descriptor.app });        const actualSourceRef = current?.data.sourceRef ?? null;        if (actualSourceRef !== expectedSourceRef) {          return yield* new AppPublishConflictError({            app: descriptor.app,            expectedSourceRef,            actualSourceRef,          });        }        const now = descriptor.publishedAt;        const existing = yield* tools.query({ where: { app: descriptor.app } });        const activeNames = new Set(descriptor.tools.map((tool) => tool.name));        const entries: PutManyEntry[] = descriptor.tools.map((tool) => ({          collection: toolCollection.name,          key: keyForTool(descriptor.app, tool.name),          data: {            app: descriptor.app,            name: tool.name,            sourceRef: descriptor.sourceRef,            descriptorKey,            bundleKey: tool.bundleKey,            description: tool.description,            inputSchema: tool.inputSchema,            outputSchema: tool.outputSchema,            integrations: tool.integrations,            annotations: tool.annotations,            tombstoned: false,            updatedAt: now,          },        }));        for (const entry of existing) {          if (!activeNames.has(entry.data.name) && !entry.data.tombstoned) {            entries.push({              collection: toolCollection.name,              key: entry.key,              data: { ...entry.data, tombstoned: true, updatedAt: now },            });          }        }        yield* input.pluginStorage.putMany({ owner, entries });        yield* descriptors.put({          owner,          key: descriptor.app,          data: {            app: descriptor.app,            sourceRef: descriptor.sourceRef,            descriptorKey,            publishedAt: descriptor.publishedAt,            toolchain: descriptor.toolchain,          },        });      }),    listActiveTools: () =>      tools.query({ where: { tombstoned: false } }).pipe(        Effect.map((entries) =>          entries.map((entry) => ({            name: entry.data.name,            sourcePath: "",            source: { path: "", sourceHash: "" },            bundleKey: entry.data.bundleKey,            description: entry.data.description,            integrations: entry.data.integrations,            inputSchema: entry.data.inputSchema,            outputSchema: entry.data.outputSchema,            annotations: entry.data.annotations,          })),        ),      ),    getTool: (name) =>      tools.query({ where: { name, tombstoned: false }, limit: 1 }).pipe(        Effect.map((entries) => {          const entry = entries[0];          return entry            ? {                app: entry.data.app,                name: entry.data.name,                bundleKey: entry.data.bundleKey,                description: entry.data.description,                inputSchema: entry.data.inputSchema,                outputSchema: entry.data.outputSchema,                integrations: entry.data.integrations,                annotations: entry.data.annotations,              }            : null;        }),      ),  };};