@owlmeans/postgres-resource 0.1.15

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 (106) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +219 -0
  3. package/agent-meta/instructions/postgres-resource.instructions.md +60 -0
  4. package/agent-meta/manifest.json +23 -0
  5. package/agent-meta/skills/postgres-resource/SKILL.md +186 -0
  6. package/build/consts.d.ts +81 -0
  7. package/build/consts.d.ts.map +1 -0
  8. package/build/consts.js +86 -0
  9. package/build/consts.js.map +1 -0
  10. package/build/declarations.d.ts +12 -0
  11. package/build/declarations.d.ts.map +1 -0
  12. package/build/declarations.js +28 -0
  13. package/build/declarations.js.map +1 -0
  14. package/build/errors.d.ts +70 -0
  15. package/build/errors.d.ts.map +1 -0
  16. package/build/errors.js +201 -0
  17. package/build/errors.js.map +1 -0
  18. package/build/helper.d.ts +4 -0
  19. package/build/helper.d.ts.map +1 -0
  20. package/build/helper.js +5 -0
  21. package/build/helper.js.map +1 -0
  22. package/build/index.d.ts +8 -0
  23. package/build/index.d.ts.map +1 -0
  24. package/build/index.js +7 -0
  25. package/build/index.js.map +1 -0
  26. package/build/resource.d.ts +4 -0
  27. package/build/resource.d.ts.map +1 -0
  28. package/build/resource.js +395 -0
  29. package/build/resource.js.map +1 -0
  30. package/build/types.d.ts +273 -0
  31. package/build/types.d.ts.map +1 -0
  32. package/build/types.js +2 -0
  33. package/build/types.js.map +1 -0
  34. package/build/utils/criteria.d.ts +24 -0
  35. package/build/utils/criteria.d.ts.map +1 -0
  36. package/build/utils/criteria.js +209 -0
  37. package/build/utils/criteria.js.map +1 -0
  38. package/build/utils/diff.d.ts +24 -0
  39. package/build/utils/diff.d.ts.map +1 -0
  40. package/build/utils/diff.js +330 -0
  41. package/build/utils/diff.js.map +1 -0
  42. package/build/utils/index.d.ts +12 -0
  43. package/build/utils/index.d.ts.map +1 -0
  44. package/build/utils/index.js +12 -0
  45. package/build/utils/index.js.map +1 -0
  46. package/build/utils/introspect.d.ts +7 -0
  47. package/build/utils/introspect.d.ts.map +1 -0
  48. package/build/utils/introspect.js +70 -0
  49. package/build/utils/introspect.js.map +1 -0
  50. package/build/utils/life-cycle.d.ts +39 -0
  51. package/build/utils/life-cycle.d.ts.map +1 -0
  52. package/build/utils/life-cycle.js +119 -0
  53. package/build/utils/life-cycle.js.map +1 -0
  54. package/build/utils/marshal.d.ts +36 -0
  55. package/build/utils/marshal.d.ts.map +1 -0
  56. package/build/utils/marshal.js +152 -0
  57. package/build/utils/marshal.js.map +1 -0
  58. package/build/utils/migrations.d.ts +16 -0
  59. package/build/utils/migrations.d.ts.map +1 -0
  60. package/build/utils/migrations.js +108 -0
  61. package/build/utils/migrations.js.map +1 -0
  62. package/build/utils/name.d.ts +39 -0
  63. package/build/utils/name.d.ts.map +1 -0
  64. package/build/utils/name.js +63 -0
  65. package/build/utils/name.js.map +1 -0
  66. package/build/utils/schema.d.ts +28 -0
  67. package/build/utils/schema.d.ts.map +1 -0
  68. package/build/utils/schema.js +356 -0
  69. package/build/utils/schema.js.map +1 -0
  70. package/build/utils/sql.d.ts +22 -0
  71. package/build/utils/sql.d.ts.map +1 -0
  72. package/build/utils/sql.js +0 -0
  73. package/build/utils/sql.js.map +1 -0
  74. package/build/utils/sync.d.ts +21 -0
  75. package/build/utils/sync.d.ts.map +1 -0
  76. package/build/utils/sync.js +93 -0
  77. package/build/utils/sync.js.map +1 -0
  78. package/build/utils/table.d.ts +8 -0
  79. package/build/utils/table.d.ts.map +1 -0
  80. package/build/utils/table.js +68 -0
  81. package/build/utils/table.js.map +1 -0
  82. package/package.json +50 -0
  83. package/src/consts.ts +95 -0
  84. package/src/declarations.ts +41 -0
  85. package/src/errors.ts +242 -0
  86. package/src/helper.ts +7 -0
  87. package/src/index.ts +7 -0
  88. package/src/resource.ts +524 -0
  89. package/src/types.ts +310 -0
  90. package/src/utils/criteria.ts +246 -0
  91. package/src/utils/diff.ts +363 -0
  92. package/src/utils/index.ts +11 -0
  93. package/src/utils/introspect.ts +87 -0
  94. package/src/utils/life-cycle.ts +155 -0
  95. package/src/utils/marshal.ts +175 -0
  96. package/src/utils/migrations.ts +135 -0
  97. package/src/utils/name.ts +79 -0
  98. package/src/utils/schema.ts +426 -0
  99. package/src/utils/sql.ts +0 -0
  100. package/src/utils/sync.ts +106 -0
  101. package/src/utils/table.ts +76 -0
  102. package/tests/errors.spec.ts +121 -0
  103. package/tests/name.spec.ts +75 -0
  104. package/tests/schema.spec.ts +258 -0
  105. package/tests/sql.spec.ts +104 -0
  106. package/tsconfig.json +16 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 OwlMeans Common — Fullstack typescript framework
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,219 @@
1
+ # @owlmeans/postgres-resource
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
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ bun add @owlmeans/postgres-resource @owlmeans/postgres pg
20
+ ```
21
+
22
+ `pg` and `ajv` are peer dependencies of this package. `@owlmeans/postgres` provides the connection
23
+ service this package resolves through.
24
+
25
+ ## Usage
26
+
27
+ Define a resource:
28
+
29
+ ```typescript
30
+ import { makePostgresResource } from '@owlmeans/postgres-resource'
31
+ import type { PostgresResource } from '@owlmeans/postgres-resource'
32
+ import type { ResourceMaker } from '@owlmeans/resource'
33
+
34
+ export interface ProjectResource extends PostgresResource<ProjectRecord> {}
35
+
36
+ export const makeProjectResource: ResourceMaker<ProjectRecord, ProjectResource> = (dbAlias, serviceAlias) => {
37
+ const resource = makePostgresResource<ProjectRecord, ProjectResource>(
38
+ RES_PROJECT, dbAlias, serviceAlias, makeProjectResource
39
+ )
40
+ resource.schema = ProjectSchema
41
+ resource.index('idx_project_entity', { columns: ['entityId'] })
42
+
43
+ return resource
44
+ }
45
+ ```
46
+
47
+ Register in context:
48
+
49
+ ```typescript
50
+ context.registerResource(makeProjectResource())
51
+ ```
52
+
53
+ Use in a handler:
54
+
55
+ ```typescript
56
+ const projects = context.resource<ProjectResource>(RES_PROJECT)
57
+ const record = await projects.create({ entityId, alias, title })
58
+ const list = await projects.list({ criteria: { entityId } })
59
+ ```
60
+
61
+ ### Schema → table
62
+
63
+ ```typescript
64
+ const ProjectSchema: JSONSchemaType<ProjectRecord> = {
65
+ type: 'object',
66
+ 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
72
+ },
73
+ required: ['id', 'email'],
74
+ pg: {
75
+ table: 'app_projects',
76
+ indexes: [{ name: 'idx_owner_created', columns: ['ownerId', 'createdAt'] }]
77
+ }
78
+ }
79
+ ```
80
+
81
+ | JSON Schema | Postgres |
82
+ |---|---|
83
+ | `string` | `text` |
84
+ | `string` + `format: 'uuid'` | `uuid` |
85
+ | `DateSchema` (`{type:'object', format:'date-time'}`) | `timestamptz` |
86
+ | `integer` / `number` / `boolean` | `integer` / `double precision` / `boolean` |
87
+ | `array`, nested `object` | `jsonb` |
88
+ | string `enum` | `text` + `CHECK` |
89
+ | `nullable: true` | nullable column |
90
+ | in `required[]` | `NOT NULL` |
91
+ | `secure: true` | ciphertext column, `lock`/`unlock` aware |
92
+ | `id` property | primary key, `gen_random_uuid()::text` default |
93
+
94
+ AJV in strict mode rejects unknown keywords — register `pgKeyword` to allow `pg:`:
95
+
96
+ ```typescript
97
+ ajv.addKeyword(pgKeyword)
98
+ ```
99
+
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 |
111
+
112
+ A cast Postgres cannot perform raises `PostgresCastRequired` rather than truncating data. Columns
113
+ listed in `pg.unmanaged` are outside reconciliation's authority entirely.
114
+
115
+ ### Migrations
116
+
117
+ ```typescript
118
+ resource.migration('0001-backfill-slug', async tx => {
119
+ await tx.execute(`UPDATE {{}} SET slug = lower(title) WHERE slug IS NULL`)
120
+ }, MigrationStage.Post)
121
+ ```
122
+
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
+ 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()`.
128
+
129
+ ### Custom SQL
130
+
131
+ Placeholders resolve **identifiers only** — values stay in `params` and are bound as `$1..$n`:
132
+
133
+ | Placeholder | Resolves to |
134
+ |---|---|
135
+ | `{{}}` / `{{self}}` | the owning resource's `"schema"."table"` |
136
+ | `{{alias}}` | another registered Postgres resource's qualified table |
137
+ | `{{alias.property}}` | that resource's qualified column |
138
+ | `{{#alias}}` | the bare quoted table name (for `ON CONFLICT ON CONSTRAINT`) |
139
+ | `{{$}}` | the owning resource's quoted schema |
140
+
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]
145
+ )
146
+ ```
147
+
148
+ An unknown alias or property raises `PostgresPlaceholderError` at parse time rather than being
149
+ substituted blindly.
150
+
151
+ ## API
152
+
153
+ ### `makePostgresResource<R, T>(alias, dbAlias?, serviceAlias?, maker?, tableName?): T`
154
+
155
+ Creates a Postgres resource. `dbAlias` and `serviceAlias` default to `DEFAULT_DB_ALIAS`
156
+ (`'postgres'`). `tableName` overrides the physical table name, which otherwise derives from the alias.
157
+
158
+ ### `PostgresResource<T>`
159
+
160
+ Extends `Resource<T>` with:
161
+
162
+ - `schema?: AnySchema` — the source of truth for the table structure
163
+ - `table: TableSpec` / `entity: PgRuntimeTable` — the compiled spec and the runtime Drizzle table
164
+ - `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
172
+ - `getDefaults(): Partial<T>`
173
+
174
+ ### `Resource<T>` methods (all implemented)
175
+
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`.
189
+
190
+ ### Constants
191
+
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`
197
+
198
+ ## Related Packages
199
+
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
203
+
204
+ <!-- owlmeans:agent-guidance:start -->
205
+ ## Agent guidance
206
+
207
+ This package ships embedded Claude Code skills and GitHub Copilot instructions under
208
+ `agent-meta/`. After installing your `@owlmeans/*` packages, run the OwlMeans
209
+ agent-skills installer to place them into your project's native locations
210
+ (`.claude/skills/` and `.github/instructions/`):
211
+
212
+ ```sh
213
+ npx @owlmeans/agent-skills
214
+ ```
215
+
216
+ The embedded files are version-matched to this package release. Do not edit them
217
+ directly — they are regenerated on each publish. To contribute guidance edits,
218
+ open a PR against the source monorepo.
219
+ <!-- owlmeans:agent-guidance:end -->
@@ -0,0 +1,60 @@
1
+ ---
2
+ description: "How to use @owlmeans/postgres-resource — PostgreSQL-backed Resource implementation. The AJV schema is the single source of truth for the table; structure reconciliation, code migrations, and {{alias}} custom SQL."
3
+ applyTo: "**/*.ts, **/*.tsx"
4
+ ---
5
+ <!-- AUTO-GENERATED — do not edit. Regenerate via sync-agent-meta. -->
6
+
7
+ # @owlmeans/postgres-resource
8
+
9
+ **Layer:** Infra
10
+ **Install:** `"@owlmeans/postgres-resource": "^0.1.15"` in `dependencies` (peers `pg`, `ajv`)
11
+
12
+ ## Key Exports
13
+
14
+ | Export | Description |
15
+ |--------|-------------|
16
+ | `makePostgresResource<R, T>(alias, dbAlias?, serviceAlias?, maker?, tableName?)` | Resource factory; aliases default to `'postgres'` |
17
+ | `PostgresResource<T>`, `PostgresTx`, `PostgresDb`, `TableSpec` | Resource, transaction, db handle and compiled table types |
18
+ | `pgKeyword` | `{ keyword: 'pg', valid: true }` — register when AJV runs in strict mode |
19
+ | `pgErrorToResourceError`, `PostgresError` family | Driver-error translation |
20
+ | `PgAutoSync`, `PgErrorCode`, `DEF_MIGRATIONS_TABLE`, `DEFAULT_DB_ALIAS` | Constants |
21
+
22
+ ## Usage
23
+
24
+ ```typescript
25
+ const resource = makePostgresResource<ProjectRecord, ProjectResource>(RES_PROJECT, dbAlias, serviceAlias, maker)
26
+ resource.schema = ProjectSchema
27
+ resource.index('idx_project_entity', { columns: ['entityId'] })
28
+ context.registerResource(resource)
29
+ ```
30
+
31
+ ## Rules
32
+
33
+ - **The AJV schema is the table.** Never call `pgTable`/`pgSchema`, never call `drizzle()`, never run
34
+ `drizzle-kit`, never hand-write `CREATE TABLE`. Two owners of the same DDL means reconciliation
35
+ drops what the other owner added.
36
+ - Express what JSON Schema can't through the `pg:` keyword — per property (`type`, `length`,
37
+ `unique`, `index`, `references`, `check`, `managed`, …) and at the root (`table`, `indexes`,
38
+ `unmanaged`, `autoSync`, …).
39
+ - `DbConfig.meta.autoSync` defaults to `Full`, which **DROPs** undeclared columns. Adopt a
40
+ pre-existing table with `Additive` first, confirm an empty plan, then flip to `Full`. Use
41
+ `pg.unmanaged` for columns reconciliation must never touch.
42
+ - Thread `nullable` at **every** recursion level of the type mapper — `date-time` and optional nested
43
+ objects are where the Mongo mapper broke twice.
44
+ - Migration `Pre` runs before the structure sync (the only place to rescue data a drop would lose),
45
+ `Post` after. Migrations on a freshly created table are baselined, not executed. Keep bodies at
46
+ **module scope** — the checksum fingerprints the function's source text.
47
+ - Custom SQL interpolates **identifiers only**: `{{}}`/`{{self}}`, `{{alias}}`, `{{alias.property}}`,
48
+ `{{#alias}}`, `{{$}}`. Values stay in `params` as `$1..$n`.
49
+ - `{{alias}}` needs the target resource **initialized** — unlike Mongo's `ref`, which is a pure
50
+ function of config. For foreign keys use `service.defer()`; don't reorder registrations.
51
+ - `create` refuses a caller-supplied id (use `insert`); `update` replaces the whole record (use
52
+ `patch`); `pick` deletes the record it returns; `load` rejects `opts.ttl`.
53
+ - Drizzle wraps driver errors in `DrizzleQueryError` with the `pg` error on `cause` — classify
54
+ through `pgErrorToResourceError`, never by reading `error.code` off the outer error.
55
+ - `DbConfig.schema` is the Postgres SCHEMA; the DATABASE is `meta.database`.
56
+ - Unit specs live here; specs building a real `ServerContext` live in `@owlmeans/postgres`.
57
+
58
+ ## Depends On
59
+
60
+ - `@owlmeans/resource`, `@owlmeans/context`, `@owlmeans/server-context`, `drizzle-orm`, peer `pg`/`ajv`
@@ -0,0 +1,23 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "package": "@owlmeans/postgres-resource",
4
+ "version": "0.1.15",
5
+ "generatedAt": "2026-08-07T18:11:56.992Z",
6
+ "canonicalRepo": "https://github.com/owlmeans/common",
7
+ "entries": [
8
+ {
9
+ "kind": "skill",
10
+ "name": "postgres-resource",
11
+ "category": "package-specific",
12
+ "file": "skills/postgres-resource/SKILL.md",
13
+ "canonicalPath": ".claude/skills/postgres-resource/SKILL.md"
14
+ },
15
+ {
16
+ "kind": "instruction",
17
+ "name": "postgres-resource",
18
+ "category": "package-specific",
19
+ "file": "instructions/postgres-resource.instructions.md",
20
+ "canonicalPath": ".github/instructions/postgres-resource.instructions.md"
21
+ }
22
+ ]
23
+ }
@@ -0,0 +1,186 @@
1
+ ---
2
+ name: postgres-resource
3
+ description: How to use @owlmeans/postgres-resource — PostgreSQL-backed Resource implementation. The AJV schema is the single source of truth for the table; structure reconciliation, code migrations, and {{alias}} custom SQL. Auto-invoked when defining a resource backed by PostgreSQL.
4
+ user-invocable: false
5
+ ---
6
+ <!-- AUTO-GENERATED — do not edit. Regenerate via sync-agent-meta. -->
7
+
8
+ # @owlmeans/postgres-resource
9
+
10
+ **Layer:** Infra
11
+ **Install:** `"@owlmeans/postgres-resource": "^0.1.15"` in `dependencies` (peers `pg`, `ajv`)
12
+
13
+ The Postgres counterpart of [[mongo-resource]]. The difference that governs everything else: a
14
+ Mongo collection has no structure, a Postgres table does — so **the resource layer owns the DDL**
15
+ and derives it from the resource's AJV schema.
16
+
17
+ ## Key Exports
18
+
19
+ | Export | Description |
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. |
24
+ | `TableSpec`, `ColumnSpec`, `PgPropertyOverride`, `PgRootOverride`, `DdlPlan` | The compiled table description and the `pg:` vocabulary types. |
25
+ | `pgKeyword` | `{ keyword: 'pg', valid: true }` — register it when running AJV in strict mode. |
26
+ | `schemaToTableSpec`, `pgTableName`, `pgIdentifier`, `quoteIdent`, `qualify`, `advisoryKey` | The compiler and identifier helpers. |
27
+ | `refOf`, `resolvePlaceholders` | `{{alias}}` resolution — identifiers only. |
28
+ | `PostgresError` family, `pgErrorToResourceError`, `describePgError` | Driver-error translation. |
29
+ | `getDeclaration`, `resetDeclarations` | Module-scope index/migration declarations, keyed by alias. |
30
+ | `DEFAULT_DB_ALIAS`, `DEFAULT_PAGE_SIZE`, `DEF_MIGRATIONS_TABLE`, `PgAutoSync`, `PgIndexMethod`, `PgReferentialAction`, `PgErrorCode` | Constants. |
31
+
32
+ ## The schema is the table — never write DDL
33
+
34
+ ```typescript
35
+ export const makeProjectResource: ResourceMaker<ProjectRecord, ProjectResource> = (dbAlias, serviceAlias) => {
36
+ const resource = makePostgresResource<ProjectRecord, ProjectResource>(
37
+ RES_PROJECT, dbAlias, serviceAlias, makeProjectResource
38
+ )
39
+ resource.schema = ProjectSchema
40
+ resource.index('idx_project_entity', { columns: ['entityId'] })
41
+
42
+ return resource
43
+ }
44
+ context.registerResource(makeProjectResource())
45
+ ```
46
+
47
+ Never call `pgTable`/`pgSchema`, never call `drizzle()`, never run `drizzle-kit`, never hand-write
48
+ `CREATE TABLE`. Two owners of the same DDL is the failure mode this package exists to remove:
49
+ reconciliation would drop what the other owner added.
50
+
51
+ | JSON Schema | Postgres |
52
+ |---|---|
53
+ | `string` · `string`+`format:'uuid'` | `text` · `uuid` |
54
+ | `DateSchema` (`{type:'object', format:'date-time'}`) | `timestamptz` |
55
+ | `integer` · `number` · `boolean` | `integer` · `double precision` · `boolean` |
56
+ | `array`, nested `object` | `jsonb` |
57
+ | string `enum` | `text` + `CHECK` |
58
+ | `nullable: true` · in `required[]` | nullable · `NOT NULL` |
59
+ | `secure: true` | ciphertext column, `lock`/`unlock` aware |
60
+ | `id` property | primary key, `gen_random_uuid()::text` default |
61
+
62
+ **Thread `nullable` at every recursion level.** Mongo's mapper shipped this bug twice in exactly the
63
+ same two places — `date-time` and optional nested objects. Cover both when you touch the mapper.
64
+
65
+ ## The `pg:` override vocabulary
66
+
67
+ For what JSON Schema can't say. Per property: `column`, `type`, `length`, `precision`, `scale`,
68
+ `nullable`, `default`/`defaultRaw`, `primaryKey`, `unique`, `index`, `references`, `jsonb`, `array`,
69
+ `check`, `using`, `managed`, `comment`. At the root: `table`, `schema`, `primaryKey`, `unique`,
70
+ `indexes`, `checks`, `unmanaged`, `autoSync`, `comment`.
71
+
72
+ ```typescript
73
+ {
74
+ type: 'object',
75
+ properties: {
76
+ email: { type: 'string', pg: { type: 'varchar', length: 320, unique: true } },
77
+ ownerId: { type: 'string', pg: { references: { resource: 'users', onDelete: 'cascade' } } }
78
+ },
79
+ required: ['email'],
80
+ pg: { indexes: [{ name: 'idx_owner_created', columns: ['ownerId', 'createdAt'] }] }
81
+ }
82
+ ```
83
+
84
+ The compiler reads the raw schema object and never validates through AJV, so `pg:` costs nothing at
85
+ runtime — but a consumer compiling that schema in **strict mode** must `ajv.addKeyword(pgKeyword)`.
86
+
87
+ ## Reconciliation is authoritative — `PgAutoSync`
88
+
89
+ At `init()`: introspect → diff → apply the whole `DdlPlan` in one transaction under
90
+ `pg_advisory_xact_lock`, so concurrent replicas don't race. `DbConfig.meta.autoSync`:
91
+
92
+ | Value | Behaviour |
93
+ |---|---|
94
+ | `Full` (default) | add / retype+backfill / drop columns, reconcile indexes and constraints |
95
+ | `Additive` | add only — never retypes, never drops |
96
+ | `Off` | create if absent, otherwise leave alone |
97
+
98
+ **`Full` DROPs columns the schema doesn't declare.** Adopting a table this package didn't create:
99
+ boot once with `Additive`, confirm the plan comes out empty, then flip to `Full`. Columns listed in
100
+ `pg.unmanaged` stay outside reconciliation's authority permanently. A cast Postgres cannot perform
101
+ raises `PostgresCastRequired` instead of truncating.
102
+
103
+ ## Migrations bracket the sync
104
+
105
+ ```typescript
106
+ resource.migration('0001-rescue-legacy', async tx => {
107
+ await tx.execute(`UPDATE {{}} SET slug = legacy WHERE slug IS NULL`)
108
+ }, MigrationStage.Pre)
109
+ ```
110
+
111
+ - `Pre` runs **before** the table is reshaped — the only place to rescue data reconciliation is
112
+ about to drop. `Post` runs **after**, so it can use the new columns.
113
+ - On a table this package just created, every declared migration is **baselined** (recorded, not
114
+ executed) — a fresh table is already at head.
115
+ - Applied once, in registration order, each in its own transaction, ledgered in
116
+ `_owlmeans_migrations` in the same Postgres schema.
117
+ - The checksum fingerprints the function's **source text**. Keep bodies at module scope; a body
118
+ closed over a loop variable fingerprints the wrapper and drifts. An edited applied body raises
119
+ `MigrationConflict`; a throwing one raises `MigrationError` and aborts `init()`.
120
+
121
+ ## Custom SQL: identifiers interpolate, values never
122
+
123
+ | Placeholder | Resolves to |
124
+ |---|---|
125
+ | `{{}}` / `{{self}}` | the owning resource's `"schema"."table"` |
126
+ | `{{alias}}` | another registered Postgres resource's qualified table |
127
+ | `{{alias.property}}` | that resource's qualified column |
128
+ | `{{#alias}}` | the bare quoted table name (`ON CONFLICT ON CONSTRAINT`) |
129
+ | `{{$}}` | the owning resource's quoted schema |
130
+
131
+ ```typescript
132
+ await projects.select(
133
+ `SELECT p.* FROM {{}} p JOIN {{users}} u ON u.id = {{self.ownerId}} WHERE u.active = $1`, [true]
134
+ )
135
+ ```
136
+
137
+ Postgres cannot bind an identifier as a parameter — that is the entire reason this mechanism exists.
138
+ Values have no such excuse: they stay in `params` as `$1..$n`. An unknown alias or property raises
139
+ `PostgresPlaceholderError` at parse time.
140
+
141
+ **Registration order matters here, unlike Mongo.** `{{alias}}` reads the other resource's
142
+ *initialized* `table` spec, where mongo's `ref` derives a collection name from config alone. A
143
+ foreign key whose target hasn't initialized is queued with `service.defer()` and drained by the
144
+ middleware `appendPostgres` installs — use that rather than reordering registrations.
145
+
146
+ ## Method semantics that differ from the base contract
147
+
148
+ | Method | Semantics |
149
+ |---|---|
150
+ | `create` | refuses a caller-supplied id (`RecordExists`) — use `insert` |
151
+ | `update` | **replaces** the whole record — use `patch` to merge |
152
+ | `pick` | `DELETE … RETURNING`: it deletes the record it returns, atomically |
153
+ | `load` | rejects `opts.ttl` with `UnsupportedArgumentError` (mongo parity) |
154
+ | `upsert` | `INSERT … ON CONFLICT DO UPDATE`, conflicting on the primary key by default |
155
+ | `select`/`selectOne` | custom SQL marshalled back into `T`; `query`/`queryOne` return raw rows |
156
+
157
+ ## Errors
158
+
159
+ Drizzle raises `DrizzleQueryError` and hangs the `pg` error off `cause`, so the driver `code` is not
160
+ at the top level. `pgErrorToResourceError` unwraps (bounded — `cause` chains can be circular) before
161
+ classifying, and keeps the original wrapper as `cause`. Unique violation → `RecordExists`, not-null →
162
+ `MisshapedRecord`, plus the `Postgres*Error` family. Raw `code`/`detail`/`hint`/`severity` survive
163
+ translation — consumers classify retryable DDL races on `42P01`/`42703`.
164
+
165
+ ## Config
166
+
167
+ `DbConfig.schema` is the Postgres **SCHEMA** (layer-suffixed by `dbName()`); the **DATABASE** comes
168
+ from `meta.database` and is never suffixed. Values starting with `/` are read as files by the
169
+ existing `fileConfigReader` middleware.
170
+
171
+ ## Tests
172
+
173
+ `bun test ./tests` in the package — unit specs (schema compilation, identifiers, placeholder
174
+ resolution, error translation), no gate, no service. Specs that build a real `ServerContext` live in
175
+ `@owlmeans/postgres` instead: a devDependency here on its own dependent is a cycle. See
176
+ [[testing-integration]].
177
+
178
+ ## Depends On
179
+
180
+ - `@owlmeans/resource` · `@owlmeans/context` · `@owlmeans/server-context` · `@owlmeans/basic-ids`
181
+ - `drizzle-orm` (internal query builder) · peer `pg`, `ajv`
182
+
183
+ ## Related
184
+
185
+ - [[postgres]] — the connection service this resolves through
186
+ - [[resource]] — `Resource<T>`, migrations, the error family · [[mongo-resource]] — the Mongo counterpart
@@ -0,0 +1,81 @@
1
+ export declare const DEFAULT_DB_ALIAS = "postgres";
2
+ export declare const DEFAULT_PAGE_SIZE = 10;
3
+ /**
4
+ * Table that records which code-registered migrations have already been applied.
5
+ * Lives in the same Postgres schema as the resources it tracks, so dropping the
6
+ * schema drops the ledger with it.
7
+ */
8
+ export declare const DEF_MIGRATIONS_TABLE = "_owlmeans_migrations";
9
+ /** JSON Schema keyword carrying the Postgres specific overrides. */
10
+ export declare const PG_KEYWORD = "pg";
11
+ /** Postgres `NAMEDATALEN - 1`. Identifiers past this are silently truncated by the server. */
12
+ export declare const PG_MAX_IDENTIFIER = 63;
13
+ /** Property name that carries the record identity across every OwlMeans resource. */
14
+ export declare const ID_FIELD = "id";
15
+ /** Fallback when a scalar property can't be mapped to anything more specific. */
16
+ export declare const DEF_SQL_TYPE = "text";
17
+ export declare const DEF_JSON_TYPE = "jsonb";
18
+ /** Server side identity default for the implicit `id` primary key. */
19
+ export declare const DEF_ID_DEFAULT = "gen_random_uuid()::text";
20
+ /**
21
+ * How far structure reconciliation is allowed to go.
22
+ *
23
+ * `additive` is the adoption path for a table this package didn't create: it adds what's
24
+ * missing but never retypes and never drops, so a first boot against an existing schema
25
+ * converges without touching data. Flip to `full` once the plan comes out empty.
26
+ */
27
+ export declare enum PgAutoSync {
28
+ Full = "full",
29
+ Additive = "additive",
30
+ Off = "off"
31
+ }
32
+ export declare enum PgIndexMethod {
33
+ BTree = "btree",
34
+ Hash = "hash",
35
+ Gin = "gin",
36
+ Gist = "gist",
37
+ Brin = "brin",
38
+ SpGist = "spgist"
39
+ }
40
+ export declare enum PgReferentialAction {
41
+ NoAction = "no action",
42
+ Restrict = "restrict",
43
+ Cascade = "cascade",
44
+ SetNull = "set null",
45
+ SetDefault = "set default"
46
+ }
47
+ /**
48
+ * Postgres error codes the resource layer translates into framework errors. Sourced
49
+ * from the `pg` driver's `DatabaseError.code`.
50
+ */
51
+ export declare enum PgErrorCode {
52
+ UniqueViolation = "23505",
53
+ ForeignKeyViolation = "23503",
54
+ NotNullViolation = "23502",
55
+ CheckViolation = "23514",
56
+ UndefinedTable = "42P01",
57
+ UndefinedColumn = "42703",
58
+ DuplicateTable = "42P07",
59
+ DuplicateColumn = "42701",
60
+ DuplicateObject = "42710",
61
+ CannotCoerce = "42846",
62
+ DatatypeMismatch = "42804",
63
+ InvalidTextRepresentation = "22P02",
64
+ StringDataRightTruncation = "22001",
65
+ NumericValueOutOfRange = "22003",
66
+ SerializationFailure = "40001",
67
+ DeadlockDetected = "40P01",
68
+ NoActiveTransaction = "25001"
69
+ }
70
+ /**
71
+ * Postgres reports scalar values of these types as strings to avoid precision loss.
72
+ * The marshaller converts them back using the resource's schema.
73
+ */
74
+ export declare const STRING_RETURNING_TYPES: readonly ["numeric", "bigint", "int8", "decimal", "money"];
75
+ /** Built-in type OIDs the marshaller has to name to reach the driver's own parsers. */
76
+ export declare enum PgTypeOid {
77
+ Date = 1082,
78
+ Timestamp = 1114,
79
+ TimestampTz = 1184
80
+ }
81
+ //# sourceMappingURL=consts.d.ts.map
@@ -0,0 +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,4DAA6D,CAAA;AAEhG,uFAAuF;AACvF,oBAAY,SAAS;IACnB,IAAI,OAAO;IACX,SAAS,OAAO;IAChB,WAAW,OAAO;CACnB"}