@owlmeans/postgres-resource 0.1.18-rc.3 → 0.1.18-rc.31

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.
Files changed (47) hide show
  1. package/README.md +185 -101
  2. package/agent-meta/manifest.json +2 -2
  3. package/agent-meta/skills/postgres-resource/SKILL.md +82 -18
  4. package/build/consts.d.ts +8 -1
  5. package/build/consts.d.ts.map +1 -1
  6. package/build/consts.js +8 -1
  7. package/build/consts.js.map +1 -1
  8. package/build/declarations.js +5 -5
  9. package/build/resource.d.ts +2 -2
  10. package/build/resource.d.ts.map +1 -1
  11. package/build/resource.js +95 -108
  12. package/build/resource.js.map +1 -1
  13. package/build/types.d.ts +27 -19
  14. package/build/types.d.ts.map +1 -1
  15. package/build/utils/criteria.d.ts +10 -6
  16. package/build/utils/criteria.d.ts.map +1 -1
  17. package/build/utils/criteria.js +13 -5
  18. package/build/utils/criteria.js.map +1 -1
  19. package/build/utils/diff.d.ts.map +1 -1
  20. package/build/utils/diff.js +31 -1
  21. package/build/utils/diff.js.map +1 -1
  22. package/build/utils/migrations.d.ts +2 -2
  23. package/build/utils/migrations.js +2 -2
  24. package/build/utils/name.d.ts +3 -3
  25. package/build/utils/name.js +3 -3
  26. package/build/utils/sql.d.ts +10 -2
  27. package/build/utils/sql.d.ts.map +1 -1
  28. package/build/utils/sql.js.map +1 -1
  29. package/build/utils/table.d.ts +4 -0
  30. package/build/utils/table.d.ts.map +1 -1
  31. package/build/utils/table.js +4 -0
  32. package/build/utils/table.js.map +1 -1
  33. package/package.json +7 -7
  34. package/src/consts.ts +8 -1
  35. package/src/declarations.ts +5 -5
  36. package/src/resource.ts +118 -135
  37. package/src/types.ts +29 -18
  38. package/src/utils/criteria.ts +25 -17
  39. package/src/utils/diff.ts +31 -1
  40. package/src/utils/migrations.ts +2 -2
  41. package/src/utils/name.ts +3 -3
  42. package/src/utils/sql.ts +0 -0
  43. package/src/utils/table.ts +5 -1
  44. package/tests/criteria.spec.ts +187 -0
  45. package/tests/diff.spec.ts +60 -0
  46. package/tests/name.spec.ts +3 -3
  47. package/tests/sql.spec.ts +4 -3
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?, maker?, 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 @owlmeans/postgres pg
16
+ bun add @owlmeans/postgres-resource@^0.1.18-rc.30 @owlmeans/postgres@^0.1.18-rc.31 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'
@@ -35,45 +51,60 @@ export interface ProjectResource extends PostgresResource<ProjectRecord> {}
35
51
 
36
52
  export const makeProjectResource: ResourceMaker<ProjectRecord, ProjectResource> = (dbAlias, serviceAlias) => {
37
53
  const resource = makePostgresResource<ProjectRecord, ProjectResource>(
38
- RES_PROJECT, dbAlias, serviceAlias, makeProjectResource
54
+ RES_PROJECT, dbAlias, serviceAlias
39
55
  )
40
56
  resource.schema = ProjectSchema
41
57
  resource.index('idx_project_entity', { columns: ['entityId'] })
42
58
 
43
59
  return resource
44
60
  }
45
- ```
46
61
 
47
- Register in context:
48
-
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 })
58
- const list = await projects.list({ criteria: { entityId } })
70
+ const record = await projects.create({ entityId, alias, title }) // the database assigns the id
71
+
72
+ const one = await projects.load({ entityId, alias }) // null when absent
73
+ const { items, total } = await projects.list(
74
+ { entityId, status: ['draft', 'active'] }, // an array means "any of these"
75
+ { page: 0, size: 20, sort: [{ field: 'createdAt', order: 'desc' }] }
76
+ )
77
+
78
+ await projects.patch({ id: record.id, status: 'active' }) // merge
79
+ await projects.update({ ...record, title: 'Renamed' }) // replace the whole row
59
80
  ```
60
81
 
61
- ### Schema → table
82
+ `size` defaults to `DEFAULT_PAGE_SIZE` (100); `list(where, { size: 0 })` lifts the limit. `total`
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.
85
+
86
+ ### Schema to table
87
+
88
+ ```ts
89
+ import { DateSchema } from '@owlmeans/auth'
90
+ import type { JSONSchemaType } from 'ajv'
62
91
 
63
- ```typescript
64
- const ProjectSchema: JSONSchemaType<ProjectRecord> = {
92
+ const InvoiceSchema: JSONSchemaType<InvoiceRecord> = {
65
93
  type: 'object',
66
94
  properties: {
67
- id: { type: 'string', format: 'uuid' },
68
- email: { type: 'string', pg: { type: 'varchar', length: 320, unique: true } },
69
- ownerId: { type: 'string', pg: { references: { resource: 'users', onDelete: 'cascade' } } },
70
- payload: { type: 'object', nullable: true }, // → jsonb
71
- 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
72
104
  },
73
- required: ['id', 'email'],
105
+ required: ['entityId', 'number', 'customerId', 'status'],
74
106
  pg: {
75
- table: 'app_projects',
76
- indexes: [{ name: 'idx_owner_created', columns: ['ownerId', 'createdAt'] }]
107
+ indexes: [{ name: 'idx_invoice_entity_created', columns: ['entityId', 'createdAt'] }]
77
108
  }
78
109
  }
79
110
  ```
@@ -84,51 +115,40 @@ const ProjectSchema: JSONSchemaType<ProjectRecord> = {
84
115
  | `string` + `format: 'uuid'` | `uuid` |
85
116
  | `DateSchema` (`{type:'object', format:'date-time'}`) | `timestamptz` |
86
117
  | `integer` / `number` / `boolean` | `integer` / `double precision` / `boolean` |
87
- | `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` |
88
120
  | string `enum` | `text` + `CHECK` |
89
121
  | `nullable: true` | nullable column |
90
122
  | in `required[]` | `NOT NULL` |
91
123
  | `secure: true` | ciphertext column, `lock`/`unlock` aware |
92
124
  | `id` property | primary key, `gen_random_uuid()::text` default |
93
125
 
94
- 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'
95
130
 
96
- ```typescript
97
131
  ajv.addKeyword(pgKeyword)
98
132
  ```
99
133
 
100
- ### Structure reconciliation
101
-
102
- At `init()` the resource introspects `information_schema` / `pg_catalog`, diffs against the compiled
103
- spec, and applies the DDL plan in one transaction under a `pg_advisory_xact_lock`, so concurrent
104
- replicas never race. Policy comes from `DbConfig.meta.autoSync`:
105
-
106
- | `PgAutoSync` | Behaviour |
107
- |---|---|
108
- | `Full` (default) | add / retype / backfill / drop columns, reconcile indexes and constraints |
109
- | `Additive` | add only — never retypes, never drops. The adoption path for a pre-existing table |
110
- | `Off` | create the table if absent, otherwise leave it alone |
134
+ ### Migrations
111
135
 
112
- A cast Postgres cannot perform raises `PostgresCastRequired` rather than truncating data. Columns
113
- listed in `pg.unmanaged` are outside reconciliation's authority entirely.
136
+ ```ts
137
+ import { MigrationStage } from '@owlmeans/resource'
114
138
 
115
- ### Migrations
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)
116
142
 
117
- ```typescript
118
- resource.migration('0001-backfill-slug', async tx => {
119
- 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'])
120
145
  }, MigrationStage.Post)
121
146
  ```
122
147
 
123
- Stages bracket the structure sync: `Pre` runs **before** the table is reshaped (so a migration can
124
- rescue data reconciliation would drop), `Post` **after** (so it can use the new columns). Applied
125
- once, in registration order, each in its own transaction, recorded in `_owlmeans_migrations`.
126
148
  Migrations declared on a table this package just created are **baselined**, not executed. An edited
127
- body raises `MigrationConflict`; a failing one raises `MigrationError` and aborts `init()`.
149
+ applied body raises `MigrationConflict`; a failing one raises `MigrationError` and aborts `init()`.
128
150
 
129
- ### Custom SQL
130
-
131
- Placeholders resolve **identifiers only** — values stay in `params` and are bound as `$1..$n`:
151
+ ### Custom SQL and transactions
132
152
 
133
153
  | Placeholder | Resolves to |
134
154
  |---|---|
@@ -138,11 +158,22 @@ Placeholders resolve **identifiers only** — values stay in `params` and are bo
138
158
  | `{{#alias}}` | the bare quoted table name (for `ON CONFLICT ON CONSTRAINT`) |
139
159
  | `{{$}}` | the owning resource's quoted schema |
140
160
 
141
- ```typescript
142
- const rows = await projects.select(
143
- `SELECT p.* FROM {{}} p JOIN {{users}} u ON u.id = {{self.ownerId}} WHERE u.active = $1`,
144
- [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]
145
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
+ })
146
177
  ```
147
178
 
148
179
  An unknown alias or property raises `PostgresPlaceholderError` at parse time rather than being
@@ -150,56 +181,109 @@ substituted blindly.
150
181
 
151
182
  ## API
152
183
 
153
- ### `makePostgresResource<R, T>(alias, dbAlias?, serviceAlias?, maker?, tableName?): T`
184
+ ### `makePostgresResource<R, T>(alias, dbAlias?, serviceAlias?, tableName?): T`
154
185
 
155
186
  Creates a Postgres resource. `dbAlias` and `serviceAlias` default to `DEFAULT_DB_ALIAS`
156
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.
157
189
 
158
190
  ### `PostgresResource<T>`
159
191
 
160
- Extends `Resource<T>` with:
192
+ Extends `Resource<T>`, `LockableResource<T>` and `MigratableResource<PostgresTx, PostgresResource<T>>` with:
161
193
 
162
- - `schema?: AnySchema` — the source of truth for the table structure
163
- - `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()`)
164
196
  - `db(): Promise<PostgresDb>` / `client(): Promise<Pool>`
165
- - `index(name, spec): this` / `migration(name, apply, stage?): this` — chainable declarations
166
- - `query` / `queryOne` / `execute` — raw rows and affected counts
167
- - `select` / `selectOne` — rows marshalled back into `T`
168
- - `ref(alias?): string` — fully qualified identifier of this or another resource
169
- - `transaction(fn)` — a `PostgresTx` with the same placeholder-aware helpers
170
- - `insert` / `upsert` / `patch` / `purge` / `count` — beyond the base contract
171
- - `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
172
204
  - `getDefaults(): Partial<T>`
173
205
 
174
206
  ### `Resource<T>` methods (all implemented)
175
207
 
176
- `get`, `load`, `create`, `update`, `save`, `delete`, `pick`, `list`
177
-
178
- `create` refuses a caller-supplied id (use `insert`); `update` replaces the whole record (use `patch`
179
- to merge); `pick` deletes the record it returns, atomically.
180
-
181
- ### Errors
182
-
183
- `PostgresError` and its subclasses — `PostgresSyncError`, `PostgresCastRequired`,
184
- `PostgresConstraintError`, `PostgresForeignKeyError`, `PostgresCheckError`, `PostgresDeadlockError`,
185
- `PostgresPlaceholderError`, `PostgresConnectionError`, `PostgresBootstrapError`. Driver errors are
186
- translated by `pgErrorToResourceError`, which unwraps Drizzle's `DrizzleQueryError` to reach the
187
- `pg` error underneath and preserves the raw `code`/`detail`/`hint`/`severity`. Unique violations
188
- surface as `RecordExists` and not-null violations as `MisshapedRecord`.
208
+ `get`, `load`, `list`, `count`, `create`, `update`, `save`, `delete`, `take`, `purge`
189
209
 
190
- ### Constants
210
+ `get` and `load` take either an id or a `Criteria<T>` (with an optional `sort`), so reading one
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.
191
216
 
192
- - `DEFAULT_DB_ALIAS` — `'postgres'`
193
- - `DEFAULT_PAGE_SIZE` — `10`
194
- - `DEF_MIGRATIONS_TABLE` — `'_owlmeans_migrations'`
195
- - `PG_KEYWORD` — `'pg'`; `PG_MAX_IDENTIFIER` — `63`
196
- - `PgAutoSync`, `PgIndexMethod`, `PgReferentialAction`, `PgErrorCode`
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.
197
221
 
198
- ## Related Packages
222
+ ### Errors
199
223
 
200
- - [`@owlmeans/resource`](../resource) — `Resource<T>`, `ResourceRecord`, migrations, error family
201
- - [`@owlmeans/postgres`](../postgres) — the connection service required by this package
202
- - [`@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
203
287
 
204
288
  <!-- owlmeans:agent-guidance:start -->
205
289
  ## Agent guidance
@@ -209,7 +293,7 @@ This package ships embedded agent skills under `agent-meta/`. After installing y
209
293
  your project's skill store (`.agents/skills/`):
210
294
 
211
295
  ```sh
212
- npx @owlmeans/agent-skills
296
+ npx @owlmeans/agent-skills@^0.1.18-rc.33
213
297
  ```
214
298
 
215
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.0",
5
- "generatedAt": "2026-08-16T22:20:50.509Z",
4
+ "version": "0.1.18-rc.31",
5
+ "generatedAt": "2026-09-22T17:37:10.918Z",
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.0"` in `dependencies` (peers `pg`, `ajv`)
11
+ **Install:** `"@owlmeans/postgres-resource": "^0.1.18-rc.31"` 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?, maker?, tableName?)` | The resource factory. Aliases default to `DEFAULT_DB_ALIAS` (`'postgres'`). |
22
- | `PostgresResource<T>` | `Resource<T>` + `table`/`entity`, custom SQL, transactions, `insert`/`upsert`/`patch`/`purge`/`count`, `lock`/`unlock`. |
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, makeProjectResource
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`, nested `object` | `jsonb` |
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
- `byName` in `utils/schema.ts` collapses them and warns.
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()`: introspect → diff → apply the whole `DdlPlan` in one transaction under
98
- `pg_advisory_xact_lock`, so concurrent replicas don't race. `DbConfig.meta.autoSync`:
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` | create if absent, otherwise leave alone |
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 p.* FROM {{}} p JOIN {{users}} u ON u.id = {{self.ownerId}} WHERE u.active = $1`, [true]
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
- | `pick` | `DELETE … RETURNING`: it deletes the record it returns, atomically |
164
- | `load` | rejects `opts.ttl` with `UnsupportedArgumentError` (mongo parity) |
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** (layer-suffixed by `dbName()`); the **DATABASE** comes
179
- from `meta.database` and is never suffixed. Values starting with `/` are read as files by the
180
- existing `fileConfigReader` middleware.
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
- export declare const DEFAULT_PAGE_SIZE = 10;
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
@@ -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,KAAK,CAAA;AAEnC;;;;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"}
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
- export const DEFAULT_PAGE_SIZE = 10;
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