@owlmeans/resource 0.1.18-rc.2 → 0.1.18-rc.21
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 +355 -76
- package/agent-meta/manifest.json +9 -2
- package/agent-meta/skills/resource/SKILL.md +143 -11
- package/agent-meta/skills/resource-choice/SKILL.md +264 -0
- package/build/consts.d.ts.map +1 -1
- package/build/consts.js +4 -27
- package/build/consts.js.map +1 -1
- package/build/helper.d.ts +0 -2
- package/build/helper.d.ts.map +1 -1
- package/build/helper.js +0 -24
- package/build/helper.js.map +1 -1
- package/build/index.d.ts +1 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1 -0
- package/build/index.js.map +1 -1
- package/build/types.d.ts +125 -44
- package/build/types.d.ts.map +1 -1
- package/build/utils/index.d.ts +1 -0
- package/build/utils/index.d.ts.map +1 -1
- package/build/utils/index.js +1 -0
- package/build/utils/index.js.map +1 -1
- package/build/utils/name.d.ts +1 -1
- package/build/utils/name.d.ts.map +1 -1
- package/build/utils/name.js +1 -19
- package/build/utils/name.js.map +1 -1
- package/build/utils/query.d.ts +29 -0
- package/build/utils/query.d.ts.map +1 -0
- package/build/utils/query.js +270 -0
- package/build/utils/query.js.map +1 -0
- package/package.json +3 -5
- package/src/consts.ts +4 -27
- package/src/helper.ts +0 -28
- package/src/index.ts +1 -0
- package/src/types.ts +125 -46
- package/src/utils/index.ts +1 -0
- package/src/utils/name.ts +3 -25
- package/src/utils/query.ts +277 -0
- package/build/.gitkeep +0 -0
|
@@ -8,20 +8,28 @@ 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.21"` in `dependencies`
|
|
12
12
|
|
|
13
13
|
## Key Exports
|
|
14
14
|
|
|
15
15
|
| Export | Description |
|
|
16
16
|
|--------|-------------|
|
|
17
|
-
| `
|
|
18
|
-
| `
|
|
17
|
+
| `ResourceRecord` | `{ id?: string }` — the constraint every record type satisfies, and the only field the contract itself knows about. |
|
|
18
|
+
| `Resource<T>` | Generic resource contract: `get`/`load`/`list`/`count`/`create`/`update`/`save`/`delete`/`take`/`purge`. Reads take **an id or a criteria object**; `list(where?, opts?)` takes `{ page, size, sort }` flat in the second argument. |
|
|
19
|
+
| `Criteria<T>`, `FieldCriteria<V>`, `FieldOperators<V>` | The query language, typed by the record — a mistyped field is a compile error. |
|
|
20
|
+
| `Sort<T>`, `SortField<T>`, `FirstOptions<T>`, `ListOptions<T>`, `ListQuery<T>`, `ListResult<T>` | Ordering, the read options, the one-object form an API carries over the wire, and the answer shape `{ items, total, page?, size? }`. |
|
|
21
|
+
| `WriteOptions`, `Ttl` | `{ ttl }` on `create`/`update`/`save`; seconds from now, or the instant to expire at. Backends without expiry refuse it. |
|
|
22
|
+
| `matchCriteria`, `filterRecords`, `sortRecords`, `firstMatch`, `applyQuery` | The shared in-memory engine — one criteria object means the same thing in a browser store as it does in SQL. |
|
|
23
|
+
| `PubSubResource<T>`, `WatchableResource<T>`, `StreamResource<T>`, `LockableResource<T>` | **Optional capabilities**, composed into a concrete resource interface alongside `Resource<T>`. `SubscribeOptions` (`{ channel, once, ttl }`) and `Unsubscribe` come with the first two. |
|
|
24
|
+
| `MigratableResource<Tx, Self>` | **Optional migration capability** — `migration(name, apply, stage?)` (chainable) + `migrations()`. Optional the way pub/sub is on redis resources: mongo and postgres extend it, backends with nothing to migrate don't. |
|
|
19
25
|
| `Migration`, `MigrationRegistry`, `MigrationStore`, `MigrationReport`, `MigrationRunOptions` | The framework's contracts. `MigrationStore` is the *migration register* a database implements to track applied migrations (`ensure`/`applied`/`baseline`/`run`). |
|
|
20
26
|
| `createMigrationRegistry<Tx>()` | Storage-agnostic ledger of code-registered migrations per alias. Declaration order = application order; re-registering an identical body is a no-op, a changed body under a used name throws `MigrationConflict`. |
|
|
21
27
|
| `runMigrations(alias, registry, store, opts?)` | Apply one stage's pending migrations through a store. `baseline: true` records instead of running (fresh structures); `strictChecksum` (default on) rejects edited applied bodies. |
|
|
22
28
|
| `MigrationStage` | `Pre` (before structure reconciliation — renames, casts, rescues) / `Post` (after — backfills into new structure). |
|
|
23
|
-
| `createDbService` | Base for `ResourceDbService` implementations (config/alias/name/client plumbing). |
|
|
24
|
-
| `
|
|
29
|
+
| `createDbService` | Base for `ResourceDbService` implementations (config/alias/name/client plumbing). The service's `name(alias?)` resolves to `config.schema ?? config.alias ?? service.alias`. |
|
|
30
|
+
| `ResourceDbService<Db, Client>`, `DbLocker<T>` | What a connection service implements: `db`/`client`/`clients`/`config`/`name`/`ensureConfigAlias`/`initialize`, plus `lock`/`unlock` when the backend encrypts fields. |
|
|
31
|
+
| `ResourceMaker<R, T>` | `(dbAlias?, serviceAlias?) => T` — the signature every resource maker is typed with, so an app registers `makeXResource()` without repeating the argument list. |
|
|
32
|
+
| `filterObject(obj, keep?)`, `createListSchema` | Drop null and undefined properties, keeping the names listed in `keep`; the AJV schema for a `ListResult<T>` envelope. |
|
|
25
33
|
| Errors | `ResourceError`, `UnknownRecordError`, `MisshapedRecord`, `RecordExists`, `RecordUpdateFailed`, `UnsupportedArgumentError`, `UnsupportedMethodError`, `MigrationError`, `MigrationConflict`. |
|
|
26
34
|
| `DbConfig`, `Config`, `Context` | Database config (`cfg.dbs`) types. |
|
|
27
35
|
|
|
@@ -35,11 +43,128 @@ A resource is a typed CRUD-over-records abstraction. Concrete implementations co
|
|
|
35
43
|
```typescript
|
|
36
44
|
import type { Resource } from '@owlmeans/resource'
|
|
37
45
|
|
|
38
|
-
interface Project { id: string; name: string; entityId: string }
|
|
46
|
+
interface Project { id: string; name: string; entityId: string; createdAt: Date }
|
|
39
47
|
const projects = ctx.resource<Resource<Project>>('projects')
|
|
40
|
-
|
|
48
|
+
|
|
49
|
+
// A read is addressed by an id OR by criteria — fetching one record by several fields is a
|
|
50
|
+
// single call, never a list whose first element is taken.
|
|
51
|
+
const project = await projects.get('abc123')
|
|
52
|
+
const newest = await projects.load({ entityId: 'abc' }, { sort: [{ field: 'createdAt', order: 'desc' }] })
|
|
53
|
+
|
|
54
|
+
// Paging and sort are FLAT in the second argument.
|
|
55
|
+
const page2 = await projects.list({ entityId: 'abc' }, { page: 1, size: 20, sort: ['createdAt'] })
|
|
56
|
+
page2.items // the window
|
|
57
|
+
page2.total // every record the criteria match, not just this page
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Calling the CRUD surface
|
|
61
|
+
|
|
62
|
+
A **write** takes the record as its only data argument, and the id travels inside it. There is no
|
|
63
|
+
`(id, changes)` overload on any of them — passing an id first fails to compile with
|
|
64
|
+
`TS2559: Type 'string' has no properties in common with type 'Partial<...>'`.
|
|
65
|
+
|
|
66
|
+
| Call | Signature | Notes |
|
|
67
|
+
|---|---|---|
|
|
68
|
+
| `create(record, opts?)` | `Partial<T> → T` | throws `RecordExists`; implementations may refuse a caller-supplied id |
|
|
69
|
+
| `save(record, opts?)` | `Partial<T> → T` | creates when the record carries no id, replaces otherwise |
|
|
70
|
+
| `update(record, opts?)` | `Partial<T> → T` | replaces the whole record; throws `UnknownRecordError` |
|
|
71
|
+
|
|
72
|
+
A **read** is addressed either by an id or by a criteria object. The id overload takes nothing
|
|
73
|
+
else; the criteria overload takes `{ sort }` to say which match is "the" one:
|
|
74
|
+
|
|
75
|
+
| Call | Signature | Notes |
|
|
76
|
+
|---|---|---|
|
|
77
|
+
| `load(id)` / `load(where, { sort })` | `→ T \| null` | the miss-tolerant read |
|
|
78
|
+
| `get(id)` / `get(where, { sort })` | `→ T` | throws `UnknownRecordError` instead of returning `null` |
|
|
79
|
+
| `list(where?, { page, size, sort })` | `→ { items, total, page?, size? }` | `total` counts every match, not the window |
|
|
80
|
+
| `count(where?)` | `→ number` | the same question without carrying the records back |
|
|
81
|
+
| `delete(id)` | `→ T \| null` | returns what it removed |
|
|
82
|
+
| `take(id)` | `→ T` | **deletes the record it returns.** It is a `delete` that throws `UnknownRecordError` on a miss, never a read — never reach for it to fetch something |
|
|
83
|
+
| `purge(where)` | `→ number` | bulk delete; refuses an empty criteria object rather than emptying the resource |
|
|
84
|
+
|
|
85
|
+
Every method returns the record(s) themselves, never a driver result object — there is no
|
|
86
|
+
`rowsAffected`/`rowCount` anywhere on this contract.
|
|
87
|
+
|
|
88
|
+
### The criteria language
|
|
89
|
+
|
|
90
|
+
`Criteria<T>` is keyed by the record's own fields, so a typo is a compile error. A dotted key
|
|
91
|
+
reaches into a nested value (or a jsonb column) and stays open.
|
|
92
|
+
|
|
93
|
+
| Written as | Means |
|
|
94
|
+
|---|---|
|
|
95
|
+
| `{ status: 'open' }` | equality |
|
|
96
|
+
| `{ status: ['open', 'done'] }` | **any of these** — a bare array is never exact-array equality (that is `{ $eq: [...] }`) |
|
|
97
|
+
| `{ archivedAt: null }` | the absence of a value |
|
|
98
|
+
| `{ status: undefined }` | **skipped** — an untouched filter must never empty a list |
|
|
99
|
+
| `{ 'profile.city': 'Kraków' }` | reach into a nested value or a jsonb column |
|
|
100
|
+
|
|
101
|
+
Operators go in an object under the field: `$eq $ne $gt $gte $lt $lte $in $nin $exists $null
|
|
102
|
+
$like $ilike $regex $startsWith $endsWith $between $contains $contained $overlaps`. Branches
|
|
103
|
+
combine with `$and` / `$or` / `$not`, each taking criteria of the same shape.
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
await projects.list({
|
|
107
|
+
entityId: 'abc',
|
|
108
|
+
createdAt: { $gte: since },
|
|
109
|
+
$or: [{ name: { $ilike: 'owl%' } }, { tags: { $overlaps: ['pinned'] } }]
|
|
110
|
+
})
|
|
41
111
|
```
|
|
42
112
|
|
|
113
|
+
Every backend answers the same object the same way — that is the point of the vocabulary. An
|
|
114
|
+
operator a store cannot express raises `UnsupportedArgumentError` rather than being quietly
|
|
115
|
+
dropped.
|
|
116
|
+
|
|
117
|
+
`Sort<T>` is a bare field name (ascending) or `{ field, order: 'asc' | 'desc' }`, and `sort` takes
|
|
118
|
+
a list of them. Where an **absent** value lands is the store's own rule, not the vocabulary's: the
|
|
119
|
+
in-memory sorter puts it last ascending, Postgres emits a bare `ASC` and so leaves it last (the
|
|
120
|
+
server's `NULLS LAST` default), and Mongo hands the sort document to the driver untouched, so BSON
|
|
121
|
+
ordering puts null and missing first. Sort on a field the schema requires when the position of an
|
|
122
|
+
absent value matters.
|
|
123
|
+
|
|
124
|
+
### Paging is a property of the backend
|
|
125
|
+
|
|
126
|
+
`ListResult<T>` is `{ items, total, page?, size? }` — `total` always counts every match. Whether a
|
|
127
|
+
missing `size` means "everything" depends on what the store can afford:
|
|
128
|
+
|
|
129
|
+
| Backend | `list(where)` with no `size` |
|
|
130
|
+
|---|---|
|
|
131
|
+
| mongo, postgres | a page of `DEFAULT_PAGE_SIZE` (100) — an unbounded read of a growing table is an incident waiting to happen |
|
|
132
|
+
| 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 |
|
|
133
|
+
|
|
134
|
+
`size: 0` means **no limit** on every backend — the explicit, greppable way to ask for a whole
|
|
135
|
+
result set. `page` without `size` against an unpaged backend throws
|
|
136
|
+
`UnsupportedArgumentError('page-without-size')`: there is no implied default to take a window of.
|
|
137
|
+
|
|
138
|
+
### The shared in-memory engine
|
|
139
|
+
|
|
140
|
+
Every store without a query engine of its own — redis after its SCAN, the client and static
|
|
141
|
+
stores, state — filters through the same helpers, so a criteria object written for an endpoint
|
|
142
|
+
selects the same records when a screen applies it locally:
|
|
143
|
+
|
|
144
|
+
| Helper | Answers |
|
|
145
|
+
|---|---|
|
|
146
|
+
| `matchCriteria(record, where)` | does this one record match |
|
|
147
|
+
| `filterRecords(records, where)` | every match, in insertion order |
|
|
148
|
+
| `sortRecords(records, sort)` | a sorted copy |
|
|
149
|
+
| `firstMatch(records, where, { sort })` | the record `load(where)` / `get(where)` return |
|
|
150
|
+
| `applyQuery(records, where, opts)` | the whole `ListResult` — filter, sort and page in one call |
|
|
151
|
+
|
|
152
|
+
Reach for them when implementing a backend, and when narrowing a list already in hand rather than
|
|
153
|
+
going back to the store.
|
|
154
|
+
|
|
155
|
+
### Optional capabilities
|
|
156
|
+
|
|
157
|
+
A concrete resource interface composes the capabilities its backend actually has beside
|
|
158
|
+
`Resource<T>`; a consumer types the **resource** once and gets exactly those methods.
|
|
159
|
+
|
|
160
|
+
| Interface | Surface |
|
|
161
|
+
|---|---|
|
|
162
|
+
| `PubSubResource<T>` | `publish(value, channel?)` · `subscribe(handler, { channel?, once?, ttl? })` |
|
|
163
|
+
| `WatchableResource<T>` | `watch(id, handler, { once?, ttl? })` — changes to ONE record |
|
|
164
|
+
| `StreamResource<T>` | `stream(key, value)` · `consume(key, { group?, consumer?, block? })` |
|
|
165
|
+
| `LockableResource<T>` | `lock`/`unlock` — field level encryption at rest |
|
|
166
|
+
| `MigratableResource<Tx, Self>` | `migration(name, apply, stage?)` · `migrations()` |
|
|
167
|
+
|
|
43
168
|
## The migration framework
|
|
44
169
|
|
|
45
170
|
Migrations are **automatic on app setup**: a backend runs them inside the resource's
|
|
@@ -51,9 +176,11 @@ Adding migration support to a new database backend:
|
|
|
51
176
|
|
|
52
177
|
1. Extend the concrete resource interface with `MigratableResource<YourTx>` and design the
|
|
53
178
|
`Tx` façade a migration receives.
|
|
54
|
-
2. Keep registrations in a **module-scope declaration keyed by alias**
|
|
55
|
-
|
|
56
|
-
|
|
179
|
+
2. Keep registrations in a **module-scope declaration keyed by alias** — the shape mongo and
|
|
180
|
+
postgres both expose as `getDeclaration(alias)` / `resetDeclarations(alias?)`. A maker that
|
|
181
|
+
runs more than once for the same alias (a custom maker, a test) then re-declares the same
|
|
182
|
+
entries and loses nothing, where a registry held on the resource object would silently lose
|
|
183
|
+
data transformations.
|
|
57
184
|
3. Implement `MigrationStore` over a durable ledger when the database can store one
|
|
58
185
|
(`_owlmeans_migrations` in both mongo and postgres); a store-less backend may run
|
|
59
186
|
migrations unconditionally if its bodies are self-checking.
|
|
@@ -66,7 +193,12 @@ Adding migration support to a new database backend:
|
|
|
66
193
|
## Depends On
|
|
67
194
|
|
|
68
195
|
- `@owlmeans/context` — service/resource lifecycle
|
|
69
|
-
- `@
|
|
196
|
+
- `@owlmeans/error` — `ResilientError`, the base every error class above extends
|
|
197
|
+
- peer `ajv` — `createListSchema` builds a `JSONSchemaType`; install it when you use that schema
|
|
198
|
+
|
|
199
|
+
The migration framework checksums bodies with `@noble/hashes` and `@scure/base`, and the package
|
|
200
|
+
declares neither. Install both alongside it in any project that loads `createMigrationRegistry` or
|
|
201
|
+
`runMigrations`, or the import fails at load time.
|
|
70
202
|
|
|
71
203
|
## Related
|
|
72
204
|
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: resource-choice
|
|
3
|
+
description: Which storage or execution mechanism a feature should use — postgres-resource, mongo-resource, redis-resource, static-resource, state, client-resource, config, storage-resource, queue + worker, or llm/agent. Guidance by similarity to worked cases, with an explicitly skeptical test for asynchronous processing. Use at design time, before writing a resource registration, a schema, or a job declaration.
|
|
4
|
+
user-invocable: true
|
|
5
|
+
---
|
|
6
|
+
<!-- AUTO-GENERATED — do not edit. Regenerate via sync-agent-meta. -->
|
|
7
|
+
|
|
8
|
+
# Choosing a resource
|
|
9
|
+
|
|
10
|
+
Every OwlMeans feature that keeps or moves data picks from the same short list. The list is short
|
|
11
|
+
on purpose: one `Resource` contract ([[resource]]) covers all of them, so the choice is about
|
|
12
|
+
where the bytes live and when the work runs — not about a new API to learn.
|
|
13
|
+
|
|
14
|
+
This is guidance by **similarity**, not an algorithm. Find the case under "Worked cases" below
|
|
15
|
+
that most resembles what you are building and take its answer; the rules in between say why those
|
|
16
|
+
answers came out that way, and how far you can carry one to a case that is not listed. When two
|
|
17
|
+
options look equally right, pick the one already registered in the app.
|
|
18
|
+
|
|
19
|
+
## The default
|
|
20
|
+
|
|
21
|
+
**Postgres.** `@owlmeans/postgres-resource` is the answer for records the product owns and the
|
|
22
|
+
product queries: users, orders, projects, comments, settings, audit rows. It gets the default
|
|
23
|
+
because it is the only backend here that gives all of relational integrity, real query planning,
|
|
24
|
+
transactions, and DDL derived from the resource's AJV schema — and because being wrong with it is
|
|
25
|
+
cheap. Nothing else on this page is a place to *start*; each one is a place to move to for a named
|
|
26
|
+
reason.
|
|
27
|
+
|
|
28
|
+
Say "start with Postgres, move only if measured" out loud whenever the argument for something else
|
|
29
|
+
is a guess about load. A resource that turns out to be hot moves later, and the criteria objects,
|
|
30
|
+
paging and sorting come with it unchanged — that portability is what makes deferring the decision
|
|
31
|
+
safe.
|
|
32
|
+
|
|
33
|
+
## The options
|
|
34
|
+
|
|
35
|
+
| Option | It is for | It is not for |
|
|
36
|
+
|---|---|---|
|
|
37
|
+
| `postgres-resource` | Records with structure, relations, and queries — the default | Anything the schema cannot describe up front |
|
|
38
|
+
| `mongo-resource` | Documents whose shape varies per record or per tenant; deep nesting read whole | Rows you will join, sum, or report across |
|
|
39
|
+
| `redis-resource` | Data with an expiry, a lock, a counter, or a fan-out — and a **small, namespaced** key set | A source of truth, or an unbounded keyspace (`list`/`count` walk it) |
|
|
40
|
+
| `static-resource` | In-process records with no database behind them — fixtures, handshake state, a per-process cache | Anything that must survive a restart or be seen by a second replica |
|
|
41
|
+
| `state` | Client-side records a screen binds to, reached through the context | Anything that must outlive the tab |
|
|
42
|
+
| `client-resource` | Client-side records that must survive a reload, over a key-value store (`web-db` gives the browser one) | A query surface — the whole store is walked per read |
|
|
43
|
+
| `config` | Reading the records the process was configured with, as a resource | Anything written at runtime — every write is refused |
|
|
44
|
+
| `storage-resource` | The **bytes** of a file, in an S3-compatible bucket (upload only) | The record describing the file — that stays in postgres or mongo |
|
|
45
|
+
| `queue` + a worker process | Work that must outlive the request, be retried, or be spread over workers | Anything the request can finish itself (see below) |
|
|
46
|
+
| `llm` / `agent` | Work whose output is judgement or language, not a computation | A rule you can write down |
|
|
47
|
+
|
|
48
|
+
### Postgres or Mongo
|
|
49
|
+
|
|
50
|
+
Ask what a *reader* does with the record. Filter, sort, join, aggregate, report — Postgres, and
|
|
51
|
+
the AJV schema becomes the table ([[postgres-resource]]). Load whole by id, with a shape that
|
|
52
|
+
differs per record and a nesting depth no column list wants to model — Mongo, and the AJV schema
|
|
53
|
+
becomes the validator ([[mongo-resource]]). Both carry migrations, indexes, locks and the same
|
|
54
|
+
criteria language, so this is a modelling decision, not a capability one.
|
|
55
|
+
|
|
56
|
+
A single varying corner inside an otherwise relational record is not a reason to move the whole
|
|
57
|
+
resource: Postgres maps a nested `object` — and an `array` whose items are not plain scalars — to
|
|
58
|
+
`jsonb`, where a criteria *object* written against the column becomes containment (`@>`). An
|
|
59
|
+
`array` of plain `string`/`integer`/`number`/`boolean` items is a different case: it becomes a
|
|
60
|
+
native array column, queried with `$contains` / `$contained` / `$overlaps`.
|
|
61
|
+
|
|
62
|
+
### Redis and the four things it is actually good at
|
|
63
|
+
|
|
64
|
+
`redis-resource` earns its place when the data has one of these properties, not when it is merely
|
|
65
|
+
"fast":
|
|
66
|
+
|
|
67
|
+
- **Expiry** — the record is meant to disappear: sessions, OTPs, nonces, magic-link tokens, an
|
|
68
|
+
idempotency key's memory. TTL is a per-call option on `create`/`update`/`save`, and `update`
|
|
69
|
+
rewrites the key, so a renewal must pass the TTL again or the record becomes permanent.
|
|
70
|
+
- **Cache** — a derived value it would be embarrassing to recompute per request. A cache is
|
|
71
|
+
correct only if a miss is invisible to the user; if losing it changes behaviour, it is state, and
|
|
72
|
+
state belongs in the database.
|
|
73
|
+
- **Lock** — one worker at a time, with an expiry so a dead holder does not block forever. Postgres
|
|
74
|
+
and Mongo resources also expose `lock`/`unlock`; use theirs when the lock guards *their* rows.
|
|
75
|
+
- **Counter / rate limit / fan-out** — increments, windows, and pub/sub between processes.
|
|
76
|
+
|
|
77
|
+
The hard limit: reads by id are single-key commands, but `load(where)`, `list`, `count` and `purge`
|
|
78
|
+
**walk the resource's namespace** with `SCAN` and evaluate criteria in memory, unpaged and not
|
|
79
|
+
cluster-safe. A resource that needs real queries belongs in Mongo or Postgres, however hot it is.
|
|
80
|
+
|
|
81
|
+
### static-resource, state and client-resource
|
|
82
|
+
|
|
83
|
+
`static-resource` is a `Map` at module scope wearing the `Resource` contract. It is right for
|
|
84
|
+
fixtures, a getting-started app with no database, and short-lived handshake state in a
|
|
85
|
+
single-process deployment — and wrong the moment there are two replicas, because each one holds a
|
|
86
|
+
different truth. Do not reach for it as a cache in a scaled service; that is Redis.
|
|
87
|
+
|
|
88
|
+
`state` is the same contract on the client: records a screen binds to through `useStoreModel` /
|
|
89
|
+
`useStoreList`, with `watch`/`query` for the live half. Draft state, the current user, a wizard in
|
|
90
|
+
progress, an optimistic list. It is a store, not a security boundary — the server re-validates
|
|
91
|
+
everything that arrives from it.
|
|
92
|
+
|
|
93
|
+
`client-resource` is the client store that **survives a reload**: same criteria language, backed by
|
|
94
|
+
a key-value store an app registers under `cfg.dbs` (`@owlmeans/web-db` supplies the browser's
|
|
95
|
+
IndexedDB one). Reach for it for an offline cache, a remembered filter set, a queued write waiting
|
|
96
|
+
for connectivity — and expect the cost, because records live under their own ids with one list of
|
|
97
|
+
ids as the only index, so every criteria read walks the store. `state` for what the screen is
|
|
98
|
+
holding right now, `client-resource` for what has to be there tomorrow, the server for the truth.
|
|
99
|
+
|
|
100
|
+
### Files
|
|
101
|
+
|
|
102
|
+
Bytes go to `storage-resource` (upload only — a bucket has no index). The **record** describing
|
|
103
|
+
them — url, size, mime type, owner, status — goes in a database resource, and that record is what
|
|
104
|
+
answers criteria, sorting and paging. Image-shaped names and schemas for it are in
|
|
105
|
+
[[image-resource]].
|
|
106
|
+
|
|
107
|
+
## Be skeptical about queues
|
|
108
|
+
|
|
109
|
+
Asynchronous processing is the most over-applied answer on this page. It multiplies the number of
|
|
110
|
+
places a feature can fail, it needs a second process to deploy and watch, and it makes every
|
|
111
|
+
processor a thing that must be safe to run twice. Do not add it because the work "feels heavy".
|
|
112
|
+
|
|
113
|
+
**A request that finishes in under ~2 seconds and makes no external call never queues.** Do it
|
|
114
|
+
inline and return the answer.
|
|
115
|
+
|
|
116
|
+
Asynchronous work earns its complexity when at least one of these is true:
|
|
117
|
+
|
|
118
|
+
1. **The user would otherwise wait more than ~5 seconds.** Not "the work takes 5 seconds" — the
|
|
119
|
+
*user waits*. Work nobody is watching can still be inline if the caller is a script.
|
|
120
|
+
2. **It must survive a process restart.** A deploy, an OOM kill, or a pod eviction mid-operation
|
|
121
|
+
would otherwise lose work that was already accepted, and nothing would retry it.
|
|
122
|
+
3. **It is retried against a third party.** Payment captures, provider syncs, mail sends and
|
|
123
|
+
webhook deliveries fail transiently and must be re-attempted with backoff, without the user
|
|
124
|
+
re-submitting.
|
|
125
|
+
|
|
126
|
+
If none holds, the honest answer is "do it in the request". If exactly one holds, look for the
|
|
127
|
+
smaller fix first: a database index, a batch write, a cached aggregate, or an HTTP call moved off
|
|
128
|
+
the critical path. Reach for the broker when the work is genuinely detached from the request.
|
|
129
|
+
|
|
130
|
+
When you do queue ([[queue]], driver [[redis-queue]]):
|
|
131
|
+
|
|
132
|
+
- A queued call is an ordinary entrypoint call — `job()` in place of `backend()`. Nothing at the
|
|
133
|
+
call site says "queue", so this stays a declaration change.
|
|
134
|
+
- Derive `JobOptions.id` from what the job is ABOUT (`develop:<storyId>`) and a duplicate enqueue
|
|
135
|
+
is a no-op. That is what makes an admission step safe to retry.
|
|
136
|
+
- `reply: false` is fire-and-forget; the default waits for the value. Choose deliberately.
|
|
137
|
+
- Every processor must be safe to run twice, and `touch()` in every long loop — the broker judges
|
|
138
|
+
liveness by the lock, and silence past `lockDuration` re-runs the job somewhere else.
|
|
139
|
+
- Retries default to one attempt, which is right for expensive work and wrong for cheap idempotent
|
|
140
|
+
work — set `attempts` per queue.
|
|
141
|
+
- Work that fans out into steps is one `flow(root)` graph, not a processor that enqueues its own
|
|
142
|
+
successors: children complete before the parent runs and it reads their results through
|
|
143
|
+
`children()`. Give the children of a cross-queue flow distinct ids.
|
|
144
|
+
|
|
145
|
+
**There is no scheduler in the framework.** `JobOptions.delay` defers one job once; anything
|
|
146
|
+
recurring — a nightly digest, an hourly reconciliation — is triggered from outside the process by
|
|
147
|
+
a platform cron (a Kubernetes `CronJob` in a [[kluster]] deployment) that calls a normal entrypoint
|
|
148
|
+
or enqueues a job. Do not simulate cron with a self-re-enqueueing job.
|
|
149
|
+
|
|
150
|
+
## Model-driven work
|
|
151
|
+
|
|
152
|
+
Reach for [[llm]] when the output is language or judgement — summaries, classification with fuzzy
|
|
153
|
+
boundaries, extraction from prose, moderation, translation — and for [[agent]] when the work needs
|
|
154
|
+
several turns, tools, or memory across a conversation. Everything a rule can express should stay a
|
|
155
|
+
rule: a regex, a criteria object, or an AJV schema is cheaper, deterministic, and testable.
|
|
156
|
+
|
|
157
|
+
Model calls are slow and paid for per attempt, so they interact with the previous section:
|
|
158
|
+
|
|
159
|
+
- A single `ask` behind a user's click is fine inline when it streams; the user sees progress.
|
|
160
|
+
- A pipeline of calls, or work over many records, is queue work — it is exactly case 1 and 3 above.
|
|
161
|
+
- A blind retry re-spends the tokens that just failed. That is why queue retries default to one.
|
|
162
|
+
- Store the *result* in a normal resource. A model output nobody persisted will be paid for again.
|
|
163
|
+
|
|
164
|
+
## Choosing more than one
|
|
165
|
+
|
|
166
|
+
Most real features use two. A file upload is `storage-resource` for the bytes plus a Postgres row
|
|
167
|
+
for the record. A rate limiter is Redis for the window plus Postgres for the policy. An import is
|
|
168
|
+
a Postgres staging table plus a queue for the work plus Redis for the progress the UI polls. Split
|
|
169
|
+
by property — durability, expiry, size, latency — and let each part sit where that property is
|
|
170
|
+
cheap.
|
|
171
|
+
|
|
172
|
+
## Worked cases
|
|
173
|
+
|
|
174
|
+
Find the row that most resembles what you are building. The answer is a starting point with a
|
|
175
|
+
reason attached, not a verdict — if your case differs in a way the rules above care about, the
|
|
176
|
+
answer changes with it. `postgres` / `mongo` / `redis` / `static` / `client` / `storage` below mean
|
|
177
|
+
the matching `@owlmeans/*-resource` package, `state` and `config` the packages of those names, and
|
|
178
|
+
`queue` means `@owlmeans/queue` plus a worker process.
|
|
179
|
+
|
|
180
|
+
### Records and reads
|
|
181
|
+
|
|
182
|
+
| Case | Choice | Why |
|
|
183
|
+
|---|---|---|
|
|
184
|
+
| Audit log | postgres | Append-only rows that are filtered, sorted and paged by whoever reads them; retention is a scheduled delete, not a TTL. |
|
|
185
|
+
| Domain records (orders, projects, tickets) | postgres | The default: structure, relations, transactions, one AJV schema that becomes the table. |
|
|
186
|
+
| Per-tenant custom forms / submissions | mongo | The shape differs per tenant, so no column list fits; each submission is read whole by id. |
|
|
187
|
+
| Generated-app content documents | mongo | Deeply nested, revised whole, never aggregated across. |
|
|
188
|
+
| Settings / feature flags per organization entity | postgres | Few rows, read constantly, always by `entityId` — cache in Redis only after measuring, and only if a stale read is harmless. |
|
|
189
|
+
| Search over the product's own records | postgres | Start with indexed columns and `jsonb` containment; move to a search engine when a measured query, not a guess, says so. |
|
|
190
|
+
| Expensive aggregate on a dashboard | postgres, then redis | Compute it in SQL first. Cache in Redis with a TTL only once the measured query is genuinely slow — a miss must stay invisible. |
|
|
191
|
+
| Analytics rollups | postgres + queue | The rollup is a scheduled batch write into its own table; the trigger is a platform cron, the work is a job because it survives restarts. |
|
|
192
|
+
| Leaderboard | postgres, redis if hot | A ranked query with an index is enough for most products; move the live top-N into a Redis counter set when it is read far more often than written. |
|
|
193
|
+
| Invoice numbering | postgres | A gapless sequence is a transaction and a unique constraint, not a Redis counter — a lost increment is an accounting problem. |
|
|
194
|
+
| Records the process was started with (plugins, seeded routes) | config | Already in `cfg`; reading them as a resource gives criteria and sorting without a second store, and every write is refused by design. |
|
|
195
|
+
|
|
196
|
+
### Expiring, counting, coordinating
|
|
197
|
+
|
|
198
|
+
| Case | Choice | Why |
|
|
199
|
+
|---|---|---|
|
|
200
|
+
| Session / OTP cache | redis | Records defined by their expiry; losing one is a re-login, not data loss. Pass the TTL again on every renewal. |
|
|
201
|
+
| Password reset token | redis | Single-use and short-lived: `take(id)` hands the record to exactly one caller and throws for the second. |
|
|
202
|
+
| Rate limiting | redis | A counter in a window, shared across replicas, that must expire on its own. |
|
|
203
|
+
| Counters (views, usage quotas) | redis, reconciled to postgres | Increment in Redis for speed; the billable total belongs in the database, written periodically. |
|
|
204
|
+
| Presence / typing indicators | redis | Sub-minute TTL plus pub/sub; nothing is worth persisting. |
|
|
205
|
+
| Distributed lock | redis, or the owning resource's `lock` | Redis when the lock guards a process or a third party; the Postgres/Mongo resource's own `lock`/`unlock` when it guards its rows. |
|
|
206
|
+
| Idempotency keys for an API | redis | The memory only has to outlive the retry window, and expiry is the cleanup. |
|
|
207
|
+
| Webhook signature nonce cache | redis | Replay protection is exactly a key with a TTL. |
|
|
208
|
+
| Handshake state in a single-process dev app | static | In-process `Map` with no database; wrong the moment there are two replicas. |
|
|
209
|
+
| Fixtures / seeded demo data | static | Records reachable through the context with nothing behind them. |
|
|
210
|
+
|
|
211
|
+
### Client side
|
|
212
|
+
|
|
213
|
+
| Case | Choice | Why |
|
|
214
|
+
|---|---|---|
|
|
215
|
+
| Wizard draft | state (`single: true`) | It belongs to the screen until submitted; only the final submit is a server write. |
|
|
216
|
+
| Shopping cart | state, plus postgres when it must persist | Anonymous cart lives on the client; a cart that survives a device change is a database record keyed by user. |
|
|
217
|
+
| Optimistic list updates | state | The store the screen binds to, re-validated by the server on write. |
|
|
218
|
+
| Notifications feed | postgres, delivered live over sockets | Rows with read/unread state that page; `state` mirrors them for rendering, it does not own them. |
|
|
219
|
+
| Offline cache of server records | client (`web-db` in the browser) | Must survive a reload and a lost connection; reads walk the store, so keep the set small and re-sync from the server. |
|
|
220
|
+
| Remembered UI preferences (filters, layout) | client | A handful of records per user that must outlive the tab, with nothing on the server worth a row. |
|
|
221
|
+
|
|
222
|
+
### Files and bytes
|
|
223
|
+
|
|
224
|
+
| Case | Choice | Why |
|
|
225
|
+
|---|---|---|
|
|
226
|
+
| File upload | storage + postgres | Bytes to the bucket, the record (url, size, mime, owner) in the database — a bucket has no index to query. |
|
|
227
|
+
| File post-processing (thumbnails, virus scan) | queue | Survives a restart, retried against tools that fail transiently, and the user must not wait for it. |
|
|
228
|
+
| PDF / report generation | queue | Multi-second CPU work whose result is a stored file; return a job id, deliver the file when it exists. |
|
|
229
|
+
| GDPR data export | queue + storage | Minutes of reads across every resource, produced once, downloaded later from a bucket. |
|
|
230
|
+
| OCR | queue + llm/model call | External, slow, retried — every reason on the list at once. |
|
|
231
|
+
|
|
232
|
+
### Asynchronous work and third parties
|
|
233
|
+
|
|
234
|
+
| Case | Choice | Why |
|
|
235
|
+
|---|---|---|
|
|
236
|
+
| Long CSV / JSON import | queue, staged in postgres | The user cannot hold a request open for thousands of rows, and a half-finished import must be resumable. |
|
|
237
|
+
| Bulk email | queue | Thousands of provider calls, each retried independently; one bad address must not fail the batch. |
|
|
238
|
+
| Transactional email (one recipient, one event) | inline | A single provider call inside the request; queue it only if the send is on a user's critical path and the provider is slow. |
|
|
239
|
+
| Payment capture | queue with idempotent job ids | Retried against a third party, must survive a restart, must never double-charge — derive the job id from the payment. |
|
|
240
|
+
| Third-party sync / reconciliation | queue, triggered by platform cron | Recurring, long, retried; the framework has no scheduler, so a `CronJob` enqueues it. |
|
|
241
|
+
| Webhook fan-out | queue, one job per subscriber | Per-endpoint retries and backoff; a slow subscriber must not delay the others. |
|
|
242
|
+
| Scheduled digest | queue, triggered by platform cron | Same shape as any recurring batch — cron enqueues, the worker sends. |
|
|
243
|
+
| Scheduled reminders | postgres row + cron sweep, or `JobOptions.delay` | Use `delay` for a one-shot minutes-away nudge; anything cancellable or far in the future is a row a periodic sweep picks up. |
|
|
244
|
+
| Inbound webhook handling | inline ack, queue the work | Answer 2xx immediately, then process — a third party's retry policy is not a design input for your handler. |
|
|
245
|
+
| Cache warmup after a deploy | queue, or nothing | Usually not worth it: let the first requests populate the cache unless a measured cold-start hurts. |
|
|
246
|
+
|
|
247
|
+
### Model-driven features
|
|
248
|
+
|
|
249
|
+
| Case | Choice | Why |
|
|
250
|
+
|---|---|---|
|
|
251
|
+
| Chat with an assistant | agent, streamed inline; postgres for the transcript | The user is watching, so stream rather than queue; persist the conversation as ordinary records. |
|
|
252
|
+
| Content moderation by model | llm + queue | Every rule that can be written down stays a rule; the model handles the rest, off the request path. |
|
|
253
|
+
| Translation of stored content | llm + queue | Batch work over many records, paid per attempt, retried carefully — never on a page render. |
|
|
254
|
+
| Recommendations refresh | llm or postgres batch + queue | A periodic recompute whose output is stored; the read path only ever reads the stored result. |
|
|
255
|
+
| Summarizing a long document on demand | llm inline if streamed, else queue | A single streamed `ask` keeps the user informed; a pipeline over sections is queue work. |
|
|
256
|
+
|
|
257
|
+
## Related
|
|
258
|
+
|
|
259
|
+
- [[resource]] — the contract every option implements, and the criteria language they share
|
|
260
|
+
- [[postgres-resource]], [[mongo-resource]], [[redis-resource]], [[static-resource]],
|
|
261
|
+
[[state]], [[client-resource]], [[web-db]], [[config]], [[storage-resource]] — the backends
|
|
262
|
+
- [[queue]], [[redis-queue]] — job queues and the worker process
|
|
263
|
+
- [[llm]], [[agent]] — model-driven work
|
|
264
|
+
- [[reuse-code]] — find the `@owlmeans/*` package before designing anything new
|
package/build/consts.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"consts.d.ts","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,KAAK,CAAA;AACzC,OAAO,KAAK,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAE5D;;;;;;;;;;;GAWG;AACH,oBAAY,cAAc;IACxB,GAAG,QAAQ;IACX,IAAI,SAAS;CACd;AAED,eAAO,MAAM,gBAAgB,GAAI,CAAC,SAAS,cAAc,UAAU,cAAc,CAAC,CAAC,CAAC,KAAG,cAAc,CAAC,UAAU,CAAC,CAAC,CAAC,
|
|
1
|
+
{"version":3,"file":"consts.d.ts","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,KAAK,CAAA;AACzC,OAAO,KAAK,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAE5D;;;;;;;;;;;GAWG;AACH,oBAAY,cAAc;IACxB,GAAG,QAAQ;IACX,IAAI,SAAS;CACd;AAED,eAAO,MAAM,gBAAgB,GAAI,CAAC,SAAS,cAAc,UAAU,cAAc,CAAC,CAAC,CAAC,KAAG,cAAc,CAAC,UAAU,CAAC,CAAC,CAAC,CAsBjH,CAAA"}
|
package/build/consts.js
CHANGED
|
@@ -31,34 +31,11 @@ export const createListSchema = (schema) => ({
|
|
|
31
31
|
additionalProperties: false,
|
|
32
32
|
}
|
|
33
33
|
},
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
sort: {
|
|
38
|
-
type: 'array',
|
|
39
|
-
items: {
|
|
40
|
-
oneOf: [
|
|
41
|
-
{ type: 'string' },
|
|
42
|
-
{
|
|
43
|
-
type: 'array', items: [
|
|
44
|
-
{ type: 'string' },
|
|
45
|
-
{ type: 'boolean', nullable: true }
|
|
46
|
-
], minItems: 1, maxItems: 2
|
|
47
|
-
}
|
|
48
|
-
]
|
|
49
|
-
},
|
|
50
|
-
nullable: true
|
|
51
|
-
},
|
|
52
|
-
page: { type: 'number', nullable: true },
|
|
53
|
-
size: { type: 'number', nullable: true },
|
|
54
|
-
total: { type: 'number', nullable: true }
|
|
55
|
-
},
|
|
56
|
-
required: [],
|
|
57
|
-
nullable: true,
|
|
58
|
-
additionalProperties: false,
|
|
59
|
-
}
|
|
34
|
+
total: { type: 'number' },
|
|
35
|
+
page: { type: 'number', nullable: true },
|
|
36
|
+
size: { type: 'number', nullable: true }
|
|
60
37
|
},
|
|
61
|
-
required: ['items'],
|
|
38
|
+
required: ['items', 'total'],
|
|
62
39
|
additionalProperties: false,
|
|
63
40
|
});
|
|
64
41
|
//# sourceMappingURL=consts.js.map
|
package/build/consts.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"consts.js","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;GAWG;AACH,MAAM,CAAN,IAAY,cAGX;AAHD,WAAY,cAAc;IACxB,6BAAW,CAAA;IACX,+BAAa,CAAA;AACf,CAAC,EAHW,cAAc,KAAd,cAAc,QAGzB;AAED,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAA2B,MAAyB,EAAiC,EAAE,CAAC,CAAC;IACvH,IAAI,EAAE,QAAQ;IACd,UAAU,EAAE;QACV,KAAK,EAAE;YACL,IAAI,EAAE,OAAO;YACb,KAAK,EAAE;gBACL,GAAG,MAAM;gBACT,IAAI,EAAE,QAAQ;gBACd,UAAU,EAAE;oBACV,EAAE,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE;oBACtC,GAAG,MAAM,CAAC,UAAU;iBACrB;gBACD,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,GAAG,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;gBACrE,oBAAoB,EAAE,KAAK;aAC5B;SACF;QACD,KAAK,EAAE
|
|
1
|
+
{"version":3,"file":"consts.js","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;GAWG;AACH,MAAM,CAAN,IAAY,cAGX;AAHD,WAAY,cAAc;IACxB,6BAAW,CAAA;IACX,+BAAa,CAAA;AACf,CAAC,EAHW,cAAc,KAAd,cAAc,QAGzB;AAED,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAA2B,MAAyB,EAAiC,EAAE,CAAC,CAAC;IACvH,IAAI,EAAE,QAAQ;IACd,UAAU,EAAE;QACV,KAAK,EAAE;YACL,IAAI,EAAE,OAAO;YACb,KAAK,EAAE;gBACL,GAAG,MAAM;gBACT,IAAI,EAAE,QAAQ;gBACd,UAAU,EAAE;oBACV,EAAE,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE;oBACtC,GAAG,MAAM,CAAC,UAAU;iBACrB;gBACD,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,GAAG,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;gBACrE,oBAAoB,EAAE,KAAK;aAC5B;SACF;QACD,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;QACzB,IAAI,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE;QACxC,IAAI,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE;KACzC;IACD,QAAQ,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC;IAC5B,oBAAoB,EAAE,KAAK;CAC5B,CAAC,CAAA"}
|
package/build/helper.d.ts
CHANGED
|
@@ -1,4 +1,2 @@
|
|
|
1
|
-
import type { ListCriteria, ListOptions } from './types.js';
|
|
2
|
-
export declare const prepareListOptions: (defPageSize?: number, criteria?: ListOptions | ListCriteria, opts?: ListOptions) => ListOptions;
|
|
3
1
|
export declare const filterObject: <T extends {}>(obj: T, keep?: string[]) => T;
|
|
4
2
|
//# sourceMappingURL=helper.d.ts.map
|
package/build/helper.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"helper.d.ts","sourceRoot":"","sources":["../src/helper.ts"],"names":[],"mappings":"AAAA,
|
|
1
|
+
{"version":3,"file":"helper.d.ts","sourceRoot":"","sources":["../src/helper.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,YAAY,GAAI,CAAC,SAAS,EAAE,OAAO,CAAC,SAAS,MAAM,EAAE,KAAG,CAMxD,CAAA"}
|
package/build/helper.js
CHANGED
|
@@ -1,27 +1,3 @@
|
|
|
1
|
-
export const prepareListOptions = (defPageSize = 10, criteria, opts) => {
|
|
2
|
-
if (opts == null) {
|
|
3
|
-
if (criteria == null) {
|
|
4
|
-
criteria = {};
|
|
5
|
-
opts = {};
|
|
6
|
-
}
|
|
7
|
-
else if ('criteria' in criteria || 'pager' in criteria) {
|
|
8
|
-
/**
|
|
9
|
-
* Both keys have to be read off the original object before it's reassigned —
|
|
10
|
-
* reading `pager` after narrowing to `.criteria` silently drops the pager.
|
|
11
|
-
*/
|
|
12
|
-
const options = criteria;
|
|
13
|
-
opts = { pager: options.pager };
|
|
14
|
-
criteria = (options.criteria ?? {});
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
const size = (opts?.pager?.size ?? defPageSize);
|
|
18
|
-
const pager = {
|
|
19
|
-
sort: opts?.pager?.sort,
|
|
20
|
-
page: opts?.pager?.page ?? 0,
|
|
21
|
-
size, total: 0
|
|
22
|
-
};
|
|
23
|
-
return { criteria: criteria, pager };
|
|
24
|
-
};
|
|
25
1
|
export const filterObject = (obj, keep) => Object.entries(obj).reduce((obj, [key, value]) => {
|
|
26
2
|
if (value == null && !keep?.includes(key)) {
|
|
27
3
|
return obj;
|
package/build/helper.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"helper.js","sourceRoot":"","sources":["../src/helper.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"helper.js","sourceRoot":"","sources":["../src/helper.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,YAAY,GAAG,CAAe,GAAM,EAAE,IAAe,EAAK,EAAE,CACvE,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE;IAC/C,IAAI,KAAK,IAAI,IAAI,IAAI,CAAC,IAAI,EAAE,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;QAC1C,OAAO,GAAG,CAAA;IACZ,CAAC;IACD,OAAO,EAAE,GAAG,GAAG,EAAE,CAAC,GAAG,CAAC,EAAE,KAAK,EAAE,CAAA;AACjC,CAAC,EAAE,EAAO,CAAC,CAAA"}
|
package/build/index.d.ts
CHANGED
package/build/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA;AAC5B,cAAc,aAAa,CAAA;AAC3B,cAAc,gBAAgB,CAAA;AAC9B,mBAAmB,YAAY,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA;AAC5B,cAAc,aAAa,CAAA;AAC3B,cAAc,kBAAkB,CAAA;AAChC,cAAc,gBAAgB,CAAA;AAC9B,mBAAmB,YAAY,CAAA"}
|
package/build/index.js
CHANGED
package/build/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA;AAC5B,cAAc,aAAa,CAAA;AAC3B,cAAc,gBAAgB,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA;AAC5B,cAAc,aAAa,CAAA;AAC3B,cAAc,kBAAkB,CAAA;AAChC,cAAc,gBAAgB,CAAA"}
|