@owlmeans/resource 0.1.18-rc.40 → 0.1.18-rc.41
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 +80 -4
- package/agent-meta/skills/resource/SKILL.md +12 -106
- package/agent-meta/skills/resource/parts/capabilities.md +23 -0
- package/agent-meta/skills/resource/parts/criteria.md +60 -0
- package/agent-meta/skills/resource/parts/memory-engine.md +27 -0
- package/agent-meta/skills/resource/parts/migrations.md +35 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ wants is the `resource-choice` decision.
|
|
|
12
12
|
## Installation
|
|
13
13
|
|
|
14
14
|
```bash
|
|
15
|
-
bun add @owlmeans/resource@^0.1.18-rc.
|
|
15
|
+
bun add @owlmeans/resource@^0.1.18-rc.41
|
|
16
16
|
```
|
|
17
17
|
|
|
18
18
|
## Concepts
|
|
@@ -404,7 +404,7 @@ This package ships embedded agent skills under `agent-meta/`. After installing y
|
|
|
404
404
|
your project's skill store (`.agents/skills/`):
|
|
405
405
|
|
|
406
406
|
```sh
|
|
407
|
-
npx @owlmeans/agent-skills@^0.1.18-rc.
|
|
407
|
+
npx @owlmeans/agent-skills@^0.1.18-rc.57
|
|
408
408
|
```
|
|
409
409
|
|
|
410
410
|
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
|
-
"schemaVersion":
|
|
2
|
+
"schemaVersion": 3,
|
|
3
3
|
"package": "@owlmeans/resource",
|
|
4
|
-
"version": "0.1.18-rc.
|
|
5
|
-
"generatedAt": "2026-10-
|
|
4
|
+
"version": "0.1.18-rc.41",
|
|
5
|
+
"generatedAt": "2026-10-20T23:19:58.613Z",
|
|
6
6
|
"canonicalRepo": "https://github.com/owlmeans/common",
|
|
7
7
|
"entries": [
|
|
8
8
|
{
|
|
@@ -10,7 +10,83 @@
|
|
|
10
10
|
"name": "resource",
|
|
11
11
|
"category": "package-specific",
|
|
12
12
|
"file": "skills/resource/SKILL.md",
|
|
13
|
-
"canonicalPath": ".agents/skills/resource/SKILL.md"
|
|
13
|
+
"canonicalPath": ".agents/skills/resource/SKILL.md",
|
|
14
|
+
"parts": [
|
|
15
|
+
{
|
|
16
|
+
"name": "capabilities",
|
|
17
|
+
"file": "skills/resource/parts/capabilities.md",
|
|
18
|
+
"description": "The optional capability interfaces a concrete resource composes beside Resource (pub/sub, watch, stream, lock, migrations).",
|
|
19
|
+
"purposes": [
|
|
20
|
+
"data"
|
|
21
|
+
],
|
|
22
|
+
"mentions": [
|
|
23
|
+
"PubSubResource",
|
|
24
|
+
"WatchableResource",
|
|
25
|
+
"StreamResource",
|
|
26
|
+
"LockableResource",
|
|
27
|
+
"MigratableResource",
|
|
28
|
+
"SubscribeOptions"
|
|
29
|
+
]
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"name": "criteria",
|
|
33
|
+
"file": "skills/resource/parts/criteria.md",
|
|
34
|
+
"description": "The criteria language (equality, any-of arrays, operators, branches, nested keys), sort and where absent values land, and paging per backend.",
|
|
35
|
+
"purposes": [
|
|
36
|
+
"query",
|
|
37
|
+
"list"
|
|
38
|
+
],
|
|
39
|
+
"mentions": [
|
|
40
|
+
"Criteria",
|
|
41
|
+
"FieldCriteria",
|
|
42
|
+
"FieldOperators",
|
|
43
|
+
"$or",
|
|
44
|
+
"$in",
|
|
45
|
+
"$ilike",
|
|
46
|
+
"$contains",
|
|
47
|
+
"$overlaps",
|
|
48
|
+
"Sort",
|
|
49
|
+
"ListOptions",
|
|
50
|
+
"ListResult",
|
|
51
|
+
"DEFAULT_PAGE_SIZE",
|
|
52
|
+
"page-without-size"
|
|
53
|
+
]
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"name": "memory-engine",
|
|
57
|
+
"file": "skills/resource/parts/memory-engine.md",
|
|
58
|
+
"description": "recordQueryHelper, the shared in-memory engine every store without a query engine of its own filters, sorts and pages through.",
|
|
59
|
+
"purposes": [
|
|
60
|
+
"test"
|
|
61
|
+
],
|
|
62
|
+
"mentions": [
|
|
63
|
+
"recordQueryHelper",
|
|
64
|
+
"matchCriteria",
|
|
65
|
+
"filterRecords",
|
|
66
|
+
"sortRecords",
|
|
67
|
+
"firstMatch",
|
|
68
|
+
"applyQuery"
|
|
69
|
+
]
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"name": "migrations",
|
|
73
|
+
"file": "skills/resource/parts/migrations.md",
|
|
74
|
+
"description": "The migration framework — automatic on init — and the five steps of adding migration support to a new database backend.",
|
|
75
|
+
"purposes": [
|
|
76
|
+
"migration"
|
|
77
|
+
],
|
|
78
|
+
"mentions": [
|
|
79
|
+
"MigratableResource",
|
|
80
|
+
"MigrationStore",
|
|
81
|
+
"createMigrationRegistry",
|
|
82
|
+
"runMigrations",
|
|
83
|
+
"MigrationStage",
|
|
84
|
+
"mongoDeclarationHelper",
|
|
85
|
+
"pgDeclarationHelper",
|
|
86
|
+
"_owlmeans_migrations"
|
|
87
|
+
]
|
|
88
|
+
}
|
|
89
|
+
]
|
|
14
90
|
},
|
|
15
91
|
{
|
|
16
92
|
"kind": "skill",
|
|
@@ -8,7 +8,7 @@ user-invocable: false
|
|
|
8
8
|
# @owlmeans/resource
|
|
9
9
|
|
|
10
10
|
**Layer:** Core
|
|
11
|
-
**Install:** `"@owlmeans/resource": "^0.1.18-rc.
|
|
11
|
+
**Install:** `"@owlmeans/resource": "^0.1.18-rc.41"` in `dependencies`
|
|
12
12
|
|
|
13
13
|
## Key Exports
|
|
14
14
|
|
|
@@ -98,111 +98,6 @@ So `get` only what the REQUEST addressed. A missing row it did not name — a co
|
|
|
98
98
|
a session, an internal lookup — is not the caller's 404: `load` it and throw the fault or the
|
|
99
99
|
refusal that fits (an auth path throws `AuthorizationError`).
|
|
100
100
|
|
|
101
|
-
### The criteria language
|
|
102
|
-
|
|
103
|
-
`Criteria<T>` is keyed by the record's own fields, so a typo is a compile error. A dotted key
|
|
104
|
-
reaches into a nested value (or a jsonb column) and stays open.
|
|
105
|
-
|
|
106
|
-
| Written as | Means |
|
|
107
|
-
|---|---|
|
|
108
|
-
| `{ status: 'open' }` | equality |
|
|
109
|
-
| `{ status: ['open', 'done'] }` | **any of these** — a bare array is never exact-array equality (that is `{ $eq: [...] }`) |
|
|
110
|
-
| `{ archivedAt: null }` | the absence of a value |
|
|
111
|
-
| `{ status: undefined }` | **skipped** — an untouched filter must never empty a list |
|
|
112
|
-
| `{ 'profile.city': 'Kraków' }` | reach into a nested value or a jsonb column |
|
|
113
|
-
|
|
114
|
-
Operators go in an object under the field: `$eq $ne $gt $gte $lt $lte $in $nin $exists $null
|
|
115
|
-
$like $ilike $regex $startsWith $endsWith $between $contains $contained $overlaps`. Branches
|
|
116
|
-
combine with `$and` / `$or` / `$not`, each taking criteria of the same shape.
|
|
117
|
-
|
|
118
|
-
```typescript
|
|
119
|
-
await projects.list({
|
|
120
|
-
entityId: 'abc',
|
|
121
|
-
createdAt: { $gte: since },
|
|
122
|
-
$or: [{ name: { $ilike: 'owl%' } }, { tags: { $overlaps: ['pinned'] } }]
|
|
123
|
-
})
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
Every backend answers the same object the same way — that is the point of the vocabulary. An
|
|
127
|
-
operator a store cannot express raises `UnsupportedArgumentError` rather than being quietly
|
|
128
|
-
dropped.
|
|
129
|
-
|
|
130
|
-
`Sort<T>` is a bare field name (ascending) or `{ field, order: 'asc' | 'desc' }`, and `sort` takes
|
|
131
|
-
a list of them. Where an **absent** value lands is the store's own rule, not the vocabulary's: the
|
|
132
|
-
in-memory sorter puts it last ascending, Postgres emits a bare `ASC` and so leaves it last (the
|
|
133
|
-
server's `NULLS LAST` default), and Mongo hands the sort document to the driver untouched, so BSON
|
|
134
|
-
ordering puts null and missing first. Sort on a field the schema requires when the position of an
|
|
135
|
-
absent value matters.
|
|
136
|
-
|
|
137
|
-
### Paging is a property of the backend
|
|
138
|
-
|
|
139
|
-
`ListResult<T>` is `{ items, total, page?, size? }` — `total` always counts every match. Whether a
|
|
140
|
-
missing `size` means "everything" depends on what the store can afford:
|
|
141
|
-
|
|
142
|
-
| Backend | `list(where)` with no `size` |
|
|
143
|
-
|---|---|
|
|
144
|
-
| mongo, postgres | a page of `DEFAULT_PAGE_SIZE` (100) — an unbounded read of a growing table is an incident waiting to happen |
|
|
145
|
-
| redis, static, client-resource, config, state | every match — answering the criteria already read the whole namespace, so an implied window would only hide records without saving work |
|
|
146
|
-
|
|
147
|
-
`size: 0` means **no limit** on every backend — the explicit, greppable way to ask for a whole
|
|
148
|
-
result set. `page` without `size` against an unpaged backend throws
|
|
149
|
-
`UnsupportedArgumentError('page-without-size')`: there is no implied default to take a window of.
|
|
150
|
-
|
|
151
|
-
### The shared in-memory engine
|
|
152
|
-
|
|
153
|
-
Every store without a query engine of its own — redis after its SCAN, the client and static stores,
|
|
154
|
-
state — filters through the same helper, `recordQueryHelper`, so a criteria object written for an
|
|
155
|
-
endpoint selects the same records when a screen applies it locally:
|
|
156
|
-
|
|
157
|
-
| Member | Answers |
|
|
158
|
-
|---|---|
|
|
159
|
-
| `recordQueryHelper.matchCriteria(record, where)` | does this one record match |
|
|
160
|
-
| `recordQueryHelper.filterRecords(records, where)` | every match, in insertion order |
|
|
161
|
-
| `recordQueryHelper.sortRecords(records, sort)` | a sorted copy |
|
|
162
|
-
| `recordQueryHelper.firstMatch(records, where, { sort })` | the record `load(where)` / `get(where)` return |
|
|
163
|
-
| `recordQueryHelper.applyQuery(records, where, opts)` | the whole `ListResult` — filter, sort and page in one call |
|
|
164
|
-
|
|
165
|
-
Reach for them when implementing a backend, and when narrowing a list already in hand rather than
|
|
166
|
-
going back to the store.
|
|
167
|
-
|
|
168
|
-
### Optional capabilities
|
|
169
|
-
|
|
170
|
-
A concrete resource interface composes the capabilities its backend actually has beside
|
|
171
|
-
`Resource<T>`; a consumer types the **resource** once and gets exactly those methods.
|
|
172
|
-
|
|
173
|
-
| Interface | Surface |
|
|
174
|
-
|---|---|
|
|
175
|
-
| `PubSubResource<T>` | `publish(value, channel?)` · `subscribe(handler, { channel?, once?, ttl? })` |
|
|
176
|
-
| `WatchableResource<T>` | `watch(id, handler, { once?, ttl? })` — changes to ONE record |
|
|
177
|
-
| `StreamResource<T>` | `stream(key, value)` · `consume(key, { group?, consumer?, block? })` |
|
|
178
|
-
| `LockableResource<T>` | `lock`/`unlock` — field level encryption at rest |
|
|
179
|
-
| `MigratableResource<Tx, Self>` | `migration(name, apply, stage?)` · `migrations()` |
|
|
180
|
-
|
|
181
|
-
## The migration framework
|
|
182
|
-
|
|
183
|
-
Migrations are **automatic on app setup**: a backend runs them inside the resource's
|
|
184
|
-
`init()` — registering one is all an app ever does. Every migration self-describes when it
|
|
185
|
-
applies: the registry skips names the store has recorded, `baseline` covers structures
|
|
186
|
-
created already at head, and bodies are written idempotent so an interrupted run can repeat.
|
|
187
|
-
|
|
188
|
-
Adding migration support to a new database backend:
|
|
189
|
-
|
|
190
|
-
1. Extend the concrete resource interface with `MigratableResource<YourTx>` and design the
|
|
191
|
-
`Tx` façade a migration receives.
|
|
192
|
-
2. Keep registrations in a **module-scope declaration keyed by alias** — the shape mongo and
|
|
193
|
-
postgres both expose as `mongoDeclarationHelper` / `pgDeclarationHelper` with
|
|
194
|
-
`.getDeclaration(alias)` / `.resetDeclarations(alias?)`. A maker that runs more than once for the
|
|
195
|
-
same alias (a custom maker, a test) then re-declares the same entries and loses nothing, where a
|
|
196
|
-
registry held on the resource object would silently lose data transformations.
|
|
197
|
-
3. Implement `MigrationStore` over a durable ledger when the database can store one
|
|
198
|
-
(`_owlmeans_migrations` in both mongo and postgres); a store-less backend may run
|
|
199
|
-
migrations unconditionally if its bodies are self-checking.
|
|
200
|
-
4. In the resource's `init()`: probe structure → absent: `runMigrations(..., { baseline: true })`;
|
|
201
|
-
present: run `Pre` → reconcile structure → run `Post`.
|
|
202
|
-
5. Make `run` atomic with the ledger write where the database allows it (postgres: same
|
|
203
|
-
transaction). Where it doesn't (mongo: no multi-doc transactions on standalone), use
|
|
204
|
-
claim-then-complete on a unique `(alias, name)` key and require idempotent bodies.
|
|
205
|
-
|
|
206
101
|
## Depends On
|
|
207
102
|
|
|
208
103
|
- `@owlmeans/context` — service/resource lifecycle
|
|
@@ -217,3 +112,14 @@ declares neither. Install both alongside it in any project that loads `createMig
|
|
|
217
112
|
|
|
218
113
|
- [[mongo-resource]] · [[postgres-resource]] — the two `MigratableResource` implementations
|
|
219
114
|
- [[redis-resource]] — a non-migratable backend (pub/sub capability instead)
|
|
115
|
+
|
|
116
|
+
## Parts
|
|
117
|
+
|
|
118
|
+
Load a part on top of this core when its job is in play.
|
|
119
|
+
|
|
120
|
+
| Part | Load it when |
|
|
121
|
+
|---|---|
|
|
122
|
+
| [criteria](parts/criteria.md) | Writing a `where` (operators, `$or`, nested keys), a `sort`, or paging a `list` (`size`, `page`, `size: 0`) |
|
|
123
|
+
| [memory-engine](parts/memory-engine.md) | Implementing a store without a query engine, or narrowing a list already in hand (`recordQueryHelper`) |
|
|
124
|
+
| [capabilities](parts/capabilities.md) | Typing a resource with pub/sub, watch, stream, lock or migration capabilities |
|
|
125
|
+
| [migrations](parts/migrations.md) | Registering a migration or adding migration support to a database backend |
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: capabilities
|
|
3
|
+
description: The optional capability interfaces a concrete resource composes beside Resource (pub/sub, watch, stream, lock, migrations).
|
|
4
|
+
metadata:
|
|
5
|
+
purposes: data
|
|
6
|
+
mentions: PubSubResource, WatchableResource, StreamResource, LockableResource, MigratableResource, SubscribeOptions
|
|
7
|
+
---
|
|
8
|
+
<!-- AUTO-GENERATED — do not edit. Regenerate via sync-agent-meta. -->
|
|
9
|
+
|
|
10
|
+
# @owlmeans/resource — Optional capabilities
|
|
11
|
+
|
|
12
|
+
## Optional capabilities
|
|
13
|
+
|
|
14
|
+
A concrete resource interface composes the capabilities its backend actually has beside
|
|
15
|
+
`Resource<T>`; a consumer types the **resource** once and gets exactly those methods.
|
|
16
|
+
|
|
17
|
+
| Interface | Surface |
|
|
18
|
+
|---|---|
|
|
19
|
+
| `PubSubResource<T>` | `publish(value, channel?)` · `subscribe(handler, { channel?, once?, ttl? })` |
|
|
20
|
+
| `WatchableResource<T>` | `watch(id, handler, { once?, ttl? })` — changes to ONE record |
|
|
21
|
+
| `StreamResource<T>` | `stream(key, value)` · `consume(key, { group?, consumer?, block? })` |
|
|
22
|
+
| `LockableResource<T>` | `lock`/`unlock` — field level encryption at rest |
|
|
23
|
+
| `MigratableResource<Tx, Self>` | `migration(name, apply, stage?)` · `migrations()` |
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: criteria
|
|
3
|
+
description: The criteria language (equality, any-of arrays, operators, branches, nested keys), sort and where absent values land, and paging per backend.
|
|
4
|
+
metadata:
|
|
5
|
+
purposes: query, list
|
|
6
|
+
mentions: Criteria, FieldCriteria, FieldOperators, $or, $in, $ilike, $contains, $overlaps, Sort, ListOptions, ListResult, DEFAULT_PAGE_SIZE, page-without-size
|
|
7
|
+
---
|
|
8
|
+
<!-- AUTO-GENERATED — do not edit. Regenerate via sync-agent-meta. -->
|
|
9
|
+
|
|
10
|
+
# @owlmeans/resource — Criteria, sort and paging
|
|
11
|
+
|
|
12
|
+
## The criteria language
|
|
13
|
+
|
|
14
|
+
`Criteria<T>` is keyed by the record's own fields, so a typo is a compile error. A dotted key
|
|
15
|
+
reaches into a nested value (or a jsonb column) and stays open.
|
|
16
|
+
|
|
17
|
+
| Written as | Means |
|
|
18
|
+
|---|---|
|
|
19
|
+
| `{ status: 'open' }` | equality |
|
|
20
|
+
| `{ status: ['open', 'done'] }` | **any of these** — a bare array is never exact-array equality (that is `{ $eq: [...] }`) |
|
|
21
|
+
| `{ archivedAt: null }` | the absence of a value |
|
|
22
|
+
| `{ status: undefined }` | **skipped** — an untouched filter must never empty a list |
|
|
23
|
+
| `{ 'profile.city': 'Kraków' }` | reach into a nested value or a jsonb column |
|
|
24
|
+
|
|
25
|
+
Operators go in an object under the field: `$eq $ne $gt $gte $lt $lte $in $nin $exists $null
|
|
26
|
+
$like $ilike $regex $startsWith $endsWith $between $contains $contained $overlaps`. Branches
|
|
27
|
+
combine with `$and` / `$or` / `$not`, each taking criteria of the same shape.
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
await projects.list({
|
|
31
|
+
entityId: 'abc',
|
|
32
|
+
createdAt: { $gte: since },
|
|
33
|
+
$or: [{ name: { $ilike: 'owl%' } }, { tags: { $overlaps: ['pinned'] } }]
|
|
34
|
+
})
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Every backend answers the same object the same way — that is the point of the vocabulary. An
|
|
38
|
+
operator a store cannot express raises `UnsupportedArgumentError` rather than being quietly
|
|
39
|
+
dropped.
|
|
40
|
+
|
|
41
|
+
`Sort<T>` is a bare field name (ascending) or `{ field, order: 'asc' | 'desc' }`, and `sort` takes
|
|
42
|
+
a list of them. Where an **absent** value lands is the store's own rule, not the vocabulary's: the
|
|
43
|
+
in-memory sorter puts it last ascending, Postgres emits a bare `ASC` and so leaves it last (the
|
|
44
|
+
server's `NULLS LAST` default), and Mongo hands the sort document to the driver untouched, so BSON
|
|
45
|
+
ordering puts null and missing first. Sort on a field the schema requires when the position of an
|
|
46
|
+
absent value matters.
|
|
47
|
+
|
|
48
|
+
## Paging is a property of the backend
|
|
49
|
+
|
|
50
|
+
`ListResult<T>` is `{ items, total, page?, size? }` — `total` always counts every match. Whether a
|
|
51
|
+
missing `size` means "everything" depends on what the store can afford:
|
|
52
|
+
|
|
53
|
+
| Backend | `list(where)` with no `size` |
|
|
54
|
+
|---|---|
|
|
55
|
+
| mongo, postgres | a page of `DEFAULT_PAGE_SIZE` (100) — an unbounded read of a growing table is an incident waiting to happen |
|
|
56
|
+
| redis, static, client-resource, config, state | every match — answering the criteria already read the whole namespace, so an implied window would only hide records without saving work |
|
|
57
|
+
|
|
58
|
+
`size: 0` means **no limit** on every backend — the explicit, greppable way to ask for a whole
|
|
59
|
+
result set. `page` without `size` against an unpaged backend throws
|
|
60
|
+
`UnsupportedArgumentError('page-without-size')`: there is no implied default to take a window of.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: memory-engine
|
|
3
|
+
description: recordQueryHelper, the shared in-memory engine every store without a query engine of its own filters, sorts and pages through.
|
|
4
|
+
metadata:
|
|
5
|
+
purposes: test
|
|
6
|
+
mentions: recordQueryHelper, matchCriteria, filterRecords, sortRecords, firstMatch, applyQuery
|
|
7
|
+
---
|
|
8
|
+
<!-- AUTO-GENERATED — do not edit. Regenerate via sync-agent-meta. -->
|
|
9
|
+
|
|
10
|
+
# @owlmeans/resource — The shared in-memory engine
|
|
11
|
+
|
|
12
|
+
## The shared in-memory engine
|
|
13
|
+
|
|
14
|
+
Every store without a query engine of its own — redis after its SCAN, the client and static stores,
|
|
15
|
+
state — filters through the same helper, `recordQueryHelper`, so a criteria object written for an
|
|
16
|
+
endpoint selects the same records when a screen applies it locally:
|
|
17
|
+
|
|
18
|
+
| Member | Answers |
|
|
19
|
+
|---|---|
|
|
20
|
+
| `recordQueryHelper.matchCriteria(record, where)` | does this one record match |
|
|
21
|
+
| `recordQueryHelper.filterRecords(records, where)` | every match, in insertion order |
|
|
22
|
+
| `recordQueryHelper.sortRecords(records, sort)` | a sorted copy |
|
|
23
|
+
| `recordQueryHelper.firstMatch(records, where, { sort })` | the record `load(where)` / `get(where)` return |
|
|
24
|
+
| `recordQueryHelper.applyQuery(records, where, opts)` | the whole `ListResult` — filter, sort and page in one call |
|
|
25
|
+
|
|
26
|
+
Reach for them when implementing a backend, and when narrowing a list already in hand rather than
|
|
27
|
+
going back to the store.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: migrations
|
|
3
|
+
description: The migration framework — automatic on init — and the five steps of adding migration support to a new database backend.
|
|
4
|
+
metadata:
|
|
5
|
+
purposes: migration
|
|
6
|
+
mentions: MigratableResource, MigrationStore, createMigrationRegistry, runMigrations, MigrationStage, mongoDeclarationHelper, pgDeclarationHelper, _owlmeans_migrations
|
|
7
|
+
---
|
|
8
|
+
<!-- AUTO-GENERATED — do not edit. Regenerate via sync-agent-meta. -->
|
|
9
|
+
|
|
10
|
+
# @owlmeans/resource — Migrations
|
|
11
|
+
|
|
12
|
+
## The migration framework
|
|
13
|
+
|
|
14
|
+
Migrations are **automatic on app setup**: a backend runs them inside the resource's
|
|
15
|
+
`init()` — registering one is all an app ever does. Every migration self-describes when it
|
|
16
|
+
applies: the registry skips names the store has recorded, `baseline` covers structures
|
|
17
|
+
created already at head, and bodies are written idempotent so an interrupted run can repeat.
|
|
18
|
+
|
|
19
|
+
Adding migration support to a new database backend:
|
|
20
|
+
|
|
21
|
+
1. Extend the concrete resource interface with `MigratableResource<YourTx>` and design the
|
|
22
|
+
`Tx` façade a migration receives.
|
|
23
|
+
2. Keep registrations in a **module-scope declaration keyed by alias** — the shape mongo and
|
|
24
|
+
postgres both expose as `mongoDeclarationHelper` / `pgDeclarationHelper` with
|
|
25
|
+
`.getDeclaration(alias)` / `.resetDeclarations(alias?)`. A maker that runs more than once for the
|
|
26
|
+
same alias (a custom maker, a test) then re-declares the same entries and loses nothing, where a
|
|
27
|
+
registry held on the resource object would silently lose data transformations.
|
|
28
|
+
3. Implement `MigrationStore` over a durable ledger when the database can store one
|
|
29
|
+
(`_owlmeans_migrations` in both mongo and postgres); a store-less backend may run
|
|
30
|
+
migrations unconditionally if its bodies are self-checking.
|
|
31
|
+
4. In the resource's `init()`: probe structure → absent: `runMigrations(..., { baseline: true })`;
|
|
32
|
+
present: run `Pre` → reconcile structure → run `Post`.
|
|
33
|
+
5. Make `run` atomic with the ledger write where the database allows it (postgres: same
|
|
34
|
+
transaction). Where it doesn't (mongo: no multi-doc transactions on standalone), use
|
|
35
|
+
claim-then-complete on a unique `(alias, name)` key and require idempotent bodies.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@owlmeans/resource",
|
|
3
|
-
"version": "0.1.18-rc.
|
|
3
|
+
"version": "0.1.18-rc.41",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
}
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@owlmeans/context": "^0.1.18-rc.
|
|
26
|
-
"@owlmeans/error": "^0.1.18-rc.
|
|
25
|
+
"@owlmeans/context": "^0.1.18-rc.35",
|
|
26
|
+
"@owlmeans/error": "^0.1.18-rc.39"
|
|
27
27
|
},
|
|
28
28
|
"peerDependencies": {
|
|
29
29
|
"ajv": "*"
|