zerobucket 0.6.1 → 0.7.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
@@ -10,23 +10,26 @@ This is the Node.js/TypeScript sibling of the [Python package](https://pypi.org/
10
10
  **It uses the exact same table**, so a Node service and a Python service can share one database
11
11
  and read each other's images.
12
12
 
13
- > **Status: early (0.6.x).** PostgreSQL **and SQLite**, each in classic and dedup modes, with S3-compatible
14
- > object-storage tiering, HEIC (iPhone photo) support and a `zerobucket` command line tool. MySQL is on the
15
- > roadmap (see [What's not here yet](#whats-not-here-yet)).
13
+ > **Status: early (0.7.x).** PostgreSQL **and SQLite**, each in classic and dedup modes, plus **MySQL/MariaDB** (classic
14
+ > mode; dedup and the CLI are not available for MySQL yet, see [What's not here yet](#whats-not-here-yet)), with
15
+ > S3-compatible object-storage tiering, HEIC (iPhone photo) support and a `zerobucket` command line tool.
16
16
 
17
17
  ## Install
18
18
 
19
19
  ```bash
20
20
  npm install zerobucket pg sharp # PostgreSQL
21
21
  npm install zerobucket better-sqlite3 sharp # or SQLite (see below): no database server at all
22
+ npm install zerobucket mysql2 sharp # or MySQL / MariaDB (see below)
22
23
  npm install libheif-js # optional: accept HEIC (iPhone) photos, see below
23
24
  ```
24
25
 
25
- `pg` (the Postgres driver), `better-sqlite3` (SQLite) and `sharp` (image decoding/optimizing) are peer dependencies, so you
26
- control their versions. If you only store PDFs through a custom validator, `sharp` is not needed.
26
+ `pg` (the Postgres driver), `better-sqlite3` (SQLite), `mysql2` (MySQL/MariaDB) and `sharp` (image decoding/optimizing) are
27
+ peer dependencies, so you control their versions. If you only store PDFs through a custom validator, `sharp` is not needed.
27
28
  `sharp` must be **0.35.5 or newer**: older versions bundle libvips/libheif/librsvg builds with published high-severity
28
29
  advisories, and this package decodes untrusted uploads with it. A plain `npm install sharp` already gets a fixed version;
29
30
  if you pinned an older one, upgrade with `npm install sharp@latest`.
31
+ `mysql2` must be **3.23.1 or newer** for the same reason: every earlier release has a published advisory (the oldest range
32
+ includes critical ones), and 3.23.1 is the first that `npm audit` reports clean.
30
33
  For [object-storage tiering](#object-storage-tiering) also install `@aws-sdk/client-s3` (optional; loaded only when used).
31
34
  Requires Node 20+ (SQLite needs Node 22+, see below). Ships ESM and CommonJS with full types.
32
35
 
@@ -217,7 +220,7 @@ const id2 = await images.put(iphonePhoto, {
217
220
 
218
221
  The package installs a `zerobucket` command (`npx zerobucket ...`, or install globally with `npm i -g zerobucket`).
219
222
  It has the same commands, flags, messages and exit codes as the Python package's CLI, and additionally works with
220
- **SQLite** and **dedup mode**, and understands images that were tiered to object storage.
223
+ **SQLite** and **dedup mode**, and understands images that were tiered to object storage. It does not know MySQL/MariaDB yet.
221
224
 
222
225
  ```bash
223
226
  export ZEROBUCKET_DATABASE_URL=postgresql://user:pass@localhost/mydb # or: --database-url URL
@@ -294,6 +297,55 @@ How it behaves in Node, and why:
294
297
  (`better-sqlite3` 12 declares Node 20 support, but it is untested here.)
295
298
  - Not available on SQLite: `migrateClassicToDedup()` (Python has it for Postgres only, too).
296
299
 
300
+ ## MySQL / MariaDB
301
+
302
+ ```ts
303
+ const images = new ZeroBucket({
304
+ mysql: "mysql://user:password@localhost:3306/mydb",
305
+ }); // npm install mysql2
306
+ // "mariadb://..." works too. Bring your own pool with ZeroBucket({ mysqlPool }) (never closed by close()).
307
+ ```
308
+
309
+ Same API as everywhere else: `put`, `get`, `getMany`, `putMany`, `delete`, `deleteMany`, `exists`, `metadata`, streaming
310
+ and ranges, access-control hooks, the optimize pipeline, custom validators, HEIC, `onOperation` metrics, retry with
311
+ backoff, pooling (`poolMaxSize`, `poolTimeoutMs`), **object-storage tiering** and `connection` (your own transaction).
312
+ It uses the Python package's table, byte for byte (`SHOW CREATE TABLE` is identical), so a Node app and a Python app can
313
+ share one MySQL/MariaDB database (the conformance harness verifies both directions, including tiered rows).
314
+
315
+ **Not available on MySQL yet:** dedup mode (`dedup: true` throws, rather than being ignored) and the `zerobucket` command
316
+ line tool (it knows PostgreSQL and SQLite). The Python package has both for MySQL.
317
+
318
+ What is different from Postgres, and why (each verified against a real MariaDB 10.11):
319
+
320
+ - **No `RETURNING`.** MySQL 8.0 does not have it (MariaDB 10.5+ does, but one code path serves both), so `delete()` and
321
+ `deleteMany()` run `SELECT ... FOR UPDATE` and then `DELETE` in one transaction. That is also how a tiered row's object
322
+ gets cleaned up.
323
+ - **Timestamps are written as UTC strings** (`DATETIME(6)`, naive UTC, like the Python package), so your server's or your
324
+ process's timezone setting cannot change what is stored.
325
+ - **Pooling.** `mysql2` opens connections on demand, so `poolMinSize` is accepted for parity but there is no minimum to hold.
326
+ `mysql2` has no acquire timeout, so `poolTimeoutMs` is implemented here: waiting longer than that for a free connection
327
+ throws a clear `StorageError`, which is **not** retried (retrying would only wait again).
328
+ - **Retry** covers connection loss, lock-wait timeout (1205), deadlock (1213) and "too many connections". Every retry
329
+ re-runs the **whole** operation on a fresh connection after a full rollback, never a single statement, so a multi-statement
330
+ operation can never be half-applied. A connection that fails is destroyed, never returned to the pool.
331
+ - **Migration takes a server-side lock** (`GET_LOCK`), so many processes starting at once do not race on the DDL, and it
332
+ issues **no DDL at all** when the schema is current. (Even a no-op `ALTER TABLE` needs an exclusive metadata lock that
333
+ waits behind every open transaction, and everything else on that table then queues behind _it_.)
334
+ - **Large images and `max_allowed_packet`.** An image bigger than the server's limit fails with an error that says so and
335
+ how to raise it (`SET GLOBAL max_allowed_packet = ...`). Images are sent as raw bytes, so the limit applies to the image
336
+ size, not twice that.
337
+ - **Tiering** locks only the image being moved (InnoDB row lock) for the duration of the upload: writes to _that_ image wait
338
+ (up to `innodb_lock_wait_timeout`, 50 s by default), every other row and every plain read is unaffected. Postgres behaves
339
+ the same way; SQLite has to lock the whole file.
340
+ - **`connection`** takes your own `mysql2/promise` connection. Multi-statement operations first check whether a transaction
341
+ is open: inside one they use a `SAVEPOINT` (a failure undoes only this call's work), in autocommit mode they open and
342
+ commit their own. The adapter never commits, rolls back or retries your transaction. Object cleanup after a `delete` is
343
+ skipped on your connection (it cannot know whether you will commit).
344
+ - **Streaming** uses ranged `SUBSTRING()` reads, so a delete mid-stream raises `StorageError` (like Postgres). Tiered images
345
+ stream with real S3 `Range` requests.
346
+ - **Tested on MariaDB 10.11.** Real MySQL 8.x is expected to behave the same (the SQL avoids MariaDB-only features) but has
347
+ not been run here. Use MariaDB 10.2.1+ or MySQL 8.0.16+: older servers accept the `CHECK` constraint syntax but ignore it.
348
+
297
349
  ## Deduplication
298
350
 
299
351
  Store identical bytes once. In dedup mode every `put()` still returns its own id and keeps its own filename,
@@ -391,12 +443,12 @@ same corpus of valid and corrupt files identically.
391
443
 
392
444
  ## What's not here yet
393
445
 
394
- Planned: **MySQL/MariaDB**.
446
+ Planned for MySQL/MariaDB: **dedup mode**, the **`zerobucket` CLI**, and a MySQL-specific operations guide.
395
447
  Tiering and dedup cannot be combined (same rule as the Python package).
396
448
 
397
449
  ## Runtime notes
398
450
 
399
- Node 20+ for Postgres, Node 22+ for SQLite. Bun/Deno: expected to work, not yet tested. Edge runtimes (Cloudflare Workers, Vercel Edge) cannot open raw
451
+ Node 20+ for Postgres and MySQL/MariaDB, Node 22+ for SQLite. Bun/Deno: expected to work, not yet tested. Edge runtimes (Cloudflare Workers, Vercel Edge) cannot open raw
400
452
  Postgres TCP connections through `pg` without a proxy such as Hyperdrive; use the Node runtime for these routes.
401
453
  In serverless, keep `poolMaxSize` small (1-2) or inject a pool shared across invocations.
402
454
 
@@ -77,8 +77,30 @@ interface SqliteDatabaseLike {
77
77
  };
78
78
  exec(sql: string): unknown;
79
79
  }
80
- /** What `connection` accepts: a `pg` client for the Postgres backend, a `better-sqlite3` Database for SQLite. */
81
- type Connection = Queryable | SqliteDatabaseLike;
80
+ /**
81
+ * A `mysql2/promise` connection (or pool connection): both `query()` and `execute()` resolve to
82
+ * `[rowsOrResultHeader, fields]`. Pass one as `connection` to make a call part of YOUR MySQL/MariaDB
83
+ * transaction. Declared structurally so consumers do not need `mysql2` installed to type-check.
84
+ */
85
+ interface MysqlQueryable {
86
+ query(sql: string, values?: any): Promise<[any, any]>;
87
+ execute(sql: string, values?: any): Promise<[any, any]>;
88
+ }
89
+ /** The slice of a `mysql2/promise` pool connection that the MySQL backend uses. */
90
+ interface MysqlPoolConnectionLike extends MysqlQueryable {
91
+ beginTransaction(): Promise<void>;
92
+ commit(): Promise<void>;
93
+ rollback(): Promise<void>;
94
+ release(): void;
95
+ destroy(): void;
96
+ }
97
+ /** The slice of a `mysql2/promise` Pool that the MySQL backend uses (bring your own via `pool`). */
98
+ interface MysqlPoolLike {
99
+ getConnection(): Promise<MysqlPoolConnectionLike>;
100
+ end(): Promise<void>;
101
+ }
102
+ /** What `connection` accepts: a `pg` client for Postgres, a `better-sqlite3` Database for SQLite, a `mysql2/promise` connection for MySQL/MariaDB. */
103
+ type Connection = Queryable | SqliteDatabaseLike | MysqlQueryable;
82
104
  /** The row shape a storage backend persists (after validation and checksumming). */
83
105
  interface PreparedRow {
84
106
  data: Buffer;
@@ -314,7 +336,17 @@ interface ZeroBucketOptions<C = unknown> {
314
336
  * package's SQLiteBackend, so both can share one `.db` file.
315
337
  */
316
338
  sqlite?: string;
317
- /** Advanced: inject a custom StorageBackend instead of building one from `connectionString` / `pool` / `sqlite`. */
339
+ /**
340
+ * Use MySQL or MariaDB instead of PostgreSQL: `mysql://user:password@host:3306/database`
341
+ * (`mariadb://` works too). Needs `npm install mysql2`. Same schema as the Python package's
342
+ * MySQLBackend, so both can share one database. Classic mode only for now (`dedup` is not
343
+ * available for MySQL yet). `maxRetries`, `retryBaseDelayMs`, `poolMaxSize`, `poolTimeoutMs`,
344
+ * `autoMigrate`, `objectStorage` and `onOperation` apply to it too.
345
+ */
346
+ mysql?: string;
347
+ /** Bring your own `mysql2/promise` Pool for MySQL/MariaDB. It is NOT closed by close(). */
348
+ mysqlPool?: MysqlPoolLike;
349
+ /** Advanced: inject a custom StorageBackend instead of building one from `connectionString` / `pool` / `sqlite` / `mysql`. */
318
350
  backend?: StorageBackend;
319
351
  /** Max accepted size in bytes. Default 8 MiB. */
320
352
  maxBytes?: number;
@@ -470,4 +502,4 @@ declare function readInput(image: ImageInput, filename: string | undefined, maxB
470
502
  filename: string | undefined;
471
503
  }>;
472
504
 
473
- export { type BatchDeleteResult as B, type CallOptions as C, DEFAULT_MAX_BYTES as D, type GetOptions as G, type ImageListOptions as I, type OperationEvent as O, type PreparedRow as P, type Queryable as Q, type StorageBackend as S, type ValidatedContent as V, type WritableLike as W, ZeroBucket as Z, type ObjectStorageLike as a, type StoredRecord as b, type StoredRecordMetadata as c, type StreamOptions$1 as d, type StorageInfo as e, type ImageListEntry as f, type ContentValidator as g, type BatchGetResult as h, type BatchPutResult as i, type BeforeGetHook as j, type BeforePutHook as k, type Connection as l, type Image as m, type ImageInput as n, type ImageMetadata as o, ObjectStorage as p, type ObjectStorageOptions as q, type OptimizationResult as r, type OptimizeOptions as s, type PutManyOptions as t, type PutOptions as u, type SqliteDatabaseLike as v, type StreamOptions as w, type ZeroBucketOptions as x, optimizeImage as y, readInput as z };
505
+ export { optimizeImage as A, type BatchDeleteResult as B, type CallOptions as C, DEFAULT_MAX_BYTES as D, readInput as E, type GetOptions as G, type ImageListOptions as I, type MysqlPoolLike as M, type OperationEvent as O, type PreparedRow as P, type Queryable as Q, type StorageBackend as S, type ValidatedContent as V, type WritableLike as W, ZeroBucket as Z, type ObjectStorageLike as a, type StoredRecord as b, type StoredRecordMetadata as c, type StreamOptions$1 as d, type StorageInfo as e, type ImageListEntry as f, type ContentValidator as g, type BatchGetResult as h, type BatchPutResult as i, type BeforeGetHook as j, type BeforePutHook as k, type Connection as l, type Image as m, type ImageInput as n, type ImageMetadata as o, type MysqlPoolConnectionLike as p, type MysqlQueryable as q, ObjectStorage as r, type ObjectStorageOptions as s, type OptimizationResult as t, type OptimizeOptions as u, type PutManyOptions as v, type PutOptions as w, type SqliteDatabaseLike as x, type StreamOptions as y, type ZeroBucketOptions as z };
@@ -77,8 +77,30 @@ interface SqliteDatabaseLike {
77
77
  };
78
78
  exec(sql: string): unknown;
79
79
  }
80
- /** What `connection` accepts: a `pg` client for the Postgres backend, a `better-sqlite3` Database for SQLite. */
81
- type Connection = Queryable | SqliteDatabaseLike;
80
+ /**
81
+ * A `mysql2/promise` connection (or pool connection): both `query()` and `execute()` resolve to
82
+ * `[rowsOrResultHeader, fields]`. Pass one as `connection` to make a call part of YOUR MySQL/MariaDB
83
+ * transaction. Declared structurally so consumers do not need `mysql2` installed to type-check.
84
+ */
85
+ interface MysqlQueryable {
86
+ query(sql: string, values?: any): Promise<[any, any]>;
87
+ execute(sql: string, values?: any): Promise<[any, any]>;
88
+ }
89
+ /** The slice of a `mysql2/promise` pool connection that the MySQL backend uses. */
90
+ interface MysqlPoolConnectionLike extends MysqlQueryable {
91
+ beginTransaction(): Promise<void>;
92
+ commit(): Promise<void>;
93
+ rollback(): Promise<void>;
94
+ release(): void;
95
+ destroy(): void;
96
+ }
97
+ /** The slice of a `mysql2/promise` Pool that the MySQL backend uses (bring your own via `pool`). */
98
+ interface MysqlPoolLike {
99
+ getConnection(): Promise<MysqlPoolConnectionLike>;
100
+ end(): Promise<void>;
101
+ }
102
+ /** What `connection` accepts: a `pg` client for Postgres, a `better-sqlite3` Database for SQLite, a `mysql2/promise` connection for MySQL/MariaDB. */
103
+ type Connection = Queryable | SqliteDatabaseLike | MysqlQueryable;
82
104
  /** The row shape a storage backend persists (after validation and checksumming). */
83
105
  interface PreparedRow {
84
106
  data: Buffer;
@@ -314,7 +336,17 @@ interface ZeroBucketOptions<C = unknown> {
314
336
  * package's SQLiteBackend, so both can share one `.db` file.
315
337
  */
316
338
  sqlite?: string;
317
- /** Advanced: inject a custom StorageBackend instead of building one from `connectionString` / `pool` / `sqlite`. */
339
+ /**
340
+ * Use MySQL or MariaDB instead of PostgreSQL: `mysql://user:password@host:3306/database`
341
+ * (`mariadb://` works too). Needs `npm install mysql2`. Same schema as the Python package's
342
+ * MySQLBackend, so both can share one database. Classic mode only for now (`dedup` is not
343
+ * available for MySQL yet). `maxRetries`, `retryBaseDelayMs`, `poolMaxSize`, `poolTimeoutMs`,
344
+ * `autoMigrate`, `objectStorage` and `onOperation` apply to it too.
345
+ */
346
+ mysql?: string;
347
+ /** Bring your own `mysql2/promise` Pool for MySQL/MariaDB. It is NOT closed by close(). */
348
+ mysqlPool?: MysqlPoolLike;
349
+ /** Advanced: inject a custom StorageBackend instead of building one from `connectionString` / `pool` / `sqlite` / `mysql`. */
318
350
  backend?: StorageBackend;
319
351
  /** Max accepted size in bytes. Default 8 MiB. */
320
352
  maxBytes?: number;
@@ -470,4 +502,4 @@ declare function readInput(image: ImageInput, filename: string | undefined, maxB
470
502
  filename: string | undefined;
471
503
  }>;
472
504
 
473
- export { type BatchDeleteResult as B, type CallOptions as C, DEFAULT_MAX_BYTES as D, type GetOptions as G, type ImageListOptions as I, type OperationEvent as O, type PreparedRow as P, type Queryable as Q, type StorageBackend as S, type ValidatedContent as V, type WritableLike as W, ZeroBucket as Z, type ObjectStorageLike as a, type StoredRecord as b, type StoredRecordMetadata as c, type StreamOptions$1 as d, type StorageInfo as e, type ImageListEntry as f, type ContentValidator as g, type BatchGetResult as h, type BatchPutResult as i, type BeforeGetHook as j, type BeforePutHook as k, type Connection as l, type Image as m, type ImageInput as n, type ImageMetadata as o, ObjectStorage as p, type ObjectStorageOptions as q, type OptimizationResult as r, type OptimizeOptions as s, type PutManyOptions as t, type PutOptions as u, type SqliteDatabaseLike as v, type StreamOptions as w, type ZeroBucketOptions as x, optimizeImage as y, readInput as z };
505
+ export { optimizeImage as A, type BatchDeleteResult as B, type CallOptions as C, DEFAULT_MAX_BYTES as D, readInput as E, type GetOptions as G, type ImageListOptions as I, type MysqlPoolLike as M, type OperationEvent as O, type PreparedRow as P, type Queryable as Q, type StorageBackend as S, type ValidatedContent as V, type WritableLike as W, ZeroBucket as Z, type ObjectStorageLike as a, type StoredRecord as b, type StoredRecordMetadata as c, type StreamOptions$1 as d, type StorageInfo as e, type ImageListEntry as f, type ContentValidator as g, type BatchGetResult as h, type BatchPutResult as i, type BeforeGetHook as j, type BeforePutHook as k, type Connection as l, type Image as m, type ImageInput as n, type ImageMetadata as o, type MysqlPoolConnectionLike as p, type MysqlQueryable as q, ObjectStorage as r, type ObjectStorageOptions as s, type OptimizationResult as t, type OptimizeOptions as u, type PutManyOptions as v, type PutOptions as w, type SqliteDatabaseLike as x, type StreamOptions as y, type ZeroBucketOptions as z };
package/dist/http.d.cts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { IncomingMessage, ServerResponse } from 'node:http';
2
- import { Z as ZeroBucket } from './client-C7xs1dlr.cjs';
2
+ import { Z as ZeroBucket } from './client-XQ22b1l3.cjs';
3
3
  import 'pg';
4
4
 
5
5
  /**
package/dist/http.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { IncomingMessage, ServerResponse } from 'node:http';
2
- import { Z as ZeroBucket } from './client-C7xs1dlr.js';
2
+ import { Z as ZeroBucket } from './client-XQ22b1l3.js';
3
3
  import 'pg';
4
4
 
5
5
  /**