@owlmeans/static-resource 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 OwlMeans Common — Fullstack typescript framework
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,330 @@
1
+ # @owlmeans/static-resource
2
+
3
+ The **@owlmeans/static-resource** package provides an in-memory resource storage solution for OwlMeans Common Libraries, designed for fullstack microservices and microclients development with focus on security and proper authentication and authorization.
4
+
5
+ ## Purpose
6
+
7
+ This package serves as a lightweight, in-memory resource implementation that:
8
+
9
+ - **Provides temporary storage** for resources that don't require persistent storage
10
+ - **Supports TTL (Time To Live)** for automatic cleanup of expired records
11
+ - **Integrates with context system** for dependency injection and resource management
12
+ - **Offers CRUD operations** with validation and error handling
13
+ - **Enables testing scenarios** where persistent storage is not needed
14
+
15
+ ## Core Concepts
16
+
17
+ ### Static Resource
18
+ A static resource is an in-memory storage that implements the standard `Resource<ResourceRecord>` interface from `@owlmeans/resource`. It stores records in memory and provides methods for creating, reading, and deleting records.
19
+
20
+ ### Key-Value Storage
21
+ Records are stored using a key-value approach where each record must have an `id` field. Multiple static resource instances can share the same underlying storage by using the same key.
22
+
23
+ ### TTL Support
24
+ Records can be created with a Time To Live (TTL) option, after which they are automatically removed from the storage.
25
+
26
+ ## API Reference
27
+
28
+ ### Factory Functions
29
+
30
+ #### `createStaticResource(alias?, key?): Resource<ResourceRecord>`
31
+
32
+ Creates a new static resource instance.
33
+
34
+ ```typescript
35
+ import { createStaticResource } from '@owlmeans/static-resource'
36
+
37
+ const resource = createStaticResource('my-cache')
38
+ ```
39
+
40
+ **Parameters:**
41
+ - `alias`: string (optional) - Resource alias, defaults to `DEFAULT_ALIAS` ('static')
42
+ - `key`: string (optional) - Storage key, defaults to the alias value
43
+
44
+ **Returns:** Resource instance implementing full CRUD operations
45
+
46
+ #### `appendStaticResource<C, T>(ctx, alias?, key?): T & StaticResourceAppend`
47
+
48
+ Adds static resource functionality to a context.
49
+
50
+ ```typescript
51
+ import { appendStaticResource } from '@owlmeans/static-resource'
52
+ import { makeBasicContext, makeBasicConfig, AppType } from '@owlmeans/context'
53
+
54
+ const config = makeBasicConfig(AppType.Backend, 'my-service')
55
+ const context = makeBasicContext(config)
56
+ const contextWithStatic = appendStaticResource(context, 'cache')
57
+ ```
58
+
59
+ **Parameters:**
60
+ - `ctx`: T - The context to extend
61
+ - `alias`: string (optional) - Resource alias, defaults to `DEFAULT_ALIAS`
62
+ - `key`: string (optional) - Storage key, defaults to the alias value
63
+
64
+ **Returns:** Extended context with `getStaticResource` method
65
+
66
+ ### Resource Methods
67
+
68
+ The static resource implements the standard `Resource<ResourceRecord>` interface:
69
+
70
+ #### `get<T>(id, field?, opts?): Promise<T>`
71
+
72
+ Retrieves a record by ID or field value. Throws `UnknownRecordError` if not found.
73
+
74
+ ```typescript
75
+ const user = await resource.get<User>('user-123')
76
+ ```
77
+
78
+ #### `load<T>(id, field?, opts?): Promise<T | null>`
79
+
80
+ Loads a record by ID or field value. Returns `null` if not found.
81
+
82
+ ```typescript
83
+ const user = await resource.load<User>('user-123')
84
+ if (user) {
85
+ console.log('User found:', user.name)
86
+ }
87
+ ```
88
+
89
+ #### `list<T>(criteria?, opts?): Promise<ListResult<T>>`
90
+
91
+ Lists all records in the storage.
92
+
93
+ ```typescript
94
+ const result = await resource.list<User>()
95
+ console.log('Total users:', result.items.length)
96
+ ```
97
+
98
+ **Note:** Criteria and options parameters are not supported and will throw `UnsupportedArgumentError`.
99
+
100
+ #### `create<T>(record, opts?): Promise<T>`
101
+
102
+ Creates a new record in the storage.
103
+
104
+ ```typescript
105
+ const user = await resource.create({
106
+ id: 'user-123',
107
+ name: 'John Doe',
108
+ email: 'john@example.com'
109
+ }, { ttl: 3600 }) // TTL in seconds
110
+ ```
111
+
112
+ **Options:**
113
+ - `ttl`: number | Date - Time to live in seconds (number) or expiration date (Date)
114
+
115
+ **Throws:**
116
+ - `UnknownRecordError` - If record doesn't have an `id` field
117
+ - `RecordExists` - If record with the same ID already exists
118
+
119
+ #### `delete(id, opts?): Promise<T | null>`
120
+
121
+ Deletes a record and returns it, or `null` if not found.
122
+
123
+ ```typescript
124
+ const deletedUser = await resource.delete('user-123')
125
+ ```
126
+
127
+ #### `pick<T>(id, opts?): Promise<T>`
128
+
129
+ Deletes a record and returns it. Throws `UnknownRecordError` if not found.
130
+
131
+ ```typescript
132
+ const pickedUser = await resource.pick('user-123')
133
+ ```
134
+
135
+ ### Unsupported Methods
136
+
137
+ The following methods throw `UnsupportedMethodError`:
138
+ - `save()` - Static resources don't support updating existing records
139
+ - `update()` - Static resources don't support partial updates
140
+
141
+ ### Types
142
+
143
+ #### `StaticResourceAppend`
144
+
145
+ Interface added to contexts when using `appendStaticResource`.
146
+
147
+ ```typescript
148
+ interface StaticResourceAppend {
149
+ getStaticResource: <T extends ResourceRecord>(alias?: string) => Resource<T>
150
+ }
151
+ ```
152
+
153
+ #### `Config` and `Context`
154
+
155
+ Type aliases for basic context configuration and context interfaces.
156
+
157
+ ```typescript
158
+ interface Config extends BasicConfig {}
159
+ interface Context<C extends Config = Config> extends BasicContext<C> {}
160
+ ```
161
+
162
+ ### Constants
163
+
164
+ #### `DEFAULT_ALIAS`
165
+
166
+ Default alias for static resources.
167
+
168
+ ```typescript
169
+ const DEFAULT_ALIAS = 'static'
170
+ ```
171
+
172
+ ## Usage Examples
173
+
174
+ ### Basic Usage
175
+
176
+ ```typescript
177
+ import { createStaticResource } from '@owlmeans/static-resource'
178
+
179
+ // Create a static resource
180
+ const cache = createStaticResource('user-cache')
181
+
182
+ // Create records
183
+ await cache.create({ id: 'user-1', name: 'Alice', role: 'admin' })
184
+ await cache.create({ id: 'user-2', name: 'Bob', role: 'user' })
185
+
186
+ // Retrieve records
187
+ const alice = await cache.get('user-1')
188
+ console.log('Found user:', alice.name)
189
+
190
+ // List all records
191
+ const { items } = await cache.list()
192
+ console.log('Total users:', items.length)
193
+
194
+ // Delete a record
195
+ const deleted = await cache.delete('user-1')
196
+ console.log('Deleted user:', deleted?.name)
197
+ ```
198
+
199
+ ### Context Integration
200
+
201
+ ```typescript
202
+ import { appendStaticResource } from '@owlmeans/static-resource'
203
+ import { makeBasicContext, makeBasicConfig, AppType } from '@owlmeans/context'
204
+
205
+ const config = makeBasicConfig(AppType.Backend, 'user-service')
206
+ const context = makeBasicContext(config)
207
+
208
+ // Add static resource to context
209
+ const enhancedContext = appendStaticResource(context, 'session-cache')
210
+
211
+ // Configure and initialize context
212
+ enhancedContext.configure()
213
+ await enhancedContext.init()
214
+
215
+ // Use the static resource
216
+ const sessionCache = enhancedContext.getStaticResource('session-cache')
217
+ await sessionCache.create({
218
+ id: 'session-123',
219
+ userId: 'user-456',
220
+ expires: new Date(Date.now() + 3600000)
221
+ })
222
+ ```
223
+
224
+ ### TTL Usage
225
+
226
+ ```typescript
227
+ import { createStaticResource } from '@owlmeans/static-resource'
228
+
229
+ const cache = createStaticResource('temporary-cache')
230
+
231
+ // Create record with TTL in seconds
232
+ await cache.create({
233
+ id: 'temp-data',
234
+ value: 'This will expire'
235
+ }, { ttl: 60 }) // Expires in 60 seconds
236
+
237
+ // Create record with expiration date
238
+ const expirationDate = new Date(Date.now() + 30000) // 30 seconds from now
239
+ await cache.create({
240
+ id: 'temp-data-2',
241
+ value: 'This will also expire'
242
+ }, { ttl: expirationDate })
243
+
244
+ // Records will be automatically removed after TTL expires
245
+ setTimeout(async () => {
246
+ const record = await cache.load('temp-data')
247
+ console.log('Record exists:', record !== null) // false
248
+ }, 61000)
249
+ ```
250
+
251
+ ### Shared Storage
252
+
253
+ ```typescript
254
+ import { createStaticResource } from '@owlmeans/static-resource'
255
+
256
+ // Multiple resources sharing the same storage
257
+ const cacheA = createStaticResource('cache-a', 'shared-storage')
258
+ const cacheB = createStaticResource('cache-b', 'shared-storage')
259
+
260
+ // Record created in cacheA is accessible from cacheB
261
+ await cacheA.create({ id: 'shared-record', data: 'shared data' })
262
+ const record = await cacheB.load('shared-record')
263
+ console.log('Shared record:', record?.data) // 'shared data'
264
+ ```
265
+
266
+ ### Error Handling
267
+
268
+ ```typescript
269
+ import { createStaticResource } from '@owlmeans/static-resource'
270
+ import { UnknownRecordError, RecordExists } from '@owlmeans/resource'
271
+
272
+ const resource = createStaticResource('error-demo')
273
+
274
+ try {
275
+ // This will throw UnknownRecordError
276
+ await resource.get('non-existent-id')
277
+ } catch (error) {
278
+ if (error instanceof UnknownRecordError) {
279
+ console.log('Record not found:', error.message)
280
+ }
281
+ }
282
+
283
+ try {
284
+ await resource.create({ id: 'user-1', name: 'Alice' })
285
+ // This will throw RecordExists
286
+ await resource.create({ id: 'user-1', name: 'Bob' })
287
+ } catch (error) {
288
+ if (error instanceof RecordExists) {
289
+ console.log('Record already exists:', error.message)
290
+ }
291
+ }
292
+ ```
293
+
294
+ ## Error Handling
295
+
296
+ The package uses standard error types from `@owlmeans/resource`:
297
+
298
+ - **`UnknownRecordError`** - Thrown when trying to access a non-existent record
299
+ - **`RecordExists`** - Thrown when trying to create a record with an existing ID
300
+ - **`MisshapedRecord`** - Thrown when record structure is invalid (missing ID)
301
+ - **`UnsupportedArgumentError`** - Thrown when unsupported arguments are provided
302
+ - **`UnsupportedMethodError`** - Thrown when calling unsupported methods
303
+
304
+ ## Integration with OwlMeans Common
305
+
306
+ This package follows the OwlMeans Common library structure:
307
+ - **types**: TypeScript interfaces and type definitions
308
+ - **consts**: Static values and constants
309
+ - **resource**: Resource implementation and factory functions
310
+
311
+ The static resource integrates seamlessly with other OwlMeans packages:
312
+ - Uses `@owlmeans/context` for dependency injection
313
+ - Implements `@owlmeans/resource` interface for consistency
314
+ - Supports `@owlmeans/error` error types
315
+
316
+ ## Best Practices
317
+
318
+ 1. **Use for temporary data** - Static resources are perfect for caching, sessions, or test data
319
+ 2. **Implement TTL for cleanup** - Always set TTL for records that should expire automatically
320
+ 3. **Share storage carefully** - Use the same key only when you want resources to share data
321
+ 4. **Handle errors gracefully** - Always catch and handle resource-specific errors
322
+ 5. **Integrate with context** - Use `appendStaticResource` for proper context integration
323
+ 6. **Test scenarios** - Perfect for unit tests where you don't want persistent storage
324
+
325
+ ## Dependencies
326
+
327
+ This package depends on:
328
+ - `@owlmeans/context` - For contextual resource management
329
+ - `@owlmeans/resource` - For resource interface and error types
330
+ - `@owlmeans/error` - For error handling infrastructure
package/build/.gitkeep ADDED
File without changes
@@ -0,0 +1,2 @@
1
+ export declare const DEFAULT_ALIAS = "static";
2
+ //# sourceMappingURL=consts.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"consts.d.ts","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,aAAa,WAAW,CAAA"}
@@ -0,0 +1,2 @@
1
+ export const DEFAULT_ALIAS = 'static';
2
+ //# sourceMappingURL=consts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"consts.js","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AACA,MAAM,CAAC,MAAM,aAAa,GAAG,QAAQ,CAAA"}
@@ -0,0 +1,4 @@
1
+ export type * from './types.js';
2
+ export * from './resource.js';
3
+ export * from './consts.js';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,mBAAmB,YAAY,CAAA;AAC/B,cAAc,eAAe,CAAA;AAC7B,cAAc,aAAa,CAAA"}
package/build/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export * from './resource.js';
2
+ export * from './consts.js';
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,cAAc,eAAe,CAAA;AAC7B,cAAc,aAAa,CAAA"}
@@ -0,0 +1,5 @@
1
+ import type { Resource, ResourceRecord } from '@owlmeans/resource';
2
+ import type { Config, Context, StaticResourceAppend } from './types.js';
3
+ export declare const createStaticResource: (alias?: string, key?: string) => Resource<ResourceRecord>;
4
+ export declare const appendStaticResource: <C extends Config, T extends Context<C>>(ctx: T, alias?: string, key?: string) => T & StaticResourceAppend;
5
+ //# sourceMappingURL=resource.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resource.d.ts","sourceRoot":"","sources":["../src/resource.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAC8D,QAAQ,EAChF,cAAc,EACf,MAAM,oBAAoB,CAAA;AAE3B,OAAO,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAA;AAMvE,eAAO,MAAM,oBAAoB,WAAW,MAAM,QAAwB,MAAM,6BAmI/E,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"}
@@ -0,0 +1,122 @@
1
+ import { appendContextual } from '@owlmeans/context';
2
+ import { DEFAULT_ALIAS } from './consts.js';
3
+ import { MisshapedRecord, RecordExists, UnknownRecordError, UnsupportedArgumentError, UnsupportedMethodError } from '@owlmeans/resource';
4
+ const stores = {};
5
+ export const createStaticResource = (alias = DEFAULT_ALIAS, key) => {
6
+ key = key ?? alias;
7
+ const getStore = () => {
8
+ if (stores[key] == null) {
9
+ stores[key] = new Map();
10
+ }
11
+ return stores[key];
12
+ };
13
+ const resource = appendContextual(alias, {
14
+ get: async (id, field, opts) => {
15
+ const record = await resource.load(id, field, opts);
16
+ if (record == null) {
17
+ throw new UnknownRecordError(id);
18
+ }
19
+ return record;
20
+ },
21
+ load: async (id, field, opts) => {
22
+ field = field ?? 'id';
23
+ if (opts != null) {
24
+ throw new UnsupportedArgumentError('static:get:opts');
25
+ }
26
+ const store = getStore();
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: () => { throw new UnsupportedMethodError('static:save'); },
48
+ create: async (record, opts) => {
49
+ const store = getStore();
50
+ if (record.id == null) {
51
+ throw new UnknownRecordError('id');
52
+ }
53
+ if (store.has(record.id)) {
54
+ throw new RecordExists(record.id);
55
+ }
56
+ store.set(record.id, record);
57
+ if (opts?.ttl != null) {
58
+ const ttl = opts.ttl instanceof Date
59
+ ? (opts.ttl.getTime() - Date.now()) / 1000
60
+ : typeof opts.ttl === 'number'
61
+ ? opts.ttl * 1000
62
+ : null;
63
+ if (ttl == null) {
64
+ throw new UnsupportedArgumentError('static:opts:ttl-string');
65
+ }
66
+ setTimeout(() => record.id != null && void store.delete(record.id), ttl);
67
+ }
68
+ return record;
69
+ },
70
+ update: () => { throw new UnsupportedMethodError('static:update'); },
71
+ delete: async (id, opts) => {
72
+ let record = null;
73
+ if (typeof id === 'object') {
74
+ if (id.id == null) {
75
+ throw new MisshapedRecord('id');
76
+ }
77
+ record = await resource.load(id.id);
78
+ }
79
+ else if (typeof opts === 'string') {
80
+ record = await resource.load(id, opts);
81
+ }
82
+ if (record == null) {
83
+ return null;
84
+ }
85
+ let field = 'id';
86
+ if (typeof opts === 'string') {
87
+ field = opts;
88
+ opts = undefined;
89
+ }
90
+ else if (typeof opts === 'object') {
91
+ field = opts.field ?? field;
92
+ }
93
+ id = record.id ?? '';
94
+ if (id === '') {
95
+ throw new MisshapedRecord('id');
96
+ }
97
+ const store = getStore();
98
+ if (store.delete(id)) {
99
+ return record;
100
+ }
101
+ return null;
102
+ },
103
+ pick: async (id, opts) => {
104
+ const record = await resource.delete(id, opts);
105
+ if (record == null) {
106
+ throw new UnknownRecordError((typeof id === 'string' ? id : id.id) ?? 'unknown');
107
+ }
108
+ return record;
109
+ }
110
+ });
111
+ return resource;
112
+ };
113
+ export const appendStaticResource = (ctx, alias = DEFAULT_ALIAS, key) => {
114
+ const resource = createStaticResource(alias, key);
115
+ const _ctx = ctx;
116
+ _ctx.registerResource(resource);
117
+ if (_ctx.getStaticResource == null) {
118
+ _ctx.getStaticResource = alias => ctx.resource(alias ?? resource.alias);
119
+ }
120
+ return _ctx;
121
+ };
122
+ //# sourceMappingURL=resource.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resource.js","sourceRoot":"","sources":["../src/resource.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ import type { Resource, ResourceRecord } from '@owlmeans/resource';
2
+ import type { BasicConfig, BasicContext } from '@owlmeans/context';
3
+ export interface StaticResourceAppend {
4
+ getStaticResource: <T extends ResourceRecord>(alias?: string) => Resource<T>;
5
+ }
6
+ export interface Config extends BasicConfig {
7
+ }
8
+ export interface Context<C extends Config = Config> extends BasicContext<C> {
9
+ }
10
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AAClE,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAElE,MAAM,WAAW,oBAAoB;IACnC,iBAAiB,EAAE,CAAC,CAAC,SAAS,cAAc,EAAE,KAAK,CAAC,EAAE,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAA;CAC7E;AAED,MAAM,WAAW,MAAO,SAAQ,WAAW;CAAG;AAE9C,MAAM,WAAW,OAAO,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,CAAE,SAAQ,YAAY,CAAC,CAAC,CAAC;CAAG"}
package/build/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@owlmeans/static-resource",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "scripts": {
6
+ "build": "tsc -b",
7
+ "dev": "sleep 348 && nodemon -e ts,tsx,json --watch src --exec \"tsc -p ./tsconfig.json\"",
8
+ "watch": "tsc -b -w --preserveWatchOutput --pretty"
9
+ },
10
+ "main": "build/index.js",
11
+ "module": "build/index.js",
12
+ "types": "build/index.d.ts",
13
+ "exports": {
14
+ ".": {
15
+ "import": "./build/index.js",
16
+ "require": "./build/index.js",
17
+ "default": "./build/index.js",
18
+ "module": "./build/index.js",
19
+ "types": "./build/index.d.ts"
20
+ }
21
+ },
22
+ "dependencies": {
23
+ "@owlmeans/context": "^0.1.0",
24
+ "@owlmeans/error": "^0.1.0",
25
+ "@owlmeans/resource": "^0.1.0"
26
+ },
27
+ "devDependencies": {
28
+ "nodemon": "^3.1.7",
29
+ "typescript": "^5.6.3"
30
+ },
31
+ "private": false,
32
+ "publishConfig": {
33
+ "access": "public"
34
+ }
35
+ }
package/src/consts.ts ADDED
@@ -0,0 +1,2 @@
1
+
2
+ export const DEFAULT_ALIAS = 'static'
package/src/index.ts ADDED
@@ -0,0 +1,4 @@
1
+
2
+ export type * from './types.js'
3
+ export * from './resource.js'
4
+ export * from './consts.js'
@@ -0,0 +1,160 @@
1
+ import { appendContextual } from '@owlmeans/context'
2
+ import { DEFAULT_ALIAS } from './consts.js'
3
+ import type {
4
+ GetterOptions, ListOptions, ListResult, LifecycleOptions, ListCriteria, Resource,
5
+ ResourceRecord
6
+ } from '@owlmeans/resource'
7
+ import { MisshapedRecord, RecordExists, UnknownRecordError, UnsupportedArgumentError, UnsupportedMethodError } from '@owlmeans/resource'
8
+ import type { Config, Context, StaticResourceAppend } from './types.js'
9
+
10
+ type Getter = string | GetterOptions
11
+ type ResourceType = Resource<ResourceRecord>
12
+
13
+ const stores: Record<string, Map<string, ResourceRecord>> = {}
14
+ export const createStaticResource = (alias: string = DEFAULT_ALIAS, key?: string) => {
15
+ key = key ?? alias
16
+
17
+ const getStore = () => {
18
+ if (stores[key] == null) {
19
+ stores[key] = new Map()
20
+ }
21
+
22
+ return stores[key]
23
+ }
24
+
25
+ const resource: Resource<ResourceRecord> = appendContextual<ResourceType>(alias, {
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
+ }
31
+
32
+ return record
33
+ },
34
+
35
+ load: async <T extends ResourceRecord>(id: string, field?: Getter, opts?: LifecycleOptions) => {
36
+ field = field ?? 'id'
37
+ if (opts != null) {
38
+ throw new UnsupportedArgumentError('static:get:opts')
39
+ }
40
+ const store = getStore()
41
+ const record = field === 'id'
42
+ ? store.get(id) as T | null
43
+ : [...store.values()].find(
44
+ record => record[field as keyof typeof record] === id
45
+ ) as T | undefined
46
+
47
+ if (record == null) {
48
+ return null
49
+ }
50
+
51
+ return record
52
+ },
53
+
54
+ list: async <T extends ResourceRecord>(criteria?: ListOptions | ListCriteria, opts?: ListOptions) => {
55
+ if (criteria != null) {
56
+ throw new UnsupportedArgumentError('static:list:criteria')
57
+ }
58
+ if (opts != null) {
59
+ throw new UnsupportedArgumentError('static:list:opts')
60
+ }
61
+
62
+ const result: ListResult<T> = {
63
+ items: [...getStore().values()] as T[]
64
+ }
65
+
66
+ return result
67
+ },
68
+
69
+ save: () => { throw new UnsupportedMethodError('static:save') },
70
+
71
+ create: async (record, opts) => {
72
+ const store = getStore()
73
+ if (record.id == null) {
74
+ throw new UnknownRecordError('id')
75
+ }
76
+ if (store.has(record.id)) {
77
+ throw new RecordExists(record.id)
78
+ }
79
+ store.set(record.id, record)
80
+ if (opts?.ttl != null) {
81
+ const ttl = opts.ttl instanceof Date
82
+ ? (opts.ttl.getTime() - Date.now()) / 1000
83
+ : typeof opts.ttl === 'number'
84
+ ? opts.ttl * 1000
85
+ : null
86
+ if (ttl == null) {
87
+ throw new UnsupportedArgumentError('static:opts:ttl-string')
88
+ }
89
+ setTimeout(() => record.id != null && void store.delete(record.id), ttl)
90
+ }
91
+
92
+ return record as any
93
+ },
94
+
95
+ update: () => { throw new UnsupportedMethodError('static:update') },
96
+
97
+ delete: async (id, opts) => {
98
+ let record: ResourceRecord | null = null
99
+ if (typeof id === 'object') {
100
+ if (id.id == null) {
101
+ throw new MisshapedRecord('id')
102
+ }
103
+ record = await resource.load(id.id)
104
+ } else if (typeof opts === 'string') {
105
+ record = await resource.load(id, opts)
106
+ }
107
+
108
+ if (record == null) {
109
+ return null
110
+ }
111
+
112
+ let field = 'id'
113
+ if (typeof opts === 'string') {
114
+ field = opts
115
+ opts = undefined
116
+ } else if (typeof opts === 'object') {
117
+ field = opts.field ?? field
118
+ }
119
+
120
+ id = record.id ?? ''
121
+
122
+ if (id === '') {
123
+ throw new MisshapedRecord('id')
124
+ }
125
+
126
+ const store = getStore()
127
+ if (store.delete(id)) {
128
+ return record as any
129
+ }
130
+
131
+ return null
132
+ },
133
+
134
+ pick: async (id, opts) => {
135
+ const record = await resource.delete(id, opts)
136
+ if (record == null) {
137
+ throw new UnknownRecordError((typeof id === 'string' ? id : id.id) ?? 'unknown')
138
+ }
139
+
140
+ return record as any
141
+ }
142
+ })
143
+
144
+ return resource
145
+ }
146
+
147
+ export const appendStaticResource = <C extends Config, T extends Context<C>>(
148
+ ctx: T, alias: string = DEFAULT_ALIAS, key?: string
149
+ ): T & StaticResourceAppend => {
150
+ const resource = createStaticResource(alias, key)
151
+
152
+ const _ctx = ctx as T & StaticResourceAppend
153
+
154
+ _ctx.registerResource(resource)
155
+ if (_ctx.getStaticResource == null) {
156
+ _ctx.getStaticResource = alias => ctx.resource(alias ?? resource.alias)
157
+ }
158
+
159
+ return _ctx
160
+ }
package/src/types.ts ADDED
@@ -0,0 +1,10 @@
1
+ import type { Resource, ResourceRecord } from '@owlmeans/resource'
2
+ import type { BasicConfig, BasicContext } from '@owlmeans/context'
3
+
4
+ export interface StaticResourceAppend {
5
+ getStaticResource: <T extends ResourceRecord>(alias?: string) => Resource<T>
6
+ }
7
+
8
+ export interface Config extends BasicConfig {}
9
+
10
+ export interface Context<C extends Config = Config> extends BasicContext<C> {}
package/tsconfig.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "extends": [
3
+ "../tsconfig.default.json",
4
+ ],
5
+ "compilerOptions": {
6
+ "rootDir": "./src/", /* Specify the root folder within your source files. */
7
+ "outDir": "./build/", /* Specify an output folder for all emitted files. */
8
+ },
9
+ "exclude": [
10
+ "./dist/**/*",
11
+ "./build/**/*",
12
+ "./*.ts"
13
+ ]
14
+ }
@@ -0,0 +1 @@
1
+ {"root":["./src/consts.ts","./src/index.ts","./src/resource.ts","./src/types.ts"],"version":"5.6.3"}