@owlmeans/postgres-resource 0.1.18-rc.2 → 0.1.18-rc.21
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 +27 -12
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/postgres-resource/SKILL.md +82 -18
- package/build/consts.d.ts +8 -1
- package/build/consts.d.ts.map +1 -1
- package/build/consts.js +8 -1
- package/build/consts.js.map +1 -1
- package/build/declarations.js +5 -5
- package/build/resource.d.ts +2 -2
- package/build/resource.d.ts.map +1 -1
- package/build/resource.js +95 -108
- package/build/resource.js.map +1 -1
- package/build/types.d.ts +27 -19
- package/build/types.d.ts.map +1 -1
- package/build/utils/criteria.d.ts +10 -6
- package/build/utils/criteria.d.ts.map +1 -1
- package/build/utils/criteria.js +13 -5
- package/build/utils/criteria.js.map +1 -1
- package/build/utils/diff.d.ts.map +1 -1
- package/build/utils/diff.js +31 -1
- package/build/utils/diff.js.map +1 -1
- package/build/utils/migrations.d.ts +2 -2
- package/build/utils/migrations.js +2 -2
- package/build/utils/name.d.ts +3 -3
- package/build/utils/name.js +3 -3
- package/build/utils/sql.d.ts +10 -2
- package/build/utils/sql.d.ts.map +1 -1
- package/build/utils/sql.js.map +1 -1
- package/build/utils/table.d.ts +4 -0
- package/build/utils/table.d.ts.map +1 -1
- package/build/utils/table.js +4 -0
- package/build/utils/table.js.map +1 -1
- package/package.json +7 -7
- package/src/consts.ts +8 -1
- package/src/declarations.ts +5 -5
- package/src/resource.ts +118 -135
- package/src/types.ts +29 -18
- package/src/utils/criteria.ts +25 -17
- package/src/utils/diff.ts +31 -1
- package/src/utils/migrations.ts +2 -2
- package/src/utils/name.ts +3 -3
- package/src/utils/sql.ts +0 -0
- package/src/utils/table.ts +5 -1
- package/tests/criteria.spec.ts +187 -0
- package/tests/diff.spec.ts +60 -0
- package/tests/name.spec.ts +3 -3
- package/tests/sql.spec.ts +4 -3
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ code-registered migrations, and custom SQL with resource-alias placeholders.
|
|
|
5
5
|
|
|
6
6
|
## Overview
|
|
7
7
|
|
|
8
|
-
- `makePostgresResource<R, T>(alias, dbAlias?, serviceAlias?,
|
|
8
|
+
- `makePostgresResource<R, T>(alias, dbAlias?, serviceAlias?, tableName?)` — factory for Postgres resources
|
|
9
9
|
- `PostgresResource<T>` — extends `Resource<T>` with a Drizzle table, custom SQL, transactions, and field encryption
|
|
10
10
|
- The resource's **AJV schema is the single source of truth for the table structure** — columns, types,
|
|
11
11
|
nullability, defaults, primary key and indexes are all derived from it
|
|
@@ -16,7 +16,7 @@ code-registered migrations, and custom SQL with resource-alias placeholders.
|
|
|
16
16
|
## Installation
|
|
17
17
|
|
|
18
18
|
```bash
|
|
19
|
-
bun add @owlmeans/postgres-resource @owlmeans/postgres pg
|
|
19
|
+
bun add @owlmeans/postgres-resource@^0.1.18-rc.12 @owlmeans/postgres@^0.1.18-rc.13 pg
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
`pg` and `ajv` are peer dependencies of this package. `@owlmeans/postgres` provides the connection
|
|
@@ -35,7 +35,7 @@ export interface ProjectResource extends PostgresResource<ProjectRecord> {}
|
|
|
35
35
|
|
|
36
36
|
export const makeProjectResource: ResourceMaker<ProjectRecord, ProjectResource> = (dbAlias, serviceAlias) => {
|
|
37
37
|
const resource = makePostgresResource<ProjectRecord, ProjectResource>(
|
|
38
|
-
RES_PROJECT, dbAlias, serviceAlias
|
|
38
|
+
RES_PROJECT, dbAlias, serviceAlias
|
|
39
39
|
)
|
|
40
40
|
resource.schema = ProjectSchema
|
|
41
41
|
resource.index('idx_project_entity', { columns: ['entityId'] })
|
|
@@ -55,9 +55,17 @@ Use in a handler:
|
|
|
55
55
|
```typescript
|
|
56
56
|
const projects = context.resource<ProjectResource>(RES_PROJECT)
|
|
57
57
|
const record = await projects.create({ entityId, alias, title })
|
|
58
|
-
|
|
58
|
+
|
|
59
|
+
const one = await projects.load({ entityId, alias }) // null when absent
|
|
60
|
+
const { items, total } = await projects.list(
|
|
61
|
+
{ entityId, status: ['draft', 'active'] }, // an array means "any of these"
|
|
62
|
+
{ page: 0, size: 20, sort: [{ field: 'createdAt', order: 'desc' }] }
|
|
63
|
+
)
|
|
59
64
|
```
|
|
60
65
|
|
|
66
|
+
`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.
|
|
68
|
+
|
|
61
69
|
### Schema → table
|
|
62
70
|
|
|
63
71
|
```typescript
|
|
@@ -150,7 +158,7 @@ substituted blindly.
|
|
|
150
158
|
|
|
151
159
|
## API
|
|
152
160
|
|
|
153
|
-
### `makePostgresResource<R, T>(alias, dbAlias?, serviceAlias?,
|
|
161
|
+
### `makePostgresResource<R, T>(alias, dbAlias?, serviceAlias?, tableName?): T`
|
|
154
162
|
|
|
155
163
|
Creates a Postgres resource. `dbAlias` and `serviceAlias` default to `DEFAULT_DB_ALIAS`
|
|
156
164
|
(`'postgres'`). `tableName` overrides the physical table name, which otherwise derives from the alias.
|
|
@@ -162,21 +170,28 @@ Extends `Resource<T>` with:
|
|
|
162
170
|
- `schema?: AnySchema` — the source of truth for the table structure
|
|
163
171
|
- `table: TableSpec` / `entity: PgRuntimeTable` — the compiled spec and the runtime Drizzle table
|
|
164
172
|
- `db(): Promise<PostgresDb>` / `client(): Promise<Pool>`
|
|
165
|
-
- `index(name, spec)
|
|
173
|
+
- `index(name, spec)` / `migration(name, apply, stage?)` — chainable declarations
|
|
166
174
|
- `query` / `queryOne` / `execute` — raw rows and affected counts
|
|
167
175
|
- `select` / `selectOne` — rows marshalled back into `T`
|
|
168
176
|
- `ref(alias?): string` — fully qualified identifier of this or another resource
|
|
169
177
|
- `transaction(fn)` — a `PostgresTx` with the same placeholder-aware helpers
|
|
170
|
-
- `insert` / `upsert` / `patch`
|
|
178
|
+
- `insert` / `upsert` / `patch` — beyond the base contract
|
|
171
179
|
- `lock(record, fields?)` / `unlock(record, fields?)` — encrypt/decrypt `secure` fields
|
|
172
180
|
- `getDefaults(): Partial<T>`
|
|
173
181
|
|
|
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
|
+
|
|
174
185
|
### `Resource<T>` methods (all implemented)
|
|
175
186
|
|
|
176
|
-
`get`, `load`, `create`, `update`, `save`, `delete`, `
|
|
187
|
+
`get`, `load`, `list`, `count`, `create`, `update`, `save`, `delete`, `take`, `purge`
|
|
177
188
|
|
|
178
|
-
`
|
|
179
|
-
|
|
189
|
+
`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.
|
|
180
195
|
|
|
181
196
|
### Errors
|
|
182
197
|
|
|
@@ -190,7 +205,7 @@ surface as `RecordExists` and not-null violations as `MisshapedRecord`.
|
|
|
190
205
|
### Constants
|
|
191
206
|
|
|
192
207
|
- `DEFAULT_DB_ALIAS` — `'postgres'`
|
|
193
|
-
- `DEFAULT_PAGE_SIZE` — `
|
|
208
|
+
- `DEFAULT_PAGE_SIZE` — `100`, the cap `list()` applies when no `size` is named
|
|
194
209
|
- `DEF_MIGRATIONS_TABLE` — `'_owlmeans_migrations'`
|
|
195
210
|
- `PG_KEYWORD` — `'pg'`; `PG_MAX_IDENTIFIER` — `63`
|
|
196
211
|
- `PgAutoSync`, `PgIndexMethod`, `PgReferentialAction`, `PgErrorCode`
|
|
@@ -209,7 +224,7 @@ This package ships embedded agent skills under `agent-meta/`. After installing y
|
|
|
209
224
|
your project's skill store (`.agents/skills/`):
|
|
210
225
|
|
|
211
226
|
```sh
|
|
212
|
-
npx @owlmeans/agent-skills
|
|
227
|
+
npx @owlmeans/agent-skills@^0.1.18-rc.20
|
|
213
228
|
```
|
|
214
229
|
|
|
215
230
|
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-
|
|
4
|
+
"version": "0.1.18-rc.21",
|
|
5
|
+
"generatedAt": "2026-09-12T14:21:25.465Z",
|
|
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.21"` 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**
|
|
@@ -18,15 +18,18 @@ and derives it from the resource's AJV schema.
|
|
|
18
18
|
|
|
19
19
|
| Export | Description |
|
|
20
20
|
|--------|-------------|
|
|
21
|
-
| `makePostgresResource<R, T>(alias, dbAlias?, serviceAlias?,
|
|
22
|
-
| `PostgresResource<T>` | `Resource<T>` + `table`/`entity`, custom SQL
|
|
23
|
-
| `PostgresDbService`, `PostgresDb`, `PostgresTx` | Service contract implemented by `@owlmeans/postgres`; the db handle `{ drizzle, pool, schema, database }`; the transaction façade. |
|
|
21
|
+
| `makePostgresResource<R, T>(alias, dbAlias?, serviceAlias?, tableName?)` | The resource factory. Aliases default to `DEFAULT_DB_ALIAS` (`'postgres'`); `tableName` overrides the physical table (else the sanitized alias). |
|
|
22
|
+
| `PostgresResource<T>` | `Resource<T>` + `table`/`entity`, `db()`/`client()`, `index`, `ref`, `getDefaults`, custom SQL (`query`/`queryOne`/`execute`/`select`/`selectOne`), `transaction`, `insert`/`upsert`/`patch`, `lock`/`unlock`, `migration`/`migrations`. |
|
|
23
|
+
| `PostgresDbService`, `PostgresDb`, `PostgresTx` | Service contract implemented by `@owlmeans/postgres`; the db handle `{ drizzle, pool, schema, database }`; the transaction façade (`client`, `query`/`queryOne`/`execute`, `ref`). |
|
|
24
|
+
| `PostgresMeta` | The `DbConfig.meta` shape this package reads — `database`, `autoSync`, `url`, and the pool/probe knobs. |
|
|
24
25
|
| `TableSpec`, `ColumnSpec`, `PgPropertyOverride`, `PgRootOverride`, `DdlPlan` | The compiled table description and the `pg:` vocabulary types. |
|
|
26
|
+
| `PgIndexSpec`, `PgUniqueSpec`, `PgCheckSpec`, `PgReferenceSpec` | What `resource.index()` and the `pg:` overrides are written with. |
|
|
27
|
+
| `criteriaToSql`, `sortToSql` | `Criteria<T>` → a WHERE clause and `Sort<T>` → the ORDER BY, for code that builds its own statement over the same table. |
|
|
25
28
|
| `pgKeyword` | `{ keyword: 'pg', valid: true }` — register it when running AJV in strict mode. |
|
|
26
29
|
| `schemaToTableSpec`, `pgTableName`, `pgIdentifier`, `quoteIdent`, `qualify`, `advisoryKey` | The compiler and identifier helpers. |
|
|
27
30
|
| `refOf`, `resolvePlaceholders` | `{{alias}}` resolution — identifiers only. |
|
|
28
31
|
| `PostgresError` family, `pgErrorToResourceError`, `describePgError` | Driver-error translation. |
|
|
29
|
-
| `getDeclaration`, `resetDeclarations` | Module-scope index/migration declarations, keyed by alias. |
|
|
32
|
+
| `getDeclaration`, `resetDeclarations` | Module-scope schema/index/migration declarations, keyed by alias. |
|
|
30
33
|
| `DEFAULT_DB_ALIAS`, `DEFAULT_PAGE_SIZE`, `DEF_MIGRATIONS_TABLE`, `PgAutoSync`, `PgIndexMethod`, `PgReferentialAction`, `PgErrorCode` | Constants. |
|
|
31
34
|
|
|
32
35
|
## The schema is the table — never write DDL
|
|
@@ -34,7 +37,7 @@ and derives it from the resource's AJV schema.
|
|
|
34
37
|
```typescript
|
|
35
38
|
export const makeProjectResource: ResourceMaker<ProjectRecord, ProjectResource> = (dbAlias, serviceAlias) => {
|
|
36
39
|
const resource = makePostgresResource<ProjectRecord, ProjectResource>(
|
|
37
|
-
RES_PROJECT, dbAlias, serviceAlias
|
|
40
|
+
RES_PROJECT, dbAlias, serviceAlias
|
|
38
41
|
)
|
|
39
42
|
resource.schema = ProjectSchema
|
|
40
43
|
resource.index('idx_project_entity', { columns: ['entityId'] })
|
|
@@ -48,12 +51,18 @@ Never call `pgTable`/`pgSchema`, never call `drizzle()`, never run `drizzle-kit`
|
|
|
48
51
|
`CREATE TABLE`. Two owners of the same DDL is the failure mode this package exists to remove:
|
|
49
52
|
reconciliation would drop what the other owner added.
|
|
50
53
|
|
|
54
|
+
`schema`, `index()` and `migration()` all land in the module-scope declaration for the alias rather
|
|
55
|
+
than on the resource object, so a maker that runs more than once for the same alias — a custom
|
|
56
|
+
maker wrapping the built-in one, a spec calling it again — reads and extends the same declaration
|
|
57
|
+
instead of starting a fresh, emptier one.
|
|
58
|
+
|
|
51
59
|
| JSON Schema | Postgres |
|
|
52
60
|
|---|---|
|
|
53
61
|
| `string` · `string`+`format:'uuid'` | `text` · `uuid` |
|
|
54
62
|
| `DateSchema` (`{type:'object', format:'date-time'}`) | `timestamptz` |
|
|
55
63
|
| `integer` · `number` · `boolean` | `integer` · `double precision` · `boolean` |
|
|
56
|
-
| `array`,
|
|
64
|
+
| `array` of plain `string`/`integer`/`number`/`boolean` items (no `format`, no `enum`) | native `<scalar>[]`, e.g. `text[]` |
|
|
65
|
+
| any other `array` (objects, enums, formatted items, no `items`), nested `object` | `jsonb` |
|
|
57
66
|
| string `enum` | `text` + `CHECK` |
|
|
58
67
|
| `nullable: true` · in `required[]` | nullable · `NOT NULL` |
|
|
59
68
|
| `secure: true` | ciphertext column, `lock`/`unlock` aware |
|
|
@@ -90,24 +99,41 @@ declaration sites merge into one `TableSpec` — the schema root, a per-property
|
|
|
90
99
|
not an error. Two entries under one name would emit the same `CREATE INDEX` twice in a single DDL
|
|
91
100
|
transaction, and Postgres answers the second with `42P07`, rolling back the plan that created the
|
|
92
101
|
table: the resource then fails every boot with an error naming an index that does not exist.
|
|
93
|
-
|
|
102
|
+
The compiler collapses the duplicates itself, keeps the first declaration and warns on the console.
|
|
94
103
|
|
|
95
104
|
## Reconciliation is authoritative — `PgAutoSync`
|
|
96
105
|
|
|
97
|
-
At `init()`:
|
|
98
|
-
`
|
|
106
|
+
At `init()`: take a **session** advisory lock on the qualified table name → introspect → diff →
|
|
107
|
+
apply the whole `DdlPlan` in one transaction → release the lock. The lock is session level, not
|
|
108
|
+
`xact`, because it has to span the migrations as well as the DDL, and those run in transactions of
|
|
109
|
+
their own inside it — only the plan itself is one `BEGIN`/`COMMIT`. Concurrent replicas therefore
|
|
110
|
+
serialize on the whole initialization, not just on the DDL. `DbConfig.meta.autoSync`:
|
|
99
111
|
|
|
100
112
|
| Value | Behaviour |
|
|
101
113
|
|---|---|
|
|
102
114
|
| `Full` (default) | add / retype+backfill / drop columns, reconcile indexes and constraints |
|
|
103
115
|
| `Additive` | add only — never retypes, never drops |
|
|
104
|
-
| `Off` |
|
|
116
|
+
| `Off` | **no table DDL** — the structure is never diffed and no statement is emitted, so a missing table is never created and the first query dies with `42P01`. Only for a table something else already provisions |
|
|
117
|
+
|
|
118
|
+
The schema root override `pg: { autoSync }` is a boolean and wins per table: it turns reconciliation
|
|
119
|
+
back on for one table under `meta.autoSync: 'off'`, and off for one table under the other two modes.
|
|
120
|
+
`Full` versus `Additive` still comes from the config.
|
|
105
121
|
|
|
106
122
|
**`Full` DROPs columns the schema doesn't declare.** Adopting a table this package didn't create:
|
|
107
123
|
boot once with `Additive`, confirm the plan comes out empty, then flip to `Full`. Columns listed in
|
|
108
124
|
`pg.unmanaged` stay outside reconciliation's authority permanently. A cast Postgres cannot perform
|
|
109
125
|
raises `PostgresCastRequired` instead of truncating.
|
|
110
126
|
|
|
127
|
+
**A retype drops the column's default first and restores it after.** Postgres refuses
|
|
128
|
+
`ALTER COLUMN … TYPE` outright when the column carries a DEFAULT it cannot cast to the new type
|
|
129
|
+
(`42804 default for column "x" cannot be cast automatically`), and it refuses before reading a
|
|
130
|
+
single row, so `USING` never gets a chance to help. Since the plan is one transaction, that refusal
|
|
131
|
+
aborts the whole reconciliation and the resource then fails *every* boot with an error naming the
|
|
132
|
+
column it is trying to fix. The plan therefore emits `DROP DEFAULT` → `ALTER … TYPE … USING` →
|
|
133
|
+
`SET DEFAULT`, restoring the default from the spec rather than from what the column was carrying.
|
|
134
|
+
The `id` column is where this shows up in practice: it is created with a `gen_random_uuid()::text`
|
|
135
|
+
default, so any change to its declared type takes this path.
|
|
136
|
+
|
|
111
137
|
## Migrations bracket the sync
|
|
112
138
|
|
|
113
139
|
`migration`/`migrations()` implement the shared `MigratableResource` capability from
|
|
@@ -141,10 +167,16 @@ resource.migration('0001-rescue-legacy', async tx => {
|
|
|
141
167
|
|
|
142
168
|
```typescript
|
|
143
169
|
await projects.select(
|
|
144
|
-
`SELECT
|
|
170
|
+
`SELECT {{}}.* FROM {{}} JOIN {{users}} u ON u.id = {{self.ownerId}} WHERE u.active = $1`, [true]
|
|
145
171
|
)
|
|
146
172
|
```
|
|
147
173
|
|
|
174
|
+
**Do not alias a table a placeholder still names.** Every `{{…}}` expands to the *qualified*
|
|
175
|
+
`"schema"."table"` — `{{self.ownerId}}` to `"schema"."table"."ownerId"` — so aliasing the same table
|
|
176
|
+
as `p` in the `FROM` makes Postgres reject the expansion with `invalid reference to FROM-clause
|
|
177
|
+
entry`. Either write the qualified form throughout, as above, or alias the table and stop using
|
|
178
|
+
`{{self.…}}` for its columns.
|
|
179
|
+
|
|
148
180
|
Postgres cannot bind an identifier as a parameter — that is the entire reason this mechanism exists.
|
|
149
181
|
Values have no such excuse: they stay in `params` as `$1..$n`. An unknown alias or property raises
|
|
150
182
|
`PostgresPlaceholderError` at parse time.
|
|
@@ -158,13 +190,45 @@ middleware `appendPostgres` installs — use that rather than reordering registr
|
|
|
158
190
|
|
|
159
191
|
| Method | Semantics |
|
|
160
192
|
|---|---|
|
|
161
|
-
| `create` | refuses a caller-supplied id (`RecordExists`) — use `insert` |
|
|
193
|
+
| `create` | refuses a caller-supplied id (`RecordExists`) — use `insert`; rejects `opts.ttl` with `UnsupportedArgumentError` (mongo parity) |
|
|
162
194
|
| `update` | **replaces** the whole record — use `patch` to merge |
|
|
163
|
-
| `
|
|
164
|
-
| `
|
|
195
|
+
| `load(where, { sort })` / `get(where, { sort })` | one `SELECT … ORDER BY … LIMIT 1`, so "the newest matching row" is one statement rather than a list whose head is taken |
|
|
196
|
+
| `delete` / `take` | one `DELETE … RETURNING`: the row is handed back by the statement that removed it. `take` **deletes** and throws `UnknownRecordError` on a miss |
|
|
197
|
+
| `purge` | `DELETE … RETURNING` over the criteria; refuses an empty criteria object (`UnsupportedArgumentError('purge:no-criteria')`) rather than truncating the table |
|
|
198
|
+
| `count` | `count(*)` over the criteria, no rows carried back |
|
|
165
199
|
| `upsert` | `INSERT … ON CONFLICT DO UPDATE`, conflicting on the primary key by default |
|
|
166
200
|
| `select`/`selectOne` | custom SQL marshalled back into `T`; `query`/`queryOne` return raw rows |
|
|
167
201
|
|
|
202
|
+
## Paging
|
|
203
|
+
|
|
204
|
+
Postgres is **PAGED**: `DEFAULT_PAGE_SIZE` is `100`, so `list(where)` with no `size` returns the
|
|
205
|
+
first 100 rows — a table is unbounded, and an unpaged read is a production incident waiting for the
|
|
206
|
+
row count to grow. `total` is counted separately, so it describes the whole match rather than the
|
|
207
|
+
window, and `list(where, { size: 0 })` lifts the limit: the explicit, greppable way to read a whole
|
|
208
|
+
table.
|
|
209
|
+
|
|
210
|
+
`sort` becomes the `ORDER BY`, a bare field name ascending, and **the primary key is always
|
|
211
|
+
appended as a tiebreak**. Postgres has no implicit row order, so paginating on a non-unique sort
|
|
212
|
+
key silently duplicates and skips rows between pages — a difference from mongo that would surface
|
|
213
|
+
as a data bug rather than an error.
|
|
214
|
+
|
|
215
|
+
## Criteria against a table
|
|
216
|
+
|
|
217
|
+
`criteriaToSql` answers the shared vocabulary ([[resource]]) in SQL, so one criteria object selects
|
|
218
|
+
the same rows here as it does against a collection or in memory. What is specific to a table:
|
|
219
|
+
|
|
220
|
+
- **A key naming no column raises `UnsupportedArgumentError`.** A typo that silently widened a
|
|
221
|
+
query to the whole table is worth being loud about — the schemaless stores cannot detect one.
|
|
222
|
+
- **A dotted key reaches into a jsonb column** (`#>>`), and a criteria object against a jsonb
|
|
223
|
+
column becomes containment (`@>`). A dotted key over a non-jsonb column is refused, and `sort`
|
|
224
|
+
refuses dotted paths outright: ORDER BY names a column the caller actually declared.
|
|
225
|
+
- `$contains`/`$contained`/`$overlaps` are the array operators `@>`, `<@` and `&&`;
|
|
226
|
+
`$like`/`$ilike` are `LIKE`/`ILIKE`; `$exists: true` and `$null: false` are `IS NOT NULL`, their
|
|
227
|
+
negations `IS NULL`.
|
|
228
|
+
- **`{ $in: [null, …] }` is widened explicitly.** SQL `IN` never matches NULL, so a null in the
|
|
229
|
+
list would silently disappear; the condition becomes `IN (…) OR IS NULL` (and the `$nin` form
|
|
230
|
+
`NOT IN (…) AND IS NOT NULL`), which is what the other stores answer.
|
|
231
|
+
|
|
168
232
|
## Errors
|
|
169
233
|
|
|
170
234
|
Drizzle raises `DrizzleQueryError` and hangs the `pg` error off `cause`, so the driver `code` is not
|
|
@@ -175,9 +239,9 @@ translation — consumers classify retryable DDL races on `42P01`/`42703`.
|
|
|
175
239
|
|
|
176
240
|
## Config
|
|
177
241
|
|
|
178
|
-
`DbConfig.schema` is the Postgres **SCHEMA** (
|
|
179
|
-
from `meta.database
|
|
180
|
-
|
|
242
|
+
`DbConfig.schema` is the Postgres **SCHEMA** (the service's `name(alias?)` returns it as given); the
|
|
243
|
+
**DATABASE** comes from `meta.database`. Values starting with `/` are read as files by the existing
|
|
244
|
+
`fileConfigReader` middleware.
|
|
181
245
|
|
|
182
246
|
## Tests
|
|
183
247
|
|
package/build/consts.d.ts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
export declare const DEFAULT_DB_ALIAS = "postgres";
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Rows `list()` returns when the caller names no `size`.
|
|
4
|
+
*
|
|
5
|
+
* A relational table is unbounded, so an unpaged read is a production incident waiting for
|
|
6
|
+
* the row count to grow. Asking for everything stays possible — and greppable — as
|
|
7
|
+
* `list(where, { size: 0 })`.
|
|
8
|
+
*/
|
|
9
|
+
export declare const DEFAULT_PAGE_SIZE = 100;
|
|
3
10
|
/**
|
|
4
11
|
* Table that records which code-registered migrations have already been applied.
|
|
5
12
|
* Lives in the same Postgres schema as the resources it tracks, so dropping the
|
package/build/consts.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"consts.d.ts","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,gBAAgB,aAAa,CAAA;AAE1C,eAAO,MAAM,iBAAiB,
|
|
1
|
+
{"version":3,"file":"consts.d.ts","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,gBAAgB,aAAa,CAAA;AAE1C;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB,MAAM,CAAA;AAEpC;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,yBAAyB,CAAA;AAE1D,oEAAoE;AACpE,eAAO,MAAM,UAAU,OAAO,CAAA;AAE9B,8FAA8F;AAC9F,eAAO,MAAM,iBAAiB,KAAK,CAAA;AAEnC,qFAAqF;AACrF,eAAO,MAAM,QAAQ,OAAO,CAAA;AAE5B,iFAAiF;AACjF,eAAO,MAAM,YAAY,SAAS,CAAA;AAElC,eAAO,MAAM,aAAa,UAAU,CAAA;AAEpC,sEAAsE;AACtE,eAAO,MAAM,cAAc,4BAA4B,CAAA;AAEvD;;;;;;GAMG;AACH,oBAAY,UAAU;IACpB,IAAI,SAAS;IACb,QAAQ,aAAa;IACrB,GAAG,QAAQ;CACZ;AAED,oBAAY,aAAa;IACvB,KAAK,UAAU;IACf,IAAI,SAAS;IACb,GAAG,QAAQ;IACX,IAAI,SAAS;IACb,IAAI,SAAS;IACb,MAAM,WAAW;CAClB;AAED,oBAAY,mBAAmB;IAC7B,QAAQ,cAAc;IACtB,QAAQ,aAAa;IACrB,OAAO,YAAY;IACnB,OAAO,aAAa;IACpB,UAAU,gBAAgB;CAC3B;AAED;;;GAGG;AACH,oBAAY,WAAW;IACrB,eAAe,UAAU;IACzB,mBAAmB,UAAU;IAC7B,gBAAgB,UAAU;IAC1B,cAAc,UAAU;IACxB,cAAc,UAAU;IACxB,eAAe,UAAU;IACzB,cAAc,UAAU;IACxB,eAAe,UAAU;IACzB,eAAe,UAAU;IACzB,YAAY,UAAU;IACtB,gBAAgB,UAAU;IAC1B,yBAAyB,UAAU;IACnC,yBAAyB,UAAU;IACnC,sBAAsB,UAAU;IAChC,oBAAoB,UAAU;IAC9B,gBAAgB,UAAU;IAC1B,mBAAmB,UAAU;CAC9B;AAED;;;GAGG;AACH,eAAO,MAAM,sBAAsB,YAAI,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,CAAU,CAAA;AAEhG,uFAAuF;AACvF,oBAAY,SAAS;IACnB,IAAI,OAAO;IACX,SAAS,OAAO;IAChB,WAAW,OAAO;CACnB"}
|
package/build/consts.js
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
export const DEFAULT_DB_ALIAS = 'postgres';
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Rows `list()` returns when the caller names no `size`.
|
|
4
|
+
*
|
|
5
|
+
* A relational table is unbounded, so an unpaged read is a production incident waiting for
|
|
6
|
+
* the row count to grow. Asking for everything stays possible — and greppable — as
|
|
7
|
+
* `list(where, { size: 0 })`.
|
|
8
|
+
*/
|
|
9
|
+
export const DEFAULT_PAGE_SIZE = 100;
|
|
3
10
|
/**
|
|
4
11
|
* Table that records which code-registered migrations have already been applied.
|
|
5
12
|
* Lives in the same Postgres schema as the resources it tracks, so dropping the
|
package/build/consts.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"consts.js","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AACA,MAAM,CAAC,MAAM,gBAAgB,GAAG,UAAU,CAAA;AAE1C,MAAM,CAAC,MAAM,iBAAiB,GAAG,
|
|
1
|
+
{"version":3,"file":"consts.js","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AACA,MAAM,CAAC,MAAM,gBAAgB,GAAG,UAAU,CAAA;AAE1C;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,GAAG,CAAA;AAEpC;;;;GAIG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,sBAAsB,CAAA;AAE1D,oEAAoE;AACpE,MAAM,CAAC,MAAM,UAAU,GAAG,IAAI,CAAA;AAE9B,8FAA8F;AAC9F,MAAM,CAAC,MAAM,iBAAiB,GAAG,EAAE,CAAA;AAEnC,qFAAqF;AACrF,MAAM,CAAC,MAAM,QAAQ,GAAG,IAAI,CAAA;AAE5B,iFAAiF;AACjF,MAAM,CAAC,MAAM,YAAY,GAAG,MAAM,CAAA;AAElC,MAAM,CAAC,MAAM,aAAa,GAAG,OAAO,CAAA;AAEpC,sEAAsE;AACtE,MAAM,CAAC,MAAM,cAAc,GAAG,yBAAyB,CAAA;AAEvD;;;;;;GAMG;AACH,MAAM,CAAN,IAAY,UAIX;AAJD,WAAY,UAAU;IACpB,2BAAa,CAAA;IACb,mCAAqB,CAAA;IACrB,yBAAW,CAAA;AACb,CAAC,EAJW,UAAU,KAAV,UAAU,QAIrB;AAED,MAAM,CAAN,IAAY,aAOX;AAPD,WAAY,aAAa;IACvB,gCAAe,CAAA;IACf,8BAAa,CAAA;IACb,4BAAW,CAAA;IACX,8BAAa,CAAA;IACb,8BAAa,CAAA;IACb,kCAAiB,CAAA;AACnB,CAAC,EAPW,aAAa,KAAb,aAAa,QAOxB;AAED,MAAM,CAAN,IAAY,mBAMX;AAND,WAAY,mBAAmB;IAC7B,6CAAsB,CAAA;IACtB,4CAAqB,CAAA;IACrB,0CAAmB,CAAA;IACnB,2CAAoB,CAAA;IACpB,iDAA0B,CAAA;AAC5B,CAAC,EANW,mBAAmB,KAAnB,mBAAmB,QAM9B;AAED;;;GAGG;AACH,MAAM,CAAN,IAAY,WAkBX;AAlBD,WAAY,WAAW;IACrB,wCAAyB,CAAA;IACzB,4CAA6B,CAAA;IAC7B,yCAA0B,CAAA;IAC1B,uCAAwB,CAAA;IACxB,uCAAwB,CAAA;IACxB,wCAAyB,CAAA;IACzB,uCAAwB,CAAA;IACxB,wCAAyB,CAAA;IACzB,wCAAyB,CAAA;IACzB,qCAAsB,CAAA;IACtB,yCAA0B,CAAA;IAC1B,kDAAmC,CAAA;IACnC,kDAAmC,CAAA;IACnC,+CAAgC,CAAA;IAChC,6CAA8B,CAAA;IAC9B,yCAA0B,CAAA;IAC1B,4CAA6B,CAAA;AAC/B,CAAC,EAlBW,WAAW,KAAX,WAAW,QAkBtB;AAED;;;GAGG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,CAAU,CAAA;AAEhG,uFAAuF;AACvF,MAAM,CAAN,IAAY,SAIX;AAJD,WAAY,SAAS;IACnB,4CAAW,CAAA;IACX,sDAAgB,CAAA;IAChB,0DAAkB,CAAA;AACpB,CAAC,EAJW,SAAS,KAAT,SAAS,QAIpB"}
|
package/build/declarations.js
CHANGED
|
@@ -2,11 +2,11 @@ import { createMigrationRegistry } from '@owlmeans/resource';
|
|
|
2
2
|
/**
|
|
3
3
|
* Per-alias declaration store, held at module scope rather than on the resource object.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* by
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
5
|
+
* A maker may run more than once for the same alias — a custom maker wrapping the built-in
|
|
6
|
+
* one, a maker called again by an app or a spec. Keying the declarations by alias makes that
|
|
7
|
+
* a no-op: every run reads and extends the same schema, indexes and migrations, so nothing a
|
|
8
|
+
* caller chained onto an earlier resource object is lost. Losing a migration is silent — the
|
|
9
|
+
* data transformation simply never runs — which is why the store cannot live on the object.
|
|
10
10
|
*/
|
|
11
11
|
const declarations = new Map();
|
|
12
12
|
export const getDeclaration = (alias) => {
|
package/build/resource.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ResourceRecord } from '@owlmeans/resource';
|
|
2
2
|
import type { PostgresResource } from './types.js';
|
|
3
|
-
export declare const makePostgresResource: <R extends ResourceRecord, T extends PostgresResource<R> = PostgresResource<R>>(alias: string, dbAlias?: string, serviceAlias?: string,
|
|
3
|
+
export declare const makePostgresResource: <R extends ResourceRecord, T extends PostgresResource<R> = PostgresResource<R>>(alias: string, dbAlias?: string, serviceAlias?: string, tableName?: string) => T;
|
|
4
4
|
//# sourceMappingURL=resource.d.ts.map
|
package/build/resource.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resource.d.ts","sourceRoot":"","sources":["../src/resource.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"resource.d.ts","sourceRoot":"","sources":["../src/resource.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACuD,cAAc,EAChF,MAAM,oBAAoB,CAAA;AAW3B,OAAO,KAAK,EAC8D,gBAAgB,EAEzF,MAAM,YAAY,CAAA;AAiBnB,eAAO,MAAM,oBAAoB,GAC/B,CAAC,SAAS,cAAc,EAAE,CAAC,SAAS,gBAAgB,CAAC,CAAC,CAAC,GAAG,gBAAgB,CAAC,CAAC,CAAC,SAEtE,MAAM,YAAW,MAAM,iBAAmC,MAAM,cAC3D,MAAM,KACjB,CA4cF,CAAA"}
|