@owlmeans/postgres-resource 0.1.18-rc.20 → 0.1.18-rc.22

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
@@ -1,32 +1,48 @@
1
1
  # @owlmeans/postgres-resource
2
2
 
3
- PostgreSQL-backed `Resource<T>` implementation — schema-driven tables, structure reconciliation,
4
- code-registered migrations, and custom SQL with resource-alias placeholders.
5
-
6
- ## Overview
7
-
8
- - `makePostgresResource<R, T>(alias, dbAlias?, serviceAlias?, tableName?)` — factory for Postgres resources
9
- - `PostgresResource<T>` — extends `Resource<T>` with a Drizzle table, custom SQL, transactions, and field encryption
10
- - The resource's **AJV schema is the single source of truth for the table structure** — columns, types,
11
- nullability, defaults, primary key and indexes are all derived from it
12
- - A `pg:` keyword vocabulary inside that schema covers what JSON Schema can't express: column types and
13
- lengths, foreign keys, composite indexes, checks, partial indexes
14
- - Tables are created and reconciled automatically at `init()`; migrations run around that in `pre`/`post` stages
3
+ PostgreSQL-backed `Resource<T>`, and the default store for the records a product owns and queries:
4
+ users, orders, projects, invoices, settings, audit rows. The resource's AJV schema is the single
5
+ source of truth for the table, and the package creates and reconciles that table at boot. It also
6
+ runs code migrations around the reconciliation and resolves resource-alias placeholders in custom
7
+ SQL. Other stores fit other shapes:
8
+ - [`@owlmeans/mongo-resource`](../mongo-resource): documents whose shape varies per record and are read whole.
9
+ - [`@owlmeans/redis-resource`](../redis-resource): data defined by expiry, a lock, a counter or fan-out.
10
+ - [`@owlmeans/storage-resource`](../storage-resource): file bytes. The record describing the file stays here.
11
+ - [`@owlmeans/state`](../state): client-side records.
15
12
 
16
13
  ## Installation
17
14
 
18
15
  ```bash
19
- bun add @owlmeans/postgres-resource@^0.1.18-rc.12 @owlmeans/postgres@^0.1.18-rc.13 pg
16
+ bun add @owlmeans/postgres-resource@^0.1.18-rc.22 @owlmeans/postgres@^0.1.18-rc.23 pg
20
17
  ```
21
18
 
22
19
  `pg` and `ajv` are peer dependencies of this package. `@owlmeans/postgres` provides the connection
23
20
  service this package resolves through.
24
21
 
22
+ ## Concepts
23
+
24
+ - **The schema is the table**: columns, types, nullability, defaults, primary key, indexes and
25
+ foreign keys all derive from `resource.schema`. Nothing else writes DDL: no `pgTable`, no
26
+ `drizzle-kit`, no hand-written `CREATE TABLE`.
27
+ - **`pg:` overrides**: a keyword inside the schema for what JSON Schema cannot say. That covers
28
+ column type and length, `unique`, `references`, composite indexes, checks, `unmanaged` columns
29
+ and per-table `autoSync`.
30
+ - **Reconciliation** (`PgAutoSync`): at `init()` the resource introspects the live table, diffs it
31
+ against the compiled `TableSpec` and applies the DDL plan in one transaction under an advisory
32
+ lock. `Full` adds, retypes and drops. `Additive` only adds. `Off` emits no DDL.
33
+ - **Migrations**: code bodies registered with `migration(name, apply, stage?)`. `Pre` runs before
34
+ reconciliation and can rescue data it would drop. `Post` runs after it and can use new columns.
35
+ Each runs once, in its own transaction, recorded in `_owlmeans_migrations`.
36
+ - **Placeholders**: `{{}}`, `{{alias}}`, `{{alias.property}}`, `{{#alias}}` and `{{$}}` resolve
37
+ *identifiers only* in custom SQL. Values always travel as `$1..$n` parameters.
38
+ - **Paged by default**: `list(where)` returns at most `DEFAULT_PAGE_SIZE` (100) rows, with the
39
+ primary key appended to every sort as a tiebreak.
40
+
25
41
  ## Usage
26
42
 
27
- Define a resource:
43
+ ### Define and register a resource
28
44
 
29
- ```typescript
45
+ ```ts
30
46
  import { makePostgresResource } from '@owlmeans/postgres-resource'
31
47
  import type { PostgresResource } from '@owlmeans/postgres-resource'
32
48
  import type { ResourceMaker } from '@owlmeans/resource'
@@ -42,46 +58,53 @@ export const makeProjectResource: ResourceMaker<ProjectRecord, ProjectResource>
42
58
 
43
59
  return resource
44
60
  }
45
- ```
46
-
47
- Register in context:
48
61
 
49
- ```typescript
62
+ // in the server context factory, next to appendPostgres(context)
50
63
  context.registerResource(makeProjectResource())
51
64
  ```
52
65
 
53
- Use in a handler:
66
+ ### Read and write from a handler
54
67
 
55
- ```typescript
68
+ ```ts
56
69
  const projects = context.resource<ProjectResource>(RES_PROJECT)
57
- const record = await projects.create({ entityId, alias, title })
70
+ const record = await projects.create({ entityId, alias, title }) // the database assigns the id
58
71
 
59
72
  const one = await projects.load({ entityId, alias }) // null when absent
60
73
  const { items, total } = await projects.list(
61
74
  { entityId, status: ['draft', 'active'] }, // an array means "any of these"
62
75
  { page: 0, size: 20, sort: [{ field: 'createdAt', order: 'desc' }] }
63
76
  )
77
+
78
+ await projects.patch({ id: record.id, status: 'active' }) // merge
79
+ await projects.update({ ...record, title: 'Renamed' }) // replace the whole row
64
80
  ```
65
81
 
66
82
  `size` defaults to `DEFAULT_PAGE_SIZE` (100); `list(where, { size: 0 })` lifts the limit. `total`
67
- always describes the whole match, independently of the page.
83
+ always describes the whole match, independently of the page. `entityId` is the organization's
84
+ stable record id from `requireEntityKey(req)`, never a value read off the token.
68
85
 
69
- ### Schema → table
86
+ ### Schema to table
70
87
 
71
- ```typescript
72
- const ProjectSchema: JSONSchemaType<ProjectRecord> = {
88
+ ```ts
89
+ import { DateSchema } from '@owlmeans/auth'
90
+ import type { JSONSchemaType } from 'ajv'
91
+
92
+ const InvoiceSchema: JSONSchemaType<InvoiceRecord> = {
73
93
  type: 'object',
74
94
  properties: {
75
- id: { type: 'string', format: 'uuid' },
76
- email: { type: 'string', pg: { type: 'varchar', length: 320, unique: true } },
77
- ownerId: { type: 'string', pg: { references: { resource: 'users', onDelete: 'cascade' } } },
78
- payload: { type: 'object', nullable: true }, // → jsonb
79
- createdAt: DateSchema // → timestamptz
95
+ id: { type: 'string', format: 'uuid' }, // primary key; never in required
96
+ entityId: { type: 'string' },
97
+ number: { type: 'string', pg: { type: 'varchar', length: 32, unique: true } },
98
+ customerId: { type: 'string', pg: { references: { resource: 'customers', onDelete: 'cascade' } } },
99
+ status: { type: 'string', enum: ['draft', 'sent', 'paid'] }, // text + CHECK
100
+ tags: { type: 'array', items: { type: 'string' } }, // text[]
101
+ lines: { type: 'array', items: { type: 'object' } }, // jsonb
102
+ note: { type: 'string', nullable: true },
103
+ createdAt: DateSchema // timestamptz
80
104
  },
81
- required: ['id', 'email'],
105
+ required: ['entityId', 'number', 'customerId', 'status'],
82
106
  pg: {
83
- table: 'app_projects',
84
- indexes: [{ name: 'idx_owner_created', columns: ['ownerId', 'createdAt'] }]
107
+ indexes: [{ name: 'idx_invoice_entity_created', columns: ['entityId', 'createdAt'] }]
85
108
  }
86
109
  }
87
110
  ```
@@ -92,51 +115,40 @@ const ProjectSchema: JSONSchemaType<ProjectRecord> = {
92
115
  | `string` + `format: 'uuid'` | `uuid` |
93
116
  | `DateSchema` (`{type:'object', format:'date-time'}`) | `timestamptz` |
94
117
  | `integer` / `number` / `boolean` | `integer` / `double precision` / `boolean` |
95
- | `array`, nested `object` | `jsonb` |
118
+ | `array` of plain `string`/`integer`/`number`/`boolean` items | native `<scalar>[]`, e.g. `text[]` |
119
+ | any other `array`, nested `object` | `jsonb` |
96
120
  | string `enum` | `text` + `CHECK` |
97
121
  | `nullable: true` | nullable column |
98
122
  | in `required[]` | `NOT NULL` |
99
123
  | `secure: true` | ciphertext column, `lock`/`unlock` aware |
100
124
  | `id` property | primary key, `gen_random_uuid()::text` default |
101
125
 
102
- AJV in strict mode rejects unknown keywords — register `pgKeyword` to allow `pg:`:
126
+ AJV in strict mode rejects unknown keywords, so register `pgKeyword` to allow `pg:`:
127
+
128
+ ```ts
129
+ import { pgKeyword } from '@owlmeans/postgres-resource'
103
130
 
104
- ```typescript
105
131
  ajv.addKeyword(pgKeyword)
106
132
  ```
107
133
 
108
- ### Structure reconciliation
109
-
110
- At `init()` the resource introspects `information_schema` / `pg_catalog`, diffs against the compiled
111
- spec, and applies the DDL plan in one transaction under a `pg_advisory_xact_lock`, so concurrent
112
- replicas never race. Policy comes from `DbConfig.meta.autoSync`:
134
+ ### Migrations
113
135
 
114
- | `PgAutoSync` | Behaviour |
115
- |---|---|
116
- | `Full` (default) | add / retype / backfill / drop columns, reconcile indexes and constraints |
117
- | `Additive` | add only — never retypes, never drops. The adoption path for a pre-existing table |
118
- | `Off` | create the table if absent, otherwise leave it alone |
136
+ ```ts
137
+ import { MigrationStage } from '@owlmeans/resource'
119
138
 
120
- A cast Postgres cannot perform raises `PostgresCastRequired` rather than truncating data. Columns
121
- listed in `pg.unmanaged` are outside reconciliation's authority entirely.
139
+ resource.migration('0001-rescue-legacy-slug', async tx => {
140
+ await tx.execute(`UPDATE {{}} SET slug = legacy_slug WHERE slug IS NULL`)
141
+ }, MigrationStage.Pre)
122
142
 
123
- ### Migrations
124
-
125
- ```typescript
126
- resource.migration('0001-backfill-slug', async tx => {
127
- await tx.execute(`UPDATE {{}} SET slug = lower(title) WHERE slug IS NULL`)
143
+ resource.migration('0002-backfill-status', async tx => {
144
+ await tx.execute(`UPDATE {{}} SET status = $1 WHERE status IS NULL`, ['draft'])
128
145
  }, MigrationStage.Post)
129
146
  ```
130
147
 
131
- Stages bracket the structure sync: `Pre` runs **before** the table is reshaped (so a migration can
132
- rescue data reconciliation would drop), `Post` **after** (so it can use the new columns). Applied
133
- once, in registration order, each in its own transaction, recorded in `_owlmeans_migrations`.
134
148
  Migrations declared on a table this package just created are **baselined**, not executed. An edited
135
- body raises `MigrationConflict`; a failing one raises `MigrationError` and aborts `init()`.
136
-
137
- ### Custom SQL
149
+ applied body raises `MigrationConflict`; a failing one raises `MigrationError` and aborts `init()`.
138
150
 
139
- Placeholders resolve **identifiers only** — values stay in `params` and are bound as `$1..$n`:
151
+ ### Custom SQL and transactions
140
152
 
141
153
  | Placeholder | Resolves to |
142
154
  |---|---|
@@ -146,11 +158,22 @@ Placeholders resolve **identifiers only** — values stay in `params` and are bo
146
158
  | `{{#alias}}` | the bare quoted table name (for `ON CONFLICT ON CONSTRAINT`) |
147
159
  | `{{$}}` | the owning resource's quoted schema |
148
160
 
149
- ```typescript
150
- const rows = await projects.select(
151
- `SELECT p.* FROM {{}} p JOIN {{users}} u ON u.id = {{self.ownerId}} WHERE u.active = $1`,
152
- [true]
161
+ ```ts
162
+ const overdue = await invoices.select(
163
+ `SELECT {{}}.* FROM {{}} JOIN {{customers}} ON {{customers.id}} = {{self.customerId}}
164
+ WHERE {{self.entityId}} = $1 AND {{self.status}} = $2 AND {{customers.active}} = $3`,
165
+ [entityId, 'sent', true]
153
166
  )
167
+
168
+ const settled = await invoices.transaction(async tx => {
169
+ const count = await tx.execute(
170
+ `UPDATE {{}} SET status = $1 WHERE {{self.customerId}} = $2 AND {{self.status}} = $3`,
171
+ ['paid', customerId, 'sent']
172
+ )
173
+ await tx.execute(`UPDATE {{payments}} SET settled = true WHERE {{payments.customerId}} = $1`, [customerId])
174
+
175
+ return count
176
+ })
154
177
  ```
155
178
 
156
179
  An unknown alias or property raises `PostgresPlaceholderError` at parse time rather than being
@@ -162,59 +185,105 @@ substituted blindly.
162
185
 
163
186
  Creates a Postgres resource. `dbAlias` and `serviceAlias` default to `DEFAULT_DB_ALIAS`
164
187
  (`'postgres'`). `tableName` overrides the physical table name, which otherwise derives from the alias.
188
+ `schema`, `index()` and `migration()` land in a module-scope declaration keyed by alias.
165
189
 
166
190
  ### `PostgresResource<T>`
167
191
 
168
- Extends `Resource<T>` with:
192
+ Extends `Resource<T>`, `LockableResource<T>` and `MigratableResource<PostgresTx, PostgresResource<T>>` with:
169
193
 
170
- - `schema?: AnySchema` — the source of truth for the table structure
171
- - `table: TableSpec` / `entity: PgRuntimeTable` — the compiled spec and the runtime Drizzle table
194
+ - `schema?: AnySchema`, the source of truth for the table structure
195
+ - `table: TableSpec` / `entity: PgRuntimeTable`, the compiled spec and the runtime Drizzle table (after `init()`)
172
196
  - `db(): Promise<PostgresDb>` / `client(): Promise<Pool>`
173
- - `index(name, spec)` / `migration(name, apply, stage?)` — chainable declarations
174
- - `query` / `queryOne` / `execute` — raw rows and affected counts
175
- - `select` / `selectOne` — rows marshalled back into `T`
176
- - `ref(alias?): string` — fully qualified identifier of this or another resource
177
- - `transaction(fn)` — a `PostgresTx` with the same placeholder-aware helpers
178
- - `insert` / `upsert` / `patch` — beyond the base contract
179
- - `lock(record, fields?)` / `unlock(record, fields?)` — encrypt/decrypt `secure` fields
197
+ - `index(name, spec: PgIndexSpec)` / `migration(name, apply, stage?)`, chainable declarations
198
+ - `query(text, params?)` / `queryOne(text, params?)` / `execute(text, params?)`, which return raw rows or the affected count
199
+ - `select(text, params?)` / `selectOne(text, params?)`, which return rows marshalled back into `T`
200
+ - `ref(alias?): string`, the fully qualified identifier of this or another resource
201
+ - `transaction(fn)`, which runs `fn` with a `PostgresTx` (`client`, `query`/`queryOne`/`execute`, `ref`)
202
+ - `insert(record)` (a caller-supplied id), `upsert(record, conflict?)`, `patch(record, opts?)` (merge)
203
+ - `lock(record, fields?)` / `unlock(record, fields?)`, which encrypt/decrypt `secure` fields
180
204
  - `getDefaults(): Partial<T>`
181
205
 
182
- Type the resource once, where it is resolved — `context.resource<PostgresResource<Project>>(alias)` —
183
- rather than passing a record type per method call.
184
-
185
206
  ### `Resource<T>` methods (all implemented)
186
207
 
187
208
  `get`, `load`, `list`, `count`, `create`, `update`, `save`, `delete`, `take`, `purge`
188
209
 
189
210
  `get` and `load` take either an id or a `Criteria<T>` (with an optional `sort`), so reading one
190
- record by several fields is a single call. `create` refuses a caller-supplied id (use `insert`);
191
- `update` replaces the whole record (use `patch` to merge); `take` deletes the record it returns,
192
- atomically, and raises when there is nothing to take where `delete` answers `null`; `purge` refuses
193
- an empty criteria rather than emptying the table. Every write option is rejected here except
194
- absence: `ttl` raises `UnsupportedArgumentError`, since Postgres has no row expiry.
211
+ record by several fields is a single statement. `create` refuses a caller-supplied id (use
212
+ `insert`); `update` replaces the whole record (use `patch` to merge); `take` deletes the record it
213
+ returns, atomically, and raises when there is nothing to take where `delete` answers `null`; `purge`
214
+ refuses an empty criteria rather than emptying the table. `ttl` raises `UnsupportedArgumentError`,
215
+ since Postgres has no row expiry.
195
216
 
196
- ### Errors
197
-
198
- `PostgresError` and its subclasses — `PostgresSyncError`, `PostgresCastRequired`,
199
- `PostgresConstraintError`, `PostgresForeignKeyError`, `PostgresCheckError`, `PostgresDeadlockError`,
200
- `PostgresPlaceholderError`, `PostgresConnectionError`, `PostgresBootstrapError`. Driver errors are
201
- translated by `pgErrorToResourceError`, which unwraps Drizzle's `DrizzleQueryError` to reach the
202
- `pg` error underneath and preserves the raw `code`/`detail`/`hint`/`severity`. Unique violations
203
- surface as `RecordExists` and not-null violations as `MisshapedRecord`.
204
-
205
- ### Constants
217
+ Criteria follow the shared vocabulary from [`@owlmeans/resource`](../resource). A key naming no
218
+ column raises `UnsupportedArgumentError`. A dotted key reaches into a `jsonb` column, a criteria
219
+ object against a `jsonb` column becomes containment (`@>`), and `$contains`/`$contained`/`$overlaps`
220
+ are `@>`/`<@`/`&&` on array columns.
206
221
 
207
- - `DEFAULT_DB_ALIAS` — `'postgres'`
208
- - `DEFAULT_PAGE_SIZE` — `100`, the cap `list()` applies when no `size` is named
209
- - `DEF_MIGRATIONS_TABLE` — `'_owlmeans_migrations'`
210
- - `PG_KEYWORD` — `'pg'`; `PG_MAX_IDENTIFIER` — `63`
211
- - `PgAutoSync`, `PgIndexMethod`, `PgReferentialAction`, `PgErrorCode`
212
-
213
- ## Related Packages
222
+ ### Errors
214
223
 
215
- - [`@owlmeans/resource`](../resource) — `Resource<T>`, `ResourceRecord`, migrations, error family
216
- - [`@owlmeans/postgres`](../postgres) — the connection service required by this package
217
- - [`@owlmeans/mongo-resource`](../mongo-resource) — the MongoDB counterpart
224
+ `PostgresError` and its subclasses are `PostgresSyncError`, `PostgresCastRequired`,
225
+ `PostgresConstraintError`, `PostgresForeignKeyError`, `PostgresCheckError`, `PostgresDeadlockError`
226
+ (`retryable`), `PostgresPlaceholderError`, `PostgresConnectionError` and `PostgresBootstrapError`.
227
+ Driver errors are translated by `pgErrorToResourceError`, which unwraps Drizzle's
228
+ `DrizzleQueryError` to reach the `pg` error underneath and preserves the raw
229
+ `code`/`detail`/`hint`/`severity` in the message. Unique violations surface as `RecordExists` and
230
+ not-null violations as `MisshapedRecord`.
231
+
232
+ ### Exports
233
+
234
+ | Symbol | Kind | Purpose |
235
+ |---|---|---|
236
+ | `makePostgresResource` | function | The resource factory |
237
+ | `PostgresResource<T>` | type | The resource interface described above |
238
+ | `PostgresDbService`, `PostgresDb`, `PostgresTx`, `PostgresMeta` | type | Connection service contract (implemented by `@owlmeans/postgres`), db handle, transaction façade, `DbConfig.meta` shape |
239
+ | `TableSpec`, `ColumnSpec`, `ColumnJsonType`, `PgRuntimeTable` | type | The compiled table description |
240
+ | `PgPropertyOverride`, `PgRootOverride`, `PgIndexSpec`, `PgUniqueSpec`, `PgCheckSpec`, `PgReferenceSpec` | type | The `pg:` vocabulary and `index()` spec |
241
+ | `LiveTable`, `LiveColumn`, `LiveIndex`, `LiveConstraint`, `DdlPlan`, `DdlStatement`, `DdlKind` | type | Introspection results and the reconciliation plan |
242
+ | `pgKeyword` | const | `{ keyword: 'pg', valid: true }` for AJV strict mode |
243
+ | `schemaToTableSpec`, `toFormatType` | function | The schema compiler |
244
+ | `criteriaToSql`, `sortToSql` | function | `Criteria<T>` to a WHERE clause and `Sort<T>` to ORDER BY over the same table |
245
+ | `refOf`, `resolvePlaceholders`, `resetPlaceholderCache`, `PlaceholderContext` | function / type | `{{alias}}` resolution, identifiers only |
246
+ | `pgTableName`, `pgIdentifier`, `assertSqlIdentifier`, `quoteIdent`, `quoteLiteral`, `qualify`, `advisoryKey` | function | Identifier helpers |
247
+ | `rowToRecord`, `resultToRecord`, `recordToValues`, `recordToFullValues`, `specToTable` | function | Row and record marshalling, Drizzle table construction |
248
+ | `initializeTable`, `applyForeignKeys`, `TableInit` | function / type | The `init()` lifecycle |
249
+ | `introspectTable`, `countNonNull`, `planSync`, `planForeignKeys`, `applyPlan`, `ensureSchema`, `acquireLock`, `releaseLock` | function | Reconciliation machinery |
250
+ | `makeTx`, `makeMigrationStore` | function | The migration façade and ledger |
251
+ | `getDeclaration`, `resetDeclarations`, `PostgresDeclaration` | function / type | Module-scope declarations per alias; `resetDeclarations` is the testing seam |
252
+ | `getSchemaSecureFeilds` | function | `secure: true` properties used by `lock`/`unlock` |
253
+ | `pgErrorToResourceError`, `describePgError`, `PostgresError` family | function / class | Driver error translation |
254
+ | `DEFAULT_DB_ALIAS`, `DEFAULT_PAGE_SIZE`, `DEF_MIGRATIONS_TABLE` | const | `'postgres'`, `100`, `'_owlmeans_migrations'` |
255
+ | `PG_KEYWORD`, `PG_MAX_IDENTIFIER`, `ID_FIELD`, `DEF_SQL_TYPE`, `DEF_JSON_TYPE`, `DEF_ID_DEFAULT`, `STRING_RETURNING_TYPES` | const | Compiler constants |
256
+ | `PgAutoSync`, `PgIndexMethod`, `PgReferentialAction`, `PgErrorCode`, `PgTypeOid` | enum | Reconciliation policy, index methods, FK actions, driver codes, type OIDs |
257
+
258
+ ## Common pitfalls
259
+
260
+ - **A second owner of the DDL.** Drizzle table definitions, `drizzle-kit` or hand-written
261
+ `CREATE TABLE` compete with reconciliation, which drops what the other owner added.
262
+ - **`Full` sync on an adopted table drops undeclared columns.** Boot once with `Additive`, confirm
263
+ the plan comes out empty, then switch to `Full`. Or list the columns in `pg.unmanaged`.
264
+ - **`autoSync: 'off'` never creates the table.** The first query then dies with `42P01`.
265
+ - **Listing `id` in `required`**, or generating ids in application code. The database assigns them,
266
+ and `create` refuses a supplied id; use `insert` when you really have one.
267
+ - **Interpolating values into SQL.** Placeholders are for identifiers. Values go in `params`.
268
+ - **Aliasing a table that a placeholder still names.** `{{self.x}}` expands to the qualified
269
+ `"schema"."table"."x"`, which Postgres rejects next to `FROM {{}} p`.
270
+ - **Relying on registration order for `{{alias}}`.** It reads the other resource's *initialized*
271
+ table spec. Foreign keys to a later resource are deferred by the `appendPostgres` middleware, so
272
+ do not reorder registrations.
273
+ - **The same index declared twice under different names** (schema root, property override and
274
+ `index()`). Duplicates are collapsed only by name.
275
+ - **Expecting `update` to merge.** Use `patch`.
276
+ - **Reading "everything" with `list(where)`.** It stops at 100. Use `{ size: 0 }`, or page with a
277
+ unique tiebreak.
278
+ - **A migration body closed over a loop variable.** Its checksum fingerprints the wrapper and drifts.
279
+
280
+ ## Related packages
281
+
282
+ - [`@owlmeans/postgres`](../postgres): the connection service required by this package
283
+ - [`@owlmeans/resource`](../resource): `Resource<T>`, `ResourceRecord`, criteria, migrations, error family
284
+ - [`@owlmeans/mongo-resource`](../mongo-resource): the MongoDB counterpart
285
+ - [`@owlmeans/redis-resource`](../redis-resource): caches, locks and expiring records beside the table
286
+ - [`@owlmeans/queue`](../queue): long imports and batch writes that should not run inside a request
218
287
 
219
288
  <!-- owlmeans:agent-guidance:start -->
220
289
  ## Agent guidance
@@ -224,7 +293,7 @@ This package ships embedded agent skills under `agent-meta/`. After installing y
224
293
  your project's skill store (`.agents/skills/`):
225
294
 
226
295
  ```sh
227
- npx @owlmeans/agent-skills@^0.1.18-rc.19
296
+ npx @owlmeans/agent-skills@^0.1.18-rc.21
228
297
  ```
229
298
 
230
299
  The embedded files are version-matched to this package release. Do not edit them
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
3
  "package": "@owlmeans/postgres-resource",
4
- "version": "0.1.18-rc.20",
5
- "generatedAt": "2026-09-12T13:51:53.535Z",
4
+ "version": "0.1.18-rc.22",
5
+ "generatedAt": "2026-09-15T12:21:31.924Z",
6
6
  "canonicalRepo": "https://github.com/owlmeans/common",
7
7
  "entries": [
8
8
  {
@@ -8,7 +8,7 @@ user-invocable: false
8
8
  # @owlmeans/postgres-resource
9
9
 
10
10
  **Layer:** Infra
11
- **Install:** `"@owlmeans/postgres-resource": "^0.1.18-rc.20"` in `dependencies` (peers `pg`, `ajv`)
11
+ **Install:** `"@owlmeans/postgres-resource": "^0.1.18-rc.22"` in `dependencies` (peers `pg`, `ajv`)
12
12
 
13
13
  The Postgres counterpart of [[mongo-resource]]. The difference that governs everything else: a
14
14
  Mongo collection has no structure, a Postgres table does — so **the resource layer owns the DDL**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@owlmeans/postgres-resource",
3
- "version": "0.1.18-rc.20",
3
+ "version": "0.1.18-rc.22",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -27,16 +27,16 @@
27
27
  },
28
28
  "dependencies": {
29
29
  "@noble/hashes": "^1.5.0",
30
- "@owlmeans/basic-ids": "^0.1.18-rc.16",
31
- "@owlmeans/context": "^0.1.18-rc.15",
32
- "@owlmeans/resource": "^0.1.18-rc.16",
33
- "@owlmeans/server-context": "^0.1.18-rc.19",
30
+ "@owlmeans/basic-ids": "^0.1.18-rc.18",
31
+ "@owlmeans/context": "^0.1.18-rc.17",
32
+ "@owlmeans/resource": "^0.1.18-rc.18",
33
+ "@owlmeans/server-context": "^0.1.18-rc.21",
34
34
  "@scure/base": "^2.3.0",
35
35
  "drizzle-orm": "~0.45.2"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@owlmeans/dep-config": "workspace:*",
39
- "@owlmeans/test-integration": "^0.1.18-rc.15",
39
+ "@owlmeans/test-integration": "^0.1.18-rc.17",
40
40
  "@types/bun": "^1.4.0",
41
41
  "@types/node": "^26.1.0",
42
42
  "@types/pg": "^8.20.4",