@owlmeans/static-resource 0.1.18-rc.3 → 0.1.18-rc.30
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 +7 -2
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/static-resource/SKILL.md +46 -11
- package/build/resource.d.ts +1 -1
- package/build/resource.d.ts.map +1 -1
- package/build/resource.js +81 -101
- package/build/resource.js.map +1 -1
- package/package.json +4 -4
- package/src/resource.ts +99 -113
- package/build/.gitkeep +0 -0
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ In-memory `Resource<T>` backed by static config data — for read-only data load
|
|
|
12
12
|
## Installation
|
|
13
13
|
|
|
14
14
|
```bash
|
|
15
|
-
bun add @owlmeans/static-resource
|
|
15
|
+
bun add @owlmeans/static-resource@^0.1.18-rc.28
|
|
16
16
|
```
|
|
17
17
|
|
|
18
18
|
## Usage
|
|
@@ -26,8 +26,13 @@ appendStaticResource<C, T>(context, 'products')
|
|
|
26
26
|
// Access from context
|
|
27
27
|
const resource = context.getStaticResource<Product>('products')
|
|
28
28
|
const product = await resource.load('product-sku')
|
|
29
|
+
const active = await resource.list({ status: 'active' })
|
|
29
30
|
```
|
|
30
31
|
|
|
32
|
+
Reads are unpaged: `list(where)` returns every match, and `list(where, { page })` without a `size`
|
|
33
|
+
throws `UnsupportedArgumentError('page-without-size')`. Criteria and sorting are the shared
|
|
34
|
+
vocabulary from [`@owlmeans/resource`](../resource), evaluated in memory.
|
|
35
|
+
|
|
31
36
|
## API
|
|
32
37
|
|
|
33
38
|
### `createStaticResource(alias?, key?): Resource<ResourceRecord>`
|
|
@@ -58,7 +63,7 @@ This package ships embedded agent skills under `agent-meta/`. After installing y
|
|
|
58
63
|
your project's skill store (`.agents/skills/`):
|
|
59
64
|
|
|
60
65
|
```sh
|
|
61
|
-
npx @owlmeans/agent-skills
|
|
66
|
+
npx @owlmeans/agent-skills@^0.1.18-rc.33
|
|
62
67
|
```
|
|
63
68
|
|
|
64
69
|
The embedded files are version-matched to this package release. Do not edit them
|
package/agent-meta/manifest.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 2,
|
|
3
3
|
"package": "@owlmeans/static-resource",
|
|
4
|
-
"version": "0.1.18-rc.
|
|
5
|
-
"generatedAt": "2026-
|
|
4
|
+
"version": "0.1.18-rc.30",
|
|
5
|
+
"generatedAt": "2026-09-22T18:27:08.249Z",
|
|
6
6
|
"canonicalRepo": "https://github.com/owlmeans/common",
|
|
7
7
|
"entries": [
|
|
8
8
|
{
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: static-resource
|
|
3
|
-
description: How to use @owlmeans/static-resource — Resource for
|
|
3
|
+
description: How to use @owlmeans/static-resource — in-process Resource over a module-scope Map, for records an app holds in memory (fixtures, caches, short-lived handshake state). Auto-invoked when registering an in-memory resource on a context.
|
|
4
4
|
user-invocable: false
|
|
5
5
|
---
|
|
6
6
|
<!-- AUTO-GENERATED — do not edit. Regenerate via sync-agent-meta. -->
|
|
@@ -8,27 +8,62 @@ user-invocable: false
|
|
|
8
8
|
# @owlmeans/static-resource
|
|
9
9
|
|
|
10
10
|
**Layer:** Infra
|
|
11
|
-
**Install:** `"@owlmeans/static-resource": "^0.1.18-rc.
|
|
11
|
+
**Install:** `"@owlmeans/static-resource": "^0.1.18-rc.30"` in `dependencies`
|
|
12
|
+
|
|
13
|
+
The whole `Resource` contract over a `Map` held at module scope. Use it where records must be
|
|
14
|
+
reachable through the context like any other resource but have no database behind them — fixtures,
|
|
15
|
+
a process-local cache, short-lived handshake state.
|
|
12
16
|
|
|
13
17
|
## Key Exports
|
|
14
18
|
|
|
15
19
|
| Export | Description |
|
|
16
20
|
|--------|-------------|
|
|
17
|
-
| `
|
|
18
|
-
| `
|
|
19
|
-
|
|
|
21
|
+
| `appendStaticResource(context, alias?, key?)` | Register one on the context and add `getStaticResource(alias?)` to it |
|
|
22
|
+
| `createStaticResource<T>(alias?, key?)` | The bare `Resource<T>`, when you register it yourself |
|
|
23
|
+
| `StaticResourceAppend` | The `getStaticResource` accessor the append adds |
|
|
24
|
+
| `DEFAULT_ALIAS` (`static`) | Constant |
|
|
25
|
+
|
|
26
|
+
`key` names the underlying store, defaulting to the alias — two resources sharing a `key` share
|
|
27
|
+
records, which is how two contexts in one process see the same set.
|
|
20
28
|
|
|
21
29
|
## Usage
|
|
22
30
|
|
|
23
31
|
```typescript
|
|
24
|
-
import {
|
|
32
|
+
import { appendStaticResource } from '@owlmeans/static-resource'
|
|
33
|
+
|
|
34
|
+
appendStaticResource(context, 'fixtures')
|
|
25
35
|
|
|
26
|
-
context.
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}))
|
|
36
|
+
const fixtures = context.getStaticResource<Fixture>('fixtures')
|
|
37
|
+
await fixtures.save({ id: 'seed', name: 'Owl' })
|
|
38
|
+
await fixtures.list({ name: { $startsWith: 'Owl' } }, { sort: ['name'] })
|
|
30
39
|
```
|
|
31
40
|
|
|
41
|
+
## Semantics
|
|
42
|
+
|
|
43
|
+
- The store is a map keyed by id and there is nothing here to mint one from, so **every write
|
|
44
|
+
needs an id**: `create`, `update` and `save` all throw `MisshapedRecord('id')` without one.
|
|
45
|
+
`create` additionally throws `RecordExists`, `update` `UnknownRecordError`; `save` is the upsert.
|
|
46
|
+
- `load(id)` / `get(id)` / `delete(id)` / `take(id)` hit the map directly. `take(id)` **deletes**
|
|
47
|
+
the record it returns and throws `UnknownRecordError` on a miss.
|
|
48
|
+
- `load(where)`, `get(where)`, `list`, `count` and `purge` go through the shared in-memory engine
|
|
49
|
+
from `@owlmeans/resource`, so criteria and `{ sort }` mean exactly what they mean against a
|
|
50
|
+
database. `purge(where)` refuses an empty criteria object
|
|
51
|
+
(`UnsupportedArgumentError('purge:empty-criteria')` — the database backends spell the same
|
|
52
|
+
refusal `purge:no-criteria`, so match on the error type rather than the message).
|
|
53
|
+
- **Unpaged**: `list(where)` with no `size` returns every match — the whole store is already in
|
|
54
|
+
memory; `size: 0` says the same explicitly, and `page` without `size` throws
|
|
55
|
+
`UnsupportedArgumentError('page-without-size')`.
|
|
56
|
+
- `opts.ttl` on a write arms a timer that deletes the id when it elapses — a number is seconds from
|
|
57
|
+
now, a `Date` the instant to expire at.
|
|
58
|
+
- **An armed expiry cannot be postponed.** Nothing ever clears a timer, so a later write — with a
|
|
59
|
+
ttl or without one — leaves the original running, and when it fires it deletes whatever record now
|
|
60
|
+
sits under that id. Passing the ttl again renews nothing; it only adds a second timer behind the
|
|
61
|
+
first. A record that has to outlive its first ttl needs a fresh id.
|
|
62
|
+
- **The map holds the caller's own object.** `create`/`update`/`save` store the object handed in and
|
|
63
|
+
`load`/`get`/`list` hand it straight back — no clone, no serialization. Mutating a record you
|
|
64
|
+
saved or read rewrites the store in place, bypassing `update` entirely, which is not how the
|
|
65
|
+
database backends behave. Copy before mutating, or treat what comes out of here as frozen.
|
|
66
|
+
|
|
32
67
|
## Depends On
|
|
33
68
|
|
|
34
|
-
- `@owlmeans/resource`, `@owlmeans/
|
|
69
|
+
- `@owlmeans/resource`, `@owlmeans/context`
|
package/build/resource.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Resource, ResourceRecord } from '@owlmeans/resource';
|
|
2
2
|
import type { Config, Context, StaticResourceAppend } from './types.js';
|
|
3
|
-
export declare const createStaticResource: (alias?: string, key?: string) => Resource<
|
|
3
|
+
export declare const createStaticResource: <T extends ResourceRecord = ResourceRecord>(alias?: string, key?: string) => Resource<T>;
|
|
4
4
|
export declare const appendStaticResource: <C extends Config, T extends Context<C>>(ctx: T, alias?: string, key?: string) => T & StaticResourceAppend;
|
|
5
5
|
//# 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":"
|
|
1
|
+
{"version":3,"file":"resource.d.ts","sourceRoot":"","sources":["../src/resource.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAC2B,QAAQ,EAAE,cAAc,EAC9D,MAAM,oBAAoB,CAAA;AAK3B,OAAO,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAA;AAYvE,eAAO,MAAM,oBAAoB,GAAI,CAAC,SAAS,cAAc,GAAG,cAAc,UACrE,MAAM,QAAwB,MAAM,KAC1C,QAAQ,CAAC,CAAC,CAkIZ,CAAA;AAED,eAAO,MAAM,oBAAoB,GAAI,CAAC,SAAS,MAAM,EAAE,CAAC,SAAS,OAAO,CAAC,CAAC,CAAC,OACpE,CAAC,UAAS,MAAM,QAAwB,MAAM,KAClD,CAAC,GAAG,oBAWN,CAAA"}
|
package/build/resource.js
CHANGED
|
@@ -1,134 +1,114 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
1
2
|
import { appendContextual } from '@owlmeans/context';
|
|
2
3
|
import { DEFAULT_ALIAS } from './consts.js';
|
|
3
|
-
import { MisshapedRecord, RecordExists, UnknownRecordError, UnsupportedArgumentError } from '@owlmeans/resource';
|
|
4
|
+
import { applyQuery, filterRecords, firstMatch, MisshapedRecord, RecordExists, UnknownRecordError, UnsupportedArgumentError } from '@owlmeans/resource';
|
|
4
5
|
const stores = {};
|
|
6
|
+
/**
|
|
7
|
+
* Milliseconds until the record expires, which is what `setTimeout` wants. A number is seconds
|
|
8
|
+
* from now and a Date is the instant to expire at — the two spellings have to land on the same
|
|
9
|
+
* moment, or the same ttl means an hour through one and a second through the other.
|
|
10
|
+
*/
|
|
11
|
+
const expiresIn = (ttl) => ttl instanceof Date ? ttl.getTime() - Date.now() : ttl * 1000;
|
|
5
12
|
export const createStaticResource = (alias = DEFAULT_ALIAS, key) => {
|
|
6
|
-
|
|
13
|
+
const storeKey = key ?? alias;
|
|
7
14
|
const getStore = () => {
|
|
8
|
-
if (stores[
|
|
9
|
-
stores[
|
|
15
|
+
if (stores[storeKey] == null) {
|
|
16
|
+
stores[storeKey] = new Map();
|
|
10
17
|
}
|
|
11
|
-
return stores[
|
|
18
|
+
return stores[storeKey];
|
|
19
|
+
};
|
|
20
|
+
const records = () => [...getStore().values()];
|
|
21
|
+
const expire = (id, opts) => {
|
|
22
|
+
if (opts?.ttl == null) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const store = getStore();
|
|
26
|
+
setTimeout(() => void store.delete(id), expiresIn(opts.ttl));
|
|
27
|
+
};
|
|
28
|
+
/** An id hits the map directly; criteria go through the shared in-memory engine. */
|
|
29
|
+
const first = (idOrWhere, opts) => typeof idOrWhere === 'string'
|
|
30
|
+
? getStore().get(idOrWhere) ?? null
|
|
31
|
+
: firstMatch(records(), idOrWhere, opts);
|
|
32
|
+
const write = (record, opts) => {
|
|
33
|
+
if (record.id == null) {
|
|
34
|
+
/**
|
|
35
|
+
* The store is a map keyed by id and there is nothing here to mint one from, so a record
|
|
36
|
+
* without an id is misshaped rather than new — `save` cannot create it.
|
|
37
|
+
*/
|
|
38
|
+
throw new MisshapedRecord('id');
|
|
39
|
+
}
|
|
40
|
+
getStore().set(record.id, record);
|
|
41
|
+
expire(record.id, opts);
|
|
42
|
+
return record;
|
|
12
43
|
};
|
|
13
44
|
const resource = appendContextual(alias, {
|
|
14
|
-
get: async (
|
|
15
|
-
const record =
|
|
45
|
+
get: async (idOrWhere, opts) => {
|
|
46
|
+
const record = first(idOrWhere, opts);
|
|
16
47
|
if (record == null) {
|
|
17
|
-
throw new UnknownRecordError(
|
|
48
|
+
throw new UnknownRecordError(typeof idOrWhere === 'string' ? idOrWhere : 'criteria');
|
|
18
49
|
}
|
|
19
50
|
return record;
|
|
20
51
|
},
|
|
21
|
-
load: async (
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
52
|
+
load: async (idOrWhere, opts) => first(idOrWhere, opts),
|
|
53
|
+
/** Unpaged unless a size is asked for — the whole store is already in memory. */
|
|
54
|
+
list: async (where, opts) => {
|
|
55
|
+
if (opts?.page != null && opts.size == null) {
|
|
56
|
+
throw new UnsupportedArgumentError('page-without-size');
|
|
25
57
|
}
|
|
26
|
-
|
|
27
|
-
const record = field === 'id'
|
|
28
|
-
? store.get(id)
|
|
29
|
-
: [...store.values()].find(record => record[field] === id);
|
|
30
|
-
if (record == null) {
|
|
31
|
-
return null;
|
|
32
|
-
}
|
|
33
|
-
return record;
|
|
34
|
-
},
|
|
35
|
-
list: async (criteria, opts) => {
|
|
36
|
-
if (criteria != null) {
|
|
37
|
-
throw new UnsupportedArgumentError('static:list:criteria');
|
|
38
|
-
}
|
|
39
|
-
if (opts != null) {
|
|
40
|
-
throw new UnsupportedArgumentError('static:list:opts');
|
|
41
|
-
}
|
|
42
|
-
const result = {
|
|
43
|
-
items: [...getStore().values()]
|
|
44
|
-
};
|
|
45
|
-
return result;
|
|
46
|
-
},
|
|
47
|
-
save: async (record, opts) => {
|
|
48
|
-
const store = getStore();
|
|
49
|
-
if (record.id == null) {
|
|
50
|
-
throw new MisshapedRecord('id');
|
|
51
|
-
}
|
|
52
|
-
store.set(record.id, record);
|
|
53
|
-
if (typeof opts === 'object' && opts?.ttl != null) {
|
|
54
|
-
const ttl = opts.ttl instanceof Date
|
|
55
|
-
? (opts.ttl.getTime() - Date.now()) / 1000
|
|
56
|
-
: typeof opts.ttl === 'number'
|
|
57
|
-
? opts.ttl * 1000
|
|
58
|
-
: null;
|
|
59
|
-
if (ttl == null) {
|
|
60
|
-
throw new UnsupportedArgumentError('static:opts:ttl-string');
|
|
61
|
-
}
|
|
62
|
-
setTimeout(() => record.id != null && void store.delete(record.id), ttl);
|
|
63
|
-
}
|
|
64
|
-
return record;
|
|
58
|
+
return applyQuery(records(), where, opts);
|
|
65
59
|
},
|
|
60
|
+
count: async (where) => filterRecords(records(), where).length,
|
|
66
61
|
create: async (record, opts) => {
|
|
67
|
-
|
|
62
|
+
/**
|
|
63
|
+
* A supplied id is honored (many callers key by a natural id, Redis-style) — but a missing
|
|
64
|
+
* one is minted here rather than rejected, the same as a real Mongo/Postgres `create()`
|
|
65
|
+
* auto-generates its own id when the caller supplies none. Only a supplied id that already
|
|
66
|
+
* exists is `RecordExists`.
|
|
67
|
+
*/
|
|
68
68
|
if (record.id == null) {
|
|
69
|
-
|
|
69
|
+
record = { ...record, id: randomUUID() };
|
|
70
70
|
}
|
|
71
|
-
if (
|
|
71
|
+
if (getStore().has(record.id)) {
|
|
72
72
|
throw new RecordExists(record.id);
|
|
73
73
|
}
|
|
74
|
-
|
|
75
|
-
if (opts?.ttl != null) {
|
|
76
|
-
const ttl = opts.ttl instanceof Date
|
|
77
|
-
? (opts.ttl.getTime() - Date.now()) / 1000
|
|
78
|
-
: typeof opts.ttl === 'number'
|
|
79
|
-
? opts.ttl * 1000
|
|
80
|
-
: null;
|
|
81
|
-
if (ttl == null) {
|
|
82
|
-
throw new UnsupportedArgumentError('static:opts:ttl-string');
|
|
83
|
-
}
|
|
84
|
-
setTimeout(() => record.id != null && void store.delete(record.id), ttl);
|
|
85
|
-
}
|
|
86
|
-
return record;
|
|
74
|
+
return write(record, opts);
|
|
87
75
|
},
|
|
88
76
|
update: async (record, opts) => {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
delete: async (id, opts) => {
|
|
92
|
-
let record = null;
|
|
93
|
-
if (typeof id === 'object') {
|
|
94
|
-
if (id.id == null) {
|
|
95
|
-
throw new MisshapedRecord('id');
|
|
96
|
-
}
|
|
97
|
-
record = await resource.load(id.id);
|
|
98
|
-
}
|
|
99
|
-
else if (typeof opts === 'string') {
|
|
100
|
-
record = await resource.load(id, opts);
|
|
77
|
+
if (record.id == null) {
|
|
78
|
+
throw new MisshapedRecord('id');
|
|
101
79
|
}
|
|
102
|
-
|
|
103
|
-
|
|
80
|
+
if (!getStore().has(record.id)) {
|
|
81
|
+
throw new UnknownRecordError(record.id);
|
|
104
82
|
}
|
|
83
|
+
return write(record, opts);
|
|
84
|
+
},
|
|
85
|
+
save: async (record, opts) => write(record, opts),
|
|
86
|
+
delete: async (id) => {
|
|
87
|
+
const store = getStore();
|
|
88
|
+
const record = store.get(id) ?? null;
|
|
105
89
|
if (record == null) {
|
|
106
90
|
return null;
|
|
107
91
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
field = opts;
|
|
111
|
-
opts = undefined;
|
|
112
|
-
}
|
|
113
|
-
else if (typeof opts === 'object') {
|
|
114
|
-
field = opts.field ?? field;
|
|
115
|
-
}
|
|
116
|
-
id = record.id ?? '';
|
|
117
|
-
if (id === '') {
|
|
118
|
-
throw new MisshapedRecord('id');
|
|
119
|
-
}
|
|
120
|
-
const store = getStore();
|
|
121
|
-
if (store.delete(id)) {
|
|
122
|
-
return record;
|
|
123
|
-
}
|
|
124
|
-
return null;
|
|
92
|
+
store.delete(id);
|
|
93
|
+
return record;
|
|
125
94
|
},
|
|
126
|
-
|
|
127
|
-
const
|
|
95
|
+
take: async (id) => {
|
|
96
|
+
const store = getStore();
|
|
97
|
+
const record = store.get(id);
|
|
128
98
|
if (record == null) {
|
|
129
|
-
throw new UnknownRecordError(
|
|
99
|
+
throw new UnknownRecordError(id);
|
|
130
100
|
}
|
|
101
|
+
store.delete(id);
|
|
131
102
|
return record;
|
|
103
|
+
},
|
|
104
|
+
purge: async (where) => {
|
|
105
|
+
if (where == null || Object.keys(where).length < 1) {
|
|
106
|
+
throw new UnsupportedArgumentError('purge:empty-criteria');
|
|
107
|
+
}
|
|
108
|
+
const store = getStore();
|
|
109
|
+
const matched = filterRecords(records(), where);
|
|
110
|
+
matched.forEach(record => record.id != null && store.delete(record.id));
|
|
111
|
+
return matched.length;
|
|
132
112
|
}
|
|
133
113
|
});
|
|
134
114
|
return resource;
|
package/build/resource.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resource.js","sourceRoot":"","sources":["../src/resource.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAA;AACpD,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;
|
|
1
|
+
{"version":3,"file":"resource.js","sourceRoot":"","sources":["../src/resource.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@owlmeans/static-resource",
|
|
3
|
-
"version": "0.1.18-rc.
|
|
3
|
+
"version": "0.1.18-rc.30",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -21,9 +21,9 @@
|
|
|
21
21
|
}
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@owlmeans/context": "^0.1.18-rc.
|
|
25
|
-
"@owlmeans/error": "^0.1.18-rc.
|
|
26
|
-
"@owlmeans/resource": "^0.1.18-rc.
|
|
24
|
+
"@owlmeans/context": "^0.1.18-rc.29",
|
|
25
|
+
"@owlmeans/error": "^0.1.18-rc.31",
|
|
26
|
+
"@owlmeans/resource": "^0.1.18-rc.31"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"@owlmeans/dep-config": "workspace:*",
|
package/src/resource.ts
CHANGED
|
@@ -1,167 +1,153 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto'
|
|
1
2
|
import { appendContextual } from '@owlmeans/context'
|
|
2
3
|
import { DEFAULT_ALIAS } from './consts.js'
|
|
3
4
|
import type {
|
|
4
|
-
|
|
5
|
-
|
|
5
|
+
Criteria, FirstOptions, ListOptions, Resource, ResourceRecord, Ttl, WriteOptions
|
|
6
|
+
} from '@owlmeans/resource'
|
|
7
|
+
import {
|
|
8
|
+
applyQuery, filterRecords, firstMatch, MisshapedRecord, RecordExists, UnknownRecordError,
|
|
9
|
+
UnsupportedArgumentError
|
|
6
10
|
} from '@owlmeans/resource'
|
|
7
|
-
import { MisshapedRecord, RecordExists, UnknownRecordError, UnsupportedArgumentError } from '@owlmeans/resource'
|
|
8
11
|
import type { Config, Context, StaticResourceAppend } from './types.js'
|
|
9
12
|
|
|
10
|
-
type Getter = string | GetterOptions
|
|
11
|
-
type ResourceType = Resource<ResourceRecord>
|
|
12
|
-
|
|
13
13
|
const stores: Record<string, Map<string, ResourceRecord>> = {}
|
|
14
|
-
export const createStaticResource = (alias: string = DEFAULT_ALIAS, key?: string) => {
|
|
15
|
-
key = key ?? alias
|
|
16
14
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Milliseconds until the record expires, which is what `setTimeout` wants. A number is seconds
|
|
17
|
+
* from now and a Date is the instant to expire at — the two spellings have to land on the same
|
|
18
|
+
* moment, or the same ttl means an hour through one and a second through the other.
|
|
19
|
+
*/
|
|
20
|
+
const expiresIn = (ttl: Ttl): number =>
|
|
21
|
+
ttl instanceof Date ? ttl.getTime() - Date.now() : ttl * 1000
|
|
22
|
+
|
|
23
|
+
export const createStaticResource = <T extends ResourceRecord = ResourceRecord>(
|
|
24
|
+
alias: string = DEFAULT_ALIAS, key?: string
|
|
25
|
+
): Resource<T> => {
|
|
26
|
+
const storeKey = key ?? alias
|
|
27
|
+
|
|
28
|
+
const getStore = (): Map<string, T> => {
|
|
29
|
+
if (stores[storeKey] == null) {
|
|
30
|
+
stores[storeKey] = new Map()
|
|
20
31
|
}
|
|
21
32
|
|
|
22
|
-
return stores[
|
|
33
|
+
return stores[storeKey] as Map<string, T>
|
|
23
34
|
}
|
|
24
35
|
|
|
25
|
-
const
|
|
26
|
-
get: async <T extends ResourceRecord>(id: string, field?: Getter, opts?: LifecycleOptions) => {
|
|
27
|
-
const record = await resource.load<T>(id, field, opts)
|
|
28
|
-
if (record == null) {
|
|
29
|
-
throw new UnknownRecordError(id)
|
|
30
|
-
}
|
|
36
|
+
const records = (): T[] => [...getStore().values()]
|
|
31
37
|
|
|
32
|
-
|
|
33
|
-
|
|
38
|
+
const expire = (id: string, opts?: WriteOptions): void => {
|
|
39
|
+
if (opts?.ttl == null) {
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
const store = getStore()
|
|
43
|
+
setTimeout(() => void store.delete(id), expiresIn(opts.ttl))
|
|
44
|
+
}
|
|
34
45
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
+
/** An id hits the map directly; criteria go through the shared in-memory engine. */
|
|
47
|
+
const first = (idOrWhere: string | Criteria<T>, opts?: FirstOptions<T>): T | null =>
|
|
48
|
+
typeof idOrWhere === 'string'
|
|
49
|
+
? getStore().get(idOrWhere) ?? null
|
|
50
|
+
: firstMatch(records(), idOrWhere, opts)
|
|
51
|
+
|
|
52
|
+
const write = (record: Partial<T>, opts?: WriteOptions): T => {
|
|
53
|
+
if (record.id == null) {
|
|
54
|
+
/**
|
|
55
|
+
* The store is a map keyed by id and there is nothing here to mint one from, so a record
|
|
56
|
+
* without an id is misshaped rather than new — `save` cannot create it.
|
|
57
|
+
*/
|
|
58
|
+
throw new MisshapedRecord('id')
|
|
59
|
+
}
|
|
60
|
+
getStore().set(record.id, record as T)
|
|
61
|
+
expire(record.id, opts)
|
|
46
62
|
|
|
63
|
+
return record as T
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const resource: Resource<T> = appendContextual<Resource<T>>(alias, {
|
|
67
|
+
get: async (idOrWhere: string | Criteria<T>, opts?: FirstOptions<T>): Promise<T> => {
|
|
68
|
+
const record = first(idOrWhere, opts)
|
|
47
69
|
if (record == null) {
|
|
48
|
-
|
|
70
|
+
throw new UnknownRecordError(typeof idOrWhere === 'string' ? idOrWhere : 'criteria')
|
|
49
71
|
}
|
|
50
72
|
|
|
51
73
|
return record
|
|
52
74
|
},
|
|
53
75
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
throw new UnsupportedArgumentError('static:list:criteria')
|
|
57
|
-
}
|
|
58
|
-
if (opts != null) {
|
|
59
|
-
throw new UnsupportedArgumentError('static:list:opts')
|
|
60
|
-
}
|
|
76
|
+
load: async (idOrWhere: string | Criteria<T>, opts?: FirstOptions<T>): Promise<T | null> =>
|
|
77
|
+
first(idOrWhere, opts),
|
|
61
78
|
|
|
62
|
-
|
|
63
|
-
|
|
79
|
+
/** Unpaged unless a size is asked for — the whole store is already in memory. */
|
|
80
|
+
list: async (where?: Criteria<T>, opts?: ListOptions<T>) => {
|
|
81
|
+
if (opts?.page != null && opts.size == null) {
|
|
82
|
+
throw new UnsupportedArgumentError('page-without-size')
|
|
64
83
|
}
|
|
65
84
|
|
|
66
|
-
return
|
|
85
|
+
return applyQuery(records(), where, opts)
|
|
67
86
|
},
|
|
68
87
|
|
|
69
|
-
|
|
70
|
-
|
|
88
|
+
count: async (where?: Criteria<T>) => filterRecords(records(), where).length,
|
|
89
|
+
|
|
90
|
+
create: async (record: Partial<T>, opts?: WriteOptions) => {
|
|
91
|
+
/**
|
|
92
|
+
* A supplied id is honored (many callers key by a natural id, Redis-style) — but a missing
|
|
93
|
+
* one is minted here rather than rejected, the same as a real Mongo/Postgres `create()`
|
|
94
|
+
* auto-generates its own id when the caller supplies none. Only a supplied id that already
|
|
95
|
+
* exists is `RecordExists`.
|
|
96
|
+
*/
|
|
71
97
|
if (record.id == null) {
|
|
72
|
-
|
|
98
|
+
record = { ...record, id: randomUUID() }
|
|
73
99
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
if (typeof opts === 'object' && opts?.ttl != null) {
|
|
77
|
-
const ttl = opts.ttl instanceof Date
|
|
78
|
-
? (opts.ttl.getTime() - Date.now()) / 1000
|
|
79
|
-
: typeof opts.ttl === 'number'
|
|
80
|
-
? opts.ttl * 1000
|
|
81
|
-
: null
|
|
82
|
-
if (ttl == null) {
|
|
83
|
-
throw new UnsupportedArgumentError('static:opts:ttl-string')
|
|
84
|
-
}
|
|
85
|
-
setTimeout(() => record.id != null && void store.delete(record.id), ttl)
|
|
100
|
+
if (getStore().has(record.id as string)) {
|
|
101
|
+
throw new RecordExists(record.id as string)
|
|
86
102
|
}
|
|
87
103
|
|
|
88
|
-
return record
|
|
104
|
+
return write(record, opts)
|
|
89
105
|
},
|
|
90
106
|
|
|
91
|
-
|
|
92
|
-
const store = getStore()
|
|
107
|
+
update: async (record: Partial<T>, opts?: WriteOptions) => {
|
|
93
108
|
if (record.id == null) {
|
|
94
|
-
throw new
|
|
95
|
-
}
|
|
96
|
-
if (store.has(record.id)) {
|
|
97
|
-
throw new RecordExists(record.id)
|
|
109
|
+
throw new MisshapedRecord('id')
|
|
98
110
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
const ttl = opts.ttl instanceof Date
|
|
102
|
-
? (opts.ttl.getTime() - Date.now()) / 1000
|
|
103
|
-
: typeof opts.ttl === 'number'
|
|
104
|
-
? opts.ttl * 1000
|
|
105
|
-
: null
|
|
106
|
-
if (ttl == null) {
|
|
107
|
-
throw new UnsupportedArgumentError('static:opts:ttl-string')
|
|
108
|
-
}
|
|
109
|
-
setTimeout(() => record.id != null && void store.delete(record.id), ttl)
|
|
111
|
+
if (!getStore().has(record.id)) {
|
|
112
|
+
throw new UnknownRecordError(record.id)
|
|
110
113
|
}
|
|
111
114
|
|
|
112
|
-
return record
|
|
113
|
-
},
|
|
114
|
-
|
|
115
|
-
update: async (record, opts) => {
|
|
116
|
-
return resource.save(record, opts)
|
|
115
|
+
return write(record, opts)
|
|
117
116
|
},
|
|
118
117
|
|
|
119
|
-
|
|
120
|
-
let record: ResourceRecord | null = null
|
|
121
|
-
if (typeof id === 'object') {
|
|
122
|
-
if (id.id == null) {
|
|
123
|
-
throw new MisshapedRecord('id')
|
|
124
|
-
}
|
|
125
|
-
record = await resource.load(id.id)
|
|
126
|
-
} else if (typeof opts === 'string') {
|
|
127
|
-
record = await resource.load(id, opts)
|
|
128
|
-
} else if (typeof id === 'string') {
|
|
129
|
-
record = await resource.load(id)
|
|
130
|
-
}
|
|
118
|
+
save: async (record: Partial<T>, opts?: WriteOptions) => write(record, opts),
|
|
131
119
|
|
|
120
|
+
delete: async (id: string) => {
|
|
121
|
+
const store = getStore()
|
|
122
|
+
const record = store.get(id) ?? null
|
|
132
123
|
if (record == null) {
|
|
133
124
|
return null
|
|
134
125
|
}
|
|
126
|
+
store.delete(id)
|
|
135
127
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
field = opts
|
|
139
|
-
opts = undefined
|
|
140
|
-
} else if (typeof opts === 'object') {
|
|
141
|
-
field = opts.field ?? field
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
id = record.id ?? ''
|
|
145
|
-
|
|
146
|
-
if (id === '') {
|
|
147
|
-
throw new MisshapedRecord('id')
|
|
148
|
-
}
|
|
128
|
+
return record
|
|
129
|
+
},
|
|
149
130
|
|
|
131
|
+
take: async (id: string) => {
|
|
150
132
|
const store = getStore()
|
|
151
|
-
|
|
152
|
-
|
|
133
|
+
const record = store.get(id)
|
|
134
|
+
if (record == null) {
|
|
135
|
+
throw new UnknownRecordError(id)
|
|
153
136
|
}
|
|
137
|
+
store.delete(id)
|
|
154
138
|
|
|
155
|
-
return
|
|
139
|
+
return record
|
|
156
140
|
},
|
|
157
141
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
throw new UnknownRecordError((typeof id === 'string' ? id : id.id) ?? 'unknown')
|
|
142
|
+
purge: async (where: Criteria<T>) => {
|
|
143
|
+
if (where == null || Object.keys(where).length < 1) {
|
|
144
|
+
throw new UnsupportedArgumentError('purge:empty-criteria')
|
|
162
145
|
}
|
|
146
|
+
const store = getStore()
|
|
147
|
+
const matched = filterRecords(records(), where)
|
|
148
|
+
matched.forEach(record => record.id != null && store.delete(record.id))
|
|
163
149
|
|
|
164
|
-
return
|
|
150
|
+
return matched.length
|
|
165
151
|
}
|
|
166
152
|
})
|
|
167
153
|
|
package/build/.gitkeep
DELETED
|
File without changes
|