@iamjulianacosta/mobx-data 1.0.1
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 +21 -0
- package/README.md +366 -0
- package/dist/CacheHandler-BTU_rYkv.js +208 -0
- package/dist/CacheHandler-BTU_rYkv.js.map +1 -0
- package/dist/CacheHandler-CXgY9IJo.cjs +2 -0
- package/dist/CacheHandler-CXgY9IJo.cjs.map +1 -0
- package/dist/EmbeddedRecordsMixin-CBvqNdgC.cjs +2 -0
- package/dist/EmbeddedRecordsMixin-CBvqNdgC.cjs.map +1 -0
- package/dist/EmbeddedRecordsMixin-VoHluHCT.js +261 -0
- package/dist/EmbeddedRecordsMixin-VoHluHCT.js.map +1 -0
- package/dist/JsonApiSerializer-CC5HXp4b.js +194 -0
- package/dist/JsonApiSerializer-CC5HXp4b.js.map +1 -0
- package/dist/JsonApiSerializer-CKB02AgP.cjs +2 -0
- package/dist/JsonApiSerializer-CKB02AgP.cjs.map +1 -0
- package/dist/MemoryAdapter-Bx1e7ndV.js +123 -0
- package/dist/MemoryAdapter-Bx1e7ndV.js.map +1 -0
- package/dist/MemoryAdapter-D1cTyydm.cjs +2 -0
- package/dist/MemoryAdapter-D1cTyydm.cjs.map +1 -0
- package/dist/ODataAdapter-C4IHK4BK.js +157 -0
- package/dist/ODataAdapter-C4IHK4BK.js.map +1 -0
- package/dist/ODataAdapter-DyyF1sdA.cjs +2 -0
- package/dist/ODataAdapter-DyyF1sdA.cjs.map +1 -0
- package/dist/RestAdapter-B4aRvs4m.js +355 -0
- package/dist/RestAdapter-B4aRvs4m.js.map +1 -0
- package/dist/RestAdapter-CJOwTsKK.cjs +2 -0
- package/dist/RestAdapter-CJOwTsKK.cjs.map +1 -0
- package/dist/SchemaService-DZwkFgZu.js +102 -0
- package/dist/SchemaService-DZwkFgZu.js.map +1 -0
- package/dist/SchemaService-Di_yjVzU.cjs +2 -0
- package/dist/SchemaService-Di_yjVzU.cjs.map +1 -0
- package/dist/Serializer-95gi5edy.cjs +2 -0
- package/dist/Serializer-95gi5edy.cjs.map +1 -0
- package/dist/Serializer-FxJbsZ50.js +139 -0
- package/dist/Serializer-FxJbsZ50.js.map +1 -0
- package/dist/Store-BdwMrbDi.cjs +2 -0
- package/dist/Store-BdwMrbDi.cjs.map +1 -0
- package/dist/Store-CZ7Z-Nme.js +912 -0
- package/dist/Store-CZ7Z-Nme.js.map +1 -0
- package/dist/adapter/Adapter.d.ts +146 -0
- package/dist/adapter/Adapter.d.ts.map +1 -0
- package/dist/adapter/MemoryAdapter.d.ts +44 -0
- package/dist/adapter/MemoryAdapter.d.ts.map +1 -0
- package/dist/adapter/RestAdapter.d.ts +57 -0
- package/dist/adapter/RestAdapter.d.ts.map +1 -0
- package/dist/adapter/index.cjs +2 -0
- package/dist/adapter/index.cjs.map +1 -0
- package/dist/adapter/index.d.ts +4 -0
- package/dist/adapter/index.d.ts.map +1 -0
- package/dist/adapter/index.js +8 -0
- package/dist/adapter/index.js.map +1 -0
- package/dist/date-Bj4O2W1F.js +107 -0
- package/dist/date-Bj4O2W1F.js.map +1 -0
- package/dist/date-CRCe-9gf.cjs +2 -0
- package/dist/date-CRCe-9gf.cjs.map +1 -0
- package/dist/decorators-HQ1KnRdh.cjs +2 -0
- package/dist/decorators-HQ1KnRdh.cjs.map +1 -0
- package/dist/decorators-Zr35qr6A.js +50 -0
- package/dist/decorators-Zr35qr6A.js.map +1 -0
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +52 -0
- package/dist/index.js.map +1 -0
- package/dist/json-api/JsonApiAdapter.d.ts +38 -0
- package/dist/json-api/JsonApiAdapter.d.ts.map +1 -0
- package/dist/json-api/JsonApiSerializer.d.ts +73 -0
- package/dist/json-api/JsonApiSerializer.d.ts.map +1 -0
- package/dist/json-api/index.cjs +2 -0
- package/dist/json-api/index.cjs.map +1 -0
- package/dist/json-api/index.d.ts +3 -0
- package/dist/json-api/index.d.ts.map +1 -0
- package/dist/json-api/index.js +6 -0
- package/dist/json-api/index.js.map +1 -0
- package/dist/model/Errors.d.ts +46 -0
- package/dist/model/Errors.d.ts.map +1 -0
- package/dist/model/Model.d.ts +226 -0
- package/dist/model/Model.d.ts.map +1 -0
- package/dist/model/Snapshot.d.ts +72 -0
- package/dist/model/Snapshot.d.ts.map +1 -0
- package/dist/model/StateMachine.d.ts +45 -0
- package/dist/model/StateMachine.d.ts.map +1 -0
- package/dist/model/index.cjs +2 -0
- package/dist/model/index.cjs.map +1 -0
- package/dist/model/index.d.ts +6 -0
- package/dist/model/index.d.ts.map +1 -0
- package/dist/model/index.js +11 -0
- package/dist/model/index.js.map +1 -0
- package/dist/model/relationships.d.ts +182 -0
- package/dist/model/relationships.d.ts.map +1 -0
- package/dist/odata/ODataAdapter.d.ts +67 -0
- package/dist/odata/ODataAdapter.d.ts.map +1 -0
- package/dist/odata/index.cjs +2 -0
- package/dist/odata/index.cjs.map +1 -0
- package/dist/odata/index.d.ts +2 -0
- package/dist/odata/index.d.ts.map +1 -0
- package/dist/odata/index.js +5 -0
- package/dist/odata/index.js.map +1 -0
- package/dist/relationships-B55LBaCW.cjs +2 -0
- package/dist/relationships-B55LBaCW.cjs.map +1 -0
- package/dist/relationships-BEXANmWg.js +821 -0
- package/dist/relationships-BEXANmWg.js.map +1 -0
- package/dist/request/CacheHandler.d.ts +50 -0
- package/dist/request/CacheHandler.d.ts.map +1 -0
- package/dist/request/FetchHandler.d.ts +41 -0
- package/dist/request/FetchHandler.d.ts.map +1 -0
- package/dist/request/RequestManager.d.ts +52 -0
- package/dist/request/RequestManager.d.ts.map +1 -0
- package/dist/request/index.cjs +2 -0
- package/dist/request/index.cjs.map +1 -0
- package/dist/request/index.d.ts +5 -0
- package/dist/request/index.d.ts.map +1 -0
- package/dist/request/index.js +7 -0
- package/dist/request/index.js.map +1 -0
- package/dist/request/types.d.ts +111 -0
- package/dist/request/types.d.ts.map +1 -0
- package/dist/schema/SchemaService.d.ts +58 -0
- package/dist/schema/SchemaService.d.ts.map +1 -0
- package/dist/schema/decorators.d.ts +50 -0
- package/dist/schema/decorators.d.ts.map +1 -0
- package/dist/schema/index.cjs +2 -0
- package/dist/schema/index.cjs.map +1 -0
- package/dist/schema/index.d.ts +4 -0
- package/dist/schema/index.d.ts.map +1 -0
- package/dist/schema/index.js +13 -0
- package/dist/schema/index.js.map +1 -0
- package/dist/schema/types.d.ts +61 -0
- package/dist/schema/types.d.ts.map +1 -0
- package/dist/serializer/EmbeddedRecordsMixin.d.ts +80 -0
- package/dist/serializer/EmbeddedRecordsMixin.d.ts.map +1 -0
- package/dist/serializer/JsonSerializer.d.ts +52 -0
- package/dist/serializer/JsonSerializer.d.ts.map +1 -0
- package/dist/serializer/RestSerializer.d.ts +43 -0
- package/dist/serializer/RestSerializer.d.ts.map +1 -0
- package/dist/serializer/Serializer.d.ts +202 -0
- package/dist/serializer/Serializer.d.ts.map +1 -0
- package/dist/serializer/index.cjs +2 -0
- package/dist/serializer/index.cjs.map +1 -0
- package/dist/serializer/index.d.ts +5 -0
- package/dist/serializer/index.d.ts.map +1 -0
- package/dist/serializer/index.js +9 -0
- package/dist/serializer/index.js.map +1 -0
- package/dist/store/IdentityMap.d.ts +53 -0
- package/dist/store/IdentityMap.d.ts.map +1 -0
- package/dist/store/RecordArray.d.ts +114 -0
- package/dist/store/RecordArray.d.ts.map +1 -0
- package/dist/store/Store.d.ts +395 -0
- package/dist/store/Store.d.ts.map +1 -0
- package/dist/store/index.cjs +2 -0
- package/dist/store/index.cjs.map +1 -0
- package/dist/store/index.d.ts +5 -0
- package/dist/store/index.d.ts.map +1 -0
- package/dist/store/index.js +8 -0
- package/dist/store/index.js.map +1 -0
- package/dist/transforms/Transform.d.ts +49 -0
- package/dist/transforms/Transform.d.ts.map +1 -0
- package/dist/transforms/boolean.d.ts +26 -0
- package/dist/transforms/boolean.d.ts.map +1 -0
- package/dist/transforms/date.d.ts +22 -0
- package/dist/transforms/date.d.ts.map +1 -0
- package/dist/transforms/index.cjs +2 -0
- package/dist/transforms/index.cjs.map +1 -0
- package/dist/transforms/index.d.ts +6 -0
- package/dist/transforms/index.d.ts.map +1 -0
- package/dist/transforms/index.js +9 -0
- package/dist/transforms/index.js.map +1 -0
- package/dist/transforms/number.d.ts +17 -0
- package/dist/transforms/number.d.ts.map +1 -0
- package/dist/transforms/string.d.ts +18 -0
- package/dist/transforms/string.d.ts.map +1 -0
- package/dist/types-C9NB2gRj.js +7 -0
- package/dist/types-C9NB2gRj.js.map +1 -0
- package/dist/types-uWOXMPWW.cjs +2 -0
- package/dist/types-uWOXMPWW.cjs.map +1 -0
- package/package.json +140 -0
- package/src/adapter/Adapter.ts +320 -0
- package/src/adapter/MemoryAdapter.ts +216 -0
- package/src/adapter/RestAdapter.ts +248 -0
- package/src/adapter/index.ts +7 -0
- package/src/index.ts +17 -0
- package/src/json-api/JsonApiAdapter.ts +93 -0
- package/src/json-api/JsonApiSerializer.ts +245 -0
- package/src/json-api/index.ts +2 -0
- package/src/model/Errors.ts +100 -0
- package/src/model/Model.ts +683 -0
- package/src/model/Snapshot.ts +162 -0
- package/src/model/StateMachine.ts +149 -0
- package/src/model/index.ts +20 -0
- package/src/model/relationships.ts +484 -0
- package/src/odata/ODataAdapter.ts +245 -0
- package/src/odata/index.ts +1 -0
- package/src/request/CacheHandler.ts +125 -0
- package/src/request/FetchHandler.ts +119 -0
- package/src/request/RequestManager.ts +112 -0
- package/src/request/index.ts +4 -0
- package/src/request/types.ts +139 -0
- package/src/schema/SchemaService.ts +161 -0
- package/src/schema/decorators.ts +162 -0
- package/src/schema/index.ts +3 -0
- package/src/schema/types.ts +66 -0
- package/src/serializer/EmbeddedRecordsMixin.ts +257 -0
- package/src/serializer/JsonSerializer.ts +173 -0
- package/src/serializer/RestSerializer.ts +138 -0
- package/src/serializer/Serializer.ts +397 -0
- package/src/serializer/index.ts +15 -0
- package/src/store/IdentityMap.ts +110 -0
- package/src/store/RecordArray.ts +210 -0
- package/src/store/Store.ts +1391 -0
- package/src/store/index.ts +11 -0
- package/src/transforms/Transform.ts +52 -0
- package/src/transforms/boolean.ts +57 -0
- package/src/transforms/date.ts +48 -0
- package/src/transforms/index.ts +5 -0
- package/src/transforms/number.ts +42 -0
- package/src/transforms/string.ts +35 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Julián Acosta
|
|
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,366 @@
|
|
|
1
|
+
# mobx-data
|
|
2
|
+
|
|
3
|
+
[](https://github.com/IAmJulianAcosta/mobx-data)
|
|
4
|
+
[](https://github.com/IAmJulianAcosta/mobx-data)
|
|
5
|
+
[](https://github.com/IAmJulianAcosta/mobx-data/releases)
|
|
6
|
+
[](./LICENSE)
|
|
7
|
+
|
|
8
|
+
A feature-complete port of Ember Data to MobX — framework-agnostic, TypeScript-first, fully observable.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Overview
|
|
13
|
+
|
|
14
|
+
`mobx-data` brings the battle-tested Ember Data mental model (Identity Map, pluggable adapters/serializers, record state machines, async relationships) to any MobX application. All state is observable; no manual invalidation or subscriptions are needed.
|
|
15
|
+
|
|
16
|
+
### Design principles
|
|
17
|
+
|
|
18
|
+
- **Framework-agnostic** — works with React, Vue, Solid, or plain JS
|
|
19
|
+
- **MobX-native** — every piece of state is a MobX `observable`; computed props react automatically
|
|
20
|
+
- **TypeScript-first** — full generic types throughout
|
|
21
|
+
- **Ember Data parity** — near 1-to-1 API mapping so Ember Data users feel at home
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Features
|
|
26
|
+
|
|
27
|
+
| Feature | Status |
|
|
28
|
+
|---------|--------|
|
|
29
|
+
| `Store` with Identity Map | ✅ |
|
|
30
|
+
| `Model` base class + lifecycle hooks | ✅ |
|
|
31
|
+
| `@attr`, `@belongsTo`, `@hasMany` decorators | ✅ |
|
|
32
|
+
| Record state machine (`isNew`, `isDirty`, `isSaving`, …) | ✅ |
|
|
33
|
+
| Dirty tracking & `rollbackAttributes` | ✅ |
|
|
34
|
+
| `RecordArray` / `AdapterPopulatedRecordArray` | ✅ |
|
|
35
|
+
| `RestAdapter` | ✅ |
|
|
36
|
+
| `JsonApiAdapter` | ✅ |
|
|
37
|
+
| `ODataAdapter` (v4) | ✅ |
|
|
38
|
+
| `JsonSerializer` / `RestSerializer` / `JsonApiSerializer` | ✅ |
|
|
39
|
+
| `EmbeddedRecordsMixin` | ✅ |
|
|
40
|
+
| Async & sync `belongsTo` / `hasMany` | ✅ |
|
|
41
|
+
| Inverse relationship tracking | ✅ |
|
|
42
|
+
| `Snapshot` | ✅ |
|
|
43
|
+
| `RequestManager` + handler chain | ✅ |
|
|
44
|
+
| `FetchHandler` / `CacheHandler` | ✅ |
|
|
45
|
+
| Built-in transforms (`string`, `number`, `boolean`, `date`) | ✅ |
|
|
46
|
+
| `SchemaService` | ✅ |
|
|
47
|
+
| `Errors` (field-level validation) | ✅ |
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Installation
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pnpm add mobx-data mobx reflect-metadata tsyringe
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Enable decorator metadata in your `tsconfig.json`:
|
|
58
|
+
|
|
59
|
+
```json
|
|
60
|
+
{
|
|
61
|
+
"compilerOptions": {
|
|
62
|
+
"experimentalDecorators": true,
|
|
63
|
+
"emitDecoratorMetadata": true
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Import `reflect-metadata` once at your application entry point:
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
import 'reflect-metadata';
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Quick start
|
|
77
|
+
|
|
78
|
+
### 1. Define models
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
import { Model, attr, belongsTo, hasMany } from 'mobx-data/model';
|
|
82
|
+
|
|
83
|
+
class User extends Model {
|
|
84
|
+
static modelName = 'user';
|
|
85
|
+
|
|
86
|
+
@attr('string') name!: string;
|
|
87
|
+
@attr('string') email!: string;
|
|
88
|
+
|
|
89
|
+
@hasMany('post', { async: false, inverse: 'author' })
|
|
90
|
+
posts!: ManyArray<Post>;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
class Post extends Model {
|
|
94
|
+
static modelName = 'post';
|
|
95
|
+
|
|
96
|
+
@attr('string') title!: string;
|
|
97
|
+
@attr('date') publishedAt!: Date | null;
|
|
98
|
+
|
|
99
|
+
@belongsTo('user', { async: false, inverse: 'posts' })
|
|
100
|
+
author!: User | null;
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### 2. Create a store
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
import 'reflect-metadata';
|
|
108
|
+
import { container } from 'tsyringe';
|
|
109
|
+
import { Store } from 'mobx-data/store';
|
|
110
|
+
import { SchemaService } from 'mobx-data/schema';
|
|
111
|
+
import { RestAdapter } from 'mobx-data/adapter';
|
|
112
|
+
import { JsonSerializer } from 'mobx-data/serializer';
|
|
113
|
+
|
|
114
|
+
const schema = container.resolve(SchemaService);
|
|
115
|
+
schema.registerModel('user', User);
|
|
116
|
+
schema.registerModel('post', Post);
|
|
117
|
+
|
|
118
|
+
const store = container.resolve(Store);
|
|
119
|
+
|
|
120
|
+
const adapter = new RestAdapter();
|
|
121
|
+
adapter.host = 'https://api.example.com';
|
|
122
|
+
|
|
123
|
+
store.registerAdapter('application', adapter);
|
|
124
|
+
store.registerSerializer('application', new JsonSerializer());
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### 3. Use the store
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
// Find a single record
|
|
131
|
+
const user = await store.findRecord('user', '1');
|
|
132
|
+
console.log(user.name); // 'Alice'
|
|
133
|
+
|
|
134
|
+
// Find all
|
|
135
|
+
const users = await store.findAll('user');
|
|
136
|
+
|
|
137
|
+
// Query
|
|
138
|
+
const posts = await store.query('post', { filter: { published: true } });
|
|
139
|
+
|
|
140
|
+
// Create
|
|
141
|
+
const post = store.createRecord('post', { title: 'Hello World' });
|
|
142
|
+
await post.save();
|
|
143
|
+
|
|
144
|
+
// Update
|
|
145
|
+
user.name = 'Bob';
|
|
146
|
+
await user.save();
|
|
147
|
+
|
|
148
|
+
// Delete
|
|
149
|
+
await post.destroyRecord();
|
|
150
|
+
|
|
151
|
+
// Push raw data
|
|
152
|
+
store.push({
|
|
153
|
+
data: { type: 'user', id: '2', attributes: { name: 'Carol' } }
|
|
154
|
+
});
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## Adapters
|
|
160
|
+
|
|
161
|
+
### RestAdapter
|
|
162
|
+
|
|
163
|
+
Standard REST conventions (`GET /users/1`, `POST /users`, `PUT /users/1`, `DELETE /users/1`).
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
const adapter = new RestAdapter();
|
|
167
|
+
adapter.host = 'https://api.example.com';
|
|
168
|
+
adapter.namespace = 'api/v2';
|
|
169
|
+
adapter.headers = { Authorization: 'Bearer …' };
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### JsonApiAdapter
|
|
173
|
+
|
|
174
|
+
Implements the [JSON:API](https://jsonapi.org) specification, including `application/vnd.api+json` MIME type and PATCH for updates.
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
const adapter = new JsonApiAdapter();
|
|
178
|
+
adapter.host = 'https://api.example.com';
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### ODataAdapter
|
|
182
|
+
|
|
183
|
+
Implements OData v4 conventions (PascalCase entity sets, key-in-parentheses URLs, `$filter` / `$expand` / `$select` / `$top` / `$skip` / `$orderby` / `$count`).
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
import { ODataAdapter } from 'mobx-data/odata';
|
|
187
|
+
|
|
188
|
+
const adapter = new ODataAdapter();
|
|
189
|
+
adapter.host = 'https://api.example.com/odata';
|
|
190
|
+
|
|
191
|
+
// $expand navigation properties
|
|
192
|
+
const posts = await adapter.query(store, 'post', {
|
|
193
|
+
$expand: 'author,comments',
|
|
194
|
+
$filter: "publishedAt gt '2026-01-01'",
|
|
195
|
+
$orderby: 'title asc',
|
|
196
|
+
});
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## Serializers
|
|
202
|
+
|
|
203
|
+
| Class | Format |
|
|
204
|
+
|-------|--------|
|
|
205
|
+
| `JsonSerializer` | Flat JSON objects / arrays |
|
|
206
|
+
| `RestSerializer` | Root-key format with optional sideloading |
|
|
207
|
+
| `JsonApiSerializer` | [JSON:API](https://jsonapi.org) compound documents |
|
|
208
|
+
|
|
209
|
+
### EmbeddedRecordsMixin
|
|
210
|
+
|
|
211
|
+
```ts
|
|
212
|
+
import { RestSerializer, EmbeddedRecordsMixin } from 'mobx-data/serializer';
|
|
213
|
+
|
|
214
|
+
class PostSerializer extends EmbeddedRecordsMixin(RestSerializer) {
|
|
215
|
+
attrs = {
|
|
216
|
+
comments: { embedded: 'always' },
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## Relationships
|
|
224
|
+
|
|
225
|
+
```ts
|
|
226
|
+
// Sync (records must already be in the store)
|
|
227
|
+
@belongsTo('user', { async: false }) author!: User | null;
|
|
228
|
+
@hasMany('tag', { async: false }) tags!: ManyArray<Tag>;
|
|
229
|
+
|
|
230
|
+
// Async (loaded on demand)
|
|
231
|
+
@belongsTo('user', { async: true }) author!: AsyncBelongsTo<User>;
|
|
232
|
+
@hasMany('comment', { async: true }) comments!: AsyncHasMany<Comment>;
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Async wrappers are `PromiseLike` — they can be `await`ed or used in MobX reactions:
|
|
236
|
+
|
|
237
|
+
```ts
|
|
238
|
+
const author = await post.author; // AsyncBelongsTo<User>
|
|
239
|
+
console.log(post.author.isLoaded); // true
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Inverse tracking is automatic when `inverse` is specified:
|
|
243
|
+
|
|
244
|
+
```ts
|
|
245
|
+
@belongsTo('user', { async: false, inverse: 'posts' }) author!: User | null;
|
|
246
|
+
// Setting post.author automatically adds post to user.posts
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
## Transforms
|
|
252
|
+
|
|
253
|
+
```ts
|
|
254
|
+
@attr('string') name!: string;
|
|
255
|
+
@attr('number') age!: number;
|
|
256
|
+
@attr('boolean') active!: boolean;
|
|
257
|
+
@attr('date') createdAt!: Date | null;
|
|
258
|
+
@attr() raw!: unknown; // pass-through
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
---
|
|
262
|
+
|
|
263
|
+
## Request pipeline
|
|
264
|
+
|
|
265
|
+
The `RequestManager` provides a middleware-style handler chain:
|
|
266
|
+
|
|
267
|
+
```ts
|
|
268
|
+
import { RequestManager, FetchHandler, CacheHandler } from 'mobx-data/request';
|
|
269
|
+
|
|
270
|
+
const manager = new RequestManager()
|
|
271
|
+
.useCache(new CacheHandler())
|
|
272
|
+
.use(new FetchHandler());
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
Implement custom handlers for authentication, logging, retry logic, etc.:
|
|
276
|
+
|
|
277
|
+
```ts
|
|
278
|
+
class AuthHandler {
|
|
279
|
+
async request(context, next) {
|
|
280
|
+
context.request.headers = {
|
|
281
|
+
...context.request.headers,
|
|
282
|
+
Authorization: `Bearer ${getToken()}`,
|
|
283
|
+
};
|
|
284
|
+
return next(context.request);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
---
|
|
290
|
+
|
|
291
|
+
## Record state
|
|
292
|
+
|
|
293
|
+
Every record exposes observable boolean flags derived from its internal state machine:
|
|
294
|
+
|
|
295
|
+
| Property | Description |
|
|
296
|
+
|----------|-------------|
|
|
297
|
+
| `isNew` | Created locally, never saved |
|
|
298
|
+
| `isDirty` | Has unsaved local changes |
|
|
299
|
+
| `isSaving` | Adapter request in flight |
|
|
300
|
+
| `isLoading` | Being fetched from server |
|
|
301
|
+
| `isLoaded` | Loaded and available |
|
|
302
|
+
| `isDeleted` | Marked for deletion |
|
|
303
|
+
| `isError` | Last operation failed |
|
|
304
|
+
| `isValid` | No validation errors |
|
|
305
|
+
|
|
306
|
+
```ts
|
|
307
|
+
const post = store.createRecord('post', { title: 'Draft' });
|
|
308
|
+
post.isNew; // true
|
|
309
|
+
post.isDirty; // true
|
|
310
|
+
|
|
311
|
+
await post.save();
|
|
312
|
+
post.isNew; // false
|
|
313
|
+
post.isDirty; // false
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
---
|
|
317
|
+
|
|
318
|
+
## Testing
|
|
319
|
+
|
|
320
|
+
The project includes a full end-to-end test suite against an in-process OData server built with [`simple-odata-server`](https://github.com/pofider/simple-odata-server).
|
|
321
|
+
|
|
322
|
+
```bash
|
|
323
|
+
pnpm test # run all tests once
|
|
324
|
+
pnpm test:watch # watch mode
|
|
325
|
+
pnpm typecheck # TypeScript type check
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
To run the test OData server manually:
|
|
329
|
+
|
|
330
|
+
```bash
|
|
331
|
+
bun tests/e2e/fixtures/run-bun.ts
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
---
|
|
335
|
+
|
|
336
|
+
## Project structure
|
|
337
|
+
|
|
338
|
+
```
|
|
339
|
+
src/
|
|
340
|
+
adapter/ — Adapter, RestAdapter
|
|
341
|
+
json-api/ — JsonApiAdapter, JsonApiSerializer
|
|
342
|
+
model/ — Model, StateMachine, Errors, Snapshot, relationships
|
|
343
|
+
odata/ — ODataAdapter
|
|
344
|
+
request/ — RequestManager, FetchHandler, CacheHandler, types
|
|
345
|
+
schema/ — SchemaService, decorators (@attr, @belongsTo, @hasMany), types
|
|
346
|
+
serializer/ — Serializer, JsonSerializer, RestSerializer, EmbeddedRecordsMixin
|
|
347
|
+
store/ — Store, IdentityMap, RecordArray
|
|
348
|
+
transforms/ — Transform, BooleanTransform, DateTransform, NumberTransform, StringTransform
|
|
349
|
+
tests/
|
|
350
|
+
e2e/ — end-to-end tests against a live OData server
|
|
351
|
+
fixtures/ — in-process OData server, seed data, filter engine
|
|
352
|
+
adapter/ — adapter unit tests
|
|
353
|
+
model/ — model unit tests
|
|
354
|
+
(…)
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
---
|
|
358
|
+
|
|
359
|
+
## License
|
|
360
|
+
|
|
361
|
+
MIT
|
|
362
|
+
|
|
363
|
+
---
|
|
364
|
+
|
|
365
|
+
> **Note:** This project is developed internally and published as open source.
|
|
366
|
+
> Bug reports and issues are welcome.
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { injectable as u } from "tsyringe";
|
|
2
|
+
var p = Object.getOwnPropertyDescriptor, f = (e, r, t, s) => {
|
|
3
|
+
for (var n = s > 1 ? void 0 : s ? p(r, t) : r, a = e.length - 1, c; a >= 0; a--)
|
|
4
|
+
(c = e[a]) && (n = c(n) || n);
|
|
5
|
+
return n;
|
|
6
|
+
};
|
|
7
|
+
let d = class {
|
|
8
|
+
constructor() {
|
|
9
|
+
this._handlers = [], this._cache = null;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Appends one or more handlers to the pipeline.
|
|
13
|
+
* Returns `this` for chaining.
|
|
14
|
+
*/
|
|
15
|
+
use(e) {
|
|
16
|
+
return Array.isArray(e) ? this._handlers.push(...e) : this._handlers.push(e), this;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Registers the cache handler. It is always inserted at the front of
|
|
20
|
+
* the chain so it can intercept requests before any other handler sees them.
|
|
21
|
+
* Returns `this` for chaining.
|
|
22
|
+
*/
|
|
23
|
+
useCache(e) {
|
|
24
|
+
return this._cache = e, this;
|
|
25
|
+
}
|
|
26
|
+
/** All non-cache handlers in registration order. */
|
|
27
|
+
get handlers() {
|
|
28
|
+
return this._handlers;
|
|
29
|
+
}
|
|
30
|
+
/** The registered cache handler, or `null`. */
|
|
31
|
+
get cacheHandler() {
|
|
32
|
+
return this._cache;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Executes `request` through the full handler chain and returns the final
|
|
36
|
+
* `StoreResponse`.
|
|
37
|
+
*
|
|
38
|
+
* @throws when no handlers have been registered.
|
|
39
|
+
* @throws when the chain ends without any handler returning a response
|
|
40
|
+
* (i.e. no terminal handler such as `FetchHandler`).
|
|
41
|
+
*/
|
|
42
|
+
async request(e) {
|
|
43
|
+
const r = this._cache ? [this._cache, ...this._handlers] : [...this._handlers];
|
|
44
|
+
if (r.length === 0)
|
|
45
|
+
throw new Error(
|
|
46
|
+
"RequestManager has no handlers registered — cannot complete request"
|
|
47
|
+
);
|
|
48
|
+
let t = 0;
|
|
49
|
+
const s = async (n) => {
|
|
50
|
+
const a = r[t];
|
|
51
|
+
if (!a)
|
|
52
|
+
throw new Error(
|
|
53
|
+
"Handler chain ended without producing a response (no terminal handler)"
|
|
54
|
+
);
|
|
55
|
+
t++;
|
|
56
|
+
const c = {
|
|
57
|
+
request: n,
|
|
58
|
+
response: void 0,
|
|
59
|
+
setResponse(l) {
|
|
60
|
+
this.response = l;
|
|
61
|
+
}
|
|
62
|
+
}, i = (l) => s(l);
|
|
63
|
+
return a.request(c, i);
|
|
64
|
+
};
|
|
65
|
+
return s(e);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
d = f([
|
|
69
|
+
u()
|
|
70
|
+
], d);
|
|
71
|
+
var y = Object.getOwnPropertyDescriptor, _ = (e, r, t, s) => {
|
|
72
|
+
for (var n = s > 1 ? void 0 : s ? y(r, t) : r, a = e.length - 1, c; a >= 0; a--)
|
|
73
|
+
(c = e[a]) && (n = c(n) || n);
|
|
74
|
+
return n;
|
|
75
|
+
};
|
|
76
|
+
class v extends Error {
|
|
77
|
+
constructor(r, t, s, n) {
|
|
78
|
+
super(n ?? `Request failed with status ${r}`), this.status = r, this.content = t, this.headers = s;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
let h = class {
|
|
82
|
+
static headersToObject(e) {
|
|
83
|
+
const r = {};
|
|
84
|
+
return e.forEach((t, s) => {
|
|
85
|
+
r[s] = t;
|
|
86
|
+
}), r;
|
|
87
|
+
}
|
|
88
|
+
static async parseBody(e) {
|
|
89
|
+
const r = e.headers.get("Content-Type") ?? "";
|
|
90
|
+
if (e.status === 204 || e.headers.get("Content-Length") === "0")
|
|
91
|
+
return null;
|
|
92
|
+
if (r.includes("application/json") || r.includes("application/vnd.api+json")) {
|
|
93
|
+
const s = await e.text();
|
|
94
|
+
if (!s)
|
|
95
|
+
return null;
|
|
96
|
+
try {
|
|
97
|
+
return JSON.parse(s);
|
|
98
|
+
} catch {
|
|
99
|
+
return s;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return e.text();
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Issues the HTTP request and returns a `StoreResponse`.
|
|
106
|
+
* This is a terminal handler — it never calls `next`.
|
|
107
|
+
*
|
|
108
|
+
* @throws `FetchError` on non-2xx responses.
|
|
109
|
+
*/
|
|
110
|
+
async request(e, r) {
|
|
111
|
+
const t = e.request, s = {
|
|
112
|
+
method: t.method,
|
|
113
|
+
headers: t.headers
|
|
114
|
+
};
|
|
115
|
+
t.body !== void 0 && t.body !== null && (s.body = t.body), t.signal && (s.signal = t.signal);
|
|
116
|
+
const n = await fetch(t.url, s), a = await h.parseBody(n), c = h.headersToObject(n.headers);
|
|
117
|
+
if (!n.ok)
|
|
118
|
+
throw new v(n.status, a, c);
|
|
119
|
+
return {
|
|
120
|
+
content: a,
|
|
121
|
+
status: n.status,
|
|
122
|
+
headers: c,
|
|
123
|
+
request: t
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
h = _([
|
|
128
|
+
u()
|
|
129
|
+
], h);
|
|
130
|
+
var w = Object.getOwnPropertyDescriptor, g = (e, r, t, s) => {
|
|
131
|
+
for (var n = s > 1 ? void 0 : s ? w(r, t) : r, a = e.length - 1, c; a >= 0; a--)
|
|
132
|
+
(c = e[a]) && (n = c(n) || n);
|
|
133
|
+
return n;
|
|
134
|
+
};
|
|
135
|
+
let o = class {
|
|
136
|
+
constructor() {
|
|
137
|
+
this.cache = /* @__PURE__ */ new Map(), this.maxSize = 256, this.ttl = 3e5;
|
|
138
|
+
}
|
|
139
|
+
isExpired(e) {
|
|
140
|
+
return Date.now() - e.cachedAt > this.ttl;
|
|
141
|
+
}
|
|
142
|
+
evictLRU() {
|
|
143
|
+
for (; this.cache.size > this.maxSize; ) {
|
|
144
|
+
const e = this.cache.keys().next().value;
|
|
145
|
+
if (e !== void 0)
|
|
146
|
+
this.cache.delete(e);
|
|
147
|
+
else
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
static keyFor(e) {
|
|
152
|
+
var r;
|
|
153
|
+
return ((r = e.cacheOptions) == null ? void 0 : r.key) ?? `${e.method} ${e.url}`;
|
|
154
|
+
}
|
|
155
|
+
static isCacheable(e) {
|
|
156
|
+
return e.method === "GET";
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Handles a request by checking the in-memory cache before forwarding to
|
|
160
|
+
* the next handler.
|
|
161
|
+
*
|
|
162
|
+
* - Non-GET requests skip the cache entirely.
|
|
163
|
+
* - `cacheOptions.reload: true` forces a network request and refreshes the entry.
|
|
164
|
+
* - Cached entries expire after `ttl` milliseconds (default 5 minutes).
|
|
165
|
+
* - The cache uses LRU eviction when it exceeds `maxSize` entries (default 256).
|
|
166
|
+
*/
|
|
167
|
+
async request(e, r) {
|
|
168
|
+
var c;
|
|
169
|
+
const t = e.request;
|
|
170
|
+
if (!o.isCacheable(t))
|
|
171
|
+
return r(t);
|
|
172
|
+
const s = o.keyFor(t);
|
|
173
|
+
if (!(((c = t.cacheOptions) == null ? void 0 : c.reload) === !0)) {
|
|
174
|
+
const i = this.cache.get(s);
|
|
175
|
+
if (i)
|
|
176
|
+
if (this.isExpired(i))
|
|
177
|
+
this.cache.delete(s);
|
|
178
|
+
else
|
|
179
|
+
return this.cache.delete(s), this.cache.set(s, i), i.response;
|
|
180
|
+
}
|
|
181
|
+
const a = await r(t);
|
|
182
|
+
return this.cache.set(s, { response: a, cachedAt: Date.now() }), this.evictLRU(), a;
|
|
183
|
+
}
|
|
184
|
+
/** Returns the number of entries currently in the cache. */
|
|
185
|
+
get size() {
|
|
186
|
+
return this.cache.size;
|
|
187
|
+
}
|
|
188
|
+
/** Removes all entries from the cache. */
|
|
189
|
+
clear() {
|
|
190
|
+
this.cache.clear();
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Removes a single entry by key.
|
|
194
|
+
* @returns `true` when the entry existed and was deleted.
|
|
195
|
+
*/
|
|
196
|
+
delete(e) {
|
|
197
|
+
return this.cache.delete(e);
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
o = g([
|
|
201
|
+
u()
|
|
202
|
+
], o);
|
|
203
|
+
export {
|
|
204
|
+
o as C,
|
|
205
|
+
h as F,
|
|
206
|
+
d as R
|
|
207
|
+
};
|
|
208
|
+
//# sourceMappingURL=CacheHandler-BTU_rYkv.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CacheHandler-BTU_rYkv.js","sources":["../src/request/RequestManager.ts","../src/request/FetchHandler.ts","../src/request/CacheHandler.ts"],"sourcesContent":["/**\n * Middleware-style manager that runs a `StoreRequest` through an ordered chain\n * of `Handler` instances.\n *\n * Handlers are invoked in registration order. Each handler receives a\n * `RequestContext` and a `next` function; it may:\n * - Call `next(request)` to pass control to the next handler.\n * - Return a `StoreResponse` directly to short-circuit the chain.\n * - Wrap `next` to inspect or transform the response (e.g. logging).\n *\n * An optional *cache handler* registered via `useCache()` is always prepended\n * to the chain so it runs before every other handler.\n *\n * Usage:\n * ```ts\n * const manager = new RequestManager()\n * .useCache(new CacheHandler())\n * .use([new AuthHandler(), new FetchHandler()]);\n *\n * const response = await manager.request({ method: 'GET', url: '/posts' });\n * ```\n */\n\nimport { injectable } from 'tsyringe';\nimport type {\n Handler,\n NextFn,\n RequestContext,\n StoreRequest,\n StoreResponse,\n} from './types.js';\n\n@injectable()\nexport class RequestManager {\n private _handlers: Handler[] = [];\n private _cache: Handler | null = null;\n\n /**\n * Appends one or more handlers to the pipeline.\n * Returns `this` for chaining.\n */\n use(handlers: Handler[] | Handler): this {\n if (Array.isArray(handlers)) {\n this._handlers.push(...handlers);\n } else {\n this._handlers.push(handlers);\n }\n return this;\n }\n\n /**\n * Registers the cache handler. It is always inserted at the front of\n * the chain so it can intercept requests before any other handler sees them.\n * Returns `this` for chaining.\n */\n useCache(handler: Handler): this {\n this._cache = handler;\n return this;\n }\n\n /** All non-cache handlers in registration order. */\n get handlers(): readonly Handler[] {\n return this._handlers;\n }\n\n /** The registered cache handler, or `null`. */\n get cacheHandler(): Handler | null {\n return this._cache;\n }\n\n /**\n * Executes `request` through the full handler chain and returns the final\n * `StoreResponse`.\n *\n * @throws when no handlers have been registered.\n * @throws when the chain ends without any handler returning a response\n * (i.e. no terminal handler such as `FetchHandler`).\n */\n async request<T = unknown>(request: StoreRequest): Promise<StoreResponse<T>> {\n const chain: Handler[] = this._cache\n ? [this._cache, ...this._handlers]\n : [...this._handlers];\n\n if (chain.length === 0) {\n throw new Error(\n 'RequestManager has no handlers registered — cannot complete request',\n );\n }\n\n let index = 0;\n const invoke = async (req: StoreRequest): Promise<StoreResponse<T>> => {\n const handler = chain[index];\n if (!handler) {\n throw new Error(\n 'Handler chain ended without producing a response (no terminal handler)',\n );\n }\n index++;\n const context: RequestContext<T> = {\n request: req,\n response: undefined,\n setResponse(r) {\n this.response = r;\n },\n };\n const next: NextFn<T> = (nextRequest) => invoke(nextRequest);\n return handler.request(context, next) as Promise<StoreResponse<T>>;\n };\n\n return invoke(request);\n }\n}\n","/**\n * Terminal request handler that executes HTTP calls via the browser / Node\n * `fetch` API.\n *\n * `FetchHandler` is designed to sit at the end of the `RequestManager` chain.\n * It does not call `next` — it issues the network request and returns the\n * parsed response directly.\n *\n * Response body parsing:\n * - `204 No Content` or `Content-Length: 0` → `null`\n * - `application/json` or `application/vnd.api+json` → parsed JSON (falls\n * back to raw text if parsing fails)\n * - Everything else → raw text string\n *\n * Error handling:\n * - Non-2xx responses throw a `FetchError` that includes `status`, `content`,\n * and `headers` for downstream error handling.\n */\n\nimport { injectable } from 'tsyringe';\nimport type {\n Handler,\n NextFn,\n RequestContext,\n StoreResponse,\n} from './types.js';\n\n/**\n * Error thrown by `FetchHandler` for non-2xx HTTP responses.\n * Carries the status code, parsed body content, and response headers.\n */\nexport class FetchError extends Error {\n readonly status: number;\n readonly content: unknown;\n readonly headers: Record<string, string>;\n constructor(\n status: number,\n content: unknown,\n headers: Record<string, string>,\n message?: string,\n ) {\n super(message ?? `Request failed with status ${status}`);\n this.status = status;\n this.content = content;\n this.headers = headers;\n }\n}\n\n@injectable()\nexport class FetchHandler implements Handler {\n static headersToObject(headers: Headers): Record<string, string> {\n const out: Record<string, string> = {};\n headers.forEach((value, key) => {\n out[key] = value;\n });\n return out;\n }\n\n static async parseBody(response: Response): Promise<unknown> {\n const contentType = response.headers.get('Content-Type') ?? '';\n if (\n response.status === 204\n || response.headers.get('Content-Length') === '0'\n ) {\n return null;\n }\n const isJson = contentType.includes('application/json')\n || contentType.includes('application/vnd.api+json');\n if (isJson) {\n const text = await response.text();\n if (!text) {\n return null;\n }\n try {\n return JSON.parse(text);\n } catch {\n return text;\n }\n }\n return response.text();\n }\n\n /**\n * Issues the HTTP request and returns a `StoreResponse`.\n * This is a terminal handler — it never calls `next`.\n *\n * @throws `FetchError` on non-2xx responses.\n */\n async request<T = unknown>(\n context: RequestContext<T>,\n _next: NextFn<T>,\n ): Promise<StoreResponse<T>> {\n const req = context.request;\n const init: RequestInit = {\n method: req.method,\n headers: req.headers,\n };\n if (req.body !== undefined && req.body !== null) {\n init.body = req.body;\n }\n if (req.signal) {\n init.signal = req.signal;\n }\n\n const response = await fetch(req.url, init);\n const content = await FetchHandler.parseBody(response);\n const headers = FetchHandler.headersToObject(response.headers);\n\n if (!response.ok) {\n throw new FetchError(response.status, content, headers);\n }\n return {\n content: content as T,\n status: response.status,\n headers,\n request: req,\n };\n }\n}\n","/**\n * In-memory caching handler for the request pipeline.\n *\n * `CacheHandler` is registered via `RequestManager.useCache()` so it always\n * runs first in the chain. It caches `GET` responses and replays them on\n * subsequent requests with the same key — unless `cacheOptions.reload: true`\n * is set, in which case it bypasses the cache and stores the fresh response.\n *\n * Cache key: `cacheOptions.key` when provided, otherwise `\"<METHOD> <URL>\"`.\n *\n * Only `GET` requests are cached; mutations (POST, PUT, PATCH, DELETE) are\n * always forwarded to `next` without touching the cache.\n *\n * Usage:\n * ```ts\n * const manager = new RequestManager()\n * .useCache(new CacheHandler())\n * .use(new FetchHandler());\n * ```\n */\n\nimport { injectable } from 'tsyringe';\nimport type {\n Handler,\n NextFn,\n RequestContext,\n StoreRequest,\n StoreResponse,\n} from './types.js';\n\ninterface CacheEntry {\n response: StoreResponse;\n cachedAt: number;\n}\n\n@injectable()\nexport class CacheHandler implements Handler {\n private cache = new Map<string, CacheEntry>();\n\n maxSize: number = 256;\n\n ttl: number = 300_000;\n\n private isExpired(entry: CacheEntry): boolean {\n return Date.now() - entry.cachedAt > this.ttl;\n }\n\n private evictLRU(): void {\n while (this.cache.size > this.maxSize) {\n const firstKey = this.cache.keys().next().value;\n if (firstKey !== undefined) {\n this.cache.delete(firstKey);\n } else {\n break;\n }\n }\n }\n\n static keyFor(req: StoreRequest): string {\n return req.cacheOptions?.key ?? `${req.method} ${req.url}`;\n }\n\n static isCacheable(req: StoreRequest): boolean {\n return req.method === 'GET';\n }\n\n /**\n * Handles a request by checking the in-memory cache before forwarding to\n * the next handler.\n *\n * - Non-GET requests skip the cache entirely.\n * - `cacheOptions.reload: true` forces a network request and refreshes the entry.\n * - Cached entries expire after `ttl` milliseconds (default 5 minutes).\n * - The cache uses LRU eviction when it exceeds `maxSize` entries (default 256).\n */\n async request<T = unknown>(\n context: RequestContext<T>,\n next: NextFn<T>,\n ): Promise<StoreResponse<T>> {\n const req = context.request;\n if (!CacheHandler.isCacheable(req)) {\n return next(req);\n }\n\n const key = CacheHandler.keyFor(req);\n const reload = req.cacheOptions?.reload === true;\n\n if (!reload) {\n const hit = this.cache.get(key);\n if (hit) {\n if (this.isExpired(hit)) {\n this.cache.delete(key);\n } else {\n // LRU promotion: delete and re-set to move to end\n this.cache.delete(key);\n this.cache.set(key, hit);\n return hit.response as StoreResponse<T>;\n }\n }\n }\n\n const response = await next(req);\n this.cache.set(key, { response: response as StoreResponse, cachedAt: Date.now() });\n this.evictLRU();\n return response;\n }\n\n /** Returns the number of entries currently in the cache. */\n get size(): number {\n return this.cache.size;\n }\n\n /** Removes all entries from the cache. */\n clear(): void {\n this.cache.clear();\n }\n\n /**\n * Removes a single entry by key.\n * @returns `true` when the entry existed and was deleted.\n */\n delete(key: string): boolean {\n return this.cache.delete(key);\n }\n}\n"],"names":["RequestManager","handlers","handler","request","chain","index","invoke","req","context","r","next","nextRequest","__decorateClass","injectable","FetchError","status","content","headers","message","FetchHandler","out","value","key","response","contentType","text","_next","init","CacheHandler","entry","firstKey","_a","hit"],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";const l=require("tsyringe");var u=Object.getOwnPropertyDescriptor,d=(c,e,r,s)=>{for(var t=s>1?void 0:s?u(e,r):e,a=c.length-1,n;a>=0;a--)(n=c[a])&&(t=n(t)||t);return t};exports.RequestManager=class{constructor(){this._handlers=[],this._cache=null}use(e){return Array.isArray(e)?this._handlers.push(...e):this._handlers.push(e),this}useCache(e){return this._cache=e,this}get handlers(){return this._handlers}get cacheHandler(){return this._cache}async request(e){const r=this._cache?[this._cache,...this._handlers]:[...this._handlers];if(r.length===0)throw new Error("RequestManager has no handlers registered — cannot complete request");let s=0;const t=async a=>{const n=r[s];if(!n)throw new Error("Handler chain ended without producing a response (no terminal handler)");s++;const h={request:a,response:void 0,setResponse(o){this.response=o}},i=o=>t(o);return n.request(h,i)};return t(e)}};exports.RequestManager=d([l.injectable()],exports.RequestManager);var p=Object.getOwnPropertyDescriptor,y=(c,e,r,s)=>{for(var t=s>1?void 0:s?p(e,r):e,a=c.length-1,n;a>=0;a--)(n=c[a])&&(t=n(t)||t);return t};class g extends Error{constructor(e,r,s,t){super(t??`Request failed with status ${e}`),this.status=e,this.content=r,this.headers=s}}exports.FetchHandler=class{static headersToObject(e){const r={};return e.forEach((s,t)=>{r[t]=s}),r}static async parseBody(e){const r=e.headers.get("Content-Type")??"";if(e.status===204||e.headers.get("Content-Length")==="0")return null;if(r.includes("application/json")||r.includes("application/vnd.api+json")){const t=await e.text();if(!t)return null;try{return JSON.parse(t)}catch{return t}}return e.text()}async request(e,r){const s=e.request,t={method:s.method,headers:s.headers};s.body!==void 0&&s.body!==null&&(t.body=s.body),s.signal&&(t.signal=s.signal);const a=await fetch(s.url,t),n=await exports.FetchHandler.parseBody(a),h=exports.FetchHandler.headersToObject(a.headers);if(!a.ok)throw new g(a.status,n,h);return{content:n,status:a.status,headers:h,request:s}}};exports.FetchHandler=y([l.injectable()],exports.FetchHandler);var f=Object.getOwnPropertyDescriptor,_=(c,e,r,s)=>{for(var t=s>1?void 0:s?f(e,r):e,a=c.length-1,n;a>=0;a--)(n=c[a])&&(t=n(t)||t);return t};exports.CacheHandler=class{constructor(){this.cache=new Map,this.maxSize=256,this.ttl=3e5}isExpired(e){return Date.now()-e.cachedAt>this.ttl}evictLRU(){for(;this.cache.size>this.maxSize;){const e=this.cache.keys().next().value;if(e!==void 0)this.cache.delete(e);else break}}static keyFor(e){var r;return((r=e.cacheOptions)==null?void 0:r.key)??`${e.method} ${e.url}`}static isCacheable(e){return e.method==="GET"}async request(e,r){var h;const s=e.request;if(!exports.CacheHandler.isCacheable(s))return r(s);const t=exports.CacheHandler.keyFor(s);if(!(((h=s.cacheOptions)==null?void 0:h.reload)===!0)){const i=this.cache.get(t);if(i)if(this.isExpired(i))this.cache.delete(t);else return this.cache.delete(t),this.cache.set(t,i),i.response}const n=await r(s);return this.cache.set(t,{response:n,cachedAt:Date.now()}),this.evictLRU(),n}get size(){return this.cache.size}clear(){this.cache.clear()}delete(e){return this.cache.delete(e)}};exports.CacheHandler=_([l.injectable()],exports.CacheHandler);
|
|
2
|
+
//# sourceMappingURL=CacheHandler-CXgY9IJo.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CacheHandler-CXgY9IJo.cjs","sources":["../src/request/RequestManager.ts","../src/request/FetchHandler.ts","../src/request/CacheHandler.ts"],"sourcesContent":["/**\n * Middleware-style manager that runs a `StoreRequest` through an ordered chain\n * of `Handler` instances.\n *\n * Handlers are invoked in registration order. Each handler receives a\n * `RequestContext` and a `next` function; it may:\n * - Call `next(request)` to pass control to the next handler.\n * - Return a `StoreResponse` directly to short-circuit the chain.\n * - Wrap `next` to inspect or transform the response (e.g. logging).\n *\n * An optional *cache handler* registered via `useCache()` is always prepended\n * to the chain so it runs before every other handler.\n *\n * Usage:\n * ```ts\n * const manager = new RequestManager()\n * .useCache(new CacheHandler())\n * .use([new AuthHandler(), new FetchHandler()]);\n *\n * const response = await manager.request({ method: 'GET', url: '/posts' });\n * ```\n */\n\nimport { injectable } from 'tsyringe';\nimport type {\n Handler,\n NextFn,\n RequestContext,\n StoreRequest,\n StoreResponse,\n} from './types.js';\n\n@injectable()\nexport class RequestManager {\n private _handlers: Handler[] = [];\n private _cache: Handler | null = null;\n\n /**\n * Appends one or more handlers to the pipeline.\n * Returns `this` for chaining.\n */\n use(handlers: Handler[] | Handler): this {\n if (Array.isArray(handlers)) {\n this._handlers.push(...handlers);\n } else {\n this._handlers.push(handlers);\n }\n return this;\n }\n\n /**\n * Registers the cache handler. It is always inserted at the front of\n * the chain so it can intercept requests before any other handler sees them.\n * Returns `this` for chaining.\n */\n useCache(handler: Handler): this {\n this._cache = handler;\n return this;\n }\n\n /** All non-cache handlers in registration order. */\n get handlers(): readonly Handler[] {\n return this._handlers;\n }\n\n /** The registered cache handler, or `null`. */\n get cacheHandler(): Handler | null {\n return this._cache;\n }\n\n /**\n * Executes `request` through the full handler chain and returns the final\n * `StoreResponse`.\n *\n * @throws when no handlers have been registered.\n * @throws when the chain ends without any handler returning a response\n * (i.e. no terminal handler such as `FetchHandler`).\n */\n async request<T = unknown>(request: StoreRequest): Promise<StoreResponse<T>> {\n const chain: Handler[] = this._cache\n ? [this._cache, ...this._handlers]\n : [...this._handlers];\n\n if (chain.length === 0) {\n throw new Error(\n 'RequestManager has no handlers registered — cannot complete request',\n );\n }\n\n let index = 0;\n const invoke = async (req: StoreRequest): Promise<StoreResponse<T>> => {\n const handler = chain[index];\n if (!handler) {\n throw new Error(\n 'Handler chain ended without producing a response (no terminal handler)',\n );\n }\n index++;\n const context: RequestContext<T> = {\n request: req,\n response: undefined,\n setResponse(r) {\n this.response = r;\n },\n };\n const next: NextFn<T> = (nextRequest) => invoke(nextRequest);\n return handler.request(context, next) as Promise<StoreResponse<T>>;\n };\n\n return invoke(request);\n }\n}\n","/**\n * Terminal request handler that executes HTTP calls via the browser / Node\n * `fetch` API.\n *\n * `FetchHandler` is designed to sit at the end of the `RequestManager` chain.\n * It does not call `next` — it issues the network request and returns the\n * parsed response directly.\n *\n * Response body parsing:\n * - `204 No Content` or `Content-Length: 0` → `null`\n * - `application/json` or `application/vnd.api+json` → parsed JSON (falls\n * back to raw text if parsing fails)\n * - Everything else → raw text string\n *\n * Error handling:\n * - Non-2xx responses throw a `FetchError` that includes `status`, `content`,\n * and `headers` for downstream error handling.\n */\n\nimport { injectable } from 'tsyringe';\nimport type {\n Handler,\n NextFn,\n RequestContext,\n StoreResponse,\n} from './types.js';\n\n/**\n * Error thrown by `FetchHandler` for non-2xx HTTP responses.\n * Carries the status code, parsed body content, and response headers.\n */\nexport class FetchError extends Error {\n readonly status: number;\n readonly content: unknown;\n readonly headers: Record<string, string>;\n constructor(\n status: number,\n content: unknown,\n headers: Record<string, string>,\n message?: string,\n ) {\n super(message ?? `Request failed with status ${status}`);\n this.status = status;\n this.content = content;\n this.headers = headers;\n }\n}\n\n@injectable()\nexport class FetchHandler implements Handler {\n static headersToObject(headers: Headers): Record<string, string> {\n const out: Record<string, string> = {};\n headers.forEach((value, key) => {\n out[key] = value;\n });\n return out;\n }\n\n static async parseBody(response: Response): Promise<unknown> {\n const contentType = response.headers.get('Content-Type') ?? '';\n if (\n response.status === 204\n || response.headers.get('Content-Length') === '0'\n ) {\n return null;\n }\n const isJson = contentType.includes('application/json')\n || contentType.includes('application/vnd.api+json');\n if (isJson) {\n const text = await response.text();\n if (!text) {\n return null;\n }\n try {\n return JSON.parse(text);\n } catch {\n return text;\n }\n }\n return response.text();\n }\n\n /**\n * Issues the HTTP request and returns a `StoreResponse`.\n * This is a terminal handler — it never calls `next`.\n *\n * @throws `FetchError` on non-2xx responses.\n */\n async request<T = unknown>(\n context: RequestContext<T>,\n _next: NextFn<T>,\n ): Promise<StoreResponse<T>> {\n const req = context.request;\n const init: RequestInit = {\n method: req.method,\n headers: req.headers,\n };\n if (req.body !== undefined && req.body !== null) {\n init.body = req.body;\n }\n if (req.signal) {\n init.signal = req.signal;\n }\n\n const response = await fetch(req.url, init);\n const content = await FetchHandler.parseBody(response);\n const headers = FetchHandler.headersToObject(response.headers);\n\n if (!response.ok) {\n throw new FetchError(response.status, content, headers);\n }\n return {\n content: content as T,\n status: response.status,\n headers,\n request: req,\n };\n }\n}\n","/**\n * In-memory caching handler for the request pipeline.\n *\n * `CacheHandler` is registered via `RequestManager.useCache()` so it always\n * runs first in the chain. It caches `GET` responses and replays them on\n * subsequent requests with the same key — unless `cacheOptions.reload: true`\n * is set, in which case it bypasses the cache and stores the fresh response.\n *\n * Cache key: `cacheOptions.key` when provided, otherwise `\"<METHOD> <URL>\"`.\n *\n * Only `GET` requests are cached; mutations (POST, PUT, PATCH, DELETE) are\n * always forwarded to `next` without touching the cache.\n *\n * Usage:\n * ```ts\n * const manager = new RequestManager()\n * .useCache(new CacheHandler())\n * .use(new FetchHandler());\n * ```\n */\n\nimport { injectable } from 'tsyringe';\nimport type {\n Handler,\n NextFn,\n RequestContext,\n StoreRequest,\n StoreResponse,\n} from './types.js';\n\ninterface CacheEntry {\n response: StoreResponse;\n cachedAt: number;\n}\n\n@injectable()\nexport class CacheHandler implements Handler {\n private cache = new Map<string, CacheEntry>();\n\n maxSize: number = 256;\n\n ttl: number = 300_000;\n\n private isExpired(entry: CacheEntry): boolean {\n return Date.now() - entry.cachedAt > this.ttl;\n }\n\n private evictLRU(): void {\n while (this.cache.size > this.maxSize) {\n const firstKey = this.cache.keys().next().value;\n if (firstKey !== undefined) {\n this.cache.delete(firstKey);\n } else {\n break;\n }\n }\n }\n\n static keyFor(req: StoreRequest): string {\n return req.cacheOptions?.key ?? `${req.method} ${req.url}`;\n }\n\n static isCacheable(req: StoreRequest): boolean {\n return req.method === 'GET';\n }\n\n /**\n * Handles a request by checking the in-memory cache before forwarding to\n * the next handler.\n *\n * - Non-GET requests skip the cache entirely.\n * - `cacheOptions.reload: true` forces a network request and refreshes the entry.\n * - Cached entries expire after `ttl` milliseconds (default 5 minutes).\n * - The cache uses LRU eviction when it exceeds `maxSize` entries (default 256).\n */\n async request<T = unknown>(\n context: RequestContext<T>,\n next: NextFn<T>,\n ): Promise<StoreResponse<T>> {\n const req = context.request;\n if (!CacheHandler.isCacheable(req)) {\n return next(req);\n }\n\n const key = CacheHandler.keyFor(req);\n const reload = req.cacheOptions?.reload === true;\n\n if (!reload) {\n const hit = this.cache.get(key);\n if (hit) {\n if (this.isExpired(hit)) {\n this.cache.delete(key);\n } else {\n // LRU promotion: delete and re-set to move to end\n this.cache.delete(key);\n this.cache.set(key, hit);\n return hit.response as StoreResponse<T>;\n }\n }\n }\n\n const response = await next(req);\n this.cache.set(key, { response: response as StoreResponse, cachedAt: Date.now() });\n this.evictLRU();\n return response;\n }\n\n /** Returns the number of entries currently in the cache. */\n get size(): number {\n return this.cache.size;\n }\n\n /** Removes all entries from the cache. */\n clear(): void {\n this.cache.clear();\n }\n\n /**\n * Removes a single entry by key.\n * @returns `true` when the entry existed and was deleted.\n */\n delete(key: string): boolean {\n return this.cache.delete(key);\n }\n}\n"],"names":["RequestManager","handlers","handler","request","chain","index","invoke","req","context","r","next","nextRequest","__decorateClass","injectable","FetchError","status","content","headers","message","FetchHandler","out","value","key","response","contentType","text","_next","init","CacheHandler","entry","firstKey","_a","hit"],"mappings":"qLAiCaA,QAAAA,eAAN,KAAqB,CAArB,aAAA,CACL,KAAQ,UAAuB,CAAA,EAC/B,KAAQ,OAAyB,IAAA,CAMjC,IAAIC,EAAqC,CACvC,OAAI,MAAM,QAAQA,CAAQ,EACxB,KAAK,UAAU,KAAK,GAAGA,CAAQ,EAE/B,KAAK,UAAU,KAAKA,CAAQ,EAEvB,IACT,CAOA,SAASC,EAAwB,CAC/B,YAAK,OAASA,EACP,IACT,CAGA,IAAI,UAA+B,CACjC,OAAO,KAAK,SACd,CAGA,IAAI,cAA+B,CACjC,OAAO,KAAK,MACd,CAUA,MAAM,QAAqBC,EAAkD,CAC3E,MAAMC,EAAmB,KAAK,OAC1B,CAAC,KAAK,OAAQ,GAAG,KAAK,SAAS,EAC/B,CAAC,GAAG,KAAK,SAAS,EAEtB,GAAIA,EAAM,SAAW,EACnB,MAAM,IAAI,MACR,qEAAA,EAIJ,IAAIC,EAAQ,EACZ,MAAMC,EAAS,MAAOC,GAAiD,CACrE,MAAML,EAAUE,EAAMC,CAAK,EAC3B,GAAI,CAACH,EACH,MAAM,IAAI,MACR,wEAAA,EAGJG,IACA,MAAMG,EAA6B,CACjC,QAASD,EACT,SAAU,OACV,YAAYE,EAAG,CACb,KAAK,SAAWA,CAClB,CAAA,EAEIC,EAAmBC,GAAgBL,EAAOK,CAAW,EAC3D,OAAOT,EAAQ,QAAQM,EAASE,CAAI,CACtC,EAEA,OAAOJ,EAAOH,CAAO,CACvB,CACF,EA9EaH,QAAAA,eAANY,EAAA,CADNC,EAAAA,WAAA,CAAW,EACCb,sBAAA,8ICFN,MAAMc,UAAmB,KAAM,CAIpC,YACEC,EACAC,EACAC,EACAC,EACA,CACA,MAAMA,GAAW,8BAA8BH,CAAM,EAAE,EACvD,KAAK,OAASA,EACd,KAAK,QAAUC,EACf,KAAK,QAAUC,CACjB,CACF,CAGaE,QAAAA,aAAN,KAAsC,CAC3C,OAAO,gBAAgBF,EAA0C,CAC/D,MAAMG,EAA8B,CAAA,EACpC,OAAAH,EAAQ,QAAQ,CAACI,EAAOC,IAAQ,CAC9BF,EAAIE,CAAG,EAAID,CACb,CAAC,EACMD,CACT,CAEA,aAAa,UAAUG,EAAsC,CAC3D,MAAMC,EAAcD,EAAS,QAAQ,IAAI,cAAc,GAAK,GAC5D,GACEA,EAAS,SAAW,KACjBA,EAAS,QAAQ,IAAI,gBAAgB,IAAM,IAE9C,OAAO,KAIT,GAFeC,EAAY,SAAS,kBAAkB,GACjDA,EAAY,SAAS,0BAA0B,EACxC,CACV,MAAMC,EAAO,MAAMF,EAAS,KAAA,EAC5B,GAAI,CAACE,EACH,OAAO,KAET,GAAI,CACF,OAAO,KAAK,MAAMA,CAAI,CACxB,MAAQ,CACN,OAAOA,CACT,CACF,CACA,OAAOF,EAAS,KAAA,CAClB,CAQA,MAAM,QACJf,EACAkB,EAC2B,CAC3B,MAAMnB,EAAMC,EAAQ,QACdmB,EAAoB,CACxB,OAAQpB,EAAI,OACZ,QAASA,EAAI,OAAA,EAEXA,EAAI,OAAS,QAAaA,EAAI,OAAS,OACzCoB,EAAK,KAAOpB,EAAI,MAEdA,EAAI,SACNoB,EAAK,OAASpB,EAAI,QAGpB,MAAMgB,EAAW,MAAM,MAAMhB,EAAI,IAAKoB,CAAI,EACpCX,EAAU,MAAMG,qBAAa,UAAUI,CAAQ,EAC/CN,EAAUE,QAAAA,aAAa,gBAAgBI,EAAS,OAAO,EAE7D,GAAI,CAACA,EAAS,GACZ,MAAM,IAAIT,EAAWS,EAAS,OAAQP,EAASC,CAAO,EAExD,MAAO,CACL,QAAAD,EACA,OAAQO,EAAS,OACjB,QAAAN,EACA,QAASV,CAAA,CAEb,CACF,EArEaY,QAAAA,aAANP,EAAA,CADNC,EAAAA,WAAA,CAAW,EACCM,oBAAA,8ICbAS,QAAAA,aAAN,KAAsC,CAAtC,aAAA,CACL,KAAQ,UAAY,IAEpB,KAAA,QAAkB,IAElB,KAAA,IAAc,GAAA,CAEN,UAAUC,EAA4B,CAC5C,OAAO,KAAK,IAAA,EAAQA,EAAM,SAAW,KAAK,GAC5C,CAEQ,UAAiB,CACvB,KAAO,KAAK,MAAM,KAAO,KAAK,SAAS,CACrC,MAAMC,EAAW,KAAK,MAAM,KAAA,EAAO,OAAO,MAC1C,GAAIA,IAAa,OACf,KAAK,MAAM,OAAOA,CAAQ,MAE1B,MAEJ,CACF,CAEA,OAAO,OAAOvB,EAA2B,OACvC,QAAOwB,EAAAxB,EAAI,eAAJ,YAAAwB,EAAkB,MAAO,GAAGxB,EAAI,MAAM,IAAIA,EAAI,GAAG,EAC1D,CAEA,OAAO,YAAYA,EAA4B,CAC7C,OAAOA,EAAI,SAAW,KACxB,CAWA,MAAM,QACJC,EACAE,EAC2B,OAC3B,MAAMH,EAAMC,EAAQ,QACpB,GAAI,CAACoB,QAAAA,aAAa,YAAYrB,CAAG,EAC/B,OAAOG,EAAKH,CAAG,EAGjB,MAAMe,EAAMM,QAAAA,aAAa,OAAOrB,CAAG,EAGnC,GAAI,IAFWwB,EAAAxB,EAAI,eAAJ,YAAAwB,EAAkB,UAAW,IAE/B,CACX,MAAMC,EAAM,KAAK,MAAM,IAAIV,CAAG,EAC9B,GAAIU,EACF,GAAI,KAAK,UAAUA,CAAG,EACpB,KAAK,MAAM,OAAOV,CAAG,MAGrB,aAAK,MAAM,OAAOA,CAAG,EACrB,KAAK,MAAM,IAAIA,EAAKU,CAAG,EAChBA,EAAI,QAGjB,CAEA,MAAMT,EAAW,MAAMb,EAAKH,CAAG,EAC/B,YAAK,MAAM,IAAIe,EAAK,CAAE,SAAAC,EAAqC,SAAU,KAAK,IAAA,EAAO,EACjF,KAAK,SAAA,EACEA,CACT,CAGA,IAAI,MAAe,CACjB,OAAO,KAAK,MAAM,IACpB,CAGA,OAAc,CACZ,KAAK,MAAM,MAAA,CACb,CAMA,OAAOD,EAAsB,CAC3B,OAAO,KAAK,MAAM,OAAOA,CAAG,CAC9B,CACF,EAxFaM,QAAAA,aAANhB,EAAA,CADNC,EAAAA,WAAA,CAAW,EACCe,oBAAA"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";const A=require("tsyringe"),R=require("pluralize"),g=require("./Serializer-95gi5edy.cjs");var _=Object.getOwnPropertyDescriptor,v=(u,t,s,r)=>{for(var n=r>1?void 0:r?_(t,s):t,e=u.length-1,i;e>=0;e--)(i=u[e])&&(n=i(n)||n);return n};exports.JsonSerializer=class extends g.Serializer{static dispatchMethodName(t){switch(t){case"findRecord":return"normalizeFindRecordResponse";case"findAll":return"normalizeFindAllResponse";case"query":return"normalizeQueryResponse";case"queryRecord":return"normalizeQueryRecordResponse";case"createRecord":return"normalizeCreateRecordResponse";case"updateRecord":return"normalizeUpdateRecordResponse";case"deleteRecord":return"normalizeDeleteRecordResponse";default:return null}}normalize(t,s,r,n){if(r==null||typeof r!="object")return null;const e=r,i=this.extractId(s,e),o=this.extractAttributes(s,e),a=this.extractRelationships(s,e),d={type:s.modelName,id:i,attributes:o};return a&&Object.keys(a).length>0&&(d.relationships=a),d}normalizeResponse(t,s,r,n,e){const i=exports.JsonSerializer.dispatchMethodName(e);if(i){const o=this[i],a=g.Serializer.prototype[i];if(typeof o=="function"&&o!==a)return o.call(this,t,s,r,n,e)}return this._buildDocument(t,s,r,n,e)}_buildDocument(t,s,r,n,e){return r==null?{data:null}:Array.isArray(r)?{data:r.map(a=>this.normalize(t,s,a)).filter(a=>a!==null)}:{data:this.normalize(t,s,r)}}serialize(t,s){const r={};return s!=null&&s.includeId&&t.id!==null&&(r[this.primaryKey]=t.id),t.eachAttribute((n,e)=>{this.serializeAttribute(t,r,n,e)}),t.eachRelationship((n,e)=>{e.kind==="belongsTo"?this.serializeBelongsTo(t,r,e):this.serializeHasMany(t,r,e)}),r}};exports.JsonSerializer=v([A.injectable()],exports.JsonSerializer);var M=Object.getOwnPropertyDescriptor,w=(u,t,s,r)=>{for(var n=r>1?void 0:r?M(t,s):t,e=u.length-1,i;e>=0;e--)(i=u[e])&&(n=i(n)||n);return n};exports.RestSerializer=class extends exports.JsonSerializer{payloadKeyFromModelName(t){return R.plural(t)}modelNameFromPayloadKey(t){return R.singular(t)}_buildDocument(t,s,r,n,e){if(r==null||typeof r!="object")return{data:null};const i=r,o=s.modelName,a=this.payloadKeyFromModelName(o);let d=null;const f=new Set;if(o in i){f.add(o);const c=i[o];Array.isArray(c)?d=c.map(l=>this.normalize(t,s,l)).filter(l=>l!==null):d=this.normalize(t,s,c)}else if(a in i){f.add(a);const c=i[a];Array.isArray(c)?d=c.map(l=>this.normalize(t,s,l)).filter(l=>l!==null):d=this.normalize(t,s,c)}const m=[];for(const[c,l]of Object.entries(i)){if(f.has(c)||c==="meta"||c==="links")continue;const z={modelName:this.modelNameFromPayloadKey(c),attributes:new Map,relationships:new Map};if(Array.isArray(l))for(const p of l){const b=this.normalize(t,z,p);b&&m.push(b)}else if(l&&typeof l=="object"){const p=this.normalize(t,z,l);p&&m.push(p)}}const h={data:d};return m.length>0&&(h.included=m),i.meta&&typeof i.meta=="object"&&(h.meta=i.meta),i.links&&typeof i.links=="object"&&(h.links=i.links),h}};exports.RestSerializer=w([A.injectable()],exports.RestSerializer);function S(u){class t extends u{constructor(){super(...arguments),this.pendingIncluded=[]}normalize(r,n,e,i){if(!e||typeof e!="object"||Array.isArray(e))return super.normalize(r,n,e,i);const o=e,a=this.attrs??{},d={...o};for(const[f,m]of n.relationships){const h=a[f];if(!h||h.embedded!=="always")continue;const c=d[f];if(c!=null){if(m.kind==="hasMany"&&Array.isArray(c)){const l=[];for(const y of c){const z=this.extractEmbeddedResource(r,m,y);z&&z.id!==null&&(l.push(z.id),this.pendingIncluded.push(z))}d[f]=l}else if(m.kind==="belongsTo"&&typeof c=="object"&&!Array.isArray(c)){const l=this.extractEmbeddedResource(r,m,c);l&&l.id!==null&&(this.pendingIncluded.push(l),d[f]=l.id)}}}return super.normalize(r,n,d,i)}normalizeResponse(r,n,e,i,o){this.pendingIncluded=[];const a=super.normalizeResponse(r,n,e,i,o);if(this.pendingIncluded.length>0){const d=a.included?[...a.included]:[];d.push(...this.pendingIncluded),a.included=d,this.pendingIncluded=[]}return a}serializeHasMany(r,n,e){var o;const i=(o=this.attrs)==null?void 0:o[e.name];if((i==null?void 0:i.serialize)==="records"){const a=r.hasMany(e.name);n[this.keyForRelationship(e.name)]=a??[];return}super.serializeHasMany(r,n,e)}serializeBelongsTo(r,n,e){var o;const i=(o=this.attrs)==null?void 0:o[e.name];if((i==null?void 0:i.serialize)==="records"){const a=r.belongsTo(e.name);n[this.keyForRelationship(e.name)]=a??null;return}super.serializeBelongsTo(r,n,e)}extractEmbeddedResource(r,n,e){if(!e||typeof e!="object"||Array.isArray(e))return null;const i={modelName:n.type,attributes:new Map,relationships:new Map};return this.normalize(r,i,e)}}return t}exports.EmbeddedRecordsMixin=S;
|
|
2
|
+
//# sourceMappingURL=EmbeddedRecordsMixin-CBvqNdgC.cjs.map
|