Skip to main content
Glama
module.store.ts20.7 kB
import { defineStore } from "pinia"; import * as _ from "lodash-es"; import { useToast } from "vue-toastification"; import { addStoreHooks, ApiRequest } from "@si/vue-lib/pinia"; import { URLPattern } from "@si/vue-lib"; import { useWorkspacesStore } from "@/store/workspaces.store"; import { ChangeSetId } from "@/api/sdf/dal/change_set"; import { SchemaId, SchemaVariantId } from "@/api/sdf/dal/schema"; import { Visibility } from "@/api/sdf/dal/visibility"; import { LatestModule, ModuleContributeRequest, ModuleId, } from "@/api/sdf/dal/module"; import { useChangeSetsStore } from "./change_sets.store"; import { useRouterStore } from "./router.store"; import { useRealtimeStore } from "./realtime/realtime.store"; import { ModuleIndexApiRequest } from "."; export type ModuleName = string; export type ModuleHash = string; export type ModuleSlug = ModuleHash; export interface ModuleFuncView { name: string; displayName?: string; description?: string; } export interface LocalModuleSummary { name: string; hash: ModuleHash; isBuiltin: boolean; } export interface LocalModuleDetails { name: string; version: string; description: string; createdAt: IsoDateString; createdBy: string; schemas: string[]; funcs: ModuleFuncView[]; hash: ModuleHash; kind: "module" | "workspaceExport"; } export interface ModuleSpec { name: string; createdAt: IsoDateString; createdBy: string; version: string; funcs: { arguments: { name: string; kind: string; elementKind: string | null; }[]; data: { backendKind: string; codeBase64: string | null; description: string | null; displayName: string | null; handler: string | null; hidden: boolean; link: string | null; name: string; responseType: string; }; name: string; uniqueId: string; }[]; } export type Asset = { id: number; displayName: string; }; export type RemoteModuleSummary = { id: ModuleId; name: string; description: string; createdAt: IsoDateString; hash: ModuleHash; hashCreatedAt: IsoDateString; ownerDisplayName: string; ownerUserId: string; // userid? isBuiltin: boolean; // only set for builtins }; export type RemoteModuleDetails = RemoteModuleSummary & { metadata?: { schemas: string[]; funcs: ModuleFuncView[]; version: string; }; }; // Gather the current change set ID, since our components don't appear to be // reacting to the visibility changes that happen on the setup code // TODO: Generalize this and make the api client pass these arguments implicitly function getVisibilityParams(forceChangeSetId?: ChangeSetId) { const changeSetsStore = useChangeSetsStore(); let changeSetId = changeSetsStore.selectedChangeSetId; if (forceChangeSetId) { changeSetId = forceChangeSetId; } const workspacesStore = useWorkspacesStore(); const workspaceId = workspacesStore.selectedWorkspacePk; return { visibility_change_set_pk: changeSetId, workspaceId, }; } export const useModuleStore = () => { const changeSetsStore = useChangeSetsStore(); const changeSetId = changeSetsStore.selectedChangeSetId; const visibility = { // changeSetId should not be empty if we are actually using this store // so we can give it a bad value and let it throw an error visibility_change_set_pk: changeSetId || "XXX", }; const workspacesStore = useWorkspacesStore(); const workspaceId = workspacesStore.selectedWorkspacePk; const routerStore = useRouterStore(); const realtimeStore = useRealtimeStore(); const toast = useToast(); const API_PREFIX = [ "v2", "workspaces", { workspaceId }, "change-sets", { changeSetId }, "modules", ] as URLPattern; const WORKSPACE_API_PREFIX = ["v2", "workspaces", { workspaceId }]; return addStoreHooks( workspaceId, changeSetId, defineStore( `ws${workspaceId || "NONE"}/cs${changeSetId || "NONE"}/modules`, { state: () => ({ updatingModulesOperationId: null as string | null, updatingModulesOperationError: undefined as string | undefined, updatingModulesOperationRunning: false as boolean, upgradeableModules: {} as Record<SchemaVariantId, LatestModule>, contributableModules: [] as SchemaVariantId[], localModulesByName: {} as Record<ModuleName, LocalModuleSummary>, localModuleDetailsByName: {} as Record< ModuleName, LocalModuleDetails >, remoteModuleList: [] as RemoteModuleSummary[], builtinsSearchResults: [] as RemoteModuleSummary[], remoteModuleDetailsById: {} as Record<ModuleId, RemoteModuleDetails>, remoteModuleSpecsById: {} as Record<ModuleId, ModuleSpec>, exportingWorkspaceOperationId: null as string | null, exportingWorkspaceOperationError: undefined as string | undefined, exportingWorkspaceOperationRunning: false as boolean, }), getters: { urlSelectedModuleSlug: () => { const route = useRouterStore().currentRoute; return route?.params?.moduleSlug as ModuleSlug | undefined; }, localModules: (state) => _.values(state.localModulesByName), localModulesByHash(): Record<ModuleSlug, LocalModuleSummary> { return _.keyBy(this.localModules, (m) => m.hash); }, localModuleDetails: (state) => _.values(state.localModuleDetailsByName), localModuleDetailsByHash(): Record<ModuleSlug, LocalModuleDetails> { return _.keyBy(this.localModuleDetails, (m) => m.hash); }, remoteModuleSummaryByHash: (state) => { return _.keyBy(state.remoteModuleList, (m) => m.hash); }, remoteModuleDetailsByHash: (state) => { return _.keyBy( _.values(state.remoteModuleDetailsById), (m) => m.hash, ); }, builtins: (state) => _.values(state.builtinsSearchResults), builtinModuleSummaryByHash: (state) => _.keyBy(state.builtinsSearchResults, (m) => m.hash), builtinModuleDetailsByHash: (state) => _.keyBy(state.builtinsSearchResults, (m) => m.hash), selectedModuleLocalSummary(): LocalModuleSummary | undefined { if (!this.urlSelectedModuleSlug) return undefined; return this.localModulesByHash[this.urlSelectedModuleSlug]; }, selectedModuleLocalDetails(): LocalModuleDetails | undefined { if (!this.urlSelectedModuleSlug) return undefined; return this.localModuleDetailsByHash[this.urlSelectedModuleSlug]; }, selectedModuleRemoteSummary(): RemoteModuleDetails | undefined { if (!this.urlSelectedModuleSlug) return undefined; return this.remoteModuleSummaryByHash[this.urlSelectedModuleSlug]; }, selectedModuleRemoteDetails(): RemoteModuleDetails | undefined { if (!this.urlSelectedModuleSlug) return undefined; return this.remoteModuleDetailsByHash[this.urlSelectedModuleSlug]; }, selectedBuiltinModuleDetails(): RemoteModuleDetails | undefined { if (!this.urlSelectedModuleSlug) return undefined; return this.builtinModuleDetailsByHash[this.urlSelectedModuleSlug]; }, selectedBuiltinModuleSummary(): RemoteModuleDetails | undefined { if (!this.urlSelectedModuleSlug) return undefined; return this.builtinModuleSummaryByHash[this.urlSelectedModuleSlug]; }, }, actions: { // Modules API V2 async SYNC() { return new ApiRequest< { upgradeable: Record<SchemaVariantId, LatestModule>; contributable: SchemaVariantId[]; }, Visibility >({ url: API_PREFIX.concat(["sync"]), params: { ...visibility }, onSuccess: (response) => { this.upgradeableModules = response.upgradeable; this.contributableModules = response.contributable; }, }); }, async CONTRIBUTE(request: ModuleContributeRequest) { return new ApiRequest({ method: "post", url: API_PREFIX.concat(["contribute"]), params: { name: request.name, version: request.version, schemaVariantId: request.schemaVariantId, isPrivateModule: request.isPrivateModule, }, }); }, async LOAD_LOCAL_MODULES() { return new ApiRequest<LocalModuleSummary[]>({ method: "get", url: API_PREFIX, onSuccess: (response) => { // TODO: remove this // the backend currently needs the full tar file name // but we want the actual name in the module metadata // easier to strip off temporarily but we'll need to change what the backend is storing const modulesWithNamesFixed = _.map(response, (m) => ({ ...m, name: m.name.replace(/-\d\d\d\d-\d\d-\d\d\.sipkg/, ""), })); this.localModulesByName = _.keyBy( modulesWithNamesFixed, (m) => m.name, ); }, }); }, async GET_LOCAL_MODULE_DETAILS(hash: ModuleHash) { return new ApiRequest<LocalModuleDetails>({ method: "get", url: API_PREFIX.concat(["module_by_hash"]), params: { hash }, onSuccess: (response) => { this.localModuleDetailsByName[response.name] = response; }, }); }, async GET_REMOTE_MODULE_SPEC(id: ModuleId) { return new ApiRequest<ModuleSpec>({ method: "get", url: API_PREFIX.concat(["module_by_id"]), keyRequestStatusBy: id, params: { id }, onSuccess: (response) => { this.remoteModuleSpecsById[id] = response; }, }); }, // Module Index API async LIST_WORKSPACE_EXPORTS() { return new ModuleIndexApiRequest<{ modules: (RemoteModuleSummary & { latestHash: ModuleHash; latestHashCreatedAt: IsoDateString; })[]; }>({ method: "get", url: "/modules", params: { kind: "workspaceBackup" }, }); }, async LIST_BUILTINS() { return new ModuleIndexApiRequest<{ modules: (RemoteModuleSummary & { latestHash: ModuleHash; latestHashCreatedAt: IsoDateString; })[]; }>({ method: "get", url: "/builtins", onSuccess: (response) => { this.builtinsSearchResults = _.map(response.modules, (m) => ({ ...m, hash: m.latestHash, hashCreatedAt: m.latestHashCreatedAt, isBuiltin: true, })); }, }); }, async GET_REMOTE_MODULES_LIST(params?: { kind?: string; su?: boolean; }) { return new ModuleIndexApiRequest<{ modules: (RemoteModuleSummary & { latestHash: ModuleHash; latestHashCreatedAt: IsoDateString; })[]; }>({ method: "get", url: "/modules", params, onSuccess: (response) => { this.remoteModuleList = _.map(response.modules, (m) => ({ ...m, hash: m.latestHash, hashCreatedAt: m.latestHashCreatedAt, })); }, }); }, async GET_REMOTE_MODULE_DETAILS(id: ModuleId) { return new ModuleIndexApiRequest<RemoteModuleDetails>({ method: "get", url: `/modules/${id}`, onSuccess: ( response: RemoteModuleDetails & { latestHash: ModuleHash; latestHashCreatedAt: IsoDateString; }, ) => { response.hash = response.latestHash; response.hashCreatedAt = response.latestHashCreatedAt; this.remoteModuleDetailsById[response.id] = response; }, }); }, async INSTALL_REMOTE_MODULE(moduleIds: ModuleId[]) { if (changeSetsStore.creatingChangeSet) { throw new Error("race, wait until the change set is created"); } if (changeSetId === changeSetsStore.headChangeSetId) { changeSetsStore.creatingChangeSet = true; } return new ApiRequest<{ id: string }>({ method: "post", url: "/module/install_module", keyRequestStatusBy: moduleIds, params: { ids: moduleIds, ...getVisibilityParams(), }, onFail: () => { changeSetsStore.creatingChangeSet = false; }, onSuccess: () => { // reset installed list this.SYNC(); }, }); }, async UPGRADE_MODULES(schemaIds: SchemaId[]) { if (changeSetsStore.creatingChangeSet) { throw new Error("race, wait until the change set is created"); } if (changeSetId === changeSetsStore.headChangeSetId) { changeSetsStore.creatingChangeSet = true; } this.updatingModulesOperationRunning = true; this.updatingModulesOperationId = null; this.updatingModulesOperationError = undefined; return new ApiRequest<string>({ method: "post", url: "/module/upgrade_modules", keyRequestStatusBy: schemaIds, params: { schemaIds, ...getVisibilityParams(), }, optimistic: () => { toast("Upgrading modules..."); }, onFail: () => { changeSetsStore.creatingChangeSet = false; this.updatingModulesOperationRunning = false; }, onSuccess: (response) => { this.updatingModulesOperationId = response; }, }); }, async INSTALL_MODULE_FROM_FILE(file: File) { if (changeSetsStore.creatingChangeSet) { throw new Error("race, wait until the change set is created"); } if (changeSetId === changeSetsStore.headChangeSetId) { changeSetsStore.creatingChangeSet = true; } const formData = new FormData(); formData.append("pkg_spec", file); return new ApiRequest<{ id: string }>({ method: "post", url: API_PREFIX.concat(["install_from_file"]), headers: { "Content-Type": "multipart/form-data", }, formData, onFail: () => { changeSetsStore.creatingChangeSet = false; }, onSuccess: () => { // reset installed list this.SYNC(); }, }); }, async REJECT_REMOTE_MODULE(moduleId: ModuleId) { return new ApiRequest<{ success: true }>({ method: "post", url: API_PREFIX.concat([{ moduleId }, "builtins", "reject"]), params: { id: moduleId, ...getVisibilityParams() }, optimistic: () => { // remove selection from URL routerStore.replace(changeSetId, { name: "workspace-lab-packages", }); }, onSuccess: (_response) => { // response is just success, so we have to reload the remote modules this.LOAD_LOCAL_MODULES(); this.GET_REMOTE_MODULES_LIST({ su: true }); }, }); }, async PROMOTE_TO_BUILTIN(moduleId: ModuleId) { return new ApiRequest<{ success: true }>({ method: "post", url: API_PREFIX.concat([{ moduleId }, "builtins", "promote"]), optimistic: () => { // remove selection from URL routerStore.replace(changeSetId, { name: "workspace-lab-packages", }); }, onSuccess: (_response) => { // response is just success, so we have to reload the remote modules this.LOAD_LOCAL_MODULES(); this.GET_REMOTE_MODULES_LIST({ su: true }); }, }); }, async EXPORT_WORKSPACE() { this.exportingWorkspaceOperationId = null; this.exportingWorkspaceOperationError = undefined; this.exportingWorkspaceOperationRunning = true; return new ApiRequest<{ id: string }>({ method: "post", url: WORKSPACE_API_PREFIX.concat(["export"]), onSuccess: (response) => { this.exportingWorkspaceOperationId = response.id; }, onFail: () => { this.exportingWorkspaceOperationRunning = false; }, }); }, resetExportWorkspaceStatus() { this.exportingWorkspaceOperationRunning = false; this.exportingWorkspaceOperationId = null; this.exportingWorkspaceOperationError = undefined; }, registerRequestsBegin(requestUlid: string, actionName: string) { realtimeStore.inflightRequests.set(requestUlid, actionName); }, registerRequestsEnd(requestUlid: string) { realtimeStore.inflightRequests.delete(requestUlid); }, }, async onActivated() { // This store is activated very early, and we might not have a change // set yet. This guards against an errant 500 in that case. const visibilityParams = getVisibilityParams(); if ( visibilityParams.workspaceId && visibilityParams.visibility_change_set_pk ) { await this.LOAD_LOCAL_MODULES(); } realtimeStore.subscribe(this.$id, `workspace/${workspaceId}`, [ { eventType: "AsyncFinish", callback: async ({ id }: { id: string }) => { if (id === this.exportingWorkspaceOperationId) { this.exportingWorkspaceOperationRunning = false; } if (id === this.updatingModulesOperationId) { this.updatingModulesOperationRunning = false; this.SYNC(); } }, }, { eventType: "ModuleImported", callback: (schemaVariants, metadata) => { if (metadata.change_set_id !== changeSetId) return; for (const variant of schemaVariants) { delete this.upgradeableModules[variant.schemaVariantId]; } }, }, { eventType: "AsyncError", callback: async ({ id, error, }: { id: string; error: string; }) => { if (id === this.exportingWorkspaceOperationId) { this.exportingWorkspaceOperationError = error; this.exportingWorkspaceOperationRunning = false; } if (id === this.updatingModulesOperationId) { this.updatingModulesOperationError = error; this.updatingModulesOperationRunning = false; } }, }, ]); return () => { realtimeStore.unsubscribe(this.$id); }; }, }, ), )(); };

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/systeminit/si'

If you have feedback or need assistance with the MCP directory API, please join our Discord server