@zeroxsolutions/db 9.3.5 → 9.5.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/CHANGELOG.md CHANGED
@@ -1,3 +1,25 @@
1
+ ## 9.5.0 (2026-10-16)
2
+
3
+ ### 🚀 Features
4
+
5
+ - **db:** index the outbox's unpublished rows alone, by type and by time ([6f9dc0b](https://github.com/zeroxsolutions/zeroxsolutions/commit/6f9dc0b))
6
+
7
+ ### ❤️ Thank You
8
+
9
+ - Claude Opus 5.5
10
+ - Lương Văn Tú
11
+
12
+ ## 9.4.0 (2026-10-14)
13
+
14
+ ### 🚀 Features
15
+
16
+ - **db:** export the forced container remove from its own subpath ([fade0ea](https://github.com/zeroxsolutions/zeroxsolutions/commit/fade0ea))
17
+
18
+ ### ❤️ Thank You
19
+
20
+ - Claude Opus 5.5
21
+ - Lương Văn Tú
22
+
1
23
  ## 9.3.5 (2026-10-12)
2
24
 
3
25
  ### 🩹 Fixes
package/README.md CHANGED
@@ -33,12 +33,13 @@ Import from the subpath that matches your driver. **There is no root entry**: `p
33
33
  declares `"."`, but no `src/index.ts` exists for the build to emit it from, so
34
34
  `from '@zeroxsolutions/db'` does not resolve.
35
35
 
36
- | Export path | Description |
37
- | ------------------------ | ------------------------------------------ |
38
- | `@zeroxsolutions/db/postgres` | PostgreSQL entity, repository, view repository, unit of work, outbox, and types |
39
- | `@zeroxsolutions/db/d1` | Cloudflare D1 (SQLite) entity, repository, unit of work, outbox, and types |
40
- | `@zeroxsolutions/db/utils` | Filter and order builders, `TableQuery` and the relational-read builder, the field-whitelist guard, the filter-value schema + operator enum, omit masks, pagination constants |
41
- | `@zeroxsolutions/db/testing` | A throwaway Postgres container carrying your schema, on the engine you name, plus the container timeout and a fake uuid v7 |
36
+ | Export path | Description |
37
+ | ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
38
+ | `@zeroxsolutions/db/postgres` | PostgreSQL entity, repository, view repository, unit of work, outbox, and types |
39
+ | `@zeroxsolutions/db/d1` | Cloudflare D1 (SQLite) entity, repository, unit of work, outbox, and types |
40
+ | `@zeroxsolutions/db/utils` | Filter and order builders, `TableQuery` and the relational-read builder, the field-whitelist guard, the filter-value schema + operator enum, omit masks, pagination constants |
41
+ | `@zeroxsolutions/db/testing` | A throwaway Postgres container carrying your schema, on the engine you name, plus the container timeout and a fake uuid v7 |
42
+ | `@zeroxsolutions/db/containers` | The forced remove that tears down a container you started yourself |
42
43
 
43
44
  ---
44
45
 
@@ -98,23 +99,23 @@ export class UserRepository extends BaseRepository<typeof users, typeof schema,
98
99
 
99
100
  #### Available Methods
100
101
 
101
- | Method | Description |
102
- | ------ | ----------- |
103
- | `findAll(where, options?)` | Paginated list query |
104
- | `findAllAndCount(where, options?)` | Paginated list + total count in parallel |
105
- | `findOne(where, options?)` | First matching record or `null` |
106
- | `findOneById(id, options?)` | Find by primary key or `null` |
107
- | `create(data, options?)` | Insert one record, returns inserted row |
108
- | `createMany(data[], options?)` | Bulk insert, returns inserted rows |
109
- | `update(where, data, options?)` | Update matching records, returns first updated row |
110
- | `updateById(id, data, options?)` | Update by primary key |
111
- | `delete(where, options?)` | Hard delete matching records |
112
- | `deleteById(id, options?)` | Hard delete by primary key |
113
- | `count(where, options?)` | Count matching records |
114
- | `exists(where, options?)` | Returns `true` if any record matches |
115
- | `softDelete(where, options?)` | Sets `deletedAt` to now |
116
- | `softDeleteById(id, options?)` | Soft delete by primary key |
117
- | `nullifyBy(field, value, options?)` | Set field to `null` where field equals value |
102
+ | Method | Description |
103
+ | ----------------------------------- | -------------------------------------------------- |
104
+ | `findAll(where, options?)` | Paginated list query |
105
+ | `findAllAndCount(where, options?)` | Paginated list + total count in parallel |
106
+ | `findOne(where, options?)` | First matching record or `null` |
107
+ | `findOneById(id, options?)` | Find by primary key or `null` |
108
+ | `create(data, options?)` | Insert one record, returns inserted row |
109
+ | `createMany(data[], options?)` | Bulk insert, returns inserted rows |
110
+ | `update(where, data, options?)` | Update matching records, returns first updated row |
111
+ | `updateById(id, data, options?)` | Update by primary key |
112
+ | `delete(where, options?)` | Hard delete matching records |
113
+ | `deleteById(id, options?)` | Hard delete by primary key |
114
+ | `count(where, options?)` | Count matching records |
115
+ | `exists(where, options?)` | Returns `true` if any record matches |
116
+ | `softDelete(where, options?)` | Sets `deletedAt` to now |
117
+ | `softDeleteById(id, options?)` | Soft delete by primary key |
118
+ | `nullifyBy(field, value, options?)` | Set field to `null` where field equals value |
118
119
 
119
120
  #### FindAll Options
120
121
 
@@ -140,13 +141,13 @@ skipping another. With no `sort`, a page is ordered by the key alone.
140
141
  `fields` and `include` are `TableQuery<typeof schema, '<export name>'>`, the shape `@zeroxsolutions/query` guards, so
141
142
  a guarded query spreads straight into the options:
142
143
 
143
- | The query says | The finder loads |
144
- | --- | --- |
145
- | no fieldset for a table | every column of that table |
146
- | a fieldset | those columns and the table's primary key |
144
+ | The query says | The finder loads |
145
+ | ----------------------------- | ------------------------------------------------------------------- |
146
+ | no fieldset for a table | every column of that table |
147
+ | a fieldset | those columns and the table's primary key |
147
148
  | a relation name in a fieldset | that relation's primary keys - the linkage a relationship publishes |
148
- | an empty fieldset | the primary key alone |
149
- | an include path | each relation on it, under its own table's fieldset |
149
+ | an empty fieldset | the primary key alone |
150
+ | an include path | each relation on it, under its own table's fieldset |
150
151
 
151
152
  `with` and `include` reach the same relational query; where both name one relation, `with` wins. A read loading no
152
153
  relation takes a plain `select` of the columns it narrowed to, so a repository given no `queryKey` still narrows its
@@ -209,7 +210,7 @@ import { DrizzleUnitOfWork, DrizzleOutbox } from '@zeroxsolutions/db/postgres';
209
210
 
210
211
  // built in the transport per invocation (the db client comes from the connection string)
211
212
  const uow = new DrizzleUnitOfWork(db, (tx) => {
212
- const scope = container.createChildContainer(); // bind the transaction to a scoped container
213
+ const scope = container.createChildContainer(); // bind the transaction to a scoped container
213
214
  scope.register(TOKENS.TX, { useValue: tx });
214
215
  scope.register(TOKENS.OUTBOX, { useValue: new DrizzleOutbox(tx) });
215
216
  return scope;
@@ -223,9 +224,14 @@ await uow.run(async (tx) => {
223
224
  ```
224
225
 
225
226
  `outboxTable` columns: `id` (uuid v7), `type` (event discriminator), `payload` (`jsonb`), `occurredAt`,
226
- and `publishedAt` (NULL until the relay sends it; indexed). The relay reads unsent rows with
227
+ and `publishedAt` (NULL until the relay sends it). The relay reads unsent rows with
227
228
  `outbox.pullUnpublished(limit)` and acknowledges them with `outbox.markPublished(ids, publishedAt)`.
228
229
 
230
+ Two indexes hold the unsent rows alone, so the published ones never slow a relay down:
231
+ `idx_outbox_unpublished_occurred_at` on `occurredAt`, for a relay that sends every type, and
232
+ `idx_outbox_unpublished_type_occurred_at` on `(type, occurredAt)`, for one that sends only some. Both
233
+ answer in `occurredAt` order without a sort.
234
+
229
235
  ---
230
236
 
231
237
  ## Cloudflare D1 (SQLite)
@@ -288,7 +294,7 @@ import { DrizzleD1UnitOfWork, DrizzleOutbox } from '@zeroxsolutions/db/d1';
288
294
 
289
295
  // built in the transport per invocation (the db client comes from the Hyperdrive/D1 binding)
290
296
  const uow = new DrizzleD1UnitOfWork(db, ({ db, collector }) => {
291
- const scope = container.createChildContainer(); // reads use db; writes push to collector
297
+ const scope = container.createChildContainer(); // reads use db; writes push to collector
292
298
  scope.register(TOKENS.TX, { useValue: { db, collector } });
293
299
  scope.register(TOKENS.OUTBOX, { useValue: new DrizzleOutbox(db, collector) });
294
300
  return scope;
@@ -298,7 +304,7 @@ const uow = new DrizzleD1UnitOfWork(db, ({ db, collector }) => {
298
304
  await uow.run(async (tx) => {
299
305
  await tx.resolve<ILinkAggregateRepository>(TOKENS.LINK_AGGREGATE_REPOSITORY).save(link);
300
306
  await tx.resolve<IOutboxWriter>(TOKENS.OUTBOX).stage(link.pullEvents());
301
- }); // -> flush [save, ...stage] as ONE batch()
307
+ }); // -> flush [save, ...stage] as ONE batch()
302
308
  ```
303
309
 
304
310
  A consuming context's D1 aggregate repository takes both the `db` (for immediate `load` reads) and
@@ -314,7 +320,7 @@ The D1 `DrizzleOutbox` mirrors the Postgres surface (`stage` / `pullUnpublished(
314
320
  (so it commits with the aggregate write), or executes immediately when constructed standalone (relay
315
321
  use). `outboxTable` columns: `id` (uuid v7), `type` (event discriminator), `payload` (text - SQLite
316
322
  has no `jsonb`, so the relay `JSON.parse`s), `occurredAt`, and `publishedAt` (NULL until the relay
317
- sends it; indexed).
323
+ sends it), with the same two indexes over the unsent rows as the Postgres table.
318
324
 
319
325
  > **Limitation.** D1's `batch()` is atomic but non-interactive, so the D1 UoW gives all-or-nothing
320
326
  > write atomicity without a transaction-scoped read snapshot. A handler that loads, decides, then
@@ -350,15 +356,15 @@ const condition = buildFilterConditions({
350
356
 
351
357
  All 15, as `filterOperatorSchema` declares them:
352
358
 
353
- | Operator | Description |
354
- | -------- | ----------- |
355
- | `equals` / `notEquals` | Exact match / not equal |
356
- | `in` / `notIn` | Array membership; an empty array contributes no condition |
359
+ | Operator | Description |
360
+ | -------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
361
+ | `equals` / `notEquals` | Exact match / not equal |
362
+ | `in` / `notIn` | Array membership; an empty array contributes no condition |
357
363
  | `contains` / `startsWith` / `endsWith` | Case-insensitive text match: `ILIKE` on Postgres, `LIKE` on SQLite, which is already case-insensitive for ASCII |
358
- | `gt` / `gte` / `lt` / `lte` | Numeric/date comparisons |
359
- | `between` | Range (`from` / `to`, both optional) |
360
- | `notBetween` | Range exclusion (`from` and `to`, both required) |
361
- | `isNull` / `isNotNull` | Null checks; carry no `value` key at all |
364
+ | `gt` / `gte` / `lt` / `lte` | Numeric/date comparisons |
365
+ | `between` | Range (`from` / `to`, both optional) |
366
+ | `notBetween` | Range exclusion (`from` and `to`, both required) |
367
+ | `isNull` / `isNotNull` | Null checks; carry no `value` key at all |
362
368
 
363
369
  A `%` or `_` in a text value is escaped, so it matches literally rather than as a wildcard.
364
370
 
@@ -373,10 +379,10 @@ column under that same name in `resolvers`:
373
379
  import { permitsFor } from '@zeroxsolutions/query';
374
380
  import { buildFilterConditions, columnResolver } from '@zeroxsolutions/db/utils';
375
381
 
376
- const placesQueryPermits = permitsFor(placeAttributes, { relationships: places.relationships }).relationship(
377
- 'region',
378
- ['equals', 'in'],
379
- );
382
+ const placesQueryPermits = permitsFor(placeAttributes, { relationships: places.relationships }).relationship('region', [
383
+ 'equals',
384
+ 'in',
385
+ ]);
380
386
 
381
387
  const condition = buildFilterConditions({
382
388
  table: places,
@@ -416,11 +422,7 @@ only the offending field name; the calling service maps each error **type** to i
416
422
  own transport.
417
423
 
418
424
  ```ts
419
- import {
420
- assertQueryableFields,
421
- InvalidQueryFilter,
422
- type QueryableFields,
423
- } from '@zeroxsolutions/db/utils';
425
+ import { assertQueryableFields, InvalidQueryFilter, type QueryableFields } from '@zeroxsolutions/db/utils';
424
426
 
425
427
  const USERS_QUERYABLE = {
426
428
  filterable: ['name', 'createdAt'],
@@ -442,7 +444,10 @@ import { collectionQuerySchema } from '@zeroxsolutions/query';
442
444
 
443
445
  createRoute({ request: { query: collectionQuerySchema(usersQueryPermits) } });
444
446
  const query = c.req.valid('query');
445
- const [rows, total] = await usersRepository.findAllAndCount(buildFilterConditions({ table: users, filter: query.filter }), query);
447
+ const [rows, total] = await usersRepository.findAllAndCount(
448
+ buildFilterConditions({ table: users, filter: query.filter }),
449
+ query,
450
+ );
446
451
  ```
447
452
 
448
453
  Pass `filterItem: filterValueSchema` in the spec so the parsed clauses are the ones this package's
@@ -453,11 +458,11 @@ builders accept. A test in `utils/filter.spec.ts` pins the absence of a second q
453
458
  Three column masks, each a plain `{ column: true }` object to hand to a schema builder's `omit` or to
454
459
  `Omit<...>`. This package exports the masks only - it ships no `omit` helper of its own.
455
460
 
456
- | Mask | Drops |
457
- | ---- | ----- |
458
- | `defaultSelectOmit` | `deletedAt` |
461
+ | Mask | Drops |
462
+ | --------------------- | --------------------------------------- |
463
+ | `defaultSelectOmit` | `deletedAt` |
459
464
  | `defaultResponseOmit` | the above plus `createdAt`, `updatedAt` |
460
- | `defaultInsertOmit` | the above plus `id` |
465
+ | `defaultInsertOmit` | the above plus `id` |
461
466
 
462
467
  ```ts
463
468
  import { defaultInsertOmit } from '@zeroxsolutions/db/utils';
@@ -519,11 +524,30 @@ This package names no engine for you, and exports no constant standing in as tha
519
524
  major you run is your deployment's own fact, so `image` takes no default: a forgotten argument is a
520
525
  typecheck failure rather than a suite that quietly passed on a major you do not deploy.
521
526
 
522
- | Export | What it is |
523
- | ------ | ---------- |
527
+ | Export | What it is |
528
+ | ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
524
529
  | `startDisposablePostgres(schema, image)` | Starts the container, creates `schema` in it, and returns a Drizzle client typed against it, the `connectionString` it dialled, and the `stop()` that tears it down. A server `WARNING` prints as one line on `console.warn`; lower severities are dropped |
525
- | `CONTAINER_TIMEOUT_MS` | 180s, for the hook that starts a container: an image pull on a cold machine outruns vitest's 5s default |
526
- | `fakeUuidV7` | Random bits in a v7 layout: it passes a v7 check, and is not time-ordered like a minted id |
530
+ | `CONTAINER_TIMEOUT_MS` | 180s, for the hook that starts a container: an image pull on a cold machine outruns vitest's 5s default |
531
+ | `fakeUuidV7` | Random bits in a v7 layout: it passes a v7 check, and is not time-ordered like a minted id |
532
+
533
+ ### A container you started yourself
534
+
535
+ A global setup that starts its own container, to migrate a template or several databases, tears it down
536
+ with `removeContainer` from `@zeroxsolutions/db/containers`, not with the container's `stop()`:
537
+
538
+ ```ts
539
+ import { removeContainer } from '@zeroxsolutions/db/containers';
540
+
541
+ const container = await new PostgreSqlContainer(image).start();
542
+ // ...
543
+ return () => removeContainer(container);
544
+ ```
545
+
546
+ `stop()` sends a stop and then a separate remove, and on a loaded host the daemon answers that remove
547
+ with 409 "container is running", failing a suite whose every case passed. `removeContainer` is one forced
548
+ remove, volumes included, and it is what `startDisposablePostgres`'s `stop()` calls. It rejects for a
549
+ container the daemon no longer knows, so a teardown that runs twice shows. This subpath imports nothing
550
+ but `@testcontainers/postgresql`, so an e2e project that installs no `drizzle-kit` can use it.
527
551
 
528
552
  ---
529
553
 
@@ -1 +1 @@
1
- {"version":3,"file":"outbox-table.d.ts","sourceRoot":"","sources":["../../../src/base/d1/outbox-table.ts"],"names":[],"mappings":"AAGA;;;;;GAKG;AACH,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAcvB,CAAC;AAEF,sGAAsG;AACtG,MAAM,MAAM,SAAS,GAAG,OAAO,WAAW,CAAC,YAAY,CAAC"}
1
+ {"version":3,"file":"outbox-table.d.ts","sourceRoot":"","sources":["../../../src/base/d1/outbox-table.ts"],"names":[],"mappings":"AAIA;;;;;GAKG;AACH,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAoBvB,CAAC;AAEF,sGAAsG;AACtG,MAAM,MAAM,SAAS,GAAG,OAAO,WAAW,CAAC,YAAY,CAAC"}
@@ -1,3 +1,4 @@
1
+ import { isNull } from 'drizzle-orm';
1
2
  import { index, sqliteTable, text } from 'drizzle-orm/sqlite-core';
2
3
  import { v7 as uuidv7 } from 'uuid';
3
4
  /**
@@ -16,4 +17,11 @@ export const outboxTable = sqliteTable('outbox', {
16
17
  .notNull()
17
18
  .$defaultFn(() => new Date().toISOString()),
18
19
  publishedAt: text('published_at'),
19
- }, (table) => [index('idx_outbox_published_at').on(table.publishedAt)]);
20
+ },
21
+ // Both hold the unpublished rows alone, so a backlog of published rows never slows a relay. The
22
+ // first answers a relay that publishes every type, the second one that publishes only some, each in
23
+ // `occurredAt` order without a sort.
24
+ (table) => [
25
+ index('idx_outbox_unpublished_occurred_at').on(table.occurredAt).where(isNull(table.publishedAt)),
26
+ index('idx_outbox_unpublished_type_occurred_at').on(table.type, table.occurredAt).where(isNull(table.publishedAt)),
27
+ ]);
@@ -1 +1 @@
1
- {"version":3,"file":"outbox-table.d.ts","sourceRoot":"","sources":["../../../src/base/postgres/outbox-table.ts"],"names":[],"mappings":"AAGA;;;GAGG;AACH,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAYvB,CAAC;AAEF,sDAAsD;AACtD,MAAM,MAAM,SAAS,GAAG,OAAO,WAAW,CAAC,YAAY,CAAC"}
1
+ {"version":3,"file":"outbox-table.d.ts","sourceRoot":"","sources":["../../../src/base/postgres/outbox-table.ts"],"names":[],"mappings":"AAIA;;;GAGG;AACH,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAkBvB,CAAC;AAEF,sDAAsD;AACtD,MAAM,MAAM,SAAS,GAAG,OAAO,WAAW,CAAC,YAAY,CAAC"}
@@ -1,3 +1,4 @@
1
+ import { isNull } from 'drizzle-orm';
1
2
  import { index, jsonb, pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core';
2
3
  import { v7 as uuidv7 } from 'uuid';
3
4
  /**
@@ -12,4 +13,11 @@ export const outboxTable = pgTable('outbox', {
12
13
  payload: jsonb('payload').notNull(),
13
14
  occurredAt: timestamp('occurred_at', { withTimezone: true }).notNull().defaultNow(),
14
15
  publishedAt: timestamp('published_at', { withTimezone: true }),
15
- }, (table) => [index('idx_outbox_published_at').on(table.publishedAt)]);
16
+ },
17
+ // Both hold the unpublished rows alone, so a backlog of published rows never slows a relay. The
18
+ // first answers a relay that publishes every type, the second one that publishes only some, each in
19
+ // `occurredAt` order without a sort.
20
+ (table) => [
21
+ index('idx_outbox_unpublished_occurred_at').on(table.occurredAt).where(isNull(table.publishedAt)),
22
+ index('idx_outbox_unpublished_type_occurred_at').on(table.type, table.occurredAt).where(isNull(table.publishedAt)),
23
+ ]);
@@ -0,0 +1,2 @@
1
+ export * from './remove-container.js';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/containers/index.ts"],"names":[],"mappings":"AAAA,cAAc,uBAAuB,CAAC"}
@@ -0,0 +1 @@
1
+ export * from './remove-container.js';
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Removes `container` and its volumes in one forced call, which the daemon carries out as a kill and a
3
+ * removal together. The container's own stop sends a stop and then a remove, and on a loaded host that
4
+ * remove answered 409 "container is running", failing a suite whose every case had passed
5
+ * (testcontainers@12.1.0, Docker Desktop 29).
6
+ * @param container - a container started through `@testcontainers/postgresql`.
7
+ * @throws whatever the daemon answers, including that it knows no such container, so a second teardown
8
+ * of the same container shows.
9
+ */
10
+ export declare function removeContainer(container: {
11
+ getId(): string;
12
+ }): Promise<void>;
13
+ //# sourceMappingURL=remove-container.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"remove-container.d.ts","sourceRoot":"","sources":["../../src/containers/remove-container.ts"],"names":[],"mappings":"AAOA;;;;;;;;GAQG;AACH,wBAAsB,eAAe,CAAC,SAAS,EAAE;IAAE,KAAK,IAAI,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAGnF"}
@@ -0,0 +1,24 @@
1
+ import { createRequire } from 'node:module';
2
+ /**
3
+ * Removes `container` and its volumes in one forced call, which the daemon carries out as a kill and a
4
+ * removal together. The container's own stop sends a stop and then a remove, and on a loaded host that
5
+ * remove answered 409 "container is running", failing a suite whose every case had passed
6
+ * (testcontainers@12.1.0, Docker Desktop 29).
7
+ * @param container - a container started through `@testcontainers/postgresql`.
8
+ * @throws whatever the daemon answers, including that it knows no such container, so a second teardown
9
+ * of the same container shows.
10
+ */
11
+ export async function removeContainer(container) {
12
+ const runtime = await containerRuntime();
13
+ await runtime.container.getById(container.getId()).remove({ force: true, v: true });
14
+ }
15
+ /**
16
+ * The runtime client of the testcontainers that `@testcontainers/postgresql` itself runs on. It is
17
+ * resolved from that package rather than declared here, so a consumer installs nothing beyond the peer it
18
+ * already has, and the client is the one the container was started through.
19
+ */
20
+ function containerRuntime() {
21
+ const fromPostgres = createRequire(createRequire(import.meta.url).resolve('@testcontainers/postgresql'));
22
+ const { getContainerRuntimeClient } = fromPostgres('testcontainers');
23
+ return getContainerRuntimeClient();
24
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"disposable-postgres.d.ts","sourceRoot":"","sources":["../../src/testing/disposable-postgres.ts"],"names":[],"mappings":"AAGA,OAAO,EAAW,KAAK,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAG3E,2FAA2F;AAC3F,eAAO,MAAM,oBAAoB,SAAU,CAAC;AAE5C,8FAA8F;AAC9F,MAAM,WAAW,kBAAkB,CAAC,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IACzE,EAAE,EAAE,kBAAkB,CAAC,OAAO,CAAC,CAAC;IAChC;;;OAGG;IACH,gBAAgB,EAAE,MAAM,CAAC;IACzB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACvB;AAED;;;;;;;GAOG;AACH,wBAAsB,uBAAuB,CAAC,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACnF,MAAM,EAAE,OAAO,EACf,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,kBAAkB,CAAC,OAAO,CAAC,CAAC,CAqBtC"}
1
+ {"version":3,"file":"disposable-postgres.d.ts","sourceRoot":"","sources":["../../src/testing/disposable-postgres.ts"],"names":[],"mappings":"AAEA,OAAO,EAAW,KAAK,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAI3E,2FAA2F;AAC3F,eAAO,MAAM,oBAAoB,SAAU,CAAC;AAE5C,8FAA8F;AAC9F,MAAM,WAAW,kBAAkB,CAAC,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IACzE,EAAE,EAAE,kBAAkB,CAAC,OAAO,CAAC,CAAC;IAChC;;;OAGG;IACH,gBAAgB,EAAE,MAAM,CAAC;IACzB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACvB;AAED;;;;;;;GAOG;AACH,wBAAsB,uBAAuB,CAAC,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACnF,MAAM,EAAE,OAAO,EACf,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,kBAAkB,CAAC,OAAO,CAAC,CAAC,CAiBtC"}
@@ -1,8 +1,8 @@
1
- import { createRequire } from 'node:module';
2
1
  import { PostgreSqlContainer } from '@testcontainers/postgresql';
3
2
  import { generateDrizzleJson, generateMigration } from 'drizzle-kit/api';
4
3
  import { drizzle } from 'drizzle-orm/postgres-js';
5
4
  import postgres from 'postgres';
5
+ import { removeContainer } from '../containers/index.js';
6
6
  /** Pulling the image on a cold machine costs far more than vitest's 5s default timeout. */
7
7
  export const CONTAINER_TIMEOUT_MS = 180_000;
8
8
  /**
@@ -27,24 +27,10 @@ export async function startDisposablePostgres(schema, image) {
27
27
  connectionString,
28
28
  async stop() {
29
29
  await client.end();
30
- // One forced remove, which the daemon carries out as a kill and a removal together. The container's
31
- // own stop sends a stop and then a remove, and on a loaded host that remove answered 409 "container is
32
- // running", failing a suite whose every case had passed (testcontainers@12.1.0, Docker Desktop 29).
33
- const runtime = await containerRuntime();
34
- await runtime.container.getById(container.getId()).remove({ force: true, v: true });
30
+ await removeContainer(container);
35
31
  },
36
32
  };
37
33
  }
38
- /**
39
- * The runtime client of the testcontainers that `@testcontainers/postgresql` itself runs on. It is
40
- * resolved from that package rather than declared here, so a consumer installs nothing beyond the peer it
41
- * already has, and the client is the one the container was started through.
42
- */
43
- function containerRuntime() {
44
- const fromPostgres = createRequire(createRequire(import.meta.url).resolve('@testcontainers/postgresql'));
45
- const { getContainerRuntimeClient } = fromPostgres('testcontainers');
46
- return getContainerRuntimeClient();
47
- }
48
34
  /**
49
35
  * Prints a server WARNING as one line and drops every lower severity. Left to its default, postgres.js
50
36
  * prints each notice as a raw object, and a full-text search for a stop word alone raises two per
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeroxsolutions/db",
3
- "version": "9.3.5",
3
+ "version": "9.5.0",
4
4
  "description": "Generic Drizzle toolkit - the id column and audit timestamps a table takes, a base repository, the unit-of-work and transactional outbox impls, and the filter/sort/pagination query helpers, for PostgreSQL (postgres-js) and Cloudflare D1; ships no domain schema. Shared across ZeroXSolutions services.",
5
5
  "private": false,
6
6
  "type": "module",
@@ -32,13 +32,18 @@
32
32
  "types": "./dist/testing/index.d.ts",
33
33
  "import": "./dist/testing/index.js",
34
34
  "default": "./dist/testing/index.js"
35
+ },
36
+ "./containers": {
37
+ "types": "./dist/containers/index.d.ts",
38
+ "import": "./dist/containers/index.js",
39
+ "default": "./dist/containers/index.js"
35
40
  }
36
41
  },
37
42
  "dependencies": {
38
43
  "tslib": "^2.3.0",
39
44
  "uuid": "^14.0.1",
40
- "@zeroxsolutions/cosmic": "0.3.4",
41
- "@zeroxsolutions/query": "0.7.3"
45
+ "@zeroxsolutions/query": "0.7.3",
46
+ "@zeroxsolutions/cosmic": "0.3.4"
42
47
  },
43
48
  "peerDependencies": {
44
49
  "@testcontainers/postgresql": "^12.1.0",