@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 +173 -104
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/postgres-resource/SKILL.md +1 -1
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -1,32 +1,48 @@
|
|
|
1
1
|
# @owlmeans/postgres-resource
|
|
2
2
|
|
|
3
|
-
PostgreSQL-backed `Resource<T
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
- `
|
|
9
|
-
- `
|
|
10
|
-
-
|
|
11
|
-
|
|
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.
|
|
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
|
-
```
|
|
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
|
-
|
|
62
|
+
// in the server context factory, next to appendPostgres(context)
|
|
50
63
|
context.registerResource(makeProjectResource())
|
|
51
64
|
```
|
|
52
65
|
|
|
53
|
-
|
|
66
|
+
### Read and write from a handler
|
|
54
67
|
|
|
55
|
-
```
|
|
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
|
|
86
|
+
### Schema to table
|
|
70
87
|
|
|
71
|
-
```
|
|
72
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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: ['
|
|
105
|
+
required: ['entityId', 'number', 'customerId', 'status'],
|
|
82
106
|
pg: {
|
|
83
|
-
|
|
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
|
|
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
|
|
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
|
-
###
|
|
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
|
-
|
|
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
|
-
|
|
121
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
```
|
|
150
|
-
const
|
|
151
|
-
`SELECT
|
|
152
|
-
|
|
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
|
|
171
|
-
- `table: TableSpec` / `entity: PgRuntimeTable
|
|
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?)
|
|
174
|
-
- `query` / `queryOne` / `execute
|
|
175
|
-
- `select` / `selectOne
|
|
176
|
-
- `ref(alias?): string
|
|
177
|
-
- `transaction(fn)`
|
|
178
|
-
- `insert`
|
|
179
|
-
- `lock(record, fields?)` / `unlock(record, 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
|
|
191
|
-
`update` replaces the whole record (use `patch` to merge); `take` deletes the record it
|
|
192
|
-
atomically, and raises when there is nothing to take where `delete` answers `null`; `purge`
|
|
193
|
-
an empty criteria rather than emptying the table.
|
|
194
|
-
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
`
|
|
199
|
-
|
|
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
|
-
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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.
|
|
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
|
package/agent-meta/manifest.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 2,
|
|
3
3
|
"package": "@owlmeans/postgres-resource",
|
|
4
|
-
"version": "0.1.18-rc.
|
|
5
|
-
"generatedAt": "2026-09-
|
|
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.
|
|
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.
|
|
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.
|
|
31
|
-
"@owlmeans/context": "^0.1.18-rc.
|
|
32
|
-
"@owlmeans/resource": "^0.1.18-rc.
|
|
33
|
-
"@owlmeans/server-context": "^0.1.18-rc.
|
|
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.
|
|
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",
|