@owlmeans/static-resource 0.1.18-rc.7 → 0.1.18-rc.8
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 +5 -0
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/static-resource/SKILL.md +38 -11
- package/build/resource.d.ts +1 -1
- package/build/resource.d.ts.map +1 -1
- package/build/resource.js +74 -101
- package/build/resource.js.map +1 -1
- package/package.json +4 -4
- package/src/resource.ts +91 -112
- package/build/.gitkeep +0 -0
package/README.md
CHANGED
|
@@ -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>`
|
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.8",
|
|
5
|
+
"generatedAt": "2026-09-01T16:28:56.999Z",
|
|
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,54 @@ 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.8"` 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'
|
|
25
33
|
|
|
26
|
-
context
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
})
|
|
34
|
+
appendStaticResource(context, 'fixtures')
|
|
35
|
+
|
|
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
|
+
- **Unpaged**: `list(where)` with no `size` returns every match — the whole store is already in
|
|
52
|
+
memory; `size: 0` says the same explicitly, and `page` without `size` throws
|
|
53
|
+
`UnsupportedArgumentError('page-without-size')`.
|
|
54
|
+
- `opts.ttl` is honoured on writes — a number is seconds from now, a `Date` the instant to expire
|
|
55
|
+
at, and the record is dropped from the map when it elapses. An expiry already armed keeps its
|
|
56
|
+
appointment: rewriting the record without a ttl does not cancel it, so a renewal passes the ttl
|
|
57
|
+
again and a record that must survive gets a fresh id.
|
|
58
|
+
|
|
32
59
|
## Depends On
|
|
33
60
|
|
|
34
|
-
- `@owlmeans/resource`, `@owlmeans/
|
|
61
|
+
- `@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":"AAEA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"resource.d.ts","sourceRoot":"","sources":["../src/resource.ts"],"names":[],"mappings":"AAEA,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,EAC5E,QAAO,MAAsB,EAAE,MAAM,MAAM,KAC1C,QAAQ,CAAC,CAAC,CA4HZ,CAAA;AAED,eAAO,MAAM,oBAAoB,GAAI,CAAC,SAAS,MAAM,EAAE,CAAC,SAAS,OAAO,CAAC,CAAC,CAAC,EACzE,KAAK,CAAC,EAAE,QAAO,MAAsB,EAAE,MAAM,MAAM,KAClD,CAAC,GAAG,oBAWN,CAAA"}
|
package/build/resource.js
CHANGED
|
@@ -1,134 +1,107 @@
|
|
|
1
1
|
import { appendContextual } from '@owlmeans/context';
|
|
2
2
|
import { DEFAULT_ALIAS } from './consts.js';
|
|
3
|
-
import { MisshapedRecord, RecordExists, UnknownRecordError, UnsupportedArgumentError } from '@owlmeans/resource';
|
|
3
|
+
import { applyQuery, filterRecords, firstMatch, MisshapedRecord, RecordExists, UnknownRecordError, UnsupportedArgumentError } from '@owlmeans/resource';
|
|
4
4
|
const stores = {};
|
|
5
|
+
/**
|
|
6
|
+
* Milliseconds until the record expires, which is what `setTimeout` wants. A number is seconds
|
|
7
|
+
* from now and a Date is the instant to expire at — the two spellings have to land on the same
|
|
8
|
+
* moment, or the same ttl means an hour through one and a second through the other.
|
|
9
|
+
*/
|
|
10
|
+
const expiresIn = (ttl) => ttl instanceof Date ? ttl.getTime() - Date.now() : ttl * 1000;
|
|
5
11
|
export const createStaticResource = (alias = DEFAULT_ALIAS, key) => {
|
|
6
|
-
|
|
12
|
+
const storeKey = key ?? alias;
|
|
7
13
|
const getStore = () => {
|
|
8
|
-
if (stores[
|
|
9
|
-
stores[
|
|
14
|
+
if (stores[storeKey] == null) {
|
|
15
|
+
stores[storeKey] = new Map();
|
|
10
16
|
}
|
|
11
|
-
return stores[
|
|
17
|
+
return stores[storeKey];
|
|
18
|
+
};
|
|
19
|
+
const records = () => [...getStore().values()];
|
|
20
|
+
const expire = (id, opts) => {
|
|
21
|
+
if (opts?.ttl == null) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const store = getStore();
|
|
25
|
+
setTimeout(() => void store.delete(id), expiresIn(opts.ttl));
|
|
26
|
+
};
|
|
27
|
+
/** An id hits the map directly; criteria go through the shared in-memory engine. */
|
|
28
|
+
const first = (idOrWhere, opts) => typeof idOrWhere === 'string'
|
|
29
|
+
? getStore().get(idOrWhere) ?? null
|
|
30
|
+
: firstMatch(records(), idOrWhere, opts);
|
|
31
|
+
const write = (record, opts) => {
|
|
32
|
+
if (record.id == null) {
|
|
33
|
+
/**
|
|
34
|
+
* The store is a map keyed by id and there is nothing here to mint one from, so a record
|
|
35
|
+
* without an id is misshaped rather than new — `save` cannot create it.
|
|
36
|
+
*/
|
|
37
|
+
throw new MisshapedRecord('id');
|
|
38
|
+
}
|
|
39
|
+
getStore().set(record.id, record);
|
|
40
|
+
expire(record.id, opts);
|
|
41
|
+
return record;
|
|
12
42
|
};
|
|
13
43
|
const resource = appendContextual(alias, {
|
|
14
|
-
get: async (
|
|
15
|
-
const record =
|
|
44
|
+
get: async (idOrWhere, opts) => {
|
|
45
|
+
const record = first(idOrWhere, opts);
|
|
16
46
|
if (record == null) {
|
|
17
|
-
throw new UnknownRecordError(
|
|
47
|
+
throw new UnknownRecordError(typeof idOrWhere === 'string' ? idOrWhere : 'criteria');
|
|
18
48
|
}
|
|
19
49
|
return record;
|
|
20
50
|
},
|
|
21
|
-
load: async (
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
51
|
+
load: async (idOrWhere, opts) => first(idOrWhere, opts),
|
|
52
|
+
/** Unpaged unless a size is asked for — the whole store is already in memory. */
|
|
53
|
+
list: async (where, opts) => {
|
|
54
|
+
if (opts?.page != null && opts.size == null) {
|
|
55
|
+
throw new UnsupportedArgumentError('page-without-size');
|
|
25
56
|
}
|
|
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;
|
|
57
|
+
return applyQuery(records(), where, opts);
|
|
65
58
|
},
|
|
59
|
+
count: async (where) => filterRecords(records(), where).length,
|
|
66
60
|
create: async (record, opts) => {
|
|
67
|
-
const store = getStore();
|
|
68
61
|
if (record.id == null) {
|
|
69
|
-
throw new
|
|
62
|
+
throw new MisshapedRecord('id');
|
|
70
63
|
}
|
|
71
|
-
if (
|
|
64
|
+
if (getStore().has(record.id)) {
|
|
72
65
|
throw new RecordExists(record.id);
|
|
73
66
|
}
|
|
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;
|
|
67
|
+
return write(record, opts);
|
|
87
68
|
},
|
|
88
69
|
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);
|
|
70
|
+
if (record.id == null) {
|
|
71
|
+
throw new MisshapedRecord('id');
|
|
101
72
|
}
|
|
102
|
-
|
|
103
|
-
|
|
73
|
+
if (!getStore().has(record.id)) {
|
|
74
|
+
throw new UnknownRecordError(record.id);
|
|
104
75
|
}
|
|
76
|
+
return write(record, opts);
|
|
77
|
+
},
|
|
78
|
+
save: async (record, opts) => write(record, opts),
|
|
79
|
+
delete: async (id) => {
|
|
80
|
+
const store = getStore();
|
|
81
|
+
const record = store.get(id) ?? null;
|
|
105
82
|
if (record == null) {
|
|
106
83
|
return null;
|
|
107
84
|
}
|
|
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;
|
|
85
|
+
store.delete(id);
|
|
86
|
+
return record;
|
|
125
87
|
},
|
|
126
|
-
|
|
127
|
-
const
|
|
88
|
+
take: async (id) => {
|
|
89
|
+
const store = getStore();
|
|
90
|
+
const record = store.get(id);
|
|
128
91
|
if (record == null) {
|
|
129
|
-
throw new UnknownRecordError(
|
|
92
|
+
throw new UnknownRecordError(id);
|
|
130
93
|
}
|
|
94
|
+
store.delete(id);
|
|
131
95
|
return record;
|
|
96
|
+
},
|
|
97
|
+
purge: async (where) => {
|
|
98
|
+
if (where == null || Object.keys(where).length < 1) {
|
|
99
|
+
throw new UnsupportedArgumentError('purge:empty-criteria');
|
|
100
|
+
}
|
|
101
|
+
const store = getStore();
|
|
102
|
+
const matched = filterRecords(records(), where);
|
|
103
|
+
matched.forEach(record => record.id != null && store.delete(record.id));
|
|
104
|
+
return matched.length;
|
|
132
105
|
}
|
|
133
106
|
});
|
|
134
107
|
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.8",
|
|
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.7",
|
|
25
|
+
"@owlmeans/error": "^0.1.18-rc.7",
|
|
26
|
+
"@owlmeans/resource": "^0.1.18-rc.8"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"@owlmeans/dep-config": "workspace:*",
|
package/src/resource.ts
CHANGED
|
@@ -1,167 +1,146 @@
|
|
|
1
1
|
import { appendContextual } from '@owlmeans/context'
|
|
2
2
|
import { DEFAULT_ALIAS } from './consts.js'
|
|
3
3
|
import type {
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
Criteria, FirstOptions, ListOptions, Resource, ResourceRecord, Ttl, WriteOptions
|
|
5
|
+
} from '@owlmeans/resource'
|
|
6
|
+
import {
|
|
7
|
+
applyQuery, filterRecords, firstMatch, MisshapedRecord, RecordExists, UnknownRecordError,
|
|
8
|
+
UnsupportedArgumentError
|
|
6
9
|
} from '@owlmeans/resource'
|
|
7
|
-
import { MisshapedRecord, RecordExists, UnknownRecordError, UnsupportedArgumentError } from '@owlmeans/resource'
|
|
8
10
|
import type { Config, Context, StaticResourceAppend } from './types.js'
|
|
9
11
|
|
|
10
|
-
type Getter = string | GetterOptions
|
|
11
|
-
type ResourceType = Resource<ResourceRecord>
|
|
12
|
-
|
|
13
12
|
const stores: Record<string, Map<string, ResourceRecord>> = {}
|
|
14
|
-
export const createStaticResource = (alias: string = DEFAULT_ALIAS, key?: string) => {
|
|
15
|
-
key = key ?? alias
|
|
16
13
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Milliseconds until the record expires, which is what `setTimeout` wants. A number is seconds
|
|
16
|
+
* from now and a Date is the instant to expire at — the two spellings have to land on the same
|
|
17
|
+
* moment, or the same ttl means an hour through one and a second through the other.
|
|
18
|
+
*/
|
|
19
|
+
const expiresIn = (ttl: Ttl): number =>
|
|
20
|
+
ttl instanceof Date ? ttl.getTime() - Date.now() : ttl * 1000
|
|
21
|
+
|
|
22
|
+
export const createStaticResource = <T extends ResourceRecord = ResourceRecord>(
|
|
23
|
+
alias: string = DEFAULT_ALIAS, key?: string
|
|
24
|
+
): Resource<T> => {
|
|
25
|
+
const storeKey = key ?? alias
|
|
26
|
+
|
|
27
|
+
const getStore = (): Map<string, T> => {
|
|
28
|
+
if (stores[storeKey] == null) {
|
|
29
|
+
stores[storeKey] = new Map()
|
|
20
30
|
}
|
|
21
31
|
|
|
22
|
-
return stores[
|
|
32
|
+
return stores[storeKey] as Map<string, T>
|
|
23
33
|
}
|
|
24
34
|
|
|
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
|
-
}
|
|
35
|
+
const records = (): T[] => [...getStore().values()]
|
|
31
36
|
|
|
32
|
-
|
|
33
|
-
|
|
37
|
+
const expire = (id: string, opts?: WriteOptions): void => {
|
|
38
|
+
if (opts?.ttl == null) {
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
const store = getStore()
|
|
42
|
+
setTimeout(() => void store.delete(id), expiresIn(opts.ttl))
|
|
43
|
+
}
|
|
34
44
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
/** An id hits the map directly; criteria go through the shared in-memory engine. */
|
|
46
|
+
const first = (idOrWhere: string | Criteria<T>, opts?: FirstOptions<T>): T | null =>
|
|
47
|
+
typeof idOrWhere === 'string'
|
|
48
|
+
? getStore().get(idOrWhere) ?? null
|
|
49
|
+
: firstMatch(records(), idOrWhere, opts)
|
|
50
|
+
|
|
51
|
+
const write = (record: Partial<T>, opts?: WriteOptions): T => {
|
|
52
|
+
if (record.id == null) {
|
|
53
|
+
/**
|
|
54
|
+
* The store is a map keyed by id and there is nothing here to mint one from, so a record
|
|
55
|
+
* without an id is misshaped rather than new — `save` cannot create it.
|
|
56
|
+
*/
|
|
57
|
+
throw new MisshapedRecord('id')
|
|
58
|
+
}
|
|
59
|
+
getStore().set(record.id, record as T)
|
|
60
|
+
expire(record.id, opts)
|
|
46
61
|
|
|
62
|
+
return record as T
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const resource: Resource<T> = appendContextual<Resource<T>>(alias, {
|
|
66
|
+
get: async (idOrWhere: string | Criteria<T>, opts?: FirstOptions<T>): Promise<T> => {
|
|
67
|
+
const record = first(idOrWhere, opts)
|
|
47
68
|
if (record == null) {
|
|
48
|
-
|
|
69
|
+
throw new UnknownRecordError(typeof idOrWhere === 'string' ? idOrWhere : 'criteria')
|
|
49
70
|
}
|
|
50
71
|
|
|
51
72
|
return record
|
|
52
73
|
},
|
|
53
74
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
throw new UnsupportedArgumentError('static:list:criteria')
|
|
57
|
-
}
|
|
58
|
-
if (opts != null) {
|
|
59
|
-
throw new UnsupportedArgumentError('static:list:opts')
|
|
60
|
-
}
|
|
75
|
+
load: async (idOrWhere: string | Criteria<T>, opts?: FirstOptions<T>): Promise<T | null> =>
|
|
76
|
+
first(idOrWhere, opts),
|
|
61
77
|
|
|
62
|
-
|
|
63
|
-
|
|
78
|
+
/** Unpaged unless a size is asked for — the whole store is already in memory. */
|
|
79
|
+
list: async (where?: Criteria<T>, opts?: ListOptions<T>) => {
|
|
80
|
+
if (opts?.page != null && opts.size == null) {
|
|
81
|
+
throw new UnsupportedArgumentError('page-without-size')
|
|
64
82
|
}
|
|
65
83
|
|
|
66
|
-
return
|
|
84
|
+
return applyQuery(records(), where, opts)
|
|
67
85
|
},
|
|
68
86
|
|
|
69
|
-
|
|
70
|
-
|
|
87
|
+
count: async (where?: Criteria<T>) => filterRecords(records(), where).length,
|
|
88
|
+
|
|
89
|
+
create: async (record: Partial<T>, opts?: WriteOptions) => {
|
|
71
90
|
if (record.id == null) {
|
|
72
91
|
throw new MisshapedRecord('id')
|
|
73
92
|
}
|
|
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)
|
|
93
|
+
if (getStore().has(record.id)) {
|
|
94
|
+
throw new RecordExists(record.id)
|
|
86
95
|
}
|
|
87
96
|
|
|
88
|
-
return record
|
|
97
|
+
return write(record, opts)
|
|
89
98
|
},
|
|
90
99
|
|
|
91
|
-
|
|
92
|
-
const store = getStore()
|
|
100
|
+
update: async (record: Partial<T>, opts?: WriteOptions) => {
|
|
93
101
|
if (record.id == null) {
|
|
94
|
-
throw new
|
|
95
|
-
}
|
|
96
|
-
if (store.has(record.id)) {
|
|
97
|
-
throw new RecordExists(record.id)
|
|
102
|
+
throw new MisshapedRecord('id')
|
|
98
103
|
}
|
|
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)
|
|
104
|
+
if (!getStore().has(record.id)) {
|
|
105
|
+
throw new UnknownRecordError(record.id)
|
|
110
106
|
}
|
|
111
107
|
|
|
112
|
-
return record
|
|
113
|
-
},
|
|
114
|
-
|
|
115
|
-
update: async (record, opts) => {
|
|
116
|
-
return resource.save(record, opts)
|
|
108
|
+
return write(record, opts)
|
|
117
109
|
},
|
|
118
110
|
|
|
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
|
-
}
|
|
111
|
+
save: async (record: Partial<T>, opts?: WriteOptions) => write(record, opts),
|
|
131
112
|
|
|
113
|
+
delete: async (id: string) => {
|
|
114
|
+
const store = getStore()
|
|
115
|
+
const record = store.get(id) ?? null
|
|
132
116
|
if (record == null) {
|
|
133
117
|
return null
|
|
134
118
|
}
|
|
119
|
+
store.delete(id)
|
|
135
120
|
|
|
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
|
-
}
|
|
121
|
+
return record
|
|
122
|
+
},
|
|
149
123
|
|
|
124
|
+
take: async (id: string) => {
|
|
150
125
|
const store = getStore()
|
|
151
|
-
|
|
152
|
-
|
|
126
|
+
const record = store.get(id)
|
|
127
|
+
if (record == null) {
|
|
128
|
+
throw new UnknownRecordError(id)
|
|
153
129
|
}
|
|
130
|
+
store.delete(id)
|
|
154
131
|
|
|
155
|
-
return
|
|
132
|
+
return record
|
|
156
133
|
},
|
|
157
134
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
throw new UnknownRecordError((typeof id === 'string' ? id : id.id) ?? 'unknown')
|
|
135
|
+
purge: async (where: Criteria<T>) => {
|
|
136
|
+
if (where == null || Object.keys(where).length < 1) {
|
|
137
|
+
throw new UnsupportedArgumentError('purge:empty-criteria')
|
|
162
138
|
}
|
|
139
|
+
const store = getStore()
|
|
140
|
+
const matched = filterRecords(records(), where)
|
|
141
|
+
matched.forEach(record => record.id != null && store.delete(record.id))
|
|
163
142
|
|
|
164
|
-
return
|
|
143
|
+
return matched.length
|
|
165
144
|
}
|
|
166
145
|
})
|
|
167
146
|
|
package/build/.gitkeep
DELETED
|
File without changes
|