@checkstack/backend 0.4.4 → 0.4.6
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 +34 -0
- package/package.json +1 -1
- package/src/plugin-manager/api-router.ts +2 -2
- package/src/plugin-manager/deregistration-guard.ts +2 -2
- package/src/plugin-manager/plugin-loader.ts +76 -3
- package/src/services/config-service.ts +2 -2
- package/src/services/queue-manager.ts +43 -27
- package/src/services/queue-proxy.ts +9 -9
- package/src/utils/plugin-discovery.ts +2 -2
- package/src/utils/scoped-db.ts +5 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,39 @@
|
|
|
1
1
|
# @checkstack/backend
|
|
2
2
|
|
|
3
|
+
## 0.4.6
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 66a3963: Update plugin loader to use SafeDatabase type
|
|
8
|
+
|
|
9
|
+
- Updated `PluginLoaderDeps.db` type from `NodePgDatabase` to `SafeDatabase`
|
|
10
|
+
- Added type cast for drizzle `migrate()` function which still requires `NodePgDatabase`
|
|
11
|
+
|
|
12
|
+
- Updated dependencies [2c0822d]
|
|
13
|
+
- Updated dependencies [66a3963]
|
|
14
|
+
- @checkstack/queue-api@0.2.0
|
|
15
|
+
- @checkstack/backend-api@0.5.0
|
|
16
|
+
- @checkstack/signal-backend@0.1.6
|
|
17
|
+
|
|
18
|
+
## 0.4.5
|
|
19
|
+
|
|
20
|
+
### Patch Changes
|
|
21
|
+
|
|
22
|
+
- 8a87cd4: Added startup validation for unregistered access rules
|
|
23
|
+
|
|
24
|
+
The backend now throws an error at startup if a procedure contract references an access rule that isn't registered with the plugin system. This prevents silent runtime failures.
|
|
25
|
+
|
|
26
|
+
- Updated dependencies [8a87cd4]
|
|
27
|
+
- Updated dependencies [8a87cd4]
|
|
28
|
+
- Updated dependencies [8a87cd4]
|
|
29
|
+
- @checkstack/auth-common@0.5.2
|
|
30
|
+
- @checkstack/backend-api@0.4.1
|
|
31
|
+
- @checkstack/common@0.5.0
|
|
32
|
+
- @checkstack/queue-api@0.1.3
|
|
33
|
+
- @checkstack/signal-backend@0.1.5
|
|
34
|
+
- @checkstack/api-docs-common@0.1.3
|
|
35
|
+
- @checkstack/signal-common@0.1.3
|
|
36
|
+
|
|
3
37
|
## 0.4.4
|
|
4
38
|
|
|
5
39
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Hono, Context } from "hono";
|
|
2
|
-
import type {
|
|
2
|
+
import type { SafeDatabase } from "@checkstack/backend-api";
|
|
3
3
|
import { RPCHandler } from "@orpc/server/fetch";
|
|
4
4
|
import {
|
|
5
5
|
coreServices,
|
|
@@ -108,7 +108,7 @@ export function createApiRouteHandler({
|
|
|
108
108
|
pluginMetadata,
|
|
109
109
|
auth: auth as AuthService,
|
|
110
110
|
logger: logger as Logger,
|
|
111
|
-
db: db as
|
|
111
|
+
db: db as SafeDatabase<Record<string, unknown>>,
|
|
112
112
|
fetch: fetch as Fetch,
|
|
113
113
|
healthCheckRegistry: healthCheckRegistry as HealthCheckRegistry,
|
|
114
114
|
collectorRegistry: collectorRegistry as CollectorRegistry,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { eq } from "drizzle-orm";
|
|
2
|
-
import {
|
|
2
|
+
import { SafeDatabase } from "@checkstack/backend-api";
|
|
3
3
|
import { ORPCError } from "@orpc/server";
|
|
4
4
|
import { plugins } from "../schema";
|
|
5
5
|
|
|
@@ -12,7 +12,7 @@ export async function assertCanDeregister({
|
|
|
12
12
|
db,
|
|
13
13
|
}: {
|
|
14
14
|
pluginId: string;
|
|
15
|
-
db:
|
|
15
|
+
db: SafeDatabase<Record<string, unknown>>;
|
|
16
16
|
}): Promise<void> {
|
|
17
17
|
// 1. Check if plugin exists
|
|
18
18
|
const pluginRows = await db
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { migrate } from "drizzle-orm/node-postgres/migrator";
|
|
2
|
+
import { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import fs from "node:fs";
|
|
4
5
|
import type { Hono } from "hono";
|
|
5
6
|
import { eq, and, sql } from "drizzle-orm";
|
|
6
|
-
import type {
|
|
7
|
+
import type { SafeDatabase } from "@checkstack/backend-api";
|
|
7
8
|
import {
|
|
8
9
|
coreServices,
|
|
9
10
|
BackendPlugin,
|
|
@@ -42,7 +43,7 @@ export interface PluginLoaderDeps {
|
|
|
42
43
|
extensionPointManager: ExtensionPointManager;
|
|
43
44
|
registeredAccessRules: (AccessRule & { pluginId: string })[];
|
|
44
45
|
getAllAccessRules: () => AccessRule[];
|
|
45
|
-
db:
|
|
46
|
+
db: SafeDatabase<Record<string, unknown>>;
|
|
46
47
|
/**
|
|
47
48
|
* Map of pluginId -> PluginMetadata for request-time context injection.
|
|
48
49
|
*/
|
|
@@ -348,7 +349,11 @@ export async function loadPlugins({
|
|
|
348
349
|
await deps.db.execute(
|
|
349
350
|
sql.raw(`SET search_path = "${migrationsSchema}", public`),
|
|
350
351
|
);
|
|
351
|
-
|
|
352
|
+
// Drizzle migrate() requires NodePgDatabase, cast from SafeDatabase
|
|
353
|
+
await migrate(deps.db as NodePgDatabase<Record<string, unknown>>, {
|
|
354
|
+
migrationsFolder,
|
|
355
|
+
migrationsSchema,
|
|
356
|
+
});
|
|
352
357
|
|
|
353
358
|
// Reset search_path to public after migrations complete.
|
|
354
359
|
// This prevents search_path leaking into subsequent plugin migrations.
|
|
@@ -424,6 +429,35 @@ export async function loadPlugins({
|
|
|
424
429
|
}
|
|
425
430
|
}
|
|
426
431
|
|
|
432
|
+
// Phase 2.5: Validate that all access rules used in contracts are registered
|
|
433
|
+
// This catches bugs where access rules are used in procedures but not added to
|
|
434
|
+
// the plugin's accessRules registration array.
|
|
435
|
+
rootLogger.debug("🔍 Validating access rules in contracts...");
|
|
436
|
+
const registeredRuleIds = new Set(
|
|
437
|
+
deps.registeredAccessRules.map((r) => r.id),
|
|
438
|
+
);
|
|
439
|
+
const validationErrors: string[] = [];
|
|
440
|
+
|
|
441
|
+
for (const [pluginId, contract] of deps.pluginContractRegistry) {
|
|
442
|
+
validateContractAccessRules({
|
|
443
|
+
pluginId,
|
|
444
|
+
contract,
|
|
445
|
+
registeredRuleIds,
|
|
446
|
+
validationErrors,
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (validationErrors.length > 0) {
|
|
451
|
+
rootLogger.error("❌ Unregistered access rules found in contracts:");
|
|
452
|
+
for (const error of validationErrors) {
|
|
453
|
+
rootLogger.error(` • ${error}`);
|
|
454
|
+
}
|
|
455
|
+
throw new Error(
|
|
456
|
+
`Unregistered access rules in contracts:\n${validationErrors.join("\n")}`,
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
rootLogger.debug("✅ All access rules in contracts are registered");
|
|
460
|
+
|
|
427
461
|
// Phase 3: Run afterPluginsReady callbacks
|
|
428
462
|
rootLogger.debug("🔄 Running afterPluginsReady callbacks...");
|
|
429
463
|
|
|
@@ -507,3 +541,42 @@ export async function loadPlugins({
|
|
|
507
541
|
}
|
|
508
542
|
rootLogger.debug("✅ All afterPluginsReady callbacks complete");
|
|
509
543
|
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Validate that all access rules used in a contract are registered with the plugin system.
|
|
547
|
+
* Recursively traverses the contract to find all procedures and their access metadata.
|
|
548
|
+
*/
|
|
549
|
+
function validateContractAccessRules({
|
|
550
|
+
pluginId,
|
|
551
|
+
contract,
|
|
552
|
+
registeredRuleIds,
|
|
553
|
+
validationErrors,
|
|
554
|
+
}: {
|
|
555
|
+
pluginId: string;
|
|
556
|
+
contract: AnyContractRouter;
|
|
557
|
+
registeredRuleIds: Set<string>;
|
|
558
|
+
validationErrors: string[];
|
|
559
|
+
}): void {
|
|
560
|
+
for (const [procedureName, procedure] of Object.entries(
|
|
561
|
+
contract as Record<string, unknown>,
|
|
562
|
+
)) {
|
|
563
|
+
if (!procedure || typeof procedure !== "object") continue;
|
|
564
|
+
|
|
565
|
+
// Check if this is a procedure with oRPC metadata
|
|
566
|
+
const orpcData = (procedure as Record<string, unknown>)["~orpc"] as
|
|
567
|
+
| { meta?: { access?: Array<{ id: string }> } }
|
|
568
|
+
| undefined;
|
|
569
|
+
|
|
570
|
+
if (orpcData?.meta?.access) {
|
|
571
|
+
for (const accessRule of orpcData.meta.access) {
|
|
572
|
+
const qualifiedId = `${pluginId}.${accessRule.id}`;
|
|
573
|
+
if (!registeredRuleIds.has(qualifiedId)) {
|
|
574
|
+
validationErrors.push(
|
|
575
|
+
`Plugin "${pluginId}" procedure "${procedureName}" uses unregistered access rule "${accessRule.id}" (qualified: "${qualifiedId}"). ` +
|
|
576
|
+
`Add it to the plugin's accessRules registration array.`,
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { SafeDatabase } from "@checkstack/backend-api";
|
|
2
2
|
import { eq, and } from "drizzle-orm";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import {
|
|
@@ -20,7 +20,7 @@ import { pluginConfigs } from "../schema";
|
|
|
20
20
|
export class ConfigServiceImpl implements ConfigService {
|
|
21
21
|
constructor(
|
|
22
22
|
private readonly pluginId: string,
|
|
23
|
-
private readonly db:
|
|
23
|
+
private readonly db: SafeDatabase<Record<string, unknown>>
|
|
24
24
|
) {}
|
|
25
25
|
|
|
26
26
|
/**
|
|
@@ -41,7 +41,7 @@ export class QueueManagerImpl implements QueueManager {
|
|
|
41
41
|
constructor(
|
|
42
42
|
private registry: QueuePluginRegistryImpl,
|
|
43
43
|
private configService: ConfigService,
|
|
44
|
-
private logger: Logger
|
|
44
|
+
private logger: Logger,
|
|
45
45
|
) {}
|
|
46
46
|
|
|
47
47
|
async loadConfiguration(): Promise<void> {
|
|
@@ -50,7 +50,7 @@ export class QueueManagerImpl implements QueueManager {
|
|
|
50
50
|
const pointer = await this.configService.get<ActivePluginPointer>(
|
|
51
51
|
"queue:active",
|
|
52
52
|
activePluginPointerSchema,
|
|
53
|
-
1
|
|
53
|
+
1,
|
|
54
54
|
);
|
|
55
55
|
|
|
56
56
|
if (pointer) {
|
|
@@ -63,7 +63,7 @@ export class QueueManagerImpl implements QueueManager {
|
|
|
63
63
|
const config = await this.configService.get(
|
|
64
64
|
this.activePluginId,
|
|
65
65
|
plugin.configSchema,
|
|
66
|
-
plugin.configVersion
|
|
66
|
+
plugin.configVersion,
|
|
67
67
|
);
|
|
68
68
|
|
|
69
69
|
if (config) {
|
|
@@ -72,11 +72,11 @@ export class QueueManagerImpl implements QueueManager {
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
this.logger.info(
|
|
75
|
-
`📋 Loaded queue configuration: plugin=${this.activePluginId}, version=${this.configVersion}
|
|
75
|
+
`📋 Loaded queue configuration: plugin=${this.activePluginId}, version=${this.configVersion}`,
|
|
76
76
|
);
|
|
77
77
|
} else {
|
|
78
78
|
this.logger.info(
|
|
79
|
-
`📋 No queue configuration found, using default: plugin=${this.activePluginId}
|
|
79
|
+
`📋 No queue configuration found, using default: plugin=${this.activePluginId}`,
|
|
80
80
|
);
|
|
81
81
|
}
|
|
82
82
|
} catch (error) {
|
|
@@ -105,7 +105,7 @@ export class QueueManagerImpl implements QueueManager {
|
|
|
105
105
|
const plugin = this.registry.getPlugin(this.activePluginId);
|
|
106
106
|
if (!plugin) {
|
|
107
107
|
this.logger.warn(
|
|
108
|
-
`Queue plugin '${this.activePluginId}' not found, deferring queue creation
|
|
108
|
+
`Queue plugin '${this.activePluginId}' not found, deferring queue creation`,
|
|
109
109
|
);
|
|
110
110
|
return;
|
|
111
111
|
}
|
|
@@ -126,7 +126,7 @@ export class QueueManagerImpl implements QueueManager {
|
|
|
126
126
|
|
|
127
127
|
async setActiveBackend(
|
|
128
128
|
pluginId: string,
|
|
129
|
-
config: unknown
|
|
129
|
+
config: unknown,
|
|
130
130
|
): Promise<SwitchResult> {
|
|
131
131
|
const warnings: string[] = [];
|
|
132
132
|
|
|
@@ -145,7 +145,7 @@ export class QueueManagerImpl implements QueueManager {
|
|
|
145
145
|
const testQueue = newPlugin.createQueue(
|
|
146
146
|
"__connection_test__",
|
|
147
147
|
config,
|
|
148
|
-
this.logger
|
|
148
|
+
this.logger,
|
|
149
149
|
);
|
|
150
150
|
await testQueue.testConnection();
|
|
151
151
|
await testQueue.stop();
|
|
@@ -160,10 +160,10 @@ export class QueueManagerImpl implements QueueManager {
|
|
|
160
160
|
const inFlightCount = await this.getInFlightJobCount();
|
|
161
161
|
if (inFlightCount > 0) {
|
|
162
162
|
warnings.push(
|
|
163
|
-
`${inFlightCount} jobs are currently in-flight and may be disrupted
|
|
163
|
+
`${inFlightCount} jobs are currently in-flight and may be disrupted`,
|
|
164
164
|
);
|
|
165
165
|
this.logger.warn(
|
|
166
|
-
`⚠️ ${inFlightCount} in-flight jobs detected during backend switch
|
|
166
|
+
`⚠️ ${inFlightCount} in-flight jobs detected during backend switch`,
|
|
167
167
|
);
|
|
168
168
|
}
|
|
169
169
|
|
|
@@ -197,7 +197,7 @@ export class QueueManagerImpl implements QueueManager {
|
|
|
197
197
|
// 9. Migrate recurring jobs
|
|
198
198
|
if (recurringJobs.length > 0 && oldPlugin && pluginId !== oldPluginId) {
|
|
199
199
|
this.logger.info(
|
|
200
|
-
`📦 Migrating ${recurringJobs.length} recurring jobs
|
|
200
|
+
`📦 Migrating ${recurringJobs.length} recurring jobs...`,
|
|
201
201
|
);
|
|
202
202
|
|
|
203
203
|
for (const job of recurringJobs) {
|
|
@@ -209,18 +209,28 @@ export class QueueManagerImpl implements QueueManager {
|
|
|
209
209
|
// Since we already switched, we need to get this from the collected info
|
|
210
210
|
const details = await proxy.getRecurringJobDetails(job.jobId);
|
|
211
211
|
if (details) {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
212
|
+
// Use if/else to properly satisfy XOR type constraint
|
|
213
|
+
// eslint-disable-next-line unicorn/prefer-ternary
|
|
214
|
+
if ("cronPattern" in details && details.cronPattern) {
|
|
215
|
+
await proxy.scheduleRecurring(details.data as unknown, {
|
|
216
|
+
jobId: details.jobId,
|
|
217
|
+
priority: details.priority,
|
|
218
|
+
cronPattern: details.cronPattern,
|
|
219
|
+
});
|
|
220
|
+
} else {
|
|
221
|
+
await proxy.scheduleRecurring(details.data as unknown, {
|
|
222
|
+
jobId: details.jobId,
|
|
223
|
+
priority: details.priority,
|
|
224
|
+
intervalSeconds: details.intervalSeconds!,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
217
227
|
migratedRecurringJobs++;
|
|
218
228
|
}
|
|
219
229
|
}
|
|
220
230
|
} catch (error) {
|
|
221
231
|
this.logger.error(
|
|
222
232
|
`Failed to migrate recurring job ${job.jobId}`,
|
|
223
|
-
error
|
|
233
|
+
error,
|
|
224
234
|
);
|
|
225
235
|
warnings.push(`Failed to migrate recurring job: ${job.jobId}`);
|
|
226
236
|
}
|
|
@@ -232,7 +242,7 @@ export class QueueManagerImpl implements QueueManager {
|
|
|
232
242
|
pluginId,
|
|
233
243
|
newPlugin.configSchema,
|
|
234
244
|
newPlugin.configVersion,
|
|
235
|
-
config
|
|
245
|
+
config,
|
|
236
246
|
);
|
|
237
247
|
|
|
238
248
|
await this.configService.set("queue:active", activePluginPointerSchema, 1, {
|
|
@@ -303,12 +313,18 @@ export class QueueManagerImpl implements QueueManager {
|
|
|
303
313
|
for (const jobId of jobIds) {
|
|
304
314
|
const details = await delegate.getRecurringJobDetails(jobId);
|
|
305
315
|
if (details) {
|
|
316
|
+
// Extract schedule from details (XOR pattern - one must be defined)
|
|
317
|
+
const schedule =
|
|
318
|
+
"cronPattern" in details
|
|
319
|
+
? { cronPattern: details.cronPattern }
|
|
320
|
+
: { intervalSeconds: details.intervalSeconds };
|
|
321
|
+
|
|
306
322
|
jobs.push({
|
|
307
323
|
queueName,
|
|
308
324
|
jobId,
|
|
309
|
-
intervalSeconds: details.intervalSeconds,
|
|
310
325
|
nextRunAt: details.nextRunAt,
|
|
311
|
-
|
|
326
|
+
...schedule,
|
|
327
|
+
} as RecurringJobInfo);
|
|
312
328
|
}
|
|
313
329
|
}
|
|
314
330
|
}
|
|
@@ -332,12 +348,12 @@ export class QueueManagerImpl implements QueueManager {
|
|
|
332
348
|
const pointer = await this.configService.get<ActivePluginPointer>(
|
|
333
349
|
"queue:active",
|
|
334
350
|
activePluginPointerSchema,
|
|
335
|
-
1
|
|
351
|
+
1,
|
|
336
352
|
);
|
|
337
353
|
|
|
338
354
|
if (pointer && pointer.version !== this.configVersion) {
|
|
339
355
|
this.logger.info(
|
|
340
|
-
`🔄 Queue configuration changed (v${this.configVersion} → v${pointer.version}), reloading
|
|
356
|
+
`🔄 Queue configuration changed (v${this.configVersion} → v${pointer.version}), reloading...`,
|
|
341
357
|
);
|
|
342
358
|
await this.reloadConfiguration(pointer);
|
|
343
359
|
}
|
|
@@ -348,13 +364,13 @@ export class QueueManagerImpl implements QueueManager {
|
|
|
348
364
|
}
|
|
349
365
|
|
|
350
366
|
private async reloadConfiguration(
|
|
351
|
-
pointer: ActivePluginPointer
|
|
367
|
+
pointer: ActivePluginPointer,
|
|
352
368
|
): Promise<void> {
|
|
353
369
|
// Load new plugin config
|
|
354
370
|
const plugin = this.registry.getPlugin(pointer.activePluginId);
|
|
355
371
|
if (!plugin) {
|
|
356
372
|
this.logger.error(
|
|
357
|
-
`Queue plugin '${pointer.activePluginId}' not found during reload
|
|
373
|
+
`Queue plugin '${pointer.activePluginId}' not found during reload`,
|
|
358
374
|
);
|
|
359
375
|
return;
|
|
360
376
|
}
|
|
@@ -362,12 +378,12 @@ export class QueueManagerImpl implements QueueManager {
|
|
|
362
378
|
const config = await this.configService.get(
|
|
363
379
|
pointer.activePluginId,
|
|
364
380
|
plugin.configSchema,
|
|
365
|
-
plugin.configVersion
|
|
381
|
+
plugin.configVersion,
|
|
366
382
|
);
|
|
367
383
|
|
|
368
384
|
if (!config) {
|
|
369
385
|
this.logger.error(
|
|
370
|
-
`Failed to load config for plugin '${pointer.activePluginId}'
|
|
386
|
+
`Failed to load config for plugin '${pointer.activePluginId}'`,
|
|
371
387
|
);
|
|
372
388
|
return;
|
|
373
389
|
}
|
|
@@ -388,7 +404,7 @@ export class QueueManagerImpl implements QueueManager {
|
|
|
388
404
|
this.configVersion = pointer.version;
|
|
389
405
|
|
|
390
406
|
this.logger.info(
|
|
391
|
-
`✅ Queue configuration reloaded: plugin=${this.activePluginId}
|
|
407
|
+
`✅ Queue configuration reloaded: plugin=${this.activePluginId}`,
|
|
392
408
|
);
|
|
393
409
|
}
|
|
394
410
|
|
|
@@ -4,6 +4,7 @@ import type {
|
|
|
4
4
|
ConsumeOptions,
|
|
5
5
|
QueueStats,
|
|
6
6
|
RecurringJobDetails,
|
|
7
|
+
RecurringSchedule,
|
|
7
8
|
} from "@checkstack/queue-api";
|
|
8
9
|
import { rootLogger } from "../logger";
|
|
9
10
|
|
|
@@ -39,7 +40,7 @@ export class QueueProxy<T = unknown> implements Queue<T> {
|
|
|
39
40
|
// Wait for pending operations to complete
|
|
40
41
|
if (this.pendingOperations.length > 0) {
|
|
41
42
|
rootLogger.debug(
|
|
42
|
-
`Waiting for ${this.pendingOperations.length} pending operations
|
|
43
|
+
`Waiting for ${this.pendingOperations.length} pending operations...`,
|
|
43
44
|
);
|
|
44
45
|
await Promise.allSettled(this.pendingOperations);
|
|
45
46
|
}
|
|
@@ -56,7 +57,7 @@ export class QueueProxy<T = unknown> implements Queue<T> {
|
|
|
56
57
|
// Re-apply all stored subscriptions
|
|
57
58
|
for (const [group, { consumer, options }] of this.subscriptions) {
|
|
58
59
|
rootLogger.debug(
|
|
59
|
-
`Re-applying subscription for group '${group}' on queue '${this.name}'
|
|
60
|
+
`Re-applying subscription for group '${group}' on queue '${this.name}'`,
|
|
60
61
|
);
|
|
61
62
|
await this.delegate.consume(consumer, options);
|
|
62
63
|
}
|
|
@@ -72,7 +73,7 @@ export class QueueProxy<T = unknown> implements Queue<T> {
|
|
|
72
73
|
private ensureDelegate(): Queue<T> {
|
|
73
74
|
if (!this.delegate) {
|
|
74
75
|
throw new Error(
|
|
75
|
-
`Queue '${this.name}' not initialized. Ensure QueueManager.loadConfiguration() has been called
|
|
76
|
+
`Queue '${this.name}' not initialized. Ensure QueueManager.loadConfiguration() has been called.`,
|
|
76
77
|
);
|
|
77
78
|
}
|
|
78
79
|
if (this.stopped) {
|
|
@@ -85,7 +86,7 @@ export class QueueProxy<T = unknown> implements Queue<T> {
|
|
|
85
86
|
this.pendingOperations.push(operation);
|
|
86
87
|
return operation.finally(() => {
|
|
87
88
|
this.pendingOperations = this.pendingOperations.filter(
|
|
88
|
-
(p) => p !== operation
|
|
89
|
+
(p) => p !== operation,
|
|
89
90
|
);
|
|
90
91
|
});
|
|
91
92
|
}
|
|
@@ -96,7 +97,7 @@ export class QueueProxy<T = unknown> implements Queue<T> {
|
|
|
96
97
|
priority?: number;
|
|
97
98
|
startDelay?: number;
|
|
98
99
|
jobId?: string;
|
|
99
|
-
}
|
|
100
|
+
},
|
|
100
101
|
): Promise<string> {
|
|
101
102
|
const delegate = this.ensureDelegate();
|
|
102
103
|
return this.trackOperation(delegate.enqueue(data, options));
|
|
@@ -104,7 +105,7 @@ export class QueueProxy<T = unknown> implements Queue<T> {
|
|
|
104
105
|
|
|
105
106
|
async consume(
|
|
106
107
|
consumer: QueueConsumer<T>,
|
|
107
|
-
options: ConsumeOptions
|
|
108
|
+
options: ConsumeOptions,
|
|
108
109
|
): Promise<void> {
|
|
109
110
|
// Store subscription for replay after backend switch
|
|
110
111
|
this.subscriptions.set(options.consumerGroup, { consumer, options });
|
|
@@ -119,10 +120,9 @@ export class QueueProxy<T = unknown> implements Queue<T> {
|
|
|
119
120
|
data: T,
|
|
120
121
|
options: {
|
|
121
122
|
jobId: string;
|
|
122
|
-
intervalSeconds: number;
|
|
123
123
|
startDelay?: number;
|
|
124
124
|
priority?: number;
|
|
125
|
-
}
|
|
125
|
+
} & RecurringSchedule,
|
|
126
126
|
): Promise<string> {
|
|
127
127
|
const delegate = this.ensureDelegate();
|
|
128
128
|
return this.trackOperation(delegate.scheduleRecurring(data, options));
|
|
@@ -139,7 +139,7 @@ export class QueueProxy<T = unknown> implements Queue<T> {
|
|
|
139
139
|
}
|
|
140
140
|
|
|
141
141
|
async getRecurringJobDetails(
|
|
142
|
-
jobId: string
|
|
142
|
+
jobId: string,
|
|
143
143
|
): Promise<RecurringJobDetails<T> | undefined> {
|
|
144
144
|
const delegate = this.ensureDelegate();
|
|
145
145
|
return delegate.getRecurringJobDetails(jobId);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import { eq, and } from "drizzle-orm";
|
|
4
|
-
import type {
|
|
4
|
+
import type { SafeDatabase } from "@checkstack/backend-api";
|
|
5
5
|
import { plugins } from "../schema";
|
|
6
6
|
|
|
7
7
|
export interface PluginMetadata {
|
|
@@ -116,7 +116,7 @@ export async function syncPluginsToDatabase({
|
|
|
116
116
|
db,
|
|
117
117
|
}: {
|
|
118
118
|
localPlugins: PluginMetadata[];
|
|
119
|
-
db:
|
|
119
|
+
db: SafeDatabase<Record<string, unknown>>;
|
|
120
120
|
}): Promise<void> {
|
|
121
121
|
for (const plugin of localPlugins) {
|
|
122
122
|
// Check if plugin already exists
|
package/src/utils/scoped-db.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { SafeDatabase } from "@checkstack/backend-api";
|
|
2
2
|
import { sql, entityKind } from "drizzle-orm";
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -147,7 +147,7 @@ function isWrappableBuilder(value: unknown): boolean {
|
|
|
147
147
|
/**
|
|
148
148
|
* A schema-scoped database type that excludes the relational query API.
|
|
149
149
|
*
|
|
150
|
-
* This type explicitly removes `query` from
|
|
150
|
+
* This type explicitly removes `query` from SafeDatabase so that plugin
|
|
151
151
|
* authors get compile-time errors when trying to use `db.query.*` instead
|
|
152
152
|
* of runtime errors. This provides better DX and catches isolation bypasses
|
|
153
153
|
* at development time.
|
|
@@ -156,7 +156,7 @@ function isWrappableBuilder(value: unknown): boolean {
|
|
|
156
156
|
* execution path that bypasses our schema isolation mechanism.
|
|
157
157
|
*/
|
|
158
158
|
export type ScopedDatabase<TSchema extends Record<string, unknown>> = Omit<
|
|
159
|
-
|
|
159
|
+
SafeDatabase<TSchema>,
|
|
160
160
|
"query"
|
|
161
161
|
>;
|
|
162
162
|
|
|
@@ -182,10 +182,10 @@ export type ScopedDatabase<TSchema extends Record<string, unknown>> = Omit<
|
|
|
182
182
|
* ```
|
|
183
183
|
*/
|
|
184
184
|
export function createScopedDb<TSchema extends Record<string, unknown>>(
|
|
185
|
-
baseDb:
|
|
185
|
+
baseDb: SafeDatabase<Record<string, unknown>>,
|
|
186
186
|
schemaName: string,
|
|
187
187
|
): ScopedDatabase<TSchema> {
|
|
188
|
-
const wrappedDb = baseDb as
|
|
188
|
+
const wrappedDb = baseDb as SafeDatabase<TSchema>;
|
|
189
189
|
|
|
190
190
|
/**
|
|
191
191
|
* WeakMap to track query chains for each builder instance.
|