@zudojs/database 1.1.0 → 1.2.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.
package/README.md CHANGED
@@ -6,6 +6,12 @@ that translates to Prisma `where` clauses, managed transactions, keyset
6
6
  pagination with signed cursors, migration and seed runners guarded by advisory
7
7
  locks, health checks with reconnect, and a bounded in-memory cache.
8
8
 
9
+ <!-- zudo-docs:start -->
10
+
11
+ **Documentation:** [zudojs.oyinlola.site/docs/packages-database](https://zudojs.oyinlola.site/docs/packages-database) · **For AI agents:** [Markdown version](https://zudojs.oyinlola.site/docs/packages-database.md), [llms.txt](https://zudojs.oyinlola.site/llms.txt)
12
+
13
+ <!-- zudo-docs:end -->
14
+
9
15
  ## Installation
10
16
 
11
17
  ```bash
@@ -223,6 +229,8 @@ await assertDatabaseHealth(client); // throws DatabaseUnhealthyError with the re
223
229
  - `BaseRepository` with CRUD, `createMany` / `deleteMany`, soft delete (`softDelete`, `restore`, `findDeleted`, `withDeleted`), transaction rebinding (`withTransaction`), and `findByQuery`
224
230
  - Query builder and 40 filter helpers that translate to Prisma `where` / `orderBy` / `select` / `include`
225
231
  - Managed transactions with context, error enrichment, and `withTransactionRetry` (serialization failures retried by default)
232
+ with exponential backoff capped by `maxRetryDelayMs` (30 s by default, never above the
233
+ 2^31-1 ms timer limit) and optional `jitter: "full"`
226
234
  - Offset pagination and keyset pagination with HMAC-signed, shape-validated cursors
227
235
  - Migration and seed runners using PostgreSQL advisory locks, per-item transactions, and BIGINT versioning
228
236
  - Advisory and row locks with `lock_timeout`, `SKIP LOCKED` / `NOWAIT`, and namespaced keys
@@ -1,9 +1,20 @@
1
1
  /**
2
2
  * @zudojs/database — Default Logger
3
3
  *
4
- * Minimal logger for environments where no application logger has been configured yet.
4
+ * Fallback logger for environments where no application logger has been
5
+ * configured yet. It writes through `@zudojs/logger` (console transport,
6
+ * secret-name redaction) instead of calling `console.*` directly.
5
7
  */
8
+ import type { Logger } from "@zudojs/logger";
6
9
  import type { DatabaseLogger } from "../databaseType/databaseType.type.js";
7
- /** Creates a minimal logger for environments where no application logger has been configured yet. */
10
+ /** Logger name used by the database fallback logger. */
11
+ export declare const DEFAULT_DATABASE_LOGGER_NAME = "@zudojs/database";
12
+ /**
13
+ * Adapts a `@zudojs/logger` {@link Logger} to the {@link DatabaseLogger}
14
+ * contract. `debug`/`info` are dropped when `NODE_ENV` is `"production"`,
15
+ * checked per call as before; `warn`/`error` are always written.
16
+ */
17
+ export declare function createDatabaseLoggerAdapter(logger: Logger): DatabaseLogger;
18
+ /** Creates the fallback database logger, backed by `@zudojs/logger`. */
8
19
  export declare function createDefaultLogger(): DatabaseLogger;
9
20
  //# sourceMappingURL=databaseClient.logger.d.ts.map
@@ -1,27 +1,55 @@
1
1
  /**
2
2
  * @zudojs/database — Default Logger
3
3
  *
4
- * Minimal logger for environments where no application logger has been configured yet.
4
+ * Fallback logger for environments where no application logger has been
5
+ * configured yet. It writes through `@zudojs/logger` (console transport,
6
+ * secret-name redaction) instead of calling `console.*` directly.
5
7
  */
6
- /** Creates a minimal logger for environments where no application logger has been configured yet. */
7
- export function createDefaultLogger() {
8
- return {
8
+ import { createLogger, LoggerLevel } from "@zudojs/logger";
9
+ /** Logger name used by the database fallback logger. */
10
+ export const DEFAULT_DATABASE_LOGGER_NAME = "@zudojs/database";
11
+ function isVerboseEnabled() {
12
+ return process.env.NODE_ENV !== "production";
13
+ }
14
+ function toOptions(metadata, error) {
15
+ if (error === undefined) {
16
+ return metadata === undefined ? {} : { metadata };
17
+ }
18
+ if (error instanceof Error) {
19
+ return metadata === undefined ? { error } : { metadata, error };
20
+ }
21
+ return { metadata: { ...(metadata ?? {}), error } };
22
+ }
23
+ /**
24
+ * Adapts a `@zudojs/logger` {@link Logger} to the {@link DatabaseLogger}
25
+ * contract. `debug`/`info` are dropped when `NODE_ENV` is `"production"`,
26
+ * checked per call as before; `warn`/`error` are always written.
27
+ */
28
+ export function createDatabaseLoggerAdapter(logger) {
29
+ return Object.freeze({
9
30
  debug: (message, metadata) => {
10
- if (process.env.NODE_ENV !== "production") {
11
- console.debug(message, metadata);
31
+ if (isVerboseEnabled()) {
32
+ logger.log(LoggerLevel.DEBUG, message, toOptions(metadata));
12
33
  }
13
34
  },
14
35
  info: (message, metadata) => {
15
- if (process.env.NODE_ENV !== "production") {
16
- console.info(message, metadata);
36
+ if (isVerboseEnabled()) {
37
+ logger.log(LoggerLevel.INFO, message, toOptions(metadata));
17
38
  }
18
39
  },
19
40
  warn: (message, metadata) => {
20
- console.warn(message, metadata);
41
+ logger.log(LoggerLevel.WARN, message, toOptions(metadata));
21
42
  },
22
43
  error: (message, error, metadata) => {
23
- console.error(message, error, metadata);
44
+ logger.log(LoggerLevel.ERROR, message, toOptions(metadata, error));
24
45
  },
25
- };
46
+ });
47
+ }
48
+ /** Creates the fallback database logger, backed by `@zudojs/logger`. */
49
+ export function createDefaultLogger() {
50
+ return createDatabaseLoggerAdapter(createLogger({
51
+ name: DEFAULT_DATABASE_LOGGER_NAME,
52
+ level: LoggerLevel.DEBUG,
53
+ }));
26
54
  }
27
55
  //# sourceMappingURL=databaseClient.logger.js.map
@@ -1,3 +1,4 @@
1
+ import { isPlainObject } from "@zudojs/types";
1
2
  import { normalizePagination } from "../pagination/pagination.core.js";
2
3
  import { toPrismaInclude, } from "../relations/relations.definition.js";
3
4
  const FIELD_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
@@ -224,10 +225,4 @@ function validateFieldName(field) {
224
225
  throw new TypeError(`Invalid query field name "${String(field)}".`);
225
226
  }
226
227
  }
227
- function isPlainObject(value) {
228
- return (value !== null &&
229
- typeof value === "object" &&
230
- !Array.isArray(value) &&
231
- !(value instanceof Date));
232
- }
233
228
  //# sourceMappingURL=queryBuilder.prisma.js.map
@@ -0,0 +1,33 @@
1
+ /**
2
+ * @zudojs/database — Transaction retry backoff.
3
+ *
4
+ * `retryDelayMs * 2^(attempt-1)` passes 2^31-1 ms by attempt 26 at the
5
+ * default 100 ms base. `setTimeout` then warns and fires after 1 ms, so a
6
+ * large retry budget turned exponential backoff into a tight retry storm
7
+ * against a database that was already contended. The delay is clamped to a
8
+ * configurable ceiling and, below that, to the timer maximum.
9
+ */
10
+ /** Largest delay `setTimeout` honours (2^31-1 ms, about 24.8 days). */
11
+ export declare const MAX_TIMER_DELAY_MS = 2147483647;
12
+ /** Default ceiling for a single retry delay: 30 seconds. */
13
+ export declare const DEFAULT_MAX_RETRY_DELAY_MS = 30000;
14
+ /** Inputs to {@link computeRetryDelay}. */
15
+ export interface RetryDelayOptions {
16
+ /** Base delay, doubled each retry. */
17
+ readonly retryDelayMs: number;
18
+ /** Ceiling for any single delay. */
19
+ readonly maxRetryDelayMs?: number;
20
+ /** `"full"` picks uniformly in `[0, delay]` to spread contending retries. */
21
+ readonly jitter?: "none" | "full";
22
+ /** Random source in `[0, 1)`, injectable for tests. */
23
+ readonly random?: () => number;
24
+ }
25
+ /**
26
+ * Delay before retry number `attempt` (1-based).
27
+ *
28
+ * @param attempt - The retry about to be made, starting at 1.
29
+ * @param options - Base delay, ceiling and jitter.
30
+ * @returns A finite delay in `[0, min(maxRetryDelayMs, 2^31-1)]`.
31
+ */
32
+ export declare function computeRetryDelay(attempt: number, options: RetryDelayOptions): number;
33
+ //# sourceMappingURL=transaction.backoff.d.ts.map
@@ -0,0 +1,32 @@
1
+ /**
2
+ * @zudojs/database — Transaction retry backoff.
3
+ *
4
+ * `retryDelayMs * 2^(attempt-1)` passes 2^31-1 ms by attempt 26 at the
5
+ * default 100 ms base. `setTimeout` then warns and fires after 1 ms, so a
6
+ * large retry budget turned exponential backoff into a tight retry storm
7
+ * against a database that was already contended. The delay is clamped to a
8
+ * configurable ceiling and, below that, to the timer maximum.
9
+ */
10
+ /** Largest delay `setTimeout` honours (2^31-1 ms, about 24.8 days). */
11
+ export const MAX_TIMER_DELAY_MS = 2_147_483_647;
12
+ /** Default ceiling for a single retry delay: 30 seconds. */
13
+ export const DEFAULT_MAX_RETRY_DELAY_MS = 30_000;
14
+ /**
15
+ * Delay before retry number `attempt` (1-based).
16
+ *
17
+ * @param attempt - The retry about to be made, starting at 1.
18
+ * @param options - Base delay, ceiling and jitter.
19
+ * @returns A finite delay in `[0, min(maxRetryDelayMs, 2^31-1)]`.
20
+ */
21
+ export function computeRetryDelay(attempt, options) {
22
+ const ceiling = Math.min(Math.max(0, options.maxRetryDelayMs ?? DEFAULT_MAX_RETRY_DELAY_MS), MAX_TIMER_DELAY_MS);
23
+ const exponential = options.retryDelayMs * 2 ** Math.max(0, attempt - 1);
24
+ const capped = Number.isFinite(exponential)
25
+ ? Math.min(exponential, ceiling)
26
+ : ceiling;
27
+ if (options.jitter !== "full")
28
+ return capped;
29
+ const random = options.random ?? Math.random;
30
+ return Math.floor(random() * (capped + 1));
31
+ }
32
+ //# sourceMappingURL=transaction.backoff.js.map
@@ -41,6 +41,16 @@ export interface TransactionRetryOptions extends ManagedTransactionOptions {
41
41
  * Base delay between attempts (doubles each retry). Defaults to 100 ms.
42
42
  */
43
43
  readonly retryDelayMs?: number;
44
+ /**
45
+ * Ceiling for a single retry delay. Defaults to 30000 ms, and never
46
+ * exceeds the timer maximum (2^31-1 ms).
47
+ */
48
+ readonly maxRetryDelayMs?: number;
49
+ /**
50
+ * `"full"` spreads each delay uniformly over `[0, delay]` so contending
51
+ * callers do not retry in lockstep. Defaults to `"none"`.
52
+ */
53
+ readonly jitter?: "none" | "full";
44
54
  /**
45
55
  * Predicate deciding whether an error is retryable. Defaults to
46
56
  * {@link isRetryableTransactionError} (Prisma P2034/P2028/P1017 and
@@ -1,5 +1,6 @@
1
1
  import { DatabaseOperation } from "@zudojs/errors";
2
2
  import { isDatabaseErrorLike, isRetryableTransactionError, normalizeDatabaseError, withDatabaseErrorMetadata, } from "../databaseClient/databaseClient.errors.js";
3
+ import { computeRetryDelay } from "./transaction.backoff.js";
3
4
  /**
4
5
  * Generates a transaction identifier.
5
6
  */
@@ -94,7 +95,13 @@ export async function withTransactionRetry(client, callback, options = {}) {
94
95
  throw error;
95
96
  }
96
97
  attempt += 1;
97
- const delay = retryDelayMs * Math.pow(2, attempt - 1);
98
+ const delay = computeRetryDelay(attempt, {
99
+ retryDelayMs,
100
+ ...(options.maxRetryDelayMs !== undefined
101
+ ? { maxRetryDelayMs: options.maxRetryDelayMs }
102
+ : {}),
103
+ ...(options.jitter !== undefined ? { jitter: options.jitter } : {}),
104
+ });
98
105
  if (delay > 0)
99
106
  await sleep(delay);
100
107
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zudojs/database",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Database abstraction layer with clients, repositories, transactions, and query building for Zudojs applications.",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -41,7 +41,9 @@
41
41
  "!dist/.tsbuildinfo"
42
42
  ],
43
43
  "dependencies": {
44
- "@zudojs/errors": "1.0.1"
44
+ "@zudojs/errors": "1.1.0",
45
+ "@zudojs/logger": "1.2.0",
46
+ "@zudojs/types": "1.1.0"
45
47
  },
46
48
  "peerDependencies": {
47
49
  "@prisma/client": ">=7.0.0 <8"