@owlmeans/client-resource 0.1.18-rc.1 → 0.1.18-rc.11
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 +6 -1
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/client-resource/SKILL.md +36 -7
- package/build/resource.d.ts +8 -1
- package/build/resource.d.ts.map +1 -1
- package/build/resource.js +64 -91
- package/build/resource.js.map +1 -1
- package/build/types.d.ts +2 -1
- package/build/types.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/resource.ts +90 -115
- package/src/types.ts +2 -1
- package/build/.gitkeep +0 -0
package/README.md
CHANGED
|
@@ -33,9 +33,14 @@ import type { ClientResource } from '@owlmeans/client-resource'
|
|
|
33
33
|
const res = context.resource<ClientResource<MyRecord>>('my-resource')
|
|
34
34
|
await res.save({ id: 'key', ...data })
|
|
35
35
|
const record = await res.load('key')
|
|
36
|
+
const { items } = await res.list({ status: 'draft' })
|
|
36
37
|
await res.erase() // clear all stored data
|
|
37
38
|
```
|
|
38
39
|
|
|
40
|
+
Reads are unpaged: `list(where)` returns every match, and `list(where, { page })` without a `size`
|
|
41
|
+
throws `UnsupportedArgumentError('page-without-size')`. Criteria and sorting are the shared
|
|
42
|
+
vocabulary from [`@owlmeans/resource`](../resource), evaluated in memory over the stored records.
|
|
43
|
+
|
|
39
44
|
## API
|
|
40
45
|
|
|
41
46
|
### `appendClientResource<C, T>(context, alias): T`
|
|
@@ -51,7 +56,7 @@ Extends `Resource<T>` with:
|
|
|
51
56
|
### `ClientDb`
|
|
52
57
|
|
|
53
58
|
Low-level browser storage interface:
|
|
54
|
-
- `get<T>(id): Promise<T>`
|
|
59
|
+
- `get<T>(id): Promise<T | undefined>` — a miss is `undefined`, not an error at this level
|
|
55
60
|
- `set<T>(id, value): Promise<void>`
|
|
56
61
|
- `has(id): Promise<boolean>`
|
|
57
62
|
- `del(id): Promise<boolean>`
|
package/agent-meta/manifest.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 2,
|
|
3
3
|
"package": "@owlmeans/client-resource",
|
|
4
|
-
"version": "0.1.18-rc.
|
|
5
|
-
"generatedAt": "2026-
|
|
4
|
+
"version": "0.1.18-rc.11",
|
|
5
|
+
"generatedAt": "2026-09-01T16:28:56.993Z",
|
|
6
6
|
"canonicalRepo": "https://github.com/owlmeans/common",
|
|
7
7
|
"entries": [
|
|
8
8
|
{
|
|
@@ -8,23 +8,52 @@ user-invocable: false
|
|
|
8
8
|
# @owlmeans/client-resource
|
|
9
9
|
|
|
10
10
|
**Layer:** Client
|
|
11
|
-
**Install:** `"@owlmeans/client-resource": "^0.1.18-rc.
|
|
11
|
+
**Install:** `"@owlmeans/client-resource": "^0.1.18-rc.11"` in `dependencies`
|
|
12
12
|
|
|
13
13
|
## Key Exports
|
|
14
14
|
|
|
15
15
|
| Export | Description |
|
|
16
16
|
|--------|-------------|
|
|
17
|
-
| `
|
|
18
|
-
| `ClientResource<T>`
|
|
19
|
-
|
|
|
17
|
+
| `appendClientResource<C, T, R>(context, alias)` | Register a client-side `Resource<R>` on the context and return the context |
|
|
18
|
+
| `ClientResource<T>` | `Resource<T>` + `db` (the key-value handle) and `erase()` |
|
|
19
|
+
| `ClientDb`, `ClientDbService` | What a storage backend implements — `get`/`set`/`has`/`del`, and the service that hands out one store per alias |
|
|
20
|
+
| `DEFAULT_DB_ALIAS` (`client-db`), `LIST_KEY` (`_list`) | Constants |
|
|
20
21
|
|
|
21
22
|
## Usage
|
|
22
23
|
|
|
23
24
|
```typescript
|
|
24
|
-
import {
|
|
25
|
-
|
|
25
|
+
import { appendClientResource } from '@owlmeans/client-resource'
|
|
26
|
+
|
|
27
|
+
appendClientResource<Config, Context, Project>(context, 'projects')
|
|
28
|
+
|
|
29
|
+
const projects = context.resource<ClientResource<Project>>('projects')
|
|
30
|
+
await projects.list({ status: ['open', 'blocked'] }, { sort: ['createdAt'] })
|
|
26
31
|
```
|
|
27
32
|
|
|
33
|
+
The backing store comes from `cfg.dbs` — the entry whose `alias` matches the resource's, with
|
|
34
|
+
`schema` naming the store (`@owlmeans/web-db` supplies the browser one). Without an entry the
|
|
35
|
+
resource falls back to the default `client-db` service under its own alias.
|
|
36
|
+
|
|
37
|
+
## A key-value store dressed as a resource
|
|
38
|
+
|
|
39
|
+
Records live under their own id and a list of those ids under `LIST_KEY` is the only index there
|
|
40
|
+
is. So:
|
|
41
|
+
|
|
42
|
+
- `load(id)` / `get(id)` / `delete(id)` / `take(id)` address the key directly, no walk.
|
|
43
|
+
- `load(where)`, `get(where)`, `list`, `count` and `purge` read the whole set and evaluate through
|
|
44
|
+
the shared in-memory engine from `@owlmeans/resource`, which is what makes a criteria object mean
|
|
45
|
+
here exactly what it means against a relational store. `{ sort }` orders the result the same way.
|
|
46
|
+
- **Unpaged**: `list(where)` with no `size` returns every match; `size: 0` says the same thing
|
|
47
|
+
explicitly, and `page` without `size` throws `UnsupportedArgumentError('page-without-size')`.
|
|
48
|
+
- `create` mints an id when the record carries none and throws `RecordExists` for one already
|
|
49
|
+
stored; `update` **replaces** the whole record and throws `UnknownRecordError` for an unknown id;
|
|
50
|
+
`save` creates or replaces. `take(id)` **deletes** the record it returns.
|
|
51
|
+
- `purge(where)` refuses an empty criteria object. `erase()` empties the store, index included.
|
|
52
|
+
|
|
28
53
|
## Depends On
|
|
29
54
|
|
|
30
|
-
- `@owlmeans/resource`, `@owlmeans/client-context`
|
|
55
|
+
- `@owlmeans/resource`, `@owlmeans/client-context`, `@scure/base`, `@noble/hashes`
|
|
56
|
+
|
|
57
|
+
## Related
|
|
58
|
+
|
|
59
|
+
- [[web-db]] — the browser IndexedDB backend behind `ClientDbService`
|
package/build/resource.d.ts
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import type { ClientConfig, ClientContext } from '@owlmeans/client-context';
|
|
2
|
+
import type { ResourceRecord } from '@owlmeans/resource';
|
|
2
3
|
type Config = ClientConfig;
|
|
3
4
|
interface Context<C extends Config = Config> extends ClientContext<C> {
|
|
4
5
|
}
|
|
5
|
-
|
|
6
|
+
/**
|
|
7
|
+
* A key-value client store dressed as a resource: records live under their own id and a list of
|
|
8
|
+
* those ids under {@link LIST_KEY} is the only index there is. Queries therefore read the whole
|
|
9
|
+
* set and run through the shared in-memory engine, which is what makes a criteria object mean
|
|
10
|
+
* here exactly what it means against a relational store.
|
|
11
|
+
*/
|
|
12
|
+
export declare const appendClientResource: <C extends Config, T extends Context<C>, R extends ResourceRecord = ResourceRecord>(context: T, alias: string) => T;
|
|
6
13
|
export {};
|
|
7
14
|
//# sourceMappingURL=resource.d.ts.map
|
package/build/resource.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resource.d.ts","sourceRoot":"","sources":["../src/resource.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAA;
|
|
1
|
+
{"version":3,"file":"resource.d.ts","sourceRoot":"","sources":["../src/resource.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAA;AAM3E,OAAO,KAAK,EAAuC,cAAc,EAAE,MAAM,oBAAoB,CAAA;AAM7F,KAAK,MAAM,GAAG,YAAY,CAAA;AAC1B,UAAU,OAAO,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,CAAE,SAAQ,aAAa,CAAC,CAAC,CAAC;CAAI;AAEzE;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,GAC/B,CAAC,SAAS,MAAM,EAAE,CAAC,SAAS,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,SAAS,cAAc,GAAG,cAAc,EACjF,SAAS,CAAC,EAAE,OAAO,MAAM,KAAG,CAuJ7B,CAAA"}
|
package/build/resource.js
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import { appendContextual, assertContext } from '@owlmeans/context';
|
|
2
|
-
import { RecordExists, ResourceError, UnknownRecordError } from '@owlmeans/resource';
|
|
2
|
+
import { applyQuery, filterRecords, firstMatch, MisshapedRecord, RecordExists, ResourceError, UnknownRecordError, UnsupportedArgumentError } from '@owlmeans/resource';
|
|
3
3
|
import { DEFAULT_DB_ALIAS, LIST_KEY } from './consts.js';
|
|
4
4
|
import { base58 } from '@scure/base';
|
|
5
5
|
import { randomBytes } from '@noble/hashes/utils';
|
|
6
|
+
/**
|
|
7
|
+
* A key-value client store dressed as a resource: records live under their own id and a list of
|
|
8
|
+
* those ids under {@link LIST_KEY} is the only index there is. Queries therefore read the whole
|
|
9
|
+
* set and run through the shared in-memory engine, which is what makes a criteria object mean
|
|
10
|
+
* here exactly what it means against a relational store.
|
|
11
|
+
*/
|
|
6
12
|
export const appendClientResource = (context, alias) => {
|
|
7
13
|
const location = `client-resource:${alias}`;
|
|
8
14
|
const assert = () => {
|
|
@@ -11,6 +17,24 @@ export const appendClientResource = (context, alias) => {
|
|
|
11
17
|
}
|
|
12
18
|
return resource.db;
|
|
13
19
|
};
|
|
20
|
+
const ids = async () => await assert().get(LIST_KEY) ?? [];
|
|
21
|
+
const readAll = async () => {
|
|
22
|
+
const db = assert();
|
|
23
|
+
const records = await Promise.all((await ids()).map(id => db.get(id)));
|
|
24
|
+
return records.filter((record) => record != null);
|
|
25
|
+
};
|
|
26
|
+
const dropId = async (id) => {
|
|
27
|
+
const list = await ids();
|
|
28
|
+
const idx = list.indexOf(id);
|
|
29
|
+
if (idx > -1) {
|
|
30
|
+
list.splice(idx, 1);
|
|
31
|
+
await assert().set(LIST_KEY, list);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
/** An id is a direct key read; criteria have to walk the set. */
|
|
35
|
+
const first = async (idOrWhere, opts) => typeof idOrWhere === 'string'
|
|
36
|
+
? await assert().get(idOrWhere) ?? null
|
|
37
|
+
: firstMatch(await readAll(), idOrWhere, opts);
|
|
14
38
|
const resource = appendContextual(alias, {
|
|
15
39
|
init: async () => {
|
|
16
40
|
const context = assertContext(resource.ctx, location);
|
|
@@ -18,59 +42,27 @@ export const appendClientResource = (context, alias) => {
|
|
|
18
42
|
const dbService = context.service(config.service);
|
|
19
43
|
resource.db = await dbService.initialize(config.schema ?? alias);
|
|
20
44
|
},
|
|
21
|
-
get: async (
|
|
22
|
-
const record = await
|
|
45
|
+
get: async (idOrWhere, opts) => {
|
|
46
|
+
const record = await first(idOrWhere, opts);
|
|
23
47
|
if (record == null) {
|
|
24
|
-
throw new UnknownRecordError(
|
|
48
|
+
throw new UnknownRecordError(typeof idOrWhere === 'string' ? idOrWhere : 'criteria');
|
|
25
49
|
}
|
|
26
50
|
return record;
|
|
27
51
|
},
|
|
28
|
-
load: async (
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
if (criteria == null) {
|
|
34
|
-
criteria = {};
|
|
35
|
-
opts = { pager: { page: 0, size: 10 } };
|
|
52
|
+
load: async (idOrWhere, opts) => await first(idOrWhere, opts),
|
|
53
|
+
/** Unpaged unless a size is asked for. */
|
|
54
|
+
list: async (where, opts) => {
|
|
55
|
+
if (opts?.page != null && opts.size == null) {
|
|
56
|
+
throw new UnsupportedArgumentError('page-without-size');
|
|
36
57
|
}
|
|
37
|
-
|
|
38
|
-
opts = criteria;
|
|
39
|
-
criteria = criteria.criteria;
|
|
40
|
-
}
|
|
41
|
-
const db = assert();
|
|
42
|
-
const list = await db.get(LIST_KEY);
|
|
43
|
-
const pager = opts?.pager ?? { page: 0, size: 10 };
|
|
44
|
-
const conditions = Object.entries(criteria ?? {});
|
|
45
|
-
pager.page = pager.page ?? 0;
|
|
46
|
-
pager.size = pager.size ?? 10;
|
|
47
|
-
const result = [];
|
|
48
|
-
let skip = pager.page * pager.size;
|
|
49
|
-
let total = 0;
|
|
50
|
-
let added = 0;
|
|
51
|
-
for (const id of list) {
|
|
52
|
-
const record = await db.get(id);
|
|
53
|
-
if (record != null) {
|
|
54
|
-
if (!(conditions.length === 0 || conditions.every(([key, value]) => record[key] === value))) {
|
|
55
|
-
continue;
|
|
56
|
-
}
|
|
57
|
-
if (skip-- <= 0 && added < pager.size) {
|
|
58
|
-
added++;
|
|
59
|
-
result.push(record);
|
|
60
|
-
}
|
|
61
|
-
++total;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
return { items: result, pager: { ...pager, total } };
|
|
58
|
+
return applyQuery(await readAll(), where, opts);
|
|
65
59
|
},
|
|
60
|
+
count: async (where) => filterRecords(await readAll(), where).length,
|
|
66
61
|
create: async (record) => {
|
|
67
62
|
const db = assert();
|
|
68
63
|
record.id = record.id ?? base58.encode(randomBytes(32));
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
72
|
-
const list = await db.get(LIST_KEY) ?? [];
|
|
73
|
-
if (list.includes(record.id)) {
|
|
64
|
+
const list = await ids();
|
|
65
|
+
if (list.includes(record.id) || await db.has(record.id)) {
|
|
74
66
|
throw new RecordExists(record.id);
|
|
75
67
|
}
|
|
76
68
|
list.push(record.id);
|
|
@@ -78,76 +70,57 @@ export const appendClientResource = (context, alias) => {
|
|
|
78
70
|
await db.set(record.id, record);
|
|
79
71
|
return record;
|
|
80
72
|
},
|
|
73
|
+
/** Replaces the stored record rather than merging into it, as the contract says. */
|
|
81
74
|
update: async (record) => {
|
|
82
75
|
const db = assert();
|
|
83
76
|
if (record.id == null) {
|
|
84
|
-
throw new
|
|
77
|
+
throw new MisshapedRecord('id');
|
|
85
78
|
}
|
|
86
|
-
|
|
87
|
-
if (update == null) {
|
|
79
|
+
if (!await db.has(record.id)) {
|
|
88
80
|
throw new UnknownRecordError(record.id);
|
|
89
81
|
}
|
|
90
|
-
|
|
91
|
-
await db.set(record.id, update);
|
|
82
|
+
await db.set(record.id, record);
|
|
92
83
|
return record;
|
|
93
84
|
},
|
|
85
|
+
save: async (record) => record.id == null || !await assert().has(record.id)
|
|
86
|
+
? await resource.create(record)
|
|
87
|
+
: await resource.update(record),
|
|
94
88
|
delete: async (id) => {
|
|
95
|
-
if (typeof id === 'object') {
|
|
96
|
-
if (id.id == null) {
|
|
97
|
-
throw new UnknownRecordError('delete');
|
|
98
|
-
}
|
|
99
|
-
return resource.delete(id.id);
|
|
100
|
-
}
|
|
101
89
|
const db = assert();
|
|
102
|
-
if (!await db.has(id)) {
|
|
103
|
-
return null;
|
|
104
|
-
}
|
|
105
90
|
const record = await db.get(id);
|
|
106
91
|
if (record == null) {
|
|
107
|
-
|
|
92
|
+
return null;
|
|
108
93
|
}
|
|
109
94
|
await db.del(id);
|
|
110
|
-
|
|
111
|
-
const idx = list.indexOf(id);
|
|
112
|
-
if (idx > -1) {
|
|
113
|
-
list.splice(idx, 1);
|
|
114
|
-
await db.set(LIST_KEY, list);
|
|
115
|
-
}
|
|
95
|
+
await dropId(id);
|
|
116
96
|
return record;
|
|
117
97
|
},
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
throw new UnknownRecordError('pick');
|
|
122
|
-
}
|
|
123
|
-
return resource.pick(id.id);
|
|
124
|
-
}
|
|
125
|
-
const db = assert();
|
|
126
|
-
const record = await db.get(id);
|
|
127
|
-
if (record != null) {
|
|
128
|
-
await db.del(id);
|
|
129
|
-
}
|
|
130
|
-
else {
|
|
98
|
+
take: async (id) => {
|
|
99
|
+
const record = await resource.delete(id);
|
|
100
|
+
if (record == null) {
|
|
131
101
|
throw new UnknownRecordError(id);
|
|
132
102
|
}
|
|
133
|
-
const list = await db.get(LIST_KEY) ?? [];
|
|
134
|
-
const idx = list.indexOf(id);
|
|
135
|
-
if (idx > -1) {
|
|
136
|
-
list.splice(idx, 1);
|
|
137
|
-
await db.set(LIST_KEY, list);
|
|
138
|
-
}
|
|
139
103
|
return record;
|
|
140
104
|
},
|
|
141
|
-
|
|
142
|
-
if (
|
|
143
|
-
|
|
105
|
+
purge: async (where) => {
|
|
106
|
+
if (where == null || Object.keys(where).length < 1) {
|
|
107
|
+
throw new UnsupportedArgumentError('purge:empty-criteria');
|
|
108
|
+
}
|
|
109
|
+
const matched = filterRecords(await readAll(), where);
|
|
110
|
+
for (const record of matched) {
|
|
111
|
+
if (record.id != null) {
|
|
112
|
+
await resource.delete(record.id);
|
|
113
|
+
}
|
|
144
114
|
}
|
|
145
|
-
return
|
|
115
|
+
return matched.length;
|
|
146
116
|
},
|
|
147
117
|
erase: async () => {
|
|
148
118
|
const db = assert();
|
|
149
|
-
|
|
150
|
-
|
|
119
|
+
/** The index is emptied in one write, so a failed pass cannot leave it naming dead ids. */
|
|
120
|
+
for (const id of await ids()) {
|
|
121
|
+
await db.del(id);
|
|
122
|
+
}
|
|
123
|
+
await db.set(LIST_KEY, []);
|
|
151
124
|
}
|
|
152
125
|
});
|
|
153
126
|
context.registerResource(resource);
|
package/build/resource.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resource.js","sourceRoot":"","sources":["../src/resource.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AACnE,OAAO,EAAE,YAAY,EAAE,aAAa,
|
|
1
|
+
{"version":3,"file":"resource.js","sourceRoot":"","sources":["../src/resource.ts"],"names":[],"mappings":"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"}
|
package/build/types.d.ts
CHANGED
|
@@ -5,7 +5,8 @@ export interface ClientDbService extends InitializedService {
|
|
|
5
5
|
erase: () => Promise<void>;
|
|
6
6
|
}
|
|
7
7
|
export interface ClientDb {
|
|
8
|
-
|
|
8
|
+
/** `undefined` for a key the store does not hold — a miss is not an error at this level. */
|
|
9
|
+
get: <T>(id: string) => Promise<T | undefined>;
|
|
9
10
|
set: <T>(id: string, value: T) => Promise<void>;
|
|
10
11
|
has: (id: string) => Promise<boolean>;
|
|
11
12
|
del: (id: string) => Promise<boolean>;
|
package/build/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAA;AAC3D,OAAO,KAAK,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AAElE,MAAM,WAAW,eAAgB,SAAQ,kBAAkB;IACzD,UAAU,EAAE,CAAC,KAAK,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAA;IACjD,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAC3B;AAED,MAAM,WAAW,QAAQ;IACvB,GAAG,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,CAAA;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAA;AAC3D,OAAO,KAAK,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AAElE,MAAM,WAAW,eAAgB,SAAQ,kBAAkB;IACzD,UAAU,EAAE,CAAC,KAAK,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAA;IACjD,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAC3B;AAED,MAAM,WAAW,QAAQ;IACvB,4FAA4F;IAC5F,GAAG,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC,CAAA;IAC9C,GAAG,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IAC/C,GAAG,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;IACrC,GAAG,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;CACtC;AAED,MAAM,WAAW,cAAc,CAAC,CAAC,SAAS,cAAc,GAAG,cAAc,CAAE,SAAQ,QAAQ,CAAC,CAAC,CAAC;IAC5F,EAAE,CAAC,EAAE,QAAQ,CAAA;IACb,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAC3B"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@owlmeans/client-resource",
|
|
3
|
-
"version": "0.1.18-rc.
|
|
3
|
+
"version": "0.1.18-rc.11",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -22,9 +22,9 @@
|
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
24
|
"@noble/hashes": "^1.5.0",
|
|
25
|
-
"@owlmeans/client-context": "^0.1.18-rc.
|
|
26
|
-
"@owlmeans/context": "^0.1.18-rc.
|
|
27
|
-
"@owlmeans/resource": "^0.1.18-rc.
|
|
25
|
+
"@owlmeans/client-context": "^0.1.18-rc.11",
|
|
26
|
+
"@owlmeans/context": "^0.1.18-rc.7",
|
|
27
|
+
"@owlmeans/resource": "^0.1.18-rc.8",
|
|
28
28
|
"@scure/base": "^2.3.0"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
package/src/resource.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import type { ClientConfig, ClientContext } from '@owlmeans/client-context'
|
|
2
2
|
import { appendContextual, assertContext } from '@owlmeans/context'
|
|
3
|
-
import {
|
|
4
|
-
|
|
3
|
+
import {
|
|
4
|
+
applyQuery, filterRecords, firstMatch, MisshapedRecord, RecordExists, ResourceError,
|
|
5
|
+
UnknownRecordError, UnsupportedArgumentError
|
|
6
|
+
} from '@owlmeans/resource'
|
|
7
|
+
import type { Criteria, FirstOptions, ListOptions, ResourceRecord } from '@owlmeans/resource'
|
|
5
8
|
import { DEFAULT_DB_ALIAS, LIST_KEY } from './consts.js'
|
|
6
9
|
import type { ClientDb, ClientDbService, ClientResource } from './types.js'
|
|
7
10
|
import { base58 } from '@scure/base'
|
|
@@ -10,7 +13,15 @@ import { randomBytes } from '@noble/hashes/utils'
|
|
|
10
13
|
type Config = ClientConfig
|
|
11
14
|
interface Context<C extends Config = Config> extends ClientContext<C> { }
|
|
12
15
|
|
|
13
|
-
|
|
16
|
+
/**
|
|
17
|
+
* A key-value client store dressed as a resource: records live under their own id and a list of
|
|
18
|
+
* those ids under {@link LIST_KEY} is the only index there is. Queries therefore read the whole
|
|
19
|
+
* set and run through the shared in-memory engine, which is what makes a criteria object mean
|
|
20
|
+
* here exactly what it means against a relational store.
|
|
21
|
+
*/
|
|
22
|
+
export const appendClientResource = <
|
|
23
|
+
C extends Config, T extends Context<C>, R extends ResourceRecord = ResourceRecord
|
|
24
|
+
>(context: T, alias: string): T => {
|
|
14
25
|
|
|
15
26
|
const location = `client-resource:${alias}`
|
|
16
27
|
|
|
@@ -22,7 +33,32 @@ export const appendClientResource = <C extends Config, T extends Context<C>>(con
|
|
|
22
33
|
return resource.db
|
|
23
34
|
}
|
|
24
35
|
|
|
25
|
-
const
|
|
36
|
+
const ids = async (): Promise<string[]> => await assert().get<string[]>(LIST_KEY) ?? []
|
|
37
|
+
|
|
38
|
+
const readAll = async (): Promise<R[]> => {
|
|
39
|
+
const db = assert()
|
|
40
|
+
const records = await Promise.all((await ids()).map(id => db.get<R>(id)))
|
|
41
|
+
|
|
42
|
+
return records.filter((record): record is Awaited<R> => record != null) as R[]
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const dropId = async (id: string): Promise<void> => {
|
|
46
|
+
const list = await ids()
|
|
47
|
+
const idx = list.indexOf(id)
|
|
48
|
+
if (idx > -1) {
|
|
49
|
+
list.splice(idx, 1)
|
|
50
|
+
await assert().set(LIST_KEY, list)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** An id is a direct key read; criteria have to walk the set. */
|
|
55
|
+
const first = async (
|
|
56
|
+
idOrWhere: string | Criteria<R>, opts?: FirstOptions<R>
|
|
57
|
+
): Promise<R | null> => typeof idOrWhere === 'string'
|
|
58
|
+
? await assert().get<R>(idOrWhere) ?? null
|
|
59
|
+
: firstMatch(await readAll(), idOrWhere, opts)
|
|
60
|
+
|
|
61
|
+
const resource: ClientResource<R> = appendContextual<ClientResource<R>>(alias, {
|
|
26
62
|
init: async () => {
|
|
27
63
|
const context = assertContext<Config, Context>(resource.ctx as Context, location)
|
|
28
64
|
const config = context.cfg.dbs?.find(db => db.alias === alias) ?? { service: DEFAULT_DB_ALIAS, host: [] }
|
|
@@ -30,167 +66,106 @@ export const appendClientResource = <C extends Config, T extends Context<C>>(con
|
|
|
30
66
|
resource.db = await dbService.initialize(config.schema ?? alias)
|
|
31
67
|
},
|
|
32
68
|
|
|
33
|
-
get: async
|
|
34
|
-
const record = await
|
|
35
|
-
|
|
69
|
+
get: async (idOrWhere: string | Criteria<R>, opts?: FirstOptions<R>): Promise<R> => {
|
|
70
|
+
const record = await first(idOrWhere, opts)
|
|
36
71
|
if (record == null) {
|
|
37
|
-
throw new UnknownRecordError(
|
|
72
|
+
throw new UnknownRecordError(typeof idOrWhere === 'string' ? idOrWhere : 'criteria')
|
|
38
73
|
}
|
|
39
74
|
|
|
40
|
-
return record
|
|
75
|
+
return record
|
|
41
76
|
},
|
|
42
77
|
|
|
78
|
+
load: async (idOrWhere: string | Criteria<R>, opts?: FirstOptions<R>): Promise<R | null> =>
|
|
79
|
+
await first(idOrWhere, opts),
|
|
43
80
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
},
|
|
49
|
-
|
|
50
|
-
list: async (criteria, opts) => {
|
|
51
|
-
if (criteria == null) {
|
|
52
|
-
criteria = {}
|
|
53
|
-
opts = { pager: { page: 0, size: 10 } }
|
|
54
|
-
}
|
|
55
|
-
if ("pager" in criteria) {
|
|
56
|
-
opts = criteria
|
|
57
|
-
criteria = criteria.criteria as ListCriteria
|
|
58
|
-
}
|
|
59
|
-
const db = assert()
|
|
60
|
-
const list = await db.get<string[]>(LIST_KEY)
|
|
61
|
-
|
|
62
|
-
const pager: ListPager = opts?.pager ?? { page: 0, size: 10 }
|
|
63
|
-
const conditions = Object.entries(criteria as ListCriteria ?? {})
|
|
64
|
-
pager.page = pager.page ?? 0
|
|
65
|
-
pager.size = pager.size ?? 10
|
|
66
|
-
|
|
67
|
-
const result: ResourceRecord[] = []
|
|
68
|
-
let skip = pager.page * pager.size
|
|
69
|
-
let total = 0
|
|
70
|
-
let added = 0
|
|
71
|
-
for (const id of list) {
|
|
72
|
-
const record = await db.get<ResourceRecord>(id)
|
|
73
|
-
if (record != null) {
|
|
74
|
-
if (!(conditions.length === 0 || conditions.every(
|
|
75
|
-
([key, value]) => record[key as keyof typeof record] === value
|
|
76
|
-
))) {
|
|
77
|
-
continue
|
|
78
|
-
}
|
|
79
|
-
if (skip-- <= 0 && added < pager.size) {
|
|
80
|
-
added++
|
|
81
|
-
result.push(record)
|
|
82
|
-
}
|
|
83
|
-
++total
|
|
84
|
-
}
|
|
81
|
+
/** Unpaged unless a size is asked for. */
|
|
82
|
+
list: async (where?: Criteria<R>, opts?: ListOptions<R>) => {
|
|
83
|
+
if (opts?.page != null && opts.size == null) {
|
|
84
|
+
throw new UnsupportedArgumentError('page-without-size')
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
return
|
|
87
|
+
return applyQuery(await readAll(), where, opts)
|
|
88
88
|
},
|
|
89
89
|
|
|
90
|
-
|
|
90
|
+
count: async (where?: Criteria<R>) => filterRecords(await readAll(), where).length,
|
|
91
|
+
|
|
92
|
+
create: async (record: Partial<R>) => {
|
|
91
93
|
const db = assert()
|
|
92
94
|
|
|
93
95
|
record.id = record.id ?? base58.encode(randomBytes(32))
|
|
94
96
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const list: string[] = await db.get(LIST_KEY) ?? []
|
|
100
|
-
if (list.includes(record.id)) {
|
|
97
|
+
const list = await ids()
|
|
98
|
+
if (list.includes(record.id) || await db.has(record.id)) {
|
|
101
99
|
throw new RecordExists(record.id)
|
|
102
100
|
}
|
|
103
101
|
list.push(record.id)
|
|
104
102
|
await db.set(LIST_KEY, list)
|
|
105
103
|
await db.set(record.id, record)
|
|
106
104
|
|
|
107
|
-
return record as
|
|
105
|
+
return record as R
|
|
108
106
|
},
|
|
109
107
|
|
|
110
|
-
|
|
108
|
+
/** Replaces the stored record rather than merging into it, as the contract says. */
|
|
109
|
+
update: async (record: Partial<R>) => {
|
|
111
110
|
const db = assert()
|
|
112
111
|
|
|
113
112
|
if (record.id == null) {
|
|
114
|
-
throw new
|
|
113
|
+
throw new MisshapedRecord('id')
|
|
115
114
|
}
|
|
116
|
-
|
|
117
|
-
const update = await resource.load(record.id)
|
|
118
|
-
if (update == null) {
|
|
115
|
+
if (!await db.has(record.id)) {
|
|
119
116
|
throw new UnknownRecordError(record.id)
|
|
120
117
|
}
|
|
118
|
+
await db.set(record.id, record)
|
|
121
119
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
await db.set(record.id, update)
|
|
125
|
-
|
|
126
|
-
return record as any
|
|
120
|
+
return record as R
|
|
127
121
|
},
|
|
128
122
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
throw new UnknownRecordError('delete')
|
|
133
|
-
}
|
|
134
|
-
return resource.delete(id.id)
|
|
135
|
-
}
|
|
123
|
+
save: async (record: Partial<R>) => record.id == null || !await assert().has(record.id)
|
|
124
|
+
? await resource.create(record)
|
|
125
|
+
: await resource.update(record),
|
|
136
126
|
|
|
127
|
+
delete: async (id: string) => {
|
|
137
128
|
const db = assert()
|
|
138
|
-
|
|
139
|
-
return null
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const record = await db.get(id)
|
|
129
|
+
const record = await db.get<R>(id)
|
|
143
130
|
if (record == null) {
|
|
144
|
-
|
|
131
|
+
return null
|
|
145
132
|
}
|
|
146
133
|
await db.del(id)
|
|
147
|
-
|
|
148
|
-
const idx = list.indexOf(id)
|
|
149
|
-
if (idx > -1) {
|
|
150
|
-
list.splice(idx, 1)
|
|
151
|
-
await db.set(LIST_KEY, list)
|
|
152
|
-
}
|
|
134
|
+
await dropId(id)
|
|
153
135
|
|
|
154
|
-
return record
|
|
136
|
+
return record
|
|
155
137
|
},
|
|
156
138
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
throw new UnknownRecordError('pick')
|
|
161
|
-
}
|
|
162
|
-
return resource.pick(id.id)
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
const db = assert()
|
|
166
|
-
const record = await db.get(id)
|
|
167
|
-
if (record != null) {
|
|
168
|
-
await db.del(id)
|
|
169
|
-
} else {
|
|
139
|
+
take: async (id: string) => {
|
|
140
|
+
const record = await resource.delete(id)
|
|
141
|
+
if (record == null) {
|
|
170
142
|
throw new UnknownRecordError(id)
|
|
171
143
|
}
|
|
172
|
-
const list: string[] = await db.get(LIST_KEY) ?? []
|
|
173
|
-
const idx = list.indexOf(id)
|
|
174
|
-
if (idx > -1) {
|
|
175
|
-
list.splice(idx, 1)
|
|
176
|
-
await db.set(LIST_KEY, list)
|
|
177
|
-
}
|
|
178
144
|
|
|
179
|
-
return record
|
|
145
|
+
return record
|
|
180
146
|
},
|
|
181
147
|
|
|
182
|
-
|
|
183
|
-
if (
|
|
184
|
-
|
|
148
|
+
purge: async (where: Criteria<R>) => {
|
|
149
|
+
if (where == null || Object.keys(where).length < 1) {
|
|
150
|
+
throw new UnsupportedArgumentError('purge:empty-criteria')
|
|
151
|
+
}
|
|
152
|
+
const matched = filterRecords(await readAll(), where)
|
|
153
|
+
for (const record of matched) {
|
|
154
|
+
if (record.id != null) {
|
|
155
|
+
await resource.delete(record.id)
|
|
156
|
+
}
|
|
185
157
|
}
|
|
186
158
|
|
|
187
|
-
return
|
|
159
|
+
return matched.length
|
|
188
160
|
},
|
|
189
161
|
|
|
190
162
|
erase: async () => {
|
|
191
163
|
const db = assert()
|
|
192
|
-
|
|
193
|
-
|
|
164
|
+
/** The index is emptied in one write, so a failed pass cannot leave it naming dead ids. */
|
|
165
|
+
for (const id of await ids()) {
|
|
166
|
+
await db.del(id)
|
|
167
|
+
}
|
|
168
|
+
await db.set(LIST_KEY, [])
|
|
194
169
|
}
|
|
195
170
|
})
|
|
196
171
|
|
package/src/types.ts
CHANGED
|
@@ -7,7 +7,8 @@ export interface ClientDbService extends InitializedService {
|
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
export interface ClientDb {
|
|
10
|
-
|
|
10
|
+
/** `undefined` for a key the store does not hold — a miss is not an error at this level. */
|
|
11
|
+
get: <T>(id: string) => Promise<T | undefined>
|
|
11
12
|
set: <T>(id: string, value: T) => Promise<void>
|
|
12
13
|
has: (id: string) => Promise<boolean>
|
|
13
14
|
del: (id: string) => Promise<boolean>
|
package/build/.gitkeep
DELETED
|
File without changes
|