@dudousxd/nestjs-durable-store-drizzle 0.23.0 → 0.24.0

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.
@@ -22,12 +22,40 @@ import { SQL } from 'drizzle-orm';
22
22
  * an existing database heals on the next boot exactly like the MikroORM / TypeORM auto-schema does.
23
23
  */
24
24
  declare const DURABLE_PG_DDL: readonly string[];
25
+ /**
26
+ * Where {@link ensureDrizzlePgDurableSchema} reports a schema repair it actually applied. `console`
27
+ * (the default) satisfies it, and so does NestJS's `Logger`, so an app can route the warning into
28
+ * its own log stream.
29
+ */
30
+ interface DurableSchemaLogger {
31
+ warn(message: string): void;
32
+ }
33
+ interface EnsureDrizzlePgDurableSchemaOptions {
34
+ /** Sink for the schema-repair warning. Defaults to `console`. */
35
+ logger?: DurableSchemaLogger | undefined;
36
+ }
37
+ /**
38
+ * The warning logged when the boot-time check changed an EXISTING durable schema: created a table
39
+ * that was missing next to ones that exist, added a column, or created an index. Never logged when
40
+ * the schema was already current, and never on a fresh install (no durable table existed, so
41
+ * creating them all is the expected first boot, not a drift).
42
+ *
43
+ * The discriminator matters more than the wording: a warning on every boot would be filtered as
44
+ * noise, and a silent repair hides a drizzle-kit-managed schema that has drifted from the library.
45
+ */
46
+ declare function drizzlePgSchemaRepairWarning(repairs: readonly string[]): string;
25
47
  /**
26
48
  * Idempotently create the durable tables + indexes on `db` (any drizzle Postgres driver). Runs in one
27
49
  * transaction under a `pg_advisory_xact_lock`, so N pods booting together heal the schema once and
28
50
  * the rest see it done. Only ever touches the `durable_*` tables.
51
+ *
52
+ * The DDL is all `IF NOT EXISTS`, which does not say whether it created anything, so the current
53
+ * durable tables, columns and indexes are read from the catalog first (inside the lock) and each
54
+ * statement that will create something is recorded. When anything was created on an EXISTING durable
55
+ * schema, ONE warning listing it goes to `options.logger` (default `console`) after the transaction
56
+ * commits; see {@link drizzlePgSchemaRepairWarning}. A current schema and a fresh install log nothing.
29
57
  */
30
- declare function ensureDrizzlePgDurableSchema(db: PgDatabase<any, any, any>): Promise<void>;
58
+ declare function ensureDrizzlePgDurableSchema(db: PgDatabase<any, any, any>, options?: EnsureDrizzlePgDurableSchemaOptions): Promise<void>;
31
59
 
32
60
  /**
33
61
  * Any drizzle Postgres database — `drizzle-orm/node-postgres`, `drizzle-orm/postgres-js`,
@@ -45,6 +73,12 @@ interface DrizzlePgStateStoreOptions {
45
73
  scope?: {
46
74
  namespace?: string | undefined;
47
75
  } | undefined;
76
+ /**
77
+ * Where `ensureSchema()` (the boot-time auto-schema) reports a schema repair it actually applied
78
+ * — a missing table or index it had to create. Defaults to `console`. NestJS's `Logger`
79
+ * satisfies it.
80
+ */
81
+ schemaLogger?: DurableSchemaLogger | undefined;
48
82
  }
49
83
  /**
50
84
  * Drizzle `StateStore` for **PostgreSQL**. Pass any drizzle Postgres db (see {@link DrizzlePgDatabase});
@@ -97,7 +131,7 @@ declare class DrizzlePgStateStore implements StateStore {
97
131
  listPendingRuns(limit: number, namespace?: string): Promise<WorkflowRun[]>;
98
132
  listDueTimers(nowMs: number, namespace?: string, limit?: number): Promise<WorkflowRun[]>;
99
133
  tryLockRun(runId: string, owner: string, leaseUntilMs: number, nowMs: number): Promise<boolean>;
100
- releaseRunLock(runId: string): Promise<void>;
134
+ releaseRunLock(runId: string, owner?: string): Promise<void>;
101
135
  renewRunLock(runId: string, owner: string, leaseUntilMs: number): Promise<boolean>;
102
136
  /** The predicates every run query shares — {@link listRuns} pages them, the facets group them. */
103
137
  private runFilters;
@@ -2397,4 +2431,4 @@ declare const durablePgSchema: {
2397
2431
  */
2398
2432
  declare function durablePgManagedTables(): string[];
2399
2433
 
2400
- export { DEFAULT_NAMESPACE, DURABLE_PG_DDL, type DrizzlePgDatabase, type DrizzlePgSqlExecutor, DrizzlePgStateStore, type DrizzlePgStateStoreOptions, drizzlePgExecutor, durableBufferedEvents, durableBufferedSignals, durablePgManagedTables, durablePgSchema, durableRunAttributes, durableSchedules, durableSignalWaiters, durableStepCheckpoints, durableWorkflowRuns, ensureDrizzlePgDurableSchema, rowsOf, toDrizzleSql };
2434
+ export { DEFAULT_NAMESPACE, DURABLE_PG_DDL, type DrizzlePgDatabase, type DrizzlePgSqlExecutor, DrizzlePgStateStore, type DrizzlePgStateStoreOptions, type DurableSchemaLogger, type EnsureDrizzlePgDurableSchemaOptions, drizzlePgExecutor, drizzlePgSchemaRepairWarning, durableBufferedEvents, durableBufferedSignals, durablePgManagedTables, durablePgSchema, durableRunAttributes, durableSchedules, durableSignalWaiters, durableStepCheckpoints, durableWorkflowRuns, ensureDrizzlePgDurableSchema, rowsOf, toDrizzleSql };
@@ -22,12 +22,40 @@ import { SQL } from 'drizzle-orm';
22
22
  * an existing database heals on the next boot exactly like the MikroORM / TypeORM auto-schema does.
23
23
  */
24
24
  declare const DURABLE_PG_DDL: readonly string[];
25
+ /**
26
+ * Where {@link ensureDrizzlePgDurableSchema} reports a schema repair it actually applied. `console`
27
+ * (the default) satisfies it, and so does NestJS's `Logger`, so an app can route the warning into
28
+ * its own log stream.
29
+ */
30
+ interface DurableSchemaLogger {
31
+ warn(message: string): void;
32
+ }
33
+ interface EnsureDrizzlePgDurableSchemaOptions {
34
+ /** Sink for the schema-repair warning. Defaults to `console`. */
35
+ logger?: DurableSchemaLogger | undefined;
36
+ }
37
+ /**
38
+ * The warning logged when the boot-time check changed an EXISTING durable schema: created a table
39
+ * that was missing next to ones that exist, added a column, or created an index. Never logged when
40
+ * the schema was already current, and never on a fresh install (no durable table existed, so
41
+ * creating them all is the expected first boot, not a drift).
42
+ *
43
+ * The discriminator matters more than the wording: a warning on every boot would be filtered as
44
+ * noise, and a silent repair hides a drizzle-kit-managed schema that has drifted from the library.
45
+ */
46
+ declare function drizzlePgSchemaRepairWarning(repairs: readonly string[]): string;
25
47
  /**
26
48
  * Idempotently create the durable tables + indexes on `db` (any drizzle Postgres driver). Runs in one
27
49
  * transaction under a `pg_advisory_xact_lock`, so N pods booting together heal the schema once and
28
50
  * the rest see it done. Only ever touches the `durable_*` tables.
51
+ *
52
+ * The DDL is all `IF NOT EXISTS`, which does not say whether it created anything, so the current
53
+ * durable tables, columns and indexes are read from the catalog first (inside the lock) and each
54
+ * statement that will create something is recorded. When anything was created on an EXISTING durable
55
+ * schema, ONE warning listing it goes to `options.logger` (default `console`) after the transaction
56
+ * commits; see {@link drizzlePgSchemaRepairWarning}. A current schema and a fresh install log nothing.
29
57
  */
30
- declare function ensureDrizzlePgDurableSchema(db: PgDatabase<any, any, any>): Promise<void>;
58
+ declare function ensureDrizzlePgDurableSchema(db: PgDatabase<any, any, any>, options?: EnsureDrizzlePgDurableSchemaOptions): Promise<void>;
31
59
 
32
60
  /**
33
61
  * Any drizzle Postgres database — `drizzle-orm/node-postgres`, `drizzle-orm/postgres-js`,
@@ -45,6 +73,12 @@ interface DrizzlePgStateStoreOptions {
45
73
  scope?: {
46
74
  namespace?: string | undefined;
47
75
  } | undefined;
76
+ /**
77
+ * Where `ensureSchema()` (the boot-time auto-schema) reports a schema repair it actually applied
78
+ * — a missing table or index it had to create. Defaults to `console`. NestJS's `Logger`
79
+ * satisfies it.
80
+ */
81
+ schemaLogger?: DurableSchemaLogger | undefined;
48
82
  }
49
83
  /**
50
84
  * Drizzle `StateStore` for **PostgreSQL**. Pass any drizzle Postgres db (see {@link DrizzlePgDatabase});
@@ -97,7 +131,7 @@ declare class DrizzlePgStateStore implements StateStore {
97
131
  listPendingRuns(limit: number, namespace?: string): Promise<WorkflowRun[]>;
98
132
  listDueTimers(nowMs: number, namespace?: string, limit?: number): Promise<WorkflowRun[]>;
99
133
  tryLockRun(runId: string, owner: string, leaseUntilMs: number, nowMs: number): Promise<boolean>;
100
- releaseRunLock(runId: string): Promise<void>;
134
+ releaseRunLock(runId: string, owner?: string): Promise<void>;
101
135
  renewRunLock(runId: string, owner: string, leaseUntilMs: number): Promise<boolean>;
102
136
  /** The predicates every run query shares — {@link listRuns} pages them, the facets group them. */
103
137
  private runFilters;
@@ -2397,4 +2431,4 @@ declare const durablePgSchema: {
2397
2431
  */
2398
2432
  declare function durablePgManagedTables(): string[];
2399
2433
 
2400
- export { DEFAULT_NAMESPACE, DURABLE_PG_DDL, type DrizzlePgDatabase, type DrizzlePgSqlExecutor, DrizzlePgStateStore, type DrizzlePgStateStoreOptions, drizzlePgExecutor, durableBufferedEvents, durableBufferedSignals, durablePgManagedTables, durablePgSchema, durableRunAttributes, durableSchedules, durableSignalWaiters, durableStepCheckpoints, durableWorkflowRuns, ensureDrizzlePgDurableSchema, rowsOf, toDrizzleSql };
2434
+ export { DEFAULT_NAMESPACE, DURABLE_PG_DDL, type DrizzlePgDatabase, type DrizzlePgSqlExecutor, DrizzlePgStateStore, type DrizzlePgStateStoreOptions, type DurableSchemaLogger, type EnsureDrizzlePgDurableSchemaOptions, drizzlePgExecutor, drizzlePgSchemaRepairWarning, durableBufferedEvents, durableBufferedSignals, durablePgManagedTables, durablePgSchema, durableRunAttributes, durableSchedules, durableSignalWaiters, durableStepCheckpoints, durableWorkflowRuns, ensureDrizzlePgDurableSchema, rowsOf, toDrizzleSql };
package/dist/pg/index.js CHANGED
@@ -2,7 +2,41 @@ var __defProp = Object.defineProperty;
2
2
  var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
3
3
 
4
4
  // src/pg/ddl.ts
5
+ import { sql as sql2 } from "drizzle-orm";
6
+
7
+ // src/pg/executor.ts
5
8
  import { sql } from "drizzle-orm";
9
+ function toDrizzleSql(text2, params = []) {
10
+ const chunks = [];
11
+ let last = 0;
12
+ for (const match of text2.matchAll(/\$(\d+)/g)) {
13
+ const index2 = Number(match[1]) - 1;
14
+ if (index2 < 0 || index2 >= params.length) throw new Error(`placeholder ${match[0]} has no bound parameter`);
15
+ chunks.push(sql.raw(text2.slice(last, match.index)), sql`${params[index2]}`);
16
+ last = (match.index ?? 0) + match[0].length;
17
+ }
18
+ chunks.push(sql.raw(text2.slice(last)));
19
+ return sql.join(chunks);
20
+ }
21
+ __name(toDrizzleSql, "toDrizzleSql");
22
+ function rowsOf(result) {
23
+ if (Array.isArray(result)) return result;
24
+ return result?.rows ?? [];
25
+ }
26
+ __name(rowsOf, "rowsOf");
27
+ function drizzlePgExecutor(db) {
28
+ return {
29
+ dialect: "postgres",
30
+ escapeId: /* @__PURE__ */ __name((id) => `"${id.replace(/"/g, '""')}"`, "escapeId"),
31
+ query: /* @__PURE__ */ __name(async (text2, params) => rowsOf(await db.execute(toDrizzleSql(text2, params))), "query"),
32
+ transaction: /* @__PURE__ */ __name((fn) => db.transaction((tx) => fn({
33
+ query: /* @__PURE__ */ __name(async (text2, params) => rowsOf(await tx.execute(toDrizzleSql(text2, params))), "query")
34
+ })), "transaction")
35
+ };
36
+ }
37
+ __name(drizzlePgExecutor, "drizzlePgExecutor");
38
+
39
+ // src/pg/ddl.ts
6
40
  var DURABLE_PG_DDL = [
7
41
  `CREATE TABLE IF NOT EXISTS "durable_workflow_runs" (
8
42
  "id" text PRIMARY KEY NOT NULL,
@@ -95,11 +129,71 @@ var DURABLE_PG_DDL = [
95
129
  `CREATE INDEX IF NOT EXISTS "durable_schedules_due_idx" ON "durable_schedules" USING btree ("paused","next_fire_at")`
96
130
  ];
97
131
  var SCHEMA_LOCK_KEY = "durable_schema";
98
- async function ensureDrizzlePgDurableSchema(db) {
99
- await db.transaction(async (tx) => {
100
- await tx.execute(sql`SELECT pg_advisory_xact_lock(hashtext(${SCHEMA_LOCK_KEY}))`);
101
- for (const statement of DURABLE_PG_DDL) await tx.execute(sql.raw(statement));
132
+ function drizzlePgSchemaRepairWarning(repairs) {
133
+ return [
134
+ `[nestjs-durable] store-drizzle repaired the durable schema at boot: ${repairs.join("; ")}.`,
135
+ "The database had drifted from the schema this library version expects (it was created by an",
136
+ "older version, or a migration left these out), so they were applied at runtime and nothing",
137
+ "recorded them in your migration history. To make them permanent, generate a drizzle-kit",
138
+ "migration from `durablePgSchema` (or apply `DURABLE_PG_DDL` in a migration). This warning",
139
+ "appears only when a repair was applied; a schema that is already current logs nothing."
140
+ ].join(" ");
141
+ }
142
+ __name(drizzlePgSchemaRepairWarning, "drizzlePgSchemaRepairWarning");
143
+ function ddlTarget(statement) {
144
+ const table = statement.match(/^CREATE TABLE IF NOT EXISTS "([^"]+)"/i);
145
+ if (table?.[1]) return {
146
+ kind: "table",
147
+ table: table[1]
148
+ };
149
+ const index2 = statement.match(/^CREATE (?:UNIQUE )?INDEX (?:CONCURRENTLY )?IF NOT EXISTS "([^"]+)" ON "([^"]+)"/i);
150
+ if (index2?.[1] && index2[2]) return {
151
+ kind: "index",
152
+ index: index2[1],
153
+ table: index2[2]
154
+ };
155
+ const column = statement.match(/^ALTER TABLE "([^"]+)" ADD COLUMN IF NOT EXISTS "([^"]+)"/i);
156
+ if (column?.[1] && column[2]) return {
157
+ kind: "column",
158
+ table: column[1],
159
+ column: column[2]
160
+ };
161
+ return void 0;
162
+ }
163
+ __name(ddlTarget, "ddlTarget");
164
+ async function ensureDrizzlePgDurableSchema(db, options = {}) {
165
+ const logger = options.logger ?? console;
166
+ const { repairs, freshInstall } = await db.transaction(async (tx) => {
167
+ await tx.execute(sql2`SELECT pg_advisory_xact_lock(hashtext(${SCHEMA_LOCK_KEY}))`);
168
+ const tables = new Set(rowsOf(await tx.execute(sql2.raw("SELECT tablename AS name FROM pg_tables WHERE schemaname = current_schema() AND left(tablename, 8) = 'durable_'"))).map((row) => row.name));
169
+ const indexes = new Set(rowsOf(await tx.execute(sql2.raw("SELECT indexname AS name FROM pg_indexes WHERE schemaname = current_schema() AND left(tablename, 8) = 'durable_'"))).map((row) => row.name));
170
+ const columns = new Set(rowsOf(await tx.execute(sql2.raw("SELECT table_name || '.' || column_name AS name FROM information_schema.columns WHERE table_schema = current_schema() AND left(table_name, 8) = 'durable_'"))).map((row) => row.name));
171
+ const created = /* @__PURE__ */ new Set();
172
+ const ddlTables = /* @__PURE__ */ new Set();
173
+ const applied = [];
174
+ for (const statement of DURABLE_PG_DDL) {
175
+ const target = ddlTarget(statement);
176
+ await tx.execute(sql2.raw(statement));
177
+ if (!target) continue;
178
+ if (target.kind === "table") {
179
+ ddlTables.add(target.table);
180
+ if (!tables.has(target.table)) {
181
+ created.add(target.table);
182
+ applied.push(`created table ${target.table}`);
183
+ }
184
+ } else if (created.has(target.table)) {
185
+ } else if (target.kind === "index" && !indexes.has(target.index)) {
186
+ applied.push(`created index ${target.index} on ${target.table}`);
187
+ } else if (target.kind === "column" && !columns.has(`${target.table}.${target.column}`)) {
188
+ applied.push(`added column ${target.table}.${target.column}`);
189
+ }
190
+ }
191
+ return {
192
+ repairs: applied,
193
+ freshInstall: created.size === ddlTables.size
194
+ };
102
195
  });
196
+ if (repairs.length > 0 && !freshInstall) logger.warn(drizzlePgSchemaRepairWarning(repairs));
103
197
  }
104
198
  __name(ensureDrizzlePgDurableSchema, "ensureDrizzlePgDurableSchema");
105
199
 
@@ -107,38 +201,6 @@ __name(ensureDrizzlePgDurableSchema, "ensureDrizzlePgDurableSchema");
107
201
  import { RUN_VALUE_FACET_SCAN, SINGLETON_ACTIVE_STATUSES, TERMINAL_RUN_STATUSES, attributePredicateOperands, axisIsRunColumn, clearTerminalSingletonAdmission, mergeRunFacetRows, mergeRunValueFacetRows, normalizeAttributeRows, parseDuration, runValueFacetsFromRuns, singletonAdmissionDenialPatch, singletonAdmissionTags } from "@dudousxd/nestjs-durable-core";
108
202
  import { and, asc, desc, eq, exists, getTableColumns, gte, inArray, isNotNull, isNull, like, lt, lte, notInArray, or, sql as sql3 } from "drizzle-orm";
109
203
 
110
- // src/pg/executor.ts
111
- import { sql as sql2 } from "drizzle-orm";
112
- function toDrizzleSql(text2, params = []) {
113
- const chunks = [];
114
- let last = 0;
115
- for (const match of text2.matchAll(/\$(\d+)/g)) {
116
- const index2 = Number(match[1]) - 1;
117
- if (index2 < 0 || index2 >= params.length) throw new Error(`placeholder ${match[0]} has no bound parameter`);
118
- chunks.push(sql2.raw(text2.slice(last, match.index)), sql2`${params[index2]}`);
119
- last = (match.index ?? 0) + match[0].length;
120
- }
121
- chunks.push(sql2.raw(text2.slice(last)));
122
- return sql2.join(chunks);
123
- }
124
- __name(toDrizzleSql, "toDrizzleSql");
125
- function rowsOf(result) {
126
- if (Array.isArray(result)) return result;
127
- return result?.rows ?? [];
128
- }
129
- __name(rowsOf, "rowsOf");
130
- function drizzlePgExecutor(db) {
131
- return {
132
- dialect: "postgres",
133
- escapeId: /* @__PURE__ */ __name((id) => `"${id.replace(/"/g, '""')}"`, "escapeId"),
134
- query: /* @__PURE__ */ __name(async (text2, params) => rowsOf(await db.execute(toDrizzleSql(text2, params))), "query"),
135
- transaction: /* @__PURE__ */ __name((fn) => db.transaction((tx) => fn({
136
- query: /* @__PURE__ */ __name(async (text2, params) => rowsOf(await tx.execute(toDrizzleSql(text2, params))), "query")
137
- })), "transaction")
138
- };
139
- }
140
- __name(drizzlePgExecutor, "drizzlePgExecutor");
141
-
142
204
  // src/pg/schema.ts
143
205
  import { getTableName } from "drizzle-orm";
144
206
  import { bigserial, boolean, doublePrecision, index, integer, jsonb, pgTable, primaryKey, text, timestamp } from "drizzle-orm/pg-core";
@@ -351,7 +413,9 @@ var DrizzlePgStateStore = class _DrizzlePgStateStore {
351
413
  /** Idempotent CREATE TABLE / INDEX IF NOT EXISTS under an advisory lock — called on boot when the
352
414
  * module's `autoSchema` is on. Turn `autoSchema` off if drizzle-kit owns these tables. */
353
415
  async ensureSchema() {
354
- await ensureDrizzlePgDurableSchema(this.db);
416
+ await ensureDrizzlePgDurableSchema(this.db, {
417
+ logger: this.options.schemaLogger
418
+ });
355
419
  }
356
420
  /** The read-scope predicate on the runs table (undefined when the store is unscoped). */
357
421
  scope() {
@@ -549,11 +613,11 @@ var DrizzlePgStateStore = class _DrizzlePgStateStore {
549
613
  });
550
614
  return rows.length === 1;
551
615
  }
552
- async releaseRunLock(runId) {
616
+ async releaseRunLock(runId, owner) {
553
617
  await this.db.update(durableWorkflowRuns).set({
554
618
  lockedBy: null,
555
619
  lockedUntil: null
556
- }).where(eq(durableWorkflowRuns.id, runId));
620
+ }).where(owner === void 0 ? eq(durableWorkflowRuns.id, runId) : and(eq(durableWorkflowRuns.id, runId), eq(durableWorkflowRuns.lockedBy, owner)));
557
621
  }
558
622
  async renewRunLock(runId, owner, leaseUntilMs) {
559
623
  const rows = await this.db.update(durableWorkflowRuns).set({
@@ -1100,6 +1164,7 @@ export {
1100
1164
  DURABLE_PG_DDL,
1101
1165
  DrizzlePgStateStore,
1102
1166
  drizzlePgExecutor,
1167
+ drizzlePgSchemaRepairWarning,
1103
1168
  durableBufferedEvents,
1104
1169
  durableBufferedSignals,
1105
1170
  durablePgManagedTables,