Simplify plugin runtime and cleanup lifecycle
This commit is contained in:
@@ -1,10 +1,8 @@
|
||||
import { asc, eq, ne, sql, and, inArray } from "drizzle-orm";
|
||||
import { asc, eq, ne, sql, and } from "drizzle-orm";
|
||||
import type { Db } from "@paperclipai/db";
|
||||
import {
|
||||
plugins,
|
||||
companies,
|
||||
pluginConfig,
|
||||
pluginCompanySettings,
|
||||
pluginEntities,
|
||||
pluginJobs,
|
||||
pluginJobRuns,
|
||||
@@ -17,10 +15,6 @@ import type {
|
||||
UpdatePluginStatus,
|
||||
UpsertPluginConfig,
|
||||
PatchPluginConfig,
|
||||
PluginCompanySettings,
|
||||
CompanyPluginAvailability,
|
||||
UpsertPluginCompanySettings,
|
||||
UpdateCompanyPluginAvailability,
|
||||
PluginEntityRecord,
|
||||
PluginEntityQuery,
|
||||
PluginJobRecord,
|
||||
@@ -92,54 +86,6 @@ export function pluginRegistryService(db: Db) {
|
||||
return (result[0]?.maxOrder ?? 0) + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the persisted company override row for a plugin, if one exists.
|
||||
*
|
||||
* Missing rows are meaningful: the company inherits the default-enabled
|
||||
* behavior and the caller should treat the plugin as available.
|
||||
*/
|
||||
async function getCompanySettingsRow(companyId: string, pluginId: string) {
|
||||
return db
|
||||
.select()
|
||||
.from(pluginCompanySettings)
|
||||
.where(and(
|
||||
eq(pluginCompanySettings.companyId, companyId),
|
||||
eq(pluginCompanySettings.pluginId, pluginId),
|
||||
))
|
||||
.then((rows) => rows[0] ?? null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize registry records into the API response returned by company
|
||||
* plugin availability routes.
|
||||
*
|
||||
* The key business rule is captured here: plugins are enabled for a company
|
||||
* unless an explicit `plugin_company_settings.enabled = false` override says
|
||||
* otherwise.
|
||||
*/
|
||||
function toCompanyAvailability(
|
||||
companyId: string,
|
||||
plugin: Awaited<ReturnType<typeof getById>>,
|
||||
settings: PluginCompanySettings | null,
|
||||
): CompanyPluginAvailability {
|
||||
if (!plugin) {
|
||||
throw notFound("Plugin not found");
|
||||
}
|
||||
|
||||
return {
|
||||
companyId,
|
||||
pluginId: plugin.id,
|
||||
pluginKey: plugin.pluginKey,
|
||||
pluginDisplayName: plugin.manifestJson.displayName,
|
||||
pluginStatus: plugin.status,
|
||||
available: settings?.enabled ?? true,
|
||||
settingsJson: settings?.settingsJson ?? {},
|
||||
lastError: settings?.lastError ?? null,
|
||||
createdAt: settings?.createdAt ?? null,
|
||||
updatedAt: settings?.updatedAt ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Public API
|
||||
// -----------------------------------------------------------------------
|
||||
@@ -416,233 +362,6 @@ export function pluginRegistryService(db: Db) {
|
||||
.then((rows) => rows[0]);
|
||||
},
|
||||
|
||||
// ----- Company-scoped settings ----------------------------------------
|
||||
|
||||
/** Retrieve a plugin's company-scoped settings row, if any. */
|
||||
getCompanySettings: (companyId: string, pluginId: string) =>
|
||||
getCompanySettingsRow(companyId, pluginId),
|
||||
|
||||
/** Create or replace the company-scoped settings row for a plugin. */
|
||||
upsertCompanySettings: async (
|
||||
companyId: string,
|
||||
pluginId: string,
|
||||
input: UpsertPluginCompanySettings,
|
||||
) => {
|
||||
const plugin = await getById(pluginId);
|
||||
if (!plugin) throw notFound("Plugin not found");
|
||||
|
||||
const existing = await getCompanySettingsRow(companyId, pluginId);
|
||||
if (existing) {
|
||||
return db
|
||||
.update(pluginCompanySettings)
|
||||
.set({
|
||||
enabled: true,
|
||||
settingsJson: input.settingsJson ?? {},
|
||||
lastError: input.lastError ?? null,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(pluginCompanySettings.id, existing.id))
|
||||
.returning()
|
||||
.then((rows) => rows[0]);
|
||||
}
|
||||
|
||||
return db
|
||||
.insert(pluginCompanySettings)
|
||||
.values({
|
||||
companyId,
|
||||
pluginId,
|
||||
enabled: true,
|
||||
settingsJson: input.settingsJson ?? {},
|
||||
lastError: input.lastError ?? null,
|
||||
})
|
||||
.returning()
|
||||
.then((rows) => rows[0]);
|
||||
},
|
||||
|
||||
/** Delete the company-scoped settings row for a plugin if it exists. */
|
||||
deleteCompanySettings: async (companyId: string, pluginId: string) => {
|
||||
const plugin = await getById(pluginId);
|
||||
if (!plugin) throw notFound("Plugin not found");
|
||||
|
||||
const existing = await getCompanySettingsRow(companyId, pluginId);
|
||||
if (!existing) return null;
|
||||
|
||||
return db
|
||||
.delete(pluginCompanySettings)
|
||||
.where(eq(pluginCompanySettings.id, existing.id))
|
||||
.returning()
|
||||
.then((rows) => rows[0] ?? null);
|
||||
},
|
||||
|
||||
/** List normalized company-plugin availability records across installed plugins. */
|
||||
listCompanyAvailability: async (
|
||||
companyId: string,
|
||||
filter?: { available?: boolean },
|
||||
) => {
|
||||
const installed = await db
|
||||
.select()
|
||||
.from(plugins)
|
||||
.where(ne(plugins.status, "uninstalled"))
|
||||
.orderBy(asc(plugins.installOrder));
|
||||
|
||||
const settingsRows = await db
|
||||
.select()
|
||||
.from(pluginCompanySettings)
|
||||
.where(eq(pluginCompanySettings.companyId, companyId));
|
||||
|
||||
const settingsByPluginId = new Map(settingsRows.map((row) => [row.pluginId, row]));
|
||||
const availability = installed.map((plugin) => {
|
||||
const row = settingsByPluginId.get(plugin.id) ?? null;
|
||||
return {
|
||||
...toCompanyAvailability(companyId, plugin, row),
|
||||
};
|
||||
});
|
||||
|
||||
if (filter?.available === undefined) return availability;
|
||||
return availability.filter((item) => item.available === filter.available);
|
||||
},
|
||||
|
||||
/**
|
||||
* Batch-check which companies have this plugin explicitly disabled.
|
||||
* Returns a Set of companyIds where `enabled = false`. Companies with
|
||||
* no settings row default to enabled, so they are NOT in the result set.
|
||||
*/
|
||||
getDisabledCompanyIds: async (companyIds: string[], pluginId: string): Promise<Set<string>> => {
|
||||
if (companyIds.length === 0) return new Set();
|
||||
const rows = await db
|
||||
.select({
|
||||
companyId: pluginCompanySettings.companyId,
|
||||
enabled: pluginCompanySettings.enabled,
|
||||
})
|
||||
.from(pluginCompanySettings)
|
||||
.where(and(
|
||||
inArray(pluginCompanySettings.companyId, companyIds),
|
||||
eq(pluginCompanySettings.pluginId, pluginId),
|
||||
));
|
||||
const disabled = new Set<string>();
|
||||
for (const row of rows) {
|
||||
if (!row.enabled) disabled.add(row.companyId);
|
||||
}
|
||||
return disabled;
|
||||
},
|
||||
|
||||
/** Get the normalized availability record for a single company/plugin pair. */
|
||||
getCompanyAvailability: async (companyId: string, pluginId: string) => {
|
||||
const plugin = await getById(pluginId);
|
||||
if (!plugin || plugin.status === "uninstalled") return null;
|
||||
|
||||
const settings = await getCompanySettingsRow(companyId, pluginId);
|
||||
return toCompanyAvailability(companyId, plugin, settings);
|
||||
},
|
||||
|
||||
/** Update normalized company availability, persisting or deleting settings as needed. */
|
||||
updateCompanyAvailability: async (
|
||||
companyId: string,
|
||||
pluginId: string,
|
||||
input: UpdateCompanyPluginAvailability,
|
||||
) => {
|
||||
const plugin = await getById(pluginId);
|
||||
if (!plugin || plugin.status === "uninstalled") throw notFound("Plugin not found");
|
||||
|
||||
const existing = await getCompanySettingsRow(companyId, pluginId);
|
||||
|
||||
if (!input.available) {
|
||||
const row = await (existing
|
||||
? db
|
||||
.update(pluginCompanySettings)
|
||||
.set({
|
||||
enabled: false,
|
||||
settingsJson: input.settingsJson ?? existing.settingsJson,
|
||||
lastError: input.lastError ?? existing.lastError ?? null,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(pluginCompanySettings.id, existing.id))
|
||||
.returning()
|
||||
.then((rows) => rows[0])
|
||||
: db
|
||||
.insert(pluginCompanySettings)
|
||||
.values({
|
||||
companyId,
|
||||
pluginId,
|
||||
enabled: false,
|
||||
settingsJson: input.settingsJson ?? {},
|
||||
lastError: input.lastError ?? null,
|
||||
})
|
||||
.returning()
|
||||
.then((rows) => rows[0]));
|
||||
|
||||
return {
|
||||
...toCompanyAvailability(companyId, plugin, row),
|
||||
};
|
||||
}
|
||||
|
||||
const row = await (existing
|
||||
? db
|
||||
.update(pluginCompanySettings)
|
||||
.set({
|
||||
enabled: true,
|
||||
settingsJson: input.settingsJson ?? existing.settingsJson,
|
||||
lastError: input.lastError ?? existing.lastError ?? null,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(pluginCompanySettings.id, existing.id))
|
||||
.returning()
|
||||
.then((rows) => rows[0])
|
||||
: db
|
||||
.insert(pluginCompanySettings)
|
||||
.values({
|
||||
companyId,
|
||||
pluginId,
|
||||
enabled: true,
|
||||
settingsJson: input.settingsJson ?? {},
|
||||
lastError: input.lastError ?? null,
|
||||
})
|
||||
.returning()
|
||||
.then((rows) => rows[0]));
|
||||
|
||||
return {
|
||||
...toCompanyAvailability(companyId, plugin, row),
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Ensure all companies have an explicit enabled row for this plugin.
|
||||
*
|
||||
* Company availability defaults to enabled when no row exists, but this
|
||||
* helper persists explicit `enabled=true` rows so newly-installed plugins
|
||||
* appear as enabled immediately and consistently in company-scoped views.
|
||||
*/
|
||||
seedEnabledForAllCompanies: async (pluginId: string) => {
|
||||
const plugin = await getById(pluginId);
|
||||
if (!plugin || plugin.status === "uninstalled") throw notFound("Plugin not found");
|
||||
|
||||
const companyRows = await db
|
||||
.select({ id: companies.id })
|
||||
.from(companies);
|
||||
|
||||
if (companyRows.length === 0) return 0;
|
||||
|
||||
const now = new Date();
|
||||
await db
|
||||
.insert(pluginCompanySettings)
|
||||
.values(
|
||||
companyRows.map((company) => ({
|
||||
companyId: company.id,
|
||||
pluginId,
|
||||
enabled: true,
|
||||
settingsJson: {},
|
||||
lastError: null,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
})),
|
||||
)
|
||||
.onConflictDoNothing({
|
||||
target: [pluginCompanySettings.companyId, pluginCompanySettings.pluginId],
|
||||
});
|
||||
|
||||
return companyRows.length;
|
||||
},
|
||||
|
||||
/**
|
||||
* Record an error against a plugin's config (e.g. validation failure
|
||||
* against the plugin's instanceConfigSchema).
|
||||
|
||||
Reference in New Issue
Block a user