@stonyx/orm 0.2.0 → 0.2.3-alpha.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 → project-structure.md} +137 -6
- package/.github/workflows/publish.yml +143 -0
- package/package.json +10 -8
- package/src/model-property.js +2 -0
- package/src/orm-request.js +66 -8
- package/src/record.js +14 -6
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
4. **Data Persistence**: File-based JSON storage with auto-save
|
|
13
13
|
5. **REST API Generation**: Auto-generated RESTful endpoints with access control
|
|
14
14
|
6. **Data Transformation**: Custom type conversion and formatting
|
|
15
|
+
7. **Event System**: Pub/sub events for CRUD operations with before/after hooks
|
|
15
16
|
|
|
16
17
|
---
|
|
17
18
|
|
|
@@ -28,28 +29,29 @@
|
|
|
28
29
|
7. **Relationships** ([src/has-many.js](src/has-many.js), [src/belongs-to.js](src/belongs-to.js)) - Relationship handlers
|
|
29
30
|
8. **Include Parser** ([src/include-parser.js](src/include-parser.js)) - Parses include query params
|
|
30
31
|
9. **Include Collector** ([src/include-collector.js](src/include-collector.js)) - Collects and deduplicates included records
|
|
32
|
+
10. **Events** (from `@stonyx/events`) - Pub/sub event system for CRUD hooks
|
|
31
33
|
|
|
32
34
|
### Project Structure
|
|
33
35
|
|
|
34
36
|
```
|
|
35
37
|
stonyx-orm/
|
|
36
38
|
├── src/
|
|
37
|
-
│ ├── index.js # Main exports
|
|
39
|
+
│ ├── index.js # Main exports (includes ormEvents)
|
|
38
40
|
│ ├── main.js # Orm class
|
|
39
41
|
│ ├── model.js # Base Model
|
|
40
42
|
│ ├── record.js # Record instances
|
|
41
43
|
│ ├── serializer.js # Base Serializer
|
|
42
|
-
│ ├── store.js # In-memory storage
|
|
44
|
+
│ ├── store.js # In-memory storage (emits delete events)
|
|
43
45
|
│ ├── db.js # JSON persistence
|
|
44
46
|
│ ├── attr.js # Attribute helper (Proxy-based)
|
|
45
47
|
│ ├── has-many.js # One-to-many relationships
|
|
46
48
|
│ ├── belongs-to.js # Many-to-one relationships
|
|
47
49
|
│ ├── relationships.js # Relationship registry
|
|
48
|
-
│ ├── manage-record.js # createRecord/updateRecord
|
|
50
|
+
│ ├── manage-record.js # createRecord/updateRecord (emits create events)
|
|
49
51
|
│ ├── model-property.js # Transform handler
|
|
50
52
|
│ ├── transforms.js # Built-in transforms
|
|
51
53
|
│ ├── setup-rest-server.js # REST integration
|
|
52
|
-
│ ├── orm-request.js # CRUD request handler
|
|
54
|
+
│ ├── orm-request.js # CRUD request handler (emits update events)
|
|
53
55
|
│ └── meta-request.js # Meta endpoint (dev only)
|
|
54
56
|
├── config/
|
|
55
57
|
│ └── environment.js # Default configuration
|
|
@@ -380,9 +382,138 @@ config.orm = {
|
|
|
380
382
|
**Dependencies:**
|
|
381
383
|
- `stonyx` - Main framework (peer)
|
|
382
384
|
- `@stonyx/utils` - File/string utilities
|
|
383
|
-
- `@stonyx/
|
|
385
|
+
- `@stonyx/events` - Pub/sub event system for CRUD hooks
|
|
386
|
+
- `@stonyx/cron` - Scheduled tasks (used by DB for auto-save)
|
|
384
387
|
- `@stonyx/rest-server` - REST API
|
|
385
388
|
|
|
389
|
+
## Event System
|
|
390
|
+
|
|
391
|
+
The ORM emits events during CRUD operations, allowing applications to hook into the data lifecycle.
|
|
392
|
+
|
|
393
|
+
### Event Architecture
|
|
394
|
+
|
|
395
|
+
**Event Source**: `@stonyx/events` (Events class)
|
|
396
|
+
**Integration**: `src/index.js` initializes singleton `ormEvents` instance
|
|
397
|
+
**Event Registration**: 6 events registered on initialization:
|
|
398
|
+
- `create:before`, `create:after`
|
|
399
|
+
- `update:before`, `update:after`
|
|
400
|
+
- `delete:before`, `delete:after`
|
|
401
|
+
|
|
402
|
+
### Event Emission Points
|
|
403
|
+
|
|
404
|
+
**CREATE Events** - `src/manage-record.js`:
|
|
405
|
+
```javascript
|
|
406
|
+
// Line ~34: Before serialization
|
|
407
|
+
Events.instance?.emit('create:before', {
|
|
408
|
+
model: modelName,
|
|
409
|
+
record,
|
|
410
|
+
rawData,
|
|
411
|
+
options
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
// Line ~88: After record fully created
|
|
415
|
+
Events.instance?.emit('create:after', {
|
|
416
|
+
model: modelName,
|
|
417
|
+
record,
|
|
418
|
+
data: record.__data,
|
|
419
|
+
rawData,
|
|
420
|
+
options
|
|
421
|
+
});
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
**UPDATE Events** - `src/orm-request.js` (PATCH handler):
|
|
425
|
+
```javascript
|
|
426
|
+
// Line ~155: Before applying updates
|
|
427
|
+
const oldData = { ...record.__data };
|
|
428
|
+
Events.instance?.emit('update:before', {
|
|
429
|
+
model,
|
|
430
|
+
record,
|
|
431
|
+
data: record.__data,
|
|
432
|
+
oldData,
|
|
433
|
+
rawData: attributes,
|
|
434
|
+
options: {}
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
// Line ~173: After updates applied
|
|
438
|
+
Events.instance?.emit('update:after', {
|
|
439
|
+
model,
|
|
440
|
+
record,
|
|
441
|
+
data: record.__data,
|
|
442
|
+
oldData,
|
|
443
|
+
rawData: attributes,
|
|
444
|
+
options: {}
|
|
445
|
+
});
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
**DELETE Events** - `src/store.js` (unloadRecord):
|
|
449
|
+
```javascript
|
|
450
|
+
// Line ~45: Before cleanup
|
|
451
|
+
Events.instance?.emit('delete:before', {
|
|
452
|
+
model,
|
|
453
|
+
record,
|
|
454
|
+
data: { ...record.__data },
|
|
455
|
+
options
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
// Line ~65: After deletion
|
|
459
|
+
Events.instance?.emit('delete:after', {
|
|
460
|
+
model,
|
|
461
|
+
record: null,
|
|
462
|
+
data: null,
|
|
463
|
+
options
|
|
464
|
+
});
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
### Design Decisions
|
|
468
|
+
|
|
469
|
+
**Fire Without Await**: Events are emitted without `await` to maintain backward compatibility
|
|
470
|
+
- `createRecord()` remains synchronous
|
|
471
|
+
- `unloadRecord()` remains synchronous
|
|
472
|
+
- Synchronous handlers execute immediately
|
|
473
|
+
- Async handlers run in background
|
|
474
|
+
|
|
475
|
+
**Optional Chaining**: Uses `Events.instance?.emit()` for zero overhead when not used
|
|
476
|
+
|
|
477
|
+
**Error Isolation**: Event errors are caught in Events class, never crash ORM operations
|
|
478
|
+
|
|
479
|
+
### Usage Patterns
|
|
480
|
+
|
|
481
|
+
**Auditing**:
|
|
482
|
+
```javascript
|
|
483
|
+
import { ormEvents } from '@stonyx/orm';
|
|
484
|
+
|
|
485
|
+
ormEvents.subscribe('update:after', ({ model, data, oldData }) => {
|
|
486
|
+
auditLog.write({
|
|
487
|
+
action: 'update',
|
|
488
|
+
model,
|
|
489
|
+
changes: diff(oldData, data),
|
|
490
|
+
timestamp: Date.now()
|
|
491
|
+
});
|
|
492
|
+
});
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
**Auto-Timestamps**:
|
|
496
|
+
```javascript
|
|
497
|
+
ormEvents.subscribe('create:before', ({ record }) => {
|
|
498
|
+
record.__data.createdAt = new Date().toISOString();
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
ormEvents.subscribe('update:before', ({ record }) => {
|
|
502
|
+
record.__data.updatedAt = new Date().toISOString();
|
|
503
|
+
});
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
**Cache Invalidation**:
|
|
507
|
+
```javascript
|
|
508
|
+
ormEvents.subscribe('update:after', ({ model, record }) => {
|
|
509
|
+
cache.invalidate(`${model}:${record.id}`);
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
ormEvents.subscribe('delete:after', ({ model, record }) => {
|
|
513
|
+
if (record) cache.invalidate(`${model}:${record.id}`);
|
|
514
|
+
});
|
|
515
|
+
```
|
|
516
|
+
|
|
386
517
|
---
|
|
387
518
|
|
|
388
519
|
## Critical Files for Common Tasks
|
|
@@ -412,7 +543,7 @@ config.orm = {
|
|
|
412
543
|
|
|
413
544
|
**Import the ORM:**
|
|
414
545
|
```javascript
|
|
415
|
-
import { Orm, Model, Serializer, attr, hasMany, belongsTo, createRecord, updateRecord, store } from '@stonyx/orm';
|
|
546
|
+
import { Orm, Model, Serializer, attr, hasMany, belongsTo, createRecord, updateRecord, store, ormEvents } from '@stonyx/orm';
|
|
416
547
|
```
|
|
417
548
|
|
|
418
549
|
**Initialize:**
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
name: Publish to NPM
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
# Manual trigger (kept for flexibility)
|
|
5
|
+
workflow_dispatch:
|
|
6
|
+
inputs:
|
|
7
|
+
version-type:
|
|
8
|
+
description: 'Version type'
|
|
9
|
+
required: true
|
|
10
|
+
type: choice
|
|
11
|
+
options:
|
|
12
|
+
- alpha
|
|
13
|
+
- patch
|
|
14
|
+
- minor
|
|
15
|
+
- major
|
|
16
|
+
custom-version:
|
|
17
|
+
description: 'Custom version (optional, overrides version-type)'
|
|
18
|
+
required: false
|
|
19
|
+
type: string
|
|
20
|
+
|
|
21
|
+
# Auto-publish alpha on PR
|
|
22
|
+
pull_request:
|
|
23
|
+
types: [opened, synchronize, reopened]
|
|
24
|
+
branches: [main, dev]
|
|
25
|
+
|
|
26
|
+
# Auto-publish stable on merge to main
|
|
27
|
+
push:
|
|
28
|
+
branches: [main]
|
|
29
|
+
|
|
30
|
+
permissions:
|
|
31
|
+
contents: write
|
|
32
|
+
id-token: write # Required for npm provenance
|
|
33
|
+
pull-requests: write # For PR comments
|
|
34
|
+
|
|
35
|
+
jobs:
|
|
36
|
+
publish:
|
|
37
|
+
runs-on: ubuntu-latest
|
|
38
|
+
|
|
39
|
+
steps:
|
|
40
|
+
- name: Checkout code
|
|
41
|
+
uses: actions/checkout@v3
|
|
42
|
+
with:
|
|
43
|
+
fetch-depth: 0
|
|
44
|
+
# For PR events, check out the PR branch
|
|
45
|
+
ref: ${{ github.event_name == 'pull_request' && github.head_ref || github.ref }}
|
|
46
|
+
|
|
47
|
+
- name: Setup pnpm
|
|
48
|
+
uses: pnpm/action-setup@v4
|
|
49
|
+
with:
|
|
50
|
+
version: 9
|
|
51
|
+
|
|
52
|
+
- name: Set up Node.js
|
|
53
|
+
uses: actions/setup-node@v3
|
|
54
|
+
with:
|
|
55
|
+
node-version: 24.13.0
|
|
56
|
+
cache: 'pnpm'
|
|
57
|
+
registry-url: 'https://registry.npmjs.org'
|
|
58
|
+
|
|
59
|
+
- name: Install dependencies
|
|
60
|
+
run: pnpm install --frozen-lockfile
|
|
61
|
+
|
|
62
|
+
- name: Run tests
|
|
63
|
+
run: pnpm test
|
|
64
|
+
|
|
65
|
+
- name: Configure git
|
|
66
|
+
run: |
|
|
67
|
+
git config user.name "github-actions[bot]"
|
|
68
|
+
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
69
|
+
|
|
70
|
+
# Determine version type based on trigger
|
|
71
|
+
- name: Determine version bump type
|
|
72
|
+
id: version-type
|
|
73
|
+
run: |
|
|
74
|
+
if [ "${{ github.event_name }}" = "pull_request" ]; then
|
|
75
|
+
echo "type=alpha" >> $GITHUB_OUTPUT
|
|
76
|
+
elif [ "${{ github.event_name }}" = "push" ]; then
|
|
77
|
+
echo "type=patch" >> $GITHUB_OUTPUT
|
|
78
|
+
elif [ "${{ github.event.inputs.custom-version }}" != "" ]; then
|
|
79
|
+
echo "type=custom" >> $GITHUB_OUTPUT
|
|
80
|
+
else
|
|
81
|
+
echo "type=${{ github.event.inputs.version-type }}" >> $GITHUB_OUTPUT
|
|
82
|
+
fi
|
|
83
|
+
|
|
84
|
+
# Version bumping
|
|
85
|
+
- name: Bump version (custom)
|
|
86
|
+
if: steps.version-type.outputs.type == 'custom'
|
|
87
|
+
run: pnpm version ${{ github.event.inputs.custom-version }} --no-git-tag-version
|
|
88
|
+
|
|
89
|
+
- name: Bump version (alpha)
|
|
90
|
+
if: steps.version-type.outputs.type == 'alpha'
|
|
91
|
+
run: pnpm version prerelease --preid=alpha --no-git-tag-version
|
|
92
|
+
|
|
93
|
+
- name: Bump version (patch/minor/major)
|
|
94
|
+
if: steps.version-type.outputs.type == 'patch' || steps.version-type.outputs.type == 'minor' || steps.version-type.outputs.type == 'major'
|
|
95
|
+
run: pnpm version ${{ steps.version-type.outputs.type }} --no-git-tag-version
|
|
96
|
+
|
|
97
|
+
- name: Get package version
|
|
98
|
+
id: package-version
|
|
99
|
+
run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT
|
|
100
|
+
|
|
101
|
+
# Publishing
|
|
102
|
+
- name: Publish to NPM (alpha)
|
|
103
|
+
if: contains(steps.package-version.outputs.version, 'alpha')
|
|
104
|
+
run: pnpm publish --tag alpha --access public --no-git-checks
|
|
105
|
+
|
|
106
|
+
- name: Publish to NPM (stable)
|
|
107
|
+
if: "!contains(steps.package-version.outputs.version, 'alpha')"
|
|
108
|
+
run: pnpm publish --access public
|
|
109
|
+
|
|
110
|
+
# Only commit and tag for stable releases (push to main or manual stable)
|
|
111
|
+
- name: Commit version bump and create tag
|
|
112
|
+
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && !contains(steps.package-version.outputs.version, 'alpha'))
|
|
113
|
+
run: |
|
|
114
|
+
git add package.json
|
|
115
|
+
git commit -m "chore: release v${{ steps.package-version.outputs.version }}"
|
|
116
|
+
git tag v${{ steps.package-version.outputs.version }}
|
|
117
|
+
git push origin main --tags
|
|
118
|
+
|
|
119
|
+
- name: Create GitHub Release
|
|
120
|
+
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && !contains(steps.package-version.outputs.version, 'alpha'))
|
|
121
|
+
uses: actions/create-release@v1
|
|
122
|
+
env:
|
|
123
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
124
|
+
with:
|
|
125
|
+
tag_name: v${{ steps.package-version.outputs.version }}
|
|
126
|
+
release_name: v${{ steps.package-version.outputs.version }}
|
|
127
|
+
draft: false
|
|
128
|
+
prerelease: false
|
|
129
|
+
|
|
130
|
+
# Add PR comment with alpha version info
|
|
131
|
+
- name: Comment on PR with alpha version
|
|
132
|
+
if: github.event_name == 'pull_request'
|
|
133
|
+
uses: actions/github-script@v6
|
|
134
|
+
with:
|
|
135
|
+
script: |
|
|
136
|
+
const version = '${{ steps.package-version.outputs.version }}';
|
|
137
|
+
const packageName = require('./package.json').name;
|
|
138
|
+
github.rest.issues.createComment({
|
|
139
|
+
issue_number: context.issue.number,
|
|
140
|
+
owner: context.repo.owner,
|
|
141
|
+
repo: context.repo.repo,
|
|
142
|
+
body: `## 🚀 Alpha Version Published\n\n**Version:** \`${version}\`\n\n**Install:**\n\`\`\`bash\npnpm add ${packageName}@${version}\n# or\npnpm add ${packageName}@alpha # latest alpha\n\`\`\`\n\nThis alpha version is now available for testing!`
|
|
143
|
+
});
|
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"stonyx-async",
|
|
5
5
|
"stonyx-module"
|
|
6
6
|
],
|
|
7
|
-
"version": "0.2.0",
|
|
7
|
+
"version": "0.2.3-alpha.0",
|
|
8
8
|
"description": "",
|
|
9
9
|
"main": "src/main.js",
|
|
10
10
|
"type": "module",
|
|
@@ -12,9 +12,6 @@
|
|
|
12
12
|
".": "./src/index.js",
|
|
13
13
|
"./db": "./src/exports/db.js"
|
|
14
14
|
},
|
|
15
|
-
"scripts": {
|
|
16
|
-
"test": "qunit --require ./stonyx-bootstrap.cjs"
|
|
17
|
-
},
|
|
18
15
|
"repository": {
|
|
19
16
|
"type": "git",
|
|
20
17
|
"url": "git+https://github.com/abofs/stonyx-orm.git"
|
|
@@ -25,20 +22,25 @@
|
|
|
25
22
|
"Stone Costa <stone.costa@synamicd.com>"
|
|
26
23
|
],
|
|
27
24
|
"publishConfig": {
|
|
28
|
-
"access": "public"
|
|
25
|
+
"access": "public",
|
|
26
|
+
"provenance": true
|
|
29
27
|
},
|
|
30
28
|
"bugs": {
|
|
31
29
|
"url": "https://github.com/abofs/stonyx-orm/issues"
|
|
32
30
|
},
|
|
33
31
|
"homepage": "https://github.com/abofs/stonyx-orm#readme",
|
|
34
32
|
"dependencies": {
|
|
35
|
-
"stonyx": "^0.2.2"
|
|
33
|
+
"stonyx": "^0.2.2",
|
|
34
|
+
"@stonyx/events": "^0.1.0",
|
|
35
|
+
"@stonyx/cron": "^0.2.0"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
|
-
"@stonyx/cron": "^0.2.0",
|
|
39
38
|
"@stonyx/rest-server": "^0.2.0",
|
|
40
39
|
"@stonyx/utils": "^0.2.2",
|
|
41
40
|
"qunit": "^2.24.1",
|
|
42
41
|
"sinon": "^21.0.0"
|
|
42
|
+
},
|
|
43
|
+
"scripts": {
|
|
44
|
+
"test": "qunit --require ./stonyx-bootstrap.cjs"
|
|
43
45
|
}
|
|
44
|
-
}
|
|
46
|
+
}
|
package/src/model-property.js
CHANGED
package/src/orm-request.js
CHANGED
|
@@ -114,6 +114,51 @@ function parseInclude(includeParam) {
|
|
|
114
114
|
.map(rel => rel.split('.')); // Parse nested paths: "owner.pets" → ["owner", "pets"]
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
function parseFields(query) {
|
|
118
|
+
const fields = new Map();
|
|
119
|
+
if (!query) return fields;
|
|
120
|
+
|
|
121
|
+
for (const [key, value] of Object.entries(query)) {
|
|
122
|
+
const match = key.match(/^fields\[(\w+)\]$/);
|
|
123
|
+
if (match && typeof value === 'string') {
|
|
124
|
+
const modelName = match[1];
|
|
125
|
+
const fieldNames = value.split(',').map(f => f.trim()).filter(f => f);
|
|
126
|
+
fields.set(modelName, new Set(fieldNames));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return fields;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function parseFilters(query) {
|
|
134
|
+
const filters = [];
|
|
135
|
+
if (!query) return filters;
|
|
136
|
+
|
|
137
|
+
for (const [key, value] of Object.entries(query)) {
|
|
138
|
+
const match = key.match(/^filter\[(.+)\]$/);
|
|
139
|
+
if (match && typeof value === 'string') {
|
|
140
|
+
filters.push({ path: match[1].split('.'), value });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return filters;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function createFilterPredicate(filters) {
|
|
148
|
+
if (filters.length === 0) return null;
|
|
149
|
+
|
|
150
|
+
return (record) => filters.every(({ path, value }) => {
|
|
151
|
+
let current = record;
|
|
152
|
+
|
|
153
|
+
for (const segment of path) {
|
|
154
|
+
if (current == null) return false;
|
|
155
|
+
current = current[segment];
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return String(current) === value;
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
117
162
|
export default class OrmRequest extends Request {
|
|
118
163
|
constructor({ model, access }) {
|
|
119
164
|
super(...arguments);
|
|
@@ -123,20 +168,30 @@ export default class OrmRequest extends Request {
|
|
|
123
168
|
|
|
124
169
|
this.handlers = {
|
|
125
170
|
get: {
|
|
126
|
-
[`/${pluralizedModel}`]: (request, { filter }) => {
|
|
171
|
+
[`/${pluralizedModel}`]: (request, { filter: accessFilter }) => {
|
|
127
172
|
const allRecords = Array.from(store.get(model).values());
|
|
128
|
-
const recordsToReturn = filter ? allRecords.filter(filter) : allRecords;
|
|
129
|
-
const data = recordsToReturn.map(record => record.toJSON());
|
|
130
173
|
|
|
174
|
+
const queryFilters = parseFilters(request.query);
|
|
175
|
+
const queryFilterPredicate = createFilterPredicate(queryFilters);
|
|
176
|
+
const fieldsMap = parseFields(request.query);
|
|
177
|
+
const modelFields = fieldsMap.get(pluralizedModel) || fieldsMap.get(model);
|
|
178
|
+
|
|
179
|
+
let recordsToReturn = allRecords;
|
|
180
|
+
if (accessFilter) recordsToReturn = recordsToReturn.filter(accessFilter);
|
|
181
|
+
if (queryFilterPredicate) recordsToReturn = recordsToReturn.filter(queryFilterPredicate);
|
|
182
|
+
|
|
183
|
+
const data = recordsToReturn.map(record => record.toJSON({ fields: modelFields }));
|
|
131
184
|
return buildResponse(data, request.query?.include, recordsToReturn);
|
|
132
185
|
},
|
|
133
186
|
|
|
134
187
|
[`/${pluralizedModel}/:id`]: (request) => {
|
|
135
188
|
const record = store.get(model, getId(request.params));
|
|
189
|
+
if (!record) return 404;
|
|
136
190
|
|
|
137
|
-
|
|
191
|
+
const fieldsMap = parseFields(request.query);
|
|
192
|
+
const modelFields = fieldsMap.get(pluralizedModel) || fieldsMap.get(model);
|
|
138
193
|
|
|
139
|
-
return buildResponse(record.toJSON(), request.query?.include, record);
|
|
194
|
+
return buildResponse(record.toJSON({ fields: modelFields }), request.query?.include, record);
|
|
140
195
|
}
|
|
141
196
|
},
|
|
142
197
|
|
|
@@ -160,14 +215,17 @@ export default class OrmRequest extends Request {
|
|
|
160
215
|
},
|
|
161
216
|
|
|
162
217
|
post: {
|
|
163
|
-
[`/${pluralizedModel}`]: (
|
|
164
|
-
const { attributes } = body?.data || {};
|
|
218
|
+
[`/${pluralizedModel}`]: (request) => {
|
|
219
|
+
const { attributes } = request.body?.data || {};
|
|
165
220
|
|
|
166
221
|
if (!attributes) return 400; // Bad request
|
|
167
222
|
|
|
223
|
+
const fieldsMap = parseFields(request.query);
|
|
224
|
+
const modelFields = fieldsMap.get(pluralizedModel) || fieldsMap.get(model);
|
|
225
|
+
|
|
168
226
|
const record = createRecord(model, attributes, { serialize: false });
|
|
169
227
|
|
|
170
|
-
return { data: record.toJSON() };
|
|
228
|
+
return { data: record.toJSON({ fields: modelFields }) };
|
|
171
229
|
}
|
|
172
230
|
},
|
|
173
231
|
|
package/src/record.js
CHANGED
|
@@ -48,21 +48,29 @@ export default class Record {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
// Formats record for JSON API output
|
|
51
|
-
toJSON() {
|
|
51
|
+
toJSON(options = {}) {
|
|
52
52
|
if (!this.__serialized) throw new Error('Record must be serialized before being converted to JSON');
|
|
53
|
-
|
|
53
|
+
|
|
54
54
|
const { __data:data } = this;
|
|
55
|
+
const { fields } = options;
|
|
55
56
|
const relationships = {};
|
|
56
|
-
const attributes = {
|
|
57
|
-
|
|
57
|
+
const attributes = {};
|
|
58
|
+
|
|
59
|
+
for (const [key, value] of Object.entries(data)) {
|
|
60
|
+
if (key === 'id') continue;
|
|
61
|
+
if (fields && !fields.has(key)) continue;
|
|
62
|
+
attributes[key] = value;
|
|
63
|
+
}
|
|
58
64
|
|
|
59
65
|
for (const [key, getter] of getComputedProperties(this.__model)) {
|
|
66
|
+
if (fields && !fields.has(key)) continue;
|
|
60
67
|
attributes[key] = getter.call(this);
|
|
61
68
|
}
|
|
62
69
|
|
|
63
|
-
for (const [
|
|
70
|
+
for (const [key, childRecord] of Object.entries(this.__relationships)) {
|
|
71
|
+
if (fields && !fields.has(key)) continue;
|
|
64
72
|
relationships[key] = {
|
|
65
|
-
data: Array.isArray(childRecord)
|
|
73
|
+
data: Array.isArray(childRecord)
|
|
66
74
|
? childRecord.map(r => ({ type: r.__model.__name, id: r.id }))
|
|
67
75
|
: childRecord ? { type: childRecord.__model.__name, id: childRecord.id } : null
|
|
68
76
|
};
|