@checkstack/backend 0.4.7 → 0.4.8
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
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# @checkstack/backend
|
|
2
2
|
|
|
3
|
+
## 0.4.8
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- dd16be7: Fix plugin schema isolation: create schema before migrations run
|
|
8
|
+
|
|
9
|
+
Previously, schemas were only created when `coreServices.database` was resolved (after migrations), causing tables to be created in the `public` schema instead of plugin-specific schemas. Now schemas are created immediately before migrations run.
|
|
10
|
+
|
|
11
|
+
Also removed the `public` fallback from migration search_path to make errors more visible if schema creation fails.
|
|
12
|
+
|
|
3
13
|
## 0.4.7
|
|
4
14
|
|
|
5
15
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
import {
|
|
9
9
|
createMockQueueManager,
|
|
10
10
|
createMockLogger,
|
|
11
|
+
createMockDb,
|
|
11
12
|
} from "@checkstack/test-utils-backend";
|
|
12
13
|
import { z } from "zod";
|
|
13
14
|
|
|
@@ -75,10 +76,13 @@ describe("HealthCheck Plugin Integration", () => {
|
|
|
75
76
|
// Register mock services since core-services is mocked as no-op
|
|
76
77
|
pluginManager.registerService(
|
|
77
78
|
coreServices.queueManager,
|
|
78
|
-
createMockQueueManager()
|
|
79
|
+
createMockQueueManager(),
|
|
79
80
|
);
|
|
80
81
|
pluginManager.registerService(coreServices.logger, createMockLogger());
|
|
81
|
-
pluginManager.registerService(
|
|
82
|
+
pluginManager.registerService(
|
|
83
|
+
coreServices.database,
|
|
84
|
+
createMockDb() as never,
|
|
85
|
+
);
|
|
82
86
|
|
|
83
87
|
// 4. Load plugins using the PluginManager with manual injection
|
|
84
88
|
await pluginManager.loadPlugins(mockRouter, [testPlugin], {
|
|
@@ -87,7 +91,7 @@ describe("HealthCheck Plugin Integration", () => {
|
|
|
87
91
|
|
|
88
92
|
// 5. Verify the strategy is registered in the registry managed by PluginManager
|
|
89
93
|
const registry = await pluginManager.getService(
|
|
90
|
-
coreServices.healthCheckRegistry
|
|
94
|
+
coreServices.healthCheckRegistry,
|
|
91
95
|
);
|
|
92
96
|
expect(registry).toBeDefined();
|
|
93
97
|
|
|
@@ -33,7 +33,7 @@ import { createScopedDb } from "../utils/scoped-db.js";
|
|
|
33
33
|
async function schemaExists(pool: Pool, schemaName: string): Promise<boolean> {
|
|
34
34
|
const result = await pool.query(
|
|
35
35
|
"SELECT 1 FROM information_schema.schemata WHERE schema_name = $1",
|
|
36
|
-
[schemaName]
|
|
36
|
+
[schemaName],
|
|
37
37
|
);
|
|
38
38
|
return result.rows.length > 0;
|
|
39
39
|
}
|
|
@@ -70,19 +70,17 @@ export function registerCoreServices({
|
|
|
70
70
|
|
|
71
71
|
if (oldExists && !newExists) {
|
|
72
72
|
rootLogger.info(
|
|
73
|
-
`🔄 Renaming schema ${oldSchema} → ${assignedSchema} for plugin ${pluginId}
|
|
73
|
+
`🔄 Renaming schema ${oldSchema} → ${assignedSchema} for plugin ${pluginId}`,
|
|
74
74
|
);
|
|
75
75
|
await adminPool.query(
|
|
76
|
-
`ALTER SCHEMA "${oldSchema}" RENAME TO "${assignedSchema}"
|
|
76
|
+
`ALTER SCHEMA "${oldSchema}" RENAME TO "${assignedSchema}"`,
|
|
77
77
|
);
|
|
78
78
|
break; // Only one rename needed
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
//
|
|
84
|
-
await adminPool.query(`CREATE SCHEMA IF NOT EXISTS "${assignedSchema}"`);
|
|
85
|
-
|
|
83
|
+
// Schema is created in plugin-loader.ts before migrations run.
|
|
86
84
|
// Create scoped proxy on shared pool (no new connections)
|
|
87
85
|
return createScopedDb(db, assignedSchema);
|
|
88
86
|
});
|
|
@@ -121,12 +119,12 @@ export function registerCoreServices({
|
|
|
121
119
|
try {
|
|
122
120
|
const authStrategy = await registry.get(
|
|
123
121
|
authenticationStrategyServiceRef,
|
|
124
|
-
metadata
|
|
122
|
+
metadata,
|
|
125
123
|
);
|
|
126
124
|
if (authStrategy) {
|
|
127
125
|
// AuthenticationStrategy.validate() returns RealUser | undefined
|
|
128
126
|
return await (authStrategy as AuthenticationStrategy).validate(
|
|
129
|
-
request
|
|
127
|
+
request,
|
|
130
128
|
);
|
|
131
129
|
}
|
|
132
130
|
} catch {
|
|
@@ -165,7 +163,7 @@ export function registerCoreServices({
|
|
|
165
163
|
} catch (error) {
|
|
166
164
|
// RPC client not available yet (during startup), return empty
|
|
167
165
|
rootLogger.warn(
|
|
168
|
-
`[auth] getAnonymousAccessRules: RPC failed, returning empty array. Error: ${error}
|
|
166
|
+
`[auth] getAnonymousAccessRules: RPC failed, returning empty array. Error: ${error}`,
|
|
169
167
|
);
|
|
170
168
|
return [];
|
|
171
169
|
}
|
|
@@ -207,7 +205,7 @@ export function registerCoreServices({
|
|
|
207
205
|
|
|
208
206
|
const fetchWithAuth = async (
|
|
209
207
|
input: RequestInfo | URL,
|
|
210
|
-
init?: RequestInit
|
|
208
|
+
init?: RequestInit,
|
|
211
209
|
) => {
|
|
212
210
|
const { headers: authHeaders } = await auth.getCredentials();
|
|
213
211
|
const mergedHeaders = new Headers(init?.headers);
|
|
@@ -288,13 +286,13 @@ export function registerCoreServices({
|
|
|
288
286
|
// 6. Health Check Registry (Scoped Factory - auto-prefixes strategy IDs with pluginId)
|
|
289
287
|
const globalHealthCheckRegistry = new CoreHealthCheckRegistry();
|
|
290
288
|
registry.registerFactory(coreServices.healthCheckRegistry, (metadata) =>
|
|
291
|
-
createScopedHealthCheckRegistry(globalHealthCheckRegistry, metadata)
|
|
289
|
+
createScopedHealthCheckRegistry(globalHealthCheckRegistry, metadata),
|
|
292
290
|
);
|
|
293
291
|
|
|
294
292
|
// 6b. Collector Registry (Scoped Factory - injects ownerPlugin automatically)
|
|
295
293
|
const globalCollectorRegistry = new CoreCollectorRegistry();
|
|
296
294
|
registry.registerFactory(coreServices.collectorRegistry, (metadata) =>
|
|
297
|
-
createScopedCollectorRegistry(globalCollectorRegistry, metadata)
|
|
295
|
+
createScopedCollectorRegistry(globalCollectorRegistry, metadata),
|
|
298
296
|
);
|
|
299
297
|
|
|
300
298
|
// 7. RPC Service (Scoped Factory - uses pluginId for path derivation)
|
|
@@ -305,17 +303,17 @@ export function registerCoreServices({
|
|
|
305
303
|
pluginRpcRouters.set(pluginId, router);
|
|
306
304
|
pluginContractRegistry.set(pluginId, contract);
|
|
307
305
|
rootLogger.debug(
|
|
308
|
-
` -> Registered oRPC router and contract for '${pluginId}' at '/api/${pluginId}'
|
|
306
|
+
` -> Registered oRPC router and contract for '${pluginId}' at '/api/${pluginId}'`,
|
|
309
307
|
);
|
|
310
308
|
},
|
|
311
309
|
registerHttpHandler: (
|
|
312
310
|
handler: (req: Request) => Promise<Response>,
|
|
313
|
-
path = "/"
|
|
311
|
+
path = "/",
|
|
314
312
|
): void => {
|
|
315
313
|
const fullPath = `/api/${pluginId}${path === "/" ? "" : path}`;
|
|
316
314
|
pluginHttpHandlers.set(fullPath, handler);
|
|
317
315
|
rootLogger.debug(
|
|
318
|
-
` -> Registered HTTP handler for '${pluginId}' at '${fullPath}'
|
|
316
|
+
` -> Registered HTTP handler for '${pluginId}' at '${fullPath}'`,
|
|
319
317
|
);
|
|
320
318
|
},
|
|
321
319
|
} satisfies RpcService;
|
|
@@ -333,9 +333,12 @@ export async function loadPlugins({
|
|
|
333
333
|
*/
|
|
334
334
|
|
|
335
335
|
// Run Migrations
|
|
336
|
-
|
|
336
|
+
// Skip migrations for manual test plugins (no plugin path) - see comment above
|
|
337
|
+
const migrationsFolder = p.pluginPath
|
|
338
|
+
? path.join(p.pluginPath, "drizzle")
|
|
339
|
+
: undefined;
|
|
337
340
|
const migrationsSchema = getPluginSchemaName(p.metadata.pluginId);
|
|
338
|
-
if (fs.existsSync(migrationsFolder)) {
|
|
341
|
+
if (migrationsFolder && fs.existsSync(migrationsFolder)) {
|
|
339
342
|
try {
|
|
340
343
|
// Strip "public". schema references from migration SQL at runtime
|
|
341
344
|
stripPublicSchemaFromMigrations(migrationsFolder);
|
|
@@ -343,11 +346,19 @@ export async function loadPlugins({
|
|
|
343
346
|
` -> Running migrations for ${p.metadata.pluginId} from ${migrationsFolder}`,
|
|
344
347
|
);
|
|
345
348
|
|
|
349
|
+
// Create schema if it doesn't exist BEFORE running migrations.
|
|
350
|
+
// Without this, SET search_path to a non-existent schema causes
|
|
351
|
+
// PostgreSQL to fall back to 'public', creating tables in the wrong schema.
|
|
352
|
+
await deps.db.execute(
|
|
353
|
+
sql.raw(`CREATE SCHEMA IF NOT EXISTS "${migrationsSchema}"`),
|
|
354
|
+
);
|
|
355
|
+
|
|
346
356
|
// Set search_path to plugin schema before running migrations.
|
|
347
357
|
// Uses session-level SET (not SET LOCAL) because migrate() may run
|
|
348
358
|
// multiple statements across transaction boundaries.
|
|
359
|
+
// No 'public' fallback: schema is guaranteed to exist from CREATE above.
|
|
349
360
|
await deps.db.execute(
|
|
350
|
-
sql.raw(`SET search_path = "${migrationsSchema}"
|
|
361
|
+
sql.raw(`SET search_path = "${migrationsSchema}"`),
|
|
351
362
|
);
|
|
352
363
|
// Drizzle migrate() requires NodePgDatabase, cast from SafeDatabase
|
|
353
364
|
await migrate(deps.db as NodePgDatabase<Record<string, unknown>>, {
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
import {
|
|
10
10
|
createMockLogger,
|
|
11
11
|
createMockQueueManager,
|
|
12
|
+
createMockDb,
|
|
12
13
|
} from "@checkstack/test-utils-backend";
|
|
13
14
|
import { sortPlugins } from "./plugin-manager/dependency-sorter";
|
|
14
15
|
|
|
@@ -120,13 +121,13 @@ describe("PluginManager", () => {
|
|
|
120
121
|
// provider-1 must come before consumer and provider-2
|
|
121
122
|
// provider-2 must come before consumer
|
|
122
123
|
expect(sorted.indexOf("provider-1")).toBeLessThan(
|
|
123
|
-
sorted.indexOf("consumer")
|
|
124
|
+
sorted.indexOf("consumer"),
|
|
124
125
|
);
|
|
125
126
|
expect(sorted.indexOf("provider-1")).toBeLessThan(
|
|
126
|
-
sorted.indexOf("provider-2")
|
|
127
|
+
sorted.indexOf("provider-2"),
|
|
127
128
|
);
|
|
128
129
|
expect(sorted.indexOf("provider-2")).toBeLessThan(
|
|
129
|
-
sorted.indexOf("consumer")
|
|
130
|
+
sorted.indexOf("consumer"),
|
|
130
131
|
);
|
|
131
132
|
});
|
|
132
133
|
|
|
@@ -145,17 +146,17 @@ describe("PluginManager", () => {
|
|
|
145
146
|
]);
|
|
146
147
|
|
|
147
148
|
expect(() =>
|
|
148
|
-
sortPlugins({ pendingInits, providedBy, logger: createMockLogger() })
|
|
149
|
+
sortPlugins({ pendingInits, providedBy, logger: createMockLogger() }),
|
|
149
150
|
).toThrow("Circular dependency detected");
|
|
150
151
|
});
|
|
151
152
|
|
|
152
153
|
describe("Queue Plugin Ordering", () => {
|
|
153
154
|
it("should initialize queue plugin providers before queue consumers", () => {
|
|
154
155
|
const queueManagerRef = createServiceRef<unknown>(
|
|
155
|
-
coreServices.queueManager.id
|
|
156
|
+
coreServices.queueManager.id,
|
|
156
157
|
);
|
|
157
158
|
const queueRegistryRef = createServiceRef<unknown>(
|
|
158
|
-
coreServices.queuePluginRegistry.id
|
|
159
|
+
coreServices.queuePluginRegistry.id,
|
|
159
160
|
);
|
|
160
161
|
|
|
161
162
|
const pendingInits = [
|
|
@@ -184,16 +185,16 @@ describe("PluginManager", () => {
|
|
|
184
185
|
|
|
185
186
|
// Queue provider should come before queue consumer
|
|
186
187
|
expect(sorted.indexOf("queue-provider")).toBeLessThan(
|
|
187
|
-
sorted.indexOf("queue-consumer")
|
|
188
|
+
sorted.indexOf("queue-consumer"),
|
|
188
189
|
);
|
|
189
190
|
});
|
|
190
191
|
|
|
191
192
|
it("should handle multiple queue providers and consumers", () => {
|
|
192
193
|
const queueManagerRef = createServiceRef<unknown>(
|
|
193
|
-
coreServices.queueManager.id
|
|
194
|
+
coreServices.queueManager.id,
|
|
194
195
|
);
|
|
195
196
|
const queueRegistryRef = createServiceRef<unknown>(
|
|
196
|
-
coreServices.queuePluginRegistry.id
|
|
197
|
+
coreServices.queuePluginRegistry.id,
|
|
197
198
|
);
|
|
198
199
|
const loggerRef = createServiceRef<unknown>("core.logger");
|
|
199
200
|
|
|
@@ -253,10 +254,10 @@ describe("PluginManager", () => {
|
|
|
253
254
|
|
|
254
255
|
it("should respect existing service dependencies while prioritizing queue plugins", () => {
|
|
255
256
|
const queueManagerRef = createServiceRef<unknown>(
|
|
256
|
-
coreServices.queueManager.id
|
|
257
|
+
coreServices.queueManager.id,
|
|
257
258
|
);
|
|
258
259
|
const queueRegistryRef = createServiceRef<unknown>(
|
|
259
|
-
coreServices.queuePluginRegistry.id
|
|
260
|
+
coreServices.queuePluginRegistry.id,
|
|
260
261
|
);
|
|
261
262
|
const customServiceRef = createServiceRef<unknown>("custom.service");
|
|
262
263
|
const loggerRef = createServiceRef<unknown>("core.logger");
|
|
@@ -304,10 +305,10 @@ describe("PluginManager", () => {
|
|
|
304
305
|
|
|
305
306
|
it("should handle plugins that both provide and consume queues", () => {
|
|
306
307
|
const queueManagerRef = createServiceRef<unknown>(
|
|
307
|
-
coreServices.queueManager.id
|
|
308
|
+
coreServices.queueManager.id,
|
|
308
309
|
);
|
|
309
310
|
const queueRegistryRef = createServiceRef<unknown>(
|
|
310
|
-
coreServices.queuePluginRegistry.id
|
|
311
|
+
coreServices.queuePluginRegistry.id,
|
|
311
312
|
);
|
|
312
313
|
|
|
313
314
|
const pendingInits = [
|
|
@@ -342,10 +343,10 @@ describe("PluginManager", () => {
|
|
|
342
343
|
|
|
343
344
|
it("should not create circular dependencies with queue ordering", () => {
|
|
344
345
|
const queueManagerRef = createServiceRef<unknown>(
|
|
345
|
-
coreServices.queueManager.id
|
|
346
|
+
coreServices.queueManager.id,
|
|
346
347
|
);
|
|
347
348
|
const queueRegistryRef = createServiceRef<unknown>(
|
|
348
|
-
coreServices.queuePluginRegistry.id
|
|
349
|
+
coreServices.queuePluginRegistry.id,
|
|
349
350
|
);
|
|
350
351
|
|
|
351
352
|
const pendingInits = [
|
|
@@ -441,7 +442,7 @@ describe("PluginManager", () => {
|
|
|
441
442
|
resource: "perm",
|
|
442
443
|
level: "read",
|
|
443
444
|
description: "Access Rule 3",
|
|
444
|
-
}
|
|
445
|
+
},
|
|
445
446
|
);
|
|
446
447
|
|
|
447
448
|
const all = pluginManager.getAllAccessRules();
|
|
@@ -470,10 +471,13 @@ describe("PluginManager", () => {
|
|
|
470
471
|
// Register mock services since core-services is mocked as no-op
|
|
471
472
|
pluginManager.registerService(
|
|
472
473
|
coreServices.queueManager,
|
|
473
|
-
createMockQueueManager()
|
|
474
|
+
createMockQueueManager(),
|
|
474
475
|
);
|
|
475
476
|
pluginManager.registerService(coreServices.logger, createMockLogger());
|
|
476
|
-
pluginManager.registerService(
|
|
477
|
+
pluginManager.registerService(
|
|
478
|
+
coreServices.database,
|
|
479
|
+
createMockDb() as never,
|
|
480
|
+
);
|
|
477
481
|
|
|
478
482
|
// Use manual plugin injection with skipDiscovery to avoid loading real plugins
|
|
479
483
|
await pluginManager.loadPlugins(mockRouter, [testPlugin], {
|