@checkstack/backend 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +225 -0
- package/drizzle/0000_loose_yellow_claw.sql +28 -0
- package/drizzle/meta/0000_snapshot.json +187 -0
- package/drizzle/meta/_journal.json +13 -0
- package/drizzle.config.ts +10 -0
- package/package.json +42 -0
- package/src/db.ts +20 -0
- package/src/health-check-plugin-integration.test.ts +93 -0
- package/src/index.ts +419 -0
- package/src/integration/event-bus.integration.test.ts +313 -0
- package/src/logger.ts +65 -0
- package/src/openapi-router.ts +177 -0
- package/src/plugin-lifecycle.test.ts +276 -0
- package/src/plugin-manager/api-router.ts +163 -0
- package/src/plugin-manager/core-services.ts +312 -0
- package/src/plugin-manager/dependency-sorter.ts +103 -0
- package/src/plugin-manager/deregistration-guard.ts +41 -0
- package/src/plugin-manager/extension-points.ts +85 -0
- package/src/plugin-manager/index.ts +13 -0
- package/src/plugin-manager/plugin-admin-router.ts +89 -0
- package/src/plugin-manager/plugin-loader.ts +464 -0
- package/src/plugin-manager/types.ts +14 -0
- package/src/plugin-manager.test.ts +464 -0
- package/src/plugin-manager.ts +431 -0
- package/src/rpc-rest-compat.test.ts +80 -0
- package/src/schema.ts +46 -0
- package/src/services/config-service.test.ts +66 -0
- package/src/services/config-service.ts +322 -0
- package/src/services/event-bus.test.ts +469 -0
- package/src/services/event-bus.ts +317 -0
- package/src/services/health-check-registry.test.ts +101 -0
- package/src/services/health-check-registry.ts +27 -0
- package/src/services/jwt.ts +45 -0
- package/src/services/keystore.test.ts +198 -0
- package/src/services/keystore.ts +136 -0
- package/src/services/plugin-installer.test.ts +90 -0
- package/src/services/plugin-installer.ts +70 -0
- package/src/services/queue-manager.ts +382 -0
- package/src/services/queue-plugin-registry.ts +17 -0
- package/src/services/queue-proxy.ts +182 -0
- package/src/services/service-registry.ts +35 -0
- package/src/test-preload.ts +114 -0
- package/src/utils/plugin-discovery.test.ts +383 -0
- package/src/utils/plugin-discovery.ts +157 -0
- package/src/utils/strip-public-schema.ts +40 -0
- package/tsconfig.json +6 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import { eq, and } from "drizzle-orm";
|
|
4
|
+
import type { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
5
|
+
import { plugins } from "../schema";
|
|
6
|
+
|
|
7
|
+
export interface PluginMetadata {
|
|
8
|
+
packageName: string; // From package.json "name"
|
|
9
|
+
pluginPath: string; // Absolute path to plugin directory
|
|
10
|
+
type: "backend" | "frontend" | "common";
|
|
11
|
+
enabled: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Extracts plugin metadata from a plugin directory by reading package.json
|
|
16
|
+
* @param pluginDir - Absolute path to plugin directory
|
|
17
|
+
* @returns PluginMetadata if valid, undefined if invalid/malformed
|
|
18
|
+
*/
|
|
19
|
+
export function extractPluginMetadata({
|
|
20
|
+
pluginDir,
|
|
21
|
+
}: {
|
|
22
|
+
pluginDir: string;
|
|
23
|
+
}): PluginMetadata | undefined {
|
|
24
|
+
const pkgJsonPath = path.join(pluginDir, "package.json");
|
|
25
|
+
|
|
26
|
+
if (!fs.existsSync(pkgJsonPath)) {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, "utf8"));
|
|
32
|
+
|
|
33
|
+
if (!pkgJson.name || typeof pkgJson.name !== "string") {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Determine plugin type from package name suffix
|
|
38
|
+
let type: "backend" | "frontend" | "common";
|
|
39
|
+
if (pkgJson.name.endsWith("-backend")) {
|
|
40
|
+
type = "backend";
|
|
41
|
+
} else if (pkgJson.name.endsWith("-frontend")) {
|
|
42
|
+
type = "frontend";
|
|
43
|
+
} else if (pkgJson.name.endsWith("-common")) {
|
|
44
|
+
type = "common";
|
|
45
|
+
} else {
|
|
46
|
+
return undefined; // Not a valid plugin package
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
packageName: pkgJson.name,
|
|
51
|
+
pluginPath: pluginDir,
|
|
52
|
+
type,
|
|
53
|
+
enabled: true, // Local plugins are always enabled
|
|
54
|
+
};
|
|
55
|
+
} catch {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Discovers all local plugins in the monorepo workspace
|
|
62
|
+
* Scans both packages/ (core components) and plugins/ (replaceable providers)
|
|
63
|
+
* @param workspaceRoot - Absolute path to workspace root
|
|
64
|
+
* @param type - Optional filter for plugin type (backend, frontend, or common)
|
|
65
|
+
* @returns Array of PluginMetadata for all valid plugins
|
|
66
|
+
*/
|
|
67
|
+
export function discoverLocalPlugins({
|
|
68
|
+
workspaceRoot,
|
|
69
|
+
type,
|
|
70
|
+
}: {
|
|
71
|
+
workspaceRoot: string;
|
|
72
|
+
type?: "backend" | "frontend" | "common";
|
|
73
|
+
}): PluginMetadata[] {
|
|
74
|
+
const discovered: PluginMetadata[] = [];
|
|
75
|
+
|
|
76
|
+
// Scan both packages/ (core) and plugins/ (providers)
|
|
77
|
+
const dirsToScan = [
|
|
78
|
+
path.join(workspaceRoot, "core"),
|
|
79
|
+
path.join(workspaceRoot, "plugins"),
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
for (const scanDir of dirsToScan) {
|
|
83
|
+
if (!fs.existsSync(scanDir)) {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const entries = fs.readdirSync(scanDir, { withFileTypes: true });
|
|
88
|
+
|
|
89
|
+
for (const entry of entries) {
|
|
90
|
+
if (!entry.isDirectory()) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const pluginDir = path.join(scanDir, entry.name);
|
|
95
|
+
const metadata = extractPluginMetadata({ pluginDir });
|
|
96
|
+
|
|
97
|
+
if (metadata && (!type || metadata.type === type)) {
|
|
98
|
+
discovered.push(metadata);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return discovered;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Syncs local plugins to the database
|
|
108
|
+
* - Inserts new plugins
|
|
109
|
+
* - Updates paths for existing plugins (handles renames)
|
|
110
|
+
* - Does not modify remotely installed plugins (isUninstallable=true)
|
|
111
|
+
* @param localPlugins - Array of local plugin metadata
|
|
112
|
+
* @param db - Database connection
|
|
113
|
+
*/
|
|
114
|
+
export async function syncPluginsToDatabase({
|
|
115
|
+
localPlugins,
|
|
116
|
+
db,
|
|
117
|
+
}: {
|
|
118
|
+
localPlugins: PluginMetadata[];
|
|
119
|
+
db: NodePgDatabase<Record<string, unknown>>;
|
|
120
|
+
}): Promise<void> {
|
|
121
|
+
for (const plugin of localPlugins) {
|
|
122
|
+
// Check if plugin already exists
|
|
123
|
+
const existing = await db
|
|
124
|
+
.select()
|
|
125
|
+
.from(plugins)
|
|
126
|
+
.where(eq(plugins.name, plugin.packageName))
|
|
127
|
+
.limit(1);
|
|
128
|
+
|
|
129
|
+
if (existing.length === 0) {
|
|
130
|
+
// Insert new plugin
|
|
131
|
+
await db.insert(plugins).values({
|
|
132
|
+
name: plugin.packageName,
|
|
133
|
+
path: plugin.pluginPath,
|
|
134
|
+
type: plugin.type,
|
|
135
|
+
enabled: plugin.enabled,
|
|
136
|
+
isUninstallable: false, // Local plugins are part of monorepo
|
|
137
|
+
});
|
|
138
|
+
} else {
|
|
139
|
+
// Update existing plugin ONLY if it's a local plugin (not remotely installed)
|
|
140
|
+
// This handles the case where a plugin directory was renamed
|
|
141
|
+
if (!existing[0].isUninstallable) {
|
|
142
|
+
await db
|
|
143
|
+
.update(plugins)
|
|
144
|
+
.set({
|
|
145
|
+
path: plugin.pluginPath,
|
|
146
|
+
type: plugin.type,
|
|
147
|
+
})
|
|
148
|
+
.where(
|
|
149
|
+
and(
|
|
150
|
+
eq(plugins.name, plugin.packageName),
|
|
151
|
+
eq(plugins.isUninstallable, false)
|
|
152
|
+
)
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Strip `"public".` schema references from all SQL migration files in a folder.
|
|
6
|
+
*
|
|
7
|
+
* This is called at runtime before migrations are executed, modifying the files
|
|
8
|
+
* in-place to ensure they work correctly with the plugin's search_path.
|
|
9
|
+
*
|
|
10
|
+
* @param migrationsFolder - Path to the drizzle migrations folder
|
|
11
|
+
* @returns Number of files that were modified
|
|
12
|
+
*/
|
|
13
|
+
export function stripPublicSchemaFromMigrations(
|
|
14
|
+
migrationsFolder: string
|
|
15
|
+
): number {
|
|
16
|
+
if (!fs.existsSync(migrationsFolder)) {
|
|
17
|
+
return 0;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const sqlFiles = fs
|
|
21
|
+
.readdirSync(migrationsFolder)
|
|
22
|
+
.filter((f) => f.endsWith(".sql"));
|
|
23
|
+
|
|
24
|
+
let modifiedCount = 0;
|
|
25
|
+
|
|
26
|
+
for (const file of sqlFiles) {
|
|
27
|
+
const filePath = path.join(migrationsFolder, file);
|
|
28
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
29
|
+
|
|
30
|
+
// Replace "public". prefix (with or without quotes around table name)
|
|
31
|
+
const fixed = content.replaceAll('"public".', "");
|
|
32
|
+
|
|
33
|
+
if (fixed !== content) {
|
|
34
|
+
fs.writeFileSync(filePath, fixed, "utf8");
|
|
35
|
+
modifiedCount++;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return modifiedCount;
|
|
40
|
+
}
|