@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.
- package/LICENSE +21 -0
- package/README.md +219 -0
- package/agent-meta/instructions/postgres-resource.instructions.md +60 -0
- package/agent-meta/manifest.json +23 -0
- package/agent-meta/skills/postgres-resource/SKILL.md +186 -0
- package/build/consts.d.ts +81 -0
- package/build/consts.d.ts.map +1 -0
- package/build/consts.js +86 -0
- package/build/consts.js.map +1 -0
- package/build/declarations.d.ts +12 -0
- package/build/declarations.d.ts.map +1 -0
- package/build/declarations.js +28 -0
- package/build/declarations.js.map +1 -0
- package/build/errors.d.ts +70 -0
- package/build/errors.d.ts.map +1 -0
- package/build/errors.js +201 -0
- package/build/errors.js.map +1 -0
- package/build/helper.d.ts +4 -0
- package/build/helper.d.ts.map +1 -0
- package/build/helper.js +5 -0
- package/build/helper.js.map +1 -0
- package/build/index.d.ts +8 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +7 -0
- package/build/index.js.map +1 -0
- package/build/resource.d.ts +4 -0
- package/build/resource.d.ts.map +1 -0
- package/build/resource.js +395 -0
- package/build/resource.js.map +1 -0
- package/build/types.d.ts +273 -0
- package/build/types.d.ts.map +1 -0
- package/build/types.js +2 -0
- package/build/types.js.map +1 -0
- package/build/utils/criteria.d.ts +24 -0
- package/build/utils/criteria.d.ts.map +1 -0
- package/build/utils/criteria.js +209 -0
- package/build/utils/criteria.js.map +1 -0
- package/build/utils/diff.d.ts +24 -0
- package/build/utils/diff.d.ts.map +1 -0
- package/build/utils/diff.js +330 -0
- package/build/utils/diff.js.map +1 -0
- package/build/utils/index.d.ts +12 -0
- package/build/utils/index.d.ts.map +1 -0
- package/build/utils/index.js +12 -0
- package/build/utils/index.js.map +1 -0
- package/build/utils/introspect.d.ts +7 -0
- package/build/utils/introspect.d.ts.map +1 -0
- package/build/utils/introspect.js +70 -0
- package/build/utils/introspect.js.map +1 -0
- package/build/utils/life-cycle.d.ts +39 -0
- package/build/utils/life-cycle.d.ts.map +1 -0
- package/build/utils/life-cycle.js +119 -0
- package/build/utils/life-cycle.js.map +1 -0
- package/build/utils/marshal.d.ts +36 -0
- package/build/utils/marshal.d.ts.map +1 -0
- package/build/utils/marshal.js +152 -0
- package/build/utils/marshal.js.map +1 -0
- package/build/utils/migrations.d.ts +16 -0
- package/build/utils/migrations.d.ts.map +1 -0
- package/build/utils/migrations.js +108 -0
- package/build/utils/migrations.js.map +1 -0
- package/build/utils/name.d.ts +39 -0
- package/build/utils/name.d.ts.map +1 -0
- package/build/utils/name.js +63 -0
- package/build/utils/name.js.map +1 -0
- package/build/utils/schema.d.ts +28 -0
- package/build/utils/schema.d.ts.map +1 -0
- package/build/utils/schema.js +356 -0
- package/build/utils/schema.js.map +1 -0
- package/build/utils/sql.d.ts +22 -0
- package/build/utils/sql.d.ts.map +1 -0
- package/build/utils/sql.js +0 -0
- package/build/utils/sql.js.map +1 -0
- package/build/utils/sync.d.ts +21 -0
- package/build/utils/sync.d.ts.map +1 -0
- package/build/utils/sync.js +93 -0
- package/build/utils/sync.js.map +1 -0
- package/build/utils/table.d.ts +8 -0
- package/build/utils/table.d.ts.map +1 -0
- package/build/utils/table.js +68 -0
- package/build/utils/table.js.map +1 -0
- package/package.json +50 -0
- package/src/consts.ts +95 -0
- package/src/declarations.ts +41 -0
- package/src/errors.ts +242 -0
- package/src/helper.ts +7 -0
- package/src/index.ts +7 -0
- package/src/resource.ts +524 -0
- package/src/types.ts +310 -0
- package/src/utils/criteria.ts +246 -0
- package/src/utils/diff.ts +363 -0
- package/src/utils/index.ts +11 -0
- package/src/utils/introspect.ts +87 -0
- package/src/utils/life-cycle.ts +155 -0
- package/src/utils/marshal.ts +175 -0
- package/src/utils/migrations.ts +135 -0
- package/src/utils/name.ts +79 -0
- package/src/utils/schema.ts +426 -0
- package/src/utils/sql.ts +0 -0
- package/src/utils/sync.ts +106 -0
- package/src/utils/table.ts +76 -0
- package/tests/errors.spec.ts +121 -0
- package/tests/name.spec.ts +75 -0
- package/tests/schema.spec.ts +258 -0
- package/tests/sql.spec.ts +104 -0
- 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"}
|