@checkstack/backend 0.4.5 → 0.4.7

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,33 @@
1
1
  # @checkstack/backend
2
2
 
3
+ ## 0.4.7
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [db1f56f]
8
+ - @checkstack/common@0.6.0
9
+ - @checkstack/api-docs-common@0.1.4
10
+ - @checkstack/auth-common@0.5.3
11
+ - @checkstack/backend-api@0.5.1
12
+ - @checkstack/signal-backend@0.1.7
13
+ - @checkstack/signal-common@0.1.4
14
+ - @checkstack/queue-api@0.2.1
15
+
16
+ ## 0.4.6
17
+
18
+ ### Patch Changes
19
+
20
+ - 66a3963: Update plugin loader to use SafeDatabase type
21
+
22
+ - Updated `PluginLoaderDeps.db` type from `NodePgDatabase` to `SafeDatabase`
23
+ - Added type cast for drizzle `migrate()` function which still requires `NodePgDatabase`
24
+
25
+ - Updated dependencies [2c0822d]
26
+ - Updated dependencies [66a3963]
27
+ - @checkstack/queue-api@0.2.0
28
+ - @checkstack/backend-api@0.5.0
29
+ - @checkstack/signal-backend@0.1.6
30
+
3
31
  ## 0.4.5
4
32
 
5
33
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/backend",
3
- "version": "0.4.5",
3
+ "version": "0.4.7",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "dev": "bun --env-file=../../.env --watch src/index.ts",
@@ -1,5 +1,5 @@
1
1
  import type { Hono, Context } from "hono";
2
- import type { NodePgDatabase } from "drizzle-orm/node-postgres";
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 NodePgDatabase<Record<string, unknown>>,
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 { NodePgDatabase } from "drizzle-orm/node-postgres";
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: NodePgDatabase<Record<string, unknown>>;
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 { NodePgDatabase } from "drizzle-orm/node-postgres";
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: NodePgDatabase<Record<string, unknown>>;
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
- await migrate(deps.db, { migrationsFolder, migrationsSchema });
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.
@@ -1,4 +1,4 @@
1
- import { NodePgDatabase } from "drizzle-orm/node-postgres";
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: NodePgDatabase<Record<string, unknown>>
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
- await proxy.scheduleRecurring(details.data as unknown, {
213
- jobId: details.jobId,
214
- intervalSeconds: details.intervalSeconds,
215
- priority: details.priority,
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 { NodePgDatabase } from "drizzle-orm/node-postgres";
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: NodePgDatabase<Record<string, unknown>>;
119
+ db: SafeDatabase<Record<string, unknown>>;
120
120
  }): Promise<void> {
121
121
  for (const plugin of localPlugins) {
122
122
  // Check if plugin already exists
@@ -1,4 +1,4 @@
1
- import { NodePgDatabase } from "drizzle-orm/node-postgres";
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 NodePgDatabase so that plugin
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
- NodePgDatabase<TSchema>,
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: NodePgDatabase<Record<string, unknown>>,
185
+ baseDb: SafeDatabase<Record<string, unknown>>,
186
186
  schemaName: string,
187
187
  ): ScopedDatabase<TSchema> {
188
- const wrappedDb = baseDb as NodePgDatabase<TSchema>;
188
+ const wrappedDb = baseDb as SafeDatabase<TSchema>;
189
189
 
190
190
  /**
191
191
  * WeakMap to track query chains for each builder instance.