@stonyx/orm 0.0.7 → 0.2.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/.claude/CLAUDE.md +447 -0
- package/.github/workflows/ci.yml +36 -0
- package/README.md +301 -9
- package/config/environment.js +8 -0
- package/package.json +13 -9
- package/src/attr.js +28 -0
- package/src/belongs-to.js +63 -0
- package/src/db.js +42 -68
- package/src/exports/db.js +7 -0
- package/src/has-many.js +61 -0
- package/src/index.js +28 -0
- package/src/main.js +64 -45
- package/src/manage-record.js +103 -0
- package/src/meta-request.js +55 -0
- package/src/model-property.js +5 -0
- package/src/model.js +5 -1
- package/src/orm-request.js +189 -0
- package/src/record.js +72 -8
- package/src/relationships.js +43 -0
- package/src/serializer.js +41 -24
- package/src/setup-rest-server.js +57 -0
- package/src/store.js +211 -0
- package/src/transforms.js +4 -1
- package/stonyx-bootstrap.cjs +30 -0
- package/.nvmrc +0 -1
- package/src/utils.js +0 -19
package/README.md
CHANGED
|
@@ -1,13 +1,305 @@
|
|
|
1
|
-
# stonyx
|
|
1
|
+
# @stonyx/orm
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A lightweight ORM for Stonyx projects, featuring model definitions, serializers, relationships, transforms, and optional REST server integration.
|
|
4
|
+
`@stonyx/orm` provides a structured way to define models, manage relationships, and persist data in JSON files. It also allows integration with the Stonyx REST server for automatic route setup and access control.
|
|
5
|
+
|
|
6
|
+
## Highlights
|
|
7
|
+
|
|
8
|
+
- **Automatic Loading**: Models, serializers, transforms, and access classes are auto-registered from their configured directories.
|
|
9
|
+
- **Models**: Define attributes with type-safe proxies (`attr`) and relationships (`hasMany`, `belongsTo`).
|
|
10
|
+
- **Serializers**: Map raw data into model-friendly structures, including nested properties.
|
|
11
|
+
- **Transforms**: Apply custom transformations on data values automatically.
|
|
12
|
+
- **DB Integration**: Optional file-based persistence with auto-save support.
|
|
13
|
+
- **REST Server Integration**: Automatic route setup with customizable access control.
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install @stonyx/orm
|
|
19
|
+
````
|
|
20
|
+
|
|
21
|
+
## Usage example
|
|
22
|
+
|
|
23
|
+
This module is part of the **Stonyx framework**. To use it, first configure the `restServer` key in your `environment.js` file:
|
|
24
|
+
|
|
25
|
+
```js
|
|
26
|
+
const {
|
|
27
|
+
ORM_ACCESS_PATH,
|
|
28
|
+
ORM_MODEL_PATH,
|
|
29
|
+
ORM_REST_ROUTE,
|
|
30
|
+
ORM_SERIALIZER_PATH,
|
|
31
|
+
ORM_TRANSFORM_PATH,
|
|
32
|
+
ORM_USE_REST_SERVER,
|
|
33
|
+
DB_AUTO_SAVE,
|
|
34
|
+
DB_FILE,
|
|
35
|
+
DB_SCHEMA_PATH,
|
|
36
|
+
DB_SAVE_INTERVAL
|
|
37
|
+
} = process;
|
|
38
|
+
|
|
39
|
+
export default {
|
|
40
|
+
orm: {
|
|
41
|
+
logColor: 'white',
|
|
42
|
+
logMethod: 'db',
|
|
43
|
+
|
|
44
|
+
db: {
|
|
45
|
+
autosave: DB_AUTO_SAVE ?? 'false',
|
|
46
|
+
file: DB_FILE ?? 'db.json',
|
|
47
|
+
saveInterval: DB_SAVE_INTERVAL ?? 3600, // 1 hour
|
|
48
|
+
schema: DB_SCHEMA_PATH ?? './config/db-schema.js'
|
|
49
|
+
},
|
|
50
|
+
paths: {
|
|
51
|
+
access: ORM_ACCESS_PATH ?? './access',
|
|
52
|
+
model: ORM_MODEL_PATH ?? './models',
|
|
53
|
+
serializer: ORM_SERIALIZER_PATH ?? './serializers',
|
|
54
|
+
transform: ORM_TRANSFORM_PATH ?? './transforms'
|
|
55
|
+
},
|
|
56
|
+
restServer: {
|
|
57
|
+
enabled: ORM_USE_REST_SERVER ?? 'true',
|
|
58
|
+
route: ORM_REST_ROUTE ?? '/'
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Then initialize the Stonyx framework, which auto-initializes all of its modules, including `@stonyx/rest-server`:
|
|
65
|
+
|
|
66
|
+
```js
|
|
67
|
+
import Stonyx from 'stonyx';
|
|
68
|
+
import config from './config/environment.js';
|
|
69
|
+
|
|
70
|
+
new Stonyx(config);
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
For further framework initialization instructions, see the [Stonyx repository](https://github.com/abofs/stonyx).
|
|
74
|
+
|
|
75
|
+
## Models
|
|
76
|
+
|
|
77
|
+
Define a model with attributes and relationships:
|
|
78
|
+
|
|
79
|
+
```js
|
|
80
|
+
import { Model, attr, hasMany, belongsTo } from '@stonyx/orm';
|
|
81
|
+
|
|
82
|
+
export default class OwnerModel extends Model {
|
|
83
|
+
id = attr('string');
|
|
84
|
+
age = attr('number');
|
|
85
|
+
pets = hasMany('animal');
|
|
86
|
+
|
|
87
|
+
get totalPets() {
|
|
88
|
+
return this.pets.length;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Serializers
|
|
94
|
+
|
|
95
|
+
Based on the following sample payload structure which represents a poorly structure third-party data source:
|
|
96
|
+
|
|
97
|
+
```js
|
|
98
|
+
export default {
|
|
99
|
+
animals: [
|
|
100
|
+
{ id: 1, type: 'dog', details: { age: 2, c: 'small', x: 'black', location: { type: 'farm', owner: 'angela' }}},
|
|
101
|
+
//...
|
|
102
|
+
]
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Map raw data to model fields:
|
|
107
|
+
|
|
108
|
+
```js
|
|
109
|
+
import { Serializer } from '@stonyx/orm';
|
|
110
|
+
|
|
111
|
+
export default class AnimalSerializer extends Serializer {
|
|
112
|
+
map = {
|
|
113
|
+
age: 'details.age',
|
|
114
|
+
size: 'details.c',
|
|
115
|
+
color: 'details.x',
|
|
116
|
+
owner: 'details.location.owner'
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Relationships
|
|
122
|
+
|
|
123
|
+
### belongsTo
|
|
124
|
+
|
|
125
|
+
```js
|
|
126
|
+
import { belongsTo } from '@stonyx/orm';
|
|
127
|
+
|
|
128
|
+
class AnimalModel extends Model {
|
|
129
|
+
owner = belongsTo('owner');
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### hasMany
|
|
134
|
+
|
|
135
|
+
```js
|
|
136
|
+
import { hasMany } from '@stonyx/orm';
|
|
137
|
+
|
|
138
|
+
class OwnerModel extends Model {
|
|
139
|
+
pets = hasMany('animal');
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Transforms
|
|
144
|
+
|
|
145
|
+
Apply custom transforms on field values:
|
|
146
|
+
|
|
147
|
+
```js
|
|
148
|
+
import { ANIMALS } from '../constants.js';
|
|
149
|
+
|
|
150
|
+
export default function(value) {
|
|
151
|
+
return ANIMALS.indexOf(value) || 0;
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Database (DB) Integration
|
|
156
|
+
|
|
157
|
+
The ORM can automatically save records to a JSON file with optional auto-save intervals.
|
|
158
|
+
|
|
159
|
+
```js
|
|
160
|
+
import Orm from '@stonyx/orm';
|
|
161
|
+
|
|
162
|
+
const orm = new Orm();
|
|
163
|
+
await orm.init();
|
|
164
|
+
|
|
165
|
+
// Access the DB record
|
|
166
|
+
const dbRecord = Orm.db;
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Configuration options are in `config/environment.js`:
|
|
170
|
+
|
|
171
|
+
* `DB_AUTO_SAVE`: Whether to auto-save.
|
|
172
|
+
* `DB_FILE`: File path to store data.
|
|
173
|
+
* `DB_SAVE_INTERVAL`: Interval in seconds for auto-save.
|
|
174
|
+
* `DB_SCHEMA_PATH`: Path to DB schema.
|
|
175
|
+
|
|
176
|
+
## REST Server Integration
|
|
177
|
+
|
|
178
|
+
The ORM can automatically register REST routes using your access classes.
|
|
179
|
+
|
|
180
|
+
```js
|
|
181
|
+
import setupRestServer from '@stonyx/orm/setup-rest-server';
|
|
182
|
+
|
|
183
|
+
await setupRestServer('/', './access');
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Access classes define models and provide custom filtering/authorization logic:
|
|
187
|
+
|
|
188
|
+
```js
|
|
189
|
+
export default class GlobalAccess {
|
|
190
|
+
models = ['owner', 'animal'];
|
|
191
|
+
|
|
192
|
+
access(request) {
|
|
193
|
+
if (request.url.endsWith('/owner/angela')) return false;
|
|
194
|
+
return ['read', 'create', 'update', 'delete'];
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Include Parameter (Sideloading Relationships)
|
|
200
|
+
|
|
201
|
+
The ORM supports JSON API-compliant relationship sideloading via the `include` query parameter. This reduces the need for multiple API requests by embedding related records in a single response.
|
|
202
|
+
|
|
203
|
+
#### Basic Usage
|
|
204
|
+
|
|
205
|
+
```javascript
|
|
206
|
+
// Fetch animal with owner and traits included
|
|
207
|
+
GET /animals/1?include=owner,traits
|
|
208
|
+
|
|
209
|
+
// Response:
|
|
210
|
+
{
|
|
211
|
+
"data": {
|
|
212
|
+
"type": "animal",
|
|
213
|
+
"id": 1,
|
|
214
|
+
"attributes": { "age": 2, "size": "small" },
|
|
215
|
+
"relationships": {
|
|
216
|
+
"owner": { "data": { "type": "owner", "id": "angela" } },
|
|
217
|
+
"traits": { "data": [
|
|
218
|
+
{ "type": "trait", "id": 1 },
|
|
219
|
+
{ "type": "trait", "id": 2 }
|
|
220
|
+
]}
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
"included": [
|
|
224
|
+
{
|
|
225
|
+
"type": "owner",
|
|
226
|
+
"id": "angela",
|
|
227
|
+
"attributes": { "age": 36, "gender": "female" },
|
|
228
|
+
"relationships": { "pets": { "data": [...] } }
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
"type": "trait",
|
|
232
|
+
"id": 1,
|
|
233
|
+
"attributes": { "type": "habitat", "value": "farm" },
|
|
234
|
+
"relationships": {}
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
"type": "trait",
|
|
238
|
+
"id": 2,
|
|
239
|
+
"attributes": { "type": "color", "value": "black" },
|
|
240
|
+
"relationships": {}
|
|
241
|
+
}
|
|
242
|
+
]
|
|
243
|
+
}
|
|
4
244
|
```
|
|
5
|
-
|
|
245
|
+
|
|
246
|
+
#### Features
|
|
247
|
+
|
|
248
|
+
- **Comma-separated relationship names:** `?include=owner,traits`
|
|
249
|
+
- **Nested relationship traversal:** `?include=owner.pets,owner.company` (supports multi-level nesting)
|
|
250
|
+
- **Works with collections and single records:** Both GET endpoints support includes
|
|
251
|
+
- **Automatic deduplication:** Each unique record (by type+id) appears only once in included array
|
|
252
|
+
- **Invalid relationships ignored:** Invalid relationship names are silently skipped
|
|
253
|
+
- **Backward compatible:** Omit the include parameter for original behavior (no included array)
|
|
254
|
+
|
|
255
|
+
#### Examples
|
|
256
|
+
|
|
257
|
+
```javascript
|
|
258
|
+
// Single resource with single include
|
|
259
|
+
GET /owners/gina?include=pets
|
|
260
|
+
|
|
261
|
+
// Single resource with multiple includes
|
|
262
|
+
GET /animals/1?include=owner,traits
|
|
263
|
+
|
|
264
|
+
// Nested includes (NEW!)
|
|
265
|
+
GET /animals/1?include=owner.pets
|
|
266
|
+
|
|
267
|
+
// Deep nesting (3+ levels)
|
|
268
|
+
GET /scenes/e001-s001?include=slides.dialogue.character
|
|
269
|
+
|
|
270
|
+
// Collection with includes (deduplicates automatically)
|
|
271
|
+
GET /animals?include=owner
|
|
272
|
+
|
|
273
|
+
// Combining nested and non-nested includes
|
|
274
|
+
GET /owners?include=pets.traits,company
|
|
275
|
+
|
|
276
|
+
// No include parameter (backward compatible)
|
|
277
|
+
GET /animals/1
|
|
278
|
+
// Returns: { data: {...} } // No included array
|
|
6
279
|
```
|
|
7
280
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
281
|
+
**How Nested Includes Work:**
|
|
282
|
+
1. Query param parsed into path segments: `owner.pets` → `['owner', 'pets']`
|
|
283
|
+
2. Recursively traverses relationships depth-first
|
|
284
|
+
3. Deduplication still by type+id (no duplicates in included array)
|
|
285
|
+
4. Gracefully handles null/missing relationships at any depth
|
|
286
|
+
5. Each included record gets full `toJSON()` representation
|
|
287
|
+
|
|
288
|
+
#### Limitations
|
|
289
|
+
|
|
290
|
+
- Only available on GET endpoints (not POST/PATCH)
|
|
291
|
+
|
|
292
|
+
## Exported Helpers
|
|
293
|
+
|
|
294
|
+
| Export | Description |
|
|
295
|
+
| --------------- | ----------------------------------------------------------------------- |
|
|
296
|
+
| `attr` | Define model attributes with type-safe proxy. |
|
|
297
|
+
| `belongsTo` | Define a one-to-one relationship. |
|
|
298
|
+
| `hasMany` | Define a one-to-many relationship. |
|
|
299
|
+
| `createRecord` | Instantiate a record with proper serialization and relationships. |
|
|
300
|
+
| `store` | Singleton store for all model instances. |
|
|
301
|
+
| `relationships` | Access all relationships (`hasMany`, `belongsTo`, `global`, `pending`). |
|
|
302
|
+
|
|
303
|
+
## License
|
|
304
|
+
|
|
305
|
+
Apache — do what you want, just keep attribution.
|
package/config/environment.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
const {
|
|
2
|
+
ORM_ACCESS_PATH,
|
|
2
3
|
ORM_MODEL_PATH,
|
|
4
|
+
ORM_REST_ROUTE,
|
|
3
5
|
ORM_SERIALIZER_PATH,
|
|
4
6
|
ORM_TRANSFORM_PATH,
|
|
7
|
+
ORM_USE_REST_SERVER,
|
|
5
8
|
DB_AUTO_SAVE,
|
|
6
9
|
DB_FILE,
|
|
7
10
|
DB_SCHEMA_PATH,
|
|
@@ -19,8 +22,13 @@ export default {
|
|
|
19
22
|
schema: DB_SCHEMA_PATH ?? './config/db-schema.js'
|
|
20
23
|
},
|
|
21
24
|
paths: {
|
|
25
|
+
access: ORM_ACCESS_PATH ?? './access', // Optional for restServer access hooks
|
|
22
26
|
model: ORM_MODEL_PATH ?? './models',
|
|
23
27
|
serializer: ORM_SERIALIZER_PATH ?? './serializers',
|
|
24
28
|
transform: ORM_TRANSFORM_PATH ?? './transforms'
|
|
29
|
+
},
|
|
30
|
+
restServer: {
|
|
31
|
+
enabled: ORM_USE_REST_SERVER ?? 'true', // Whether to load restServer for automatic route setup or
|
|
32
|
+
route: ORM_REST_ROUTE ?? '/',
|
|
25
33
|
}
|
|
26
34
|
}
|
package/package.json
CHANGED
|
@@ -4,17 +4,16 @@
|
|
|
4
4
|
"stonyx-async",
|
|
5
5
|
"stonyx-module"
|
|
6
6
|
],
|
|
7
|
-
"version": "0.0
|
|
7
|
+
"version": "0.2.0",
|
|
8
8
|
"description": "",
|
|
9
9
|
"main": "src/main.js",
|
|
10
10
|
"type": "module",
|
|
11
11
|
"exports": {
|
|
12
|
-
".": "./src/
|
|
13
|
-
"./db": "./src/db.js"
|
|
14
|
-
"./utils": "./src/utils.js"
|
|
12
|
+
".": "./src/index.js",
|
|
13
|
+
"./db": "./src/exports/db.js"
|
|
15
14
|
},
|
|
16
15
|
"scripts": {
|
|
17
|
-
"test": "qunit"
|
|
16
|
+
"test": "qunit --require ./stonyx-bootstrap.cjs"
|
|
18
17
|
},
|
|
19
18
|
"repository": {
|
|
20
19
|
"type": "git",
|
|
@@ -25,16 +24,21 @@
|
|
|
25
24
|
"contributors": [
|
|
26
25
|
"Stone Costa <stone.costa@synamicd.com>"
|
|
27
26
|
],
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public"
|
|
29
|
+
},
|
|
28
30
|
"bugs": {
|
|
29
31
|
"url": "https://github.com/abofs/stonyx-orm/issues"
|
|
30
32
|
},
|
|
31
33
|
"homepage": "https://github.com/abofs/stonyx-orm#readme",
|
|
32
34
|
"dependencies": {
|
|
33
|
-
"stonyx": "^0.
|
|
35
|
+
"stonyx": "^0.2.2"
|
|
34
36
|
},
|
|
35
37
|
"devDependencies": {
|
|
36
|
-
"@stonyx/cron": "^0.0
|
|
37
|
-
"@stonyx/
|
|
38
|
-
"
|
|
38
|
+
"@stonyx/cron": "^0.2.0",
|
|
39
|
+
"@stonyx/rest-server": "^0.2.0",
|
|
40
|
+
"@stonyx/utils": "^0.2.2",
|
|
41
|
+
"qunit": "^2.24.1",
|
|
42
|
+
"sinon": "^21.0.0"
|
|
39
43
|
}
|
|
40
44
|
}
|
package/src/attr.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import ModelProperty from './model-property.js';
|
|
2
|
+
|
|
3
|
+
export default function attr() {
|
|
4
|
+
const modelProp = new ModelProperty(...arguments);
|
|
5
|
+
|
|
6
|
+
return new Proxy(modelProp, {
|
|
7
|
+
get(target, prop, receiver) {
|
|
8
|
+
if (prop === 'valueOf' || prop === 'toString') {
|
|
9
|
+
return () => target.value;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (prop in target) {
|
|
13
|
+
return Reflect.get(target, prop, receiver);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return target.value;
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
set(target, prop, value, receiver) {
|
|
20
|
+
if (prop === 'value') {
|
|
21
|
+
target.value = value;
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return Reflect.set(target, prop, value, receiver);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { createRecord, relationships, store } from '@stonyx/orm';
|
|
2
|
+
import { getRelationships } from './relationships.js';
|
|
3
|
+
|
|
4
|
+
function getOrSet(map, key, defaultValue) {
|
|
5
|
+
if (!map.has(key)) map.set(key, defaultValue);
|
|
6
|
+
return map.get(key);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export default function belongsTo(modelName) {
|
|
10
|
+
const hasManyRelationships = relationships.get('hasMany');
|
|
11
|
+
const pendingHasManyQueue = relationships.get('pending');
|
|
12
|
+
const pendingBelongsToQueue = relationships.get('pendingBelongsTo');
|
|
13
|
+
|
|
14
|
+
return (sourceRecord, rawData, options) => {
|
|
15
|
+
if (!rawData) return null;
|
|
16
|
+
|
|
17
|
+
const { __name: sourceModelName } = sourceRecord.__model;
|
|
18
|
+
const relationshipId = sourceRecord.id;
|
|
19
|
+
const relationshipKey = options._relationshipKey;
|
|
20
|
+
const relationship = getRelationships('belongsTo', sourceModelName, modelName, relationshipId);
|
|
21
|
+
const modelStore = store.get(modelName);
|
|
22
|
+
|
|
23
|
+
// Try to get existing record
|
|
24
|
+
const output = typeof rawData === 'object'
|
|
25
|
+
? createRecord(modelName, rawData, options)
|
|
26
|
+
: modelStore.get(rawData);
|
|
27
|
+
|
|
28
|
+
// If not found and is a string ID, register as pending
|
|
29
|
+
if (!output && typeof rawData !== 'object') {
|
|
30
|
+
const targetId = rawData;
|
|
31
|
+
|
|
32
|
+
// Register pending belongsTo
|
|
33
|
+
const modelPendingMap = getOrSet(pendingBelongsToQueue, modelName, new Map());
|
|
34
|
+
const targetPendingArray = getOrSet(modelPendingMap, targetId, []);
|
|
35
|
+
|
|
36
|
+
targetPendingArray.push({
|
|
37
|
+
sourceRecord,
|
|
38
|
+
sourceModelName,
|
|
39
|
+
relationshipKey,
|
|
40
|
+
relationshipId
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
relationship.set(relationshipId, null);
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
relationship.set(relationshipId, output || {});
|
|
48
|
+
|
|
49
|
+
// Populate hasMany side if the relationship is defined
|
|
50
|
+
const otherSide = hasManyRelationships.get(modelName)?.get(sourceModelName)?.get(output?.id);
|
|
51
|
+
|
|
52
|
+
if (otherSide) {
|
|
53
|
+
otherSide.push(sourceRecord);
|
|
54
|
+
|
|
55
|
+
// Remove pending queue if it was just fulfilled
|
|
56
|
+
const pendingModelRelationships = pendingHasManyQueue.get(sourceModelName);
|
|
57
|
+
|
|
58
|
+
if (pendingModelRelationships) pendingModelRelationships.delete(relationshipId);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return output;
|
|
62
|
+
}
|
|
63
|
+
}
|
package/src/db.js
CHANGED
|
@@ -1,18 +1,26 @@
|
|
|
1
|
-
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025 Stone Costa
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
10
15
|
*/
|
|
16
|
+
|
|
11
17
|
import Cron from '@stonyx/cron';
|
|
12
18
|
import config from 'stonyx/config';
|
|
13
19
|
import log from 'stonyx/log';
|
|
20
|
+
import Orm, { createRecord, store } from '@stonyx/orm';
|
|
14
21
|
import { createFile, updateFile, readFile } from '@stonyx/utils/file';
|
|
15
|
-
|
|
22
|
+
|
|
23
|
+
export const dbKey = '__db';
|
|
16
24
|
|
|
17
25
|
export default class DB {
|
|
18
26
|
constructor() {
|
|
@@ -21,10 +29,22 @@ export default class DB {
|
|
|
21
29
|
DB.instance = this;
|
|
22
30
|
}
|
|
23
31
|
|
|
24
|
-
async
|
|
25
|
-
|
|
32
|
+
async getSchema() {
|
|
33
|
+
const { rootPath } = config;
|
|
34
|
+
const { file, schema } = config.orm.db;
|
|
35
|
+
|
|
36
|
+
if (!file) throw new Error('Configuration Error: ORM DB file path must be defined.');
|
|
26
37
|
|
|
38
|
+
return (await import(`${rootPath}/${schema}`)).default;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async init() {
|
|
27
42
|
const { autosave, saveInterval } = config.orm.db;
|
|
43
|
+
|
|
44
|
+
store.set(dbKey, new Map());
|
|
45
|
+
Orm.instance.models[`${dbKey}Model`] = await this.getSchema();
|
|
46
|
+
|
|
47
|
+
this.record = await this.getRecord();
|
|
28
48
|
|
|
29
49
|
if (autosave !== 'true') return;
|
|
30
50
|
|
|
@@ -33,74 +53,28 @@ export default class DB {
|
|
|
33
53
|
|
|
34
54
|
async create() {
|
|
35
55
|
const { rootPath } = config;
|
|
36
|
-
const { file
|
|
37
|
-
|
|
38
|
-
if (!file) throw new Error('Configuration Error: ORM DB file path must be defined.');
|
|
39
|
-
|
|
40
|
-
let dbSchema;
|
|
41
|
-
|
|
42
|
-
try {
|
|
43
|
-
dbSchema = (await import(`${rootPath}/${schema}`)).default;
|
|
44
|
-
} catch (error) {
|
|
45
|
-
dbSchema = {};
|
|
46
|
-
log.db('Unable to load DB schema from file, using empty schema instead');
|
|
47
|
-
}
|
|
56
|
+
const { file } = config.orm.db;
|
|
48
57
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
createFile(`${rootPath}/${file}`, data, { json: true });
|
|
58
|
+
createFile(`${rootPath}/${file}`, {}, { json: true });
|
|
52
59
|
|
|
53
|
-
return
|
|
60
|
+
return {};
|
|
54
61
|
}
|
|
55
62
|
|
|
56
63
|
async save() {
|
|
57
64
|
const { file } = config.orm.db;
|
|
65
|
+
const jsonData = this.record.format();
|
|
66
|
+
delete jsonData.id; // Don't save id
|
|
58
67
|
|
|
59
|
-
await updateFile(file
|
|
68
|
+
await updateFile(`${config.rootPath}/${file}`, jsonData, { json: true });
|
|
60
69
|
|
|
61
70
|
log.db(`DB has been successfully saved to ${file}`);
|
|
62
71
|
}
|
|
63
72
|
|
|
64
|
-
async
|
|
73
|
+
async getRecord() {
|
|
65
74
|
const { file } = config.orm.db;
|
|
66
75
|
|
|
67
|
-
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/** TODO: We need ORM specific reload logic that replaces models attributes when loading from DB */
|
|
71
|
-
// _tempORMSerializeMeta(data) {
|
|
72
|
-
// const { meta } = data;
|
|
73
|
-
|
|
74
|
-
// // HACK: Create map to ensure we have no duplicate references
|
|
75
|
-
// // This will no longer be necessary once once gatherer method prevents duplicates
|
|
76
|
-
// const referenceIds = {};
|
|
77
|
-
// const { shipmentReportReferences } = meta;
|
|
76
|
+
const data = await readFile(file, { json: true, missingFileCallback: this.create.bind(this) });
|
|
78
77
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
// const record = shipmentReportReferences[i];
|
|
82
|
-
|
|
83
|
-
// if (!referenceIds[record.id]) {
|
|
84
|
-
// referenceIds[record.id] = record;
|
|
85
|
-
// } else {
|
|
86
|
-
// shipmentReportReferences.splice(i, 1);
|
|
87
|
-
// }
|
|
88
|
-
|
|
89
|
-
// if (!record.date) continue;
|
|
90
|
-
|
|
91
|
-
// record.date = new Date(record.date);
|
|
92
|
-
// }
|
|
93
|
-
|
|
94
|
-
// // Re-compute
|
|
95
|
-
// const metaModel = new MODELS.MetaModel();
|
|
96
|
-
|
|
97
|
-
// // Serialize computed properties
|
|
98
|
-
// for (const [key, method] of getComputedProperties(metaModel)) {
|
|
99
|
-
// const value = method.bind(meta)();
|
|
100
|
-
|
|
101
|
-
// meta[key] = value;
|
|
102
|
-
// }
|
|
103
|
-
|
|
104
|
-
// return data;
|
|
105
|
-
// }
|
|
78
|
+
return createRecord(dbKey, data, { isDbRecord: true, serialize: false, transform: false });
|
|
79
|
+
}
|
|
106
80
|
}
|
package/src/has-many.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { createRecord, relationships, store } from '@stonyx/orm';
|
|
2
|
+
import { getRelationships } from './relationships.js';
|
|
3
|
+
import { getOrSet, makeArray } from '@stonyx/utils/object';
|
|
4
|
+
import { dbKey } from './db.js';
|
|
5
|
+
|
|
6
|
+
function queuePendingRelationship(pendingRelationshipQueue, pendingRelationships, modelName, id) {
|
|
7
|
+
pendingRelationshipQueue.push({
|
|
8
|
+
pendingRelationship: getOrSet(pendingRelationships, modelName, new Map()),
|
|
9
|
+
id
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default function hasMany(modelName) {
|
|
16
|
+
const globalRelationships = relationships.get('global');
|
|
17
|
+
const pendingRelationships = relationships.get('pending');
|
|
18
|
+
|
|
19
|
+
return (sourceRecord, rawData, options) => {
|
|
20
|
+
const { __name: sourceModelName } = sourceRecord.__model;
|
|
21
|
+
const relationshipId = sourceRecord.id;
|
|
22
|
+
const relationship = getRelationships('hasMany', sourceModelName, modelName, relationshipId);
|
|
23
|
+
const modelStore = store.get(modelName);
|
|
24
|
+
const pendingRelationshipQueue = [];
|
|
25
|
+
|
|
26
|
+
const output = !rawData ? [] : makeArray(rawData).map(elementData => {
|
|
27
|
+
let record;
|
|
28
|
+
|
|
29
|
+
if (typeof elementData !== 'object') {
|
|
30
|
+
record = modelStore.get(elementData);
|
|
31
|
+
|
|
32
|
+
if (!record) {
|
|
33
|
+
return queuePendingRelationship(pendingRelationshipQueue, pendingRelationships, modelName, elementData);
|
|
34
|
+
}
|
|
35
|
+
} else {
|
|
36
|
+
if (elementData !== Object(elementData)) {
|
|
37
|
+
return queuePendingRelationship(pendingRelationshipQueue, pendingRelationships, modelName, elementData);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
record = createRecord(modelName, elementData, options);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Populate belongTo side if the relationship is defined
|
|
44
|
+
const otherSide = relationships.get('belongsTo').get(modelName)?.get(sourceModelName)?.get(record.id);
|
|
45
|
+
|
|
46
|
+
if (otherSide) Object.assign(otherSide, sourceRecord);
|
|
47
|
+
|
|
48
|
+
return record;
|
|
49
|
+
}).filter(value => value);
|
|
50
|
+
|
|
51
|
+
relationship.set(relationshipId, output);
|
|
52
|
+
|
|
53
|
+
// Assign global relationship
|
|
54
|
+
if (options.global || sourceModelName === dbKey) getOrSet(globalRelationships, modelName, []).push(output);
|
|
55
|
+
|
|
56
|
+
// Assign pending relationships
|
|
57
|
+
for (const { pendingRelationship, id } of pendingRelationshipQueue) getOrSet(pendingRelationship, id, []).push(output);
|
|
58
|
+
|
|
59
|
+
return output;
|
|
60
|
+
}
|
|
61
|
+
}
|