@owlmeans/postgres 0.1.18-rc.36 → 0.1.18-rc.38
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/postgres/SKILL.md +1 -1
- package/package.json +3 -3
- package/tests/criteria.spec.ts +107 -0
- package/tests/sync.spec.ts +91 -1
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ provisioning, and an opt-in least-privilege bootstrap path.
|
|
|
14
14
|
## Installation
|
|
15
15
|
|
|
16
16
|
```bash
|
|
17
|
-
bun add @owlmeans/postgres@^0.1.18-rc.
|
|
17
|
+
bun add @owlmeans/postgres@^0.1.18-rc.38 @owlmeans/postgres-resource@^0.1.18-rc.37
|
|
18
18
|
```
|
|
19
19
|
|
|
20
20
|
## Usage
|
|
@@ -129,7 +129,7 @@ This package ships embedded agent skills under `agent-meta/`. After installing y
|
|
|
129
129
|
your project's skill store (`.agents/skills/`):
|
|
130
130
|
|
|
131
131
|
```sh
|
|
132
|
-
npx @owlmeans/agent-skills@^0.1.18-rc.
|
|
132
|
+
npx @owlmeans/agent-skills@^0.1.18-rc.41
|
|
133
133
|
```
|
|
134
134
|
|
|
135
135
|
The embedded files are version-matched to this package release. Do not edit them
|
package/agent-meta/manifest.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 2,
|
|
3
3
|
"package": "@owlmeans/postgres",
|
|
4
|
-
"version": "0.1.18-rc.
|
|
5
|
-
"generatedAt": "2026-09-
|
|
4
|
+
"version": "0.1.18-rc.38",
|
|
5
|
+
"generatedAt": "2026-09-28T02:06:37.894Z",
|
|
6
6
|
"canonicalRepo": "https://github.com/owlmeans/common",
|
|
7
7
|
"entries": [
|
|
8
8
|
{
|
|
@@ -8,7 +8,7 @@ user-invocable: false
|
|
|
8
8
|
# @owlmeans/postgres
|
|
9
9
|
|
|
10
10
|
**Layer:** Infra
|
|
11
|
-
**Install:** `"@owlmeans/postgres": "^0.1.18-rc.
|
|
11
|
+
**Install:** `"@owlmeans/postgres": "^0.1.18-rc.38"` in `dependencies`
|
|
12
12
|
|
|
13
13
|
Pooled `pg` connections for a server context, plus the admin path that provisions the role, database
|
|
14
14
|
and schema an app connects with. All DDL for application tables belongs to [[postgres-resource]] —
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@owlmeans/postgres",
|
|
3
|
-
"version": "0.1.18-rc.
|
|
3
|
+
"version": "0.1.18-rc.38",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -22,9 +22,9 @@
|
|
|
22
22
|
}
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@owlmeans/basic-keys": "^0.1.18-rc.
|
|
25
|
+
"@owlmeans/basic-keys": "^0.1.18-rc.41",
|
|
26
26
|
"@owlmeans/context": "^0.1.18-rc.32",
|
|
27
|
-
"@owlmeans/postgres-resource": "^0.1.18-rc.
|
|
27
|
+
"@owlmeans/postgres-resource": "^0.1.18-rc.37",
|
|
28
28
|
"@owlmeans/resource": "^0.1.18-rc.36",
|
|
29
29
|
"@owlmeans/server-context": "^0.1.18-rc.37",
|
|
30
30
|
"drizzle-orm": "~0.45.2",
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from 'bun:test'
|
|
2
|
+
import { makePostgresResource } from '@owlmeans/postgres-resource'
|
|
3
|
+
import type { PostgresResource } from '@owlmeans/postgres-resource'
|
|
4
|
+
import { matchCriteria, UnsupportedArgumentError } from '@owlmeans/resource'
|
|
5
|
+
import type { Criteria, ResourceRecord } from '@owlmeans/resource'
|
|
6
|
+
|
|
7
|
+
import { gate, makeSuite } from './context.js'
|
|
8
|
+
|
|
9
|
+
interface Plot extends ResourceRecord {
|
|
10
|
+
id?: string
|
|
11
|
+
garden: string
|
|
12
|
+
crops: string[]
|
|
13
|
+
soil: Record<string, unknown>
|
|
14
|
+
season?: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** A community garden: plots carry a native text[] of crops and a jsonb soil report. */
|
|
18
|
+
const plotSchema = {
|
|
19
|
+
type: 'object',
|
|
20
|
+
properties: {
|
|
21
|
+
id: { type: 'string', format: 'uuid' },
|
|
22
|
+
garden: { type: 'string' },
|
|
23
|
+
crops: { type: 'array', items: { type: 'string' } },
|
|
24
|
+
soil: { type: 'object' },
|
|
25
|
+
season: { type: 'string', nullable: true }
|
|
26
|
+
},
|
|
27
|
+
required: ['id', 'garden', 'crops', 'soil']
|
|
28
|
+
} as never
|
|
29
|
+
|
|
30
|
+
const suite = makeSuite('criteria')
|
|
31
|
+
const it = gate.skip ? test.skip : test
|
|
32
|
+
|
|
33
|
+
describe('@owlmeans/postgres — criteria answer what the in-memory engine answers', () => {
|
|
34
|
+
if (gate.skip) {
|
|
35
|
+
test.skip(gate.reason ?? 'postgres gate closed', () => {})
|
|
36
|
+
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let plots: PostgresResource<Plot>
|
|
41
|
+
let seeded: Plot[]
|
|
42
|
+
|
|
43
|
+
beforeAll(async () => {
|
|
44
|
+
const resource = makePostgresResource<Plot, PostgresResource<Plot>>('criteria-plots')
|
|
45
|
+
resource.schema = plotSchema
|
|
46
|
+
const booted = await suite.boot({ resources: [resource] })
|
|
47
|
+
plots = booted.context.resource<PostgresResource<Plot>>('criteria-plots')
|
|
48
|
+
|
|
49
|
+
seeded = []
|
|
50
|
+
for (const plot of [
|
|
51
|
+
{ garden: 'north', crops: ['beans', 'kale'], soil: { ph: 6.5, wet: true, beds: ['a', 'b'], state: 'loam' }, season: 'spring' },
|
|
52
|
+
{ garden: 'north', crops: ['kale'], soil: { ph: 7, wet: false, beds: ['c'], state: 'clay', 'a,b': 1 } },
|
|
53
|
+
{ garden: 'south', crops: [], soil: { ph: 5.5, state: null, beds: [] }, season: 'autumn' },
|
|
54
|
+
]) {
|
|
55
|
+
seeded.push(await plots.create(plot))
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
afterAll(async () => {
|
|
60
|
+
await suite.teardown()
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
const byBoth = async (criteria: Criteria<Plot>): Promise<void> => {
|
|
64
|
+
const listed = (await plots.list(criteria, { size: 0 })).items.map(plot => plot.id).sort()
|
|
65
|
+
const expected = seeded.filter(plot => matchCriteria(plot, criteria)).map(plot => plot.id).sort()
|
|
66
|
+
expect([JSON.stringify(criteria), listed]).toEqual([JSON.stringify(criteria), expected])
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
it('native array operators take a list or a scalar', async () => {
|
|
70
|
+
await byBoth({ crops: { $contains: 'kale' } })
|
|
71
|
+
await byBoth({ crops: { $contains: ['beans', 'kale'] } })
|
|
72
|
+
await byBoth({ crops: { $overlaps: ['beans', 'rye'] } })
|
|
73
|
+
await byBoth({ crops: { $overlaps: 'rye' } })
|
|
74
|
+
await byBoth({ crops: { $contained: ['kale', 'beans', 'rye'] } })
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('dotted jsonb paths compare typed values, lists, null and operators', async () => {
|
|
78
|
+
await byBoth({ 'soil.ph': 7 } as Criteria<Plot>)
|
|
79
|
+
await byBoth({ 'soil.wet': true } as Criteria<Plot>)
|
|
80
|
+
await byBoth({ 'soil.state': ['loam', 'clay'] } as Criteria<Plot>)
|
|
81
|
+
await byBoth({ 'soil.state': null } as Criteria<Plot>)
|
|
82
|
+
await byBoth({ 'soil.state': { $in: ['clay', null] } } as Criteria<Plot>)
|
|
83
|
+
/** `$nin` reads as it does on a column: `NOT IN`, which never matches an absent value. */
|
|
84
|
+
expect((await plots.list({ 'soil.state': { $nin: ['clay'] } } as Criteria<Plot>, { size: 0 })).items.map(plot => plot.garden))
|
|
85
|
+
.toEqual(['north'])
|
|
86
|
+
await byBoth({ 'soil.ph': { $gte: 6.5 } } as Criteria<Plot>)
|
|
87
|
+
await byBoth({ 'soil.state': { $startsWith: 'lo' } } as Criteria<Plot>)
|
|
88
|
+
await byBoth({ 'soil.state': { $exists: true } } as Criteria<Plot>)
|
|
89
|
+
await byBoth({ 'soil.beds': { $contains: 'a' } } as Criteria<Plot>)
|
|
90
|
+
await byBoth({ 'soil.beds': { $overlaps: ['c', 'z'] } } as Criteria<Plot>)
|
|
91
|
+
await byBoth({ 'soil.a,b': 1 } as Criteria<Plot>)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('counts per group in one round trip', async () => {
|
|
95
|
+
const counts = await plots.countBy({ crops: { $contains: 'kale' } }, ['garden'])
|
|
96
|
+
expect(counts).toEqual([{ garden: 'north', count: 2 }])
|
|
97
|
+
|
|
98
|
+
const grouped = (await plots.countBy(undefined, ['garden', 'season']))
|
|
99
|
+
.sort((left, right) => `${left.garden}${left.season}`.localeCompare(`${right.garden}${right.season}`))
|
|
100
|
+
expect(grouped).toEqual([
|
|
101
|
+
{ garden: 'north', season: null, count: 1 },
|
|
102
|
+
{ garden: 'north', season: 'spring', count: 1 },
|
|
103
|
+
{ garden: 'south', season: 'autumn', count: 1 },
|
|
104
|
+
])
|
|
105
|
+
await expect(plots.countBy(undefined, ['nope' as never])).rejects.toThrow(UnsupportedArgumentError)
|
|
106
|
+
})
|
|
107
|
+
})
|
package/tests/sync.spec.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { afterAll, describe, expect, test } from 'bun:test'
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
introspectTable, makePostgresResource, PgAutoSync, planSync, PostgresCastRequired,
|
|
4
|
+
} from '@owlmeans/postgres-resource'
|
|
3
5
|
import type { PostgresResource } from '@owlmeans/postgres-resource'
|
|
4
6
|
import type { ResourceRecord } from '@owlmeans/resource'
|
|
5
7
|
|
|
@@ -154,3 +156,91 @@ describe('@owlmeans/postgres — structure reconciliation', () => {
|
|
|
154
156
|
})).rejects.toThrow(PostgresCastRequired)
|
|
155
157
|
})
|
|
156
158
|
})
|
|
159
|
+
|
|
160
|
+
interface Plot extends ResourceRecord {
|
|
161
|
+
id?: string
|
|
162
|
+
garden: string
|
|
163
|
+
bed?: string
|
|
164
|
+
crop?: string
|
|
165
|
+
season: string
|
|
166
|
+
soil?: Record<string, unknown>
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* A community garden's plots: a plain compound index, an expression index and two partial ones —
|
|
171
|
+
* the shapes `pg_indexes.indexdef` renders differently from the statement that created them
|
|
172
|
+
* (identifiers unquoted, literals cast, a partial predicate parenthesized), plus an enum CHECK.
|
|
173
|
+
*/
|
|
174
|
+
const plots = {
|
|
175
|
+
type: 'object',
|
|
176
|
+
properties: {
|
|
177
|
+
id: { type: 'string', format: 'uuid' },
|
|
178
|
+
garden: { type: 'string' },
|
|
179
|
+
bed: { type: 'string', nullable: true },
|
|
180
|
+
crop: { type: 'string', nullable: true },
|
|
181
|
+
season: { type: 'string', enum: ['spring', 'summer', 'autumn'] },
|
|
182
|
+
soil: { type: 'object', nullable: true }
|
|
183
|
+
},
|
|
184
|
+
required: ['id', 'garden', 'season'],
|
|
185
|
+
pg: {
|
|
186
|
+
indexes: [
|
|
187
|
+
{ name: 'sync_plots_garden_season', columns: ['garden', 'season'] },
|
|
188
|
+
{ name: 'sync_plots_scope', unique: true, expression: `"garden", COALESCE("bed", ''), "season"` },
|
|
189
|
+
{ name: 'sync_plots_crop', columns: ['garden', 'crop'], where: '"crop" IS NOT NULL' },
|
|
190
|
+
{ name: 'sync_plots_wet', columns: ['garden'], where: `("soil"->>'state') = 'wet'` }
|
|
191
|
+
]
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const plotSuite = makeSuite('sync_idx')
|
|
196
|
+
|
|
197
|
+
describe('@owlmeans/postgres — reconciling a table already at its shape', () => {
|
|
198
|
+
if (gate.skip) {
|
|
199
|
+
test.skip(gate.reason ?? 'postgres gate closed', () => {})
|
|
200
|
+
|
|
201
|
+
return
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
afterAll(async () => {
|
|
205
|
+
await plotSuite.teardown()
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
const bootPlots = async (): Promise<{ pg: PostgresService, resource: PostgresResource<Plot> }> => {
|
|
209
|
+
const resource = makePostgresResource<Plot, PostgresResource<Plot>>('sync-plots')
|
|
210
|
+
resource.schema = plots as never
|
|
211
|
+
const booted = await plotSuite.boot({ resources: [resource] })
|
|
212
|
+
|
|
213
|
+
return { pg: booted.pg, resource: booted.context.resource<PostgresResource<Plot>>('sync-plots') }
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const objectOids = async (pg: PostgresService): Promise<Record<string, number>> => Object.fromEntries(
|
|
217
|
+
(await pg.query<{ name: string, oid: number }>(
|
|
218
|
+
`SELECT c.relname AS name, c.oid::int AS oid FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
219
|
+
WHERE n.nspname = $1 AND c.relkind = 'i' AND c.relname LIKE 'sync_plots_%'
|
|
220
|
+
UNION ALL
|
|
221
|
+
SELECT k.conname AS name, k.oid::int AS oid FROM pg_constraint k JOIN pg_namespace n ON n.oid = k.connamespace
|
|
222
|
+
WHERE n.nspname = $1 AND k.contype = 'c' AND k.conname LIKE 'sync_plots_%'`, [plotSuite.schema]
|
|
223
|
+
)).map(row => [row.name, row.oid])
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
it('plans nothing on a second boot — no index or check is dropped and recreated', async () => {
|
|
227
|
+
const first = await bootPlots()
|
|
228
|
+
const before = await objectOids(first.pg)
|
|
229
|
+
expect(Object.keys(before).sort()).toEqual([
|
|
230
|
+
'sync_plots_crop', 'sync_plots_garden_season', 'sync_plots_pkey', 'sync_plots_scope', 'sync_plots_season_enum',
|
|
231
|
+
'sync_plots_wet'
|
|
232
|
+
])
|
|
233
|
+
|
|
234
|
+
const second = await bootPlots()
|
|
235
|
+
const { pool } = await second.resource.db()
|
|
236
|
+
const client = await pool.connect()
|
|
237
|
+
try {
|
|
238
|
+
const table = second.resource.table
|
|
239
|
+
const live = await introspectTable(client, table.schema, table.table, table.qualified)
|
|
240
|
+
expect(planSync(table, live).statements.map(statement => statement.sql)).toEqual([])
|
|
241
|
+
} finally {
|
|
242
|
+
client.release()
|
|
243
|
+
}
|
|
244
|
+
expect(await objectOids(second.pg)).toEqual(before)
|
|
245
|
+
})
|
|
246
|
+
})
|