@stonyx/orm 0.2.1-alpha.0 → 0.2.1-alpha.10
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/code-style-rules.md +44 -0
- package/.claude/hooks.md +250 -0
- package/.claude/index.md +281 -0
- package/.claude/usage-patterns.md +234 -0
- package/.github/workflows/ci.yml +5 -25
- package/.github/workflows/publish.yml +24 -116
- package/README.md +440 -15
- package/config/environment.js +26 -5
- package/improvements.md +139 -0
- package/package.json +19 -8
- package/project-structure.md +343 -0
- package/src/commands.js +170 -0
- package/src/db.js +132 -6
- package/src/hooks.js +124 -0
- package/src/index.js +8 -1
- package/src/main.js +47 -3
- package/src/manage-record.js +19 -4
- package/src/migrate.js +72 -0
- package/src/model.js +11 -0
- package/src/mysql/connection.js +28 -0
- package/src/mysql/migration-generator.js +188 -0
- package/src/mysql/migration-runner.js +110 -0
- package/src/mysql/mysql-db.js +422 -0
- package/src/mysql/query-builder.js +64 -0
- package/src/mysql/schema-introspector.js +160 -0
- package/src/mysql/type-map.js +37 -0
- package/src/orm-request.js +355 -41
- package/src/plural-registry.js +12 -0
- package/src/record.js +47 -12
- package/src/serializer.js +2 -2
- package/src/setup-rest-server.js +4 -1
- package/src/store.js +105 -0
- package/src/utils.js +12 -0
- package/test-events-setup.js +41 -0
- package/test-hooks-manual.js +54 -0
- package/test-hooks-with-logging.js +52 -0
- package/.claude/project-structure.md +0 -578
- package/stonyx-bootstrap.cjs +0 -30
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
# Usage Patterns
|
|
2
|
+
|
|
3
|
+
## 1. Model Definition
|
|
4
|
+
|
|
5
|
+
Models extend `Model` and use decorators for attributes and relationships:
|
|
6
|
+
|
|
7
|
+
```javascript
|
|
8
|
+
// test/sample/models/animal.js
|
|
9
|
+
import { Model, attr, belongsTo, hasMany } from '@stonyx/orm';
|
|
10
|
+
|
|
11
|
+
export default class AnimalModel extends Model {
|
|
12
|
+
// Attributes with type transforms
|
|
13
|
+
type = attr('animal'); // Custom transform
|
|
14
|
+
age = attr('number'); // Built-in transform
|
|
15
|
+
size = attr('string');
|
|
16
|
+
|
|
17
|
+
// Relationships
|
|
18
|
+
owner = belongsTo('owner'); // Many-to-one
|
|
19
|
+
traits = hasMany('trait'); // One-to-many
|
|
20
|
+
|
|
21
|
+
// Computed properties
|
|
22
|
+
get tag() {
|
|
23
|
+
return `${this.owner.id}'s ${this.size} animal`;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
**Key Points:**
|
|
29
|
+
- Use `attr(type)` for simple attributes
|
|
30
|
+
- Use `belongsTo(modelName)` for many-to-one
|
|
31
|
+
- Use `hasMany(modelName)` for one-to-many
|
|
32
|
+
- Getters work as computed properties
|
|
33
|
+
- Relationships auto-establish bidirectionally
|
|
34
|
+
- Override auto-pluralization with `static pluralName` (see [Overriding Plural Names](#overriding-plural-names))
|
|
35
|
+
|
|
36
|
+
### Overriding Plural Names
|
|
37
|
+
|
|
38
|
+
By default, model names are auto-pluralized (e.g., `animal` → `animals`) for REST routes, JSON:API URLs, and DB table names. When auto-pluralization produces the wrong result, override it with `static pluralName`:
|
|
39
|
+
|
|
40
|
+
```javascript
|
|
41
|
+
import { Model, attr } from '@stonyx/orm';
|
|
42
|
+
|
|
43
|
+
export default class PersonModel extends Model {
|
|
44
|
+
static pluralName = 'people';
|
|
45
|
+
|
|
46
|
+
name = attr('string');
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The override is picked up automatically during ORM initialization — no additional registration is needed. All internal call sites (REST routes, JSON:API type references, MySQL table names, foreign key references) use the overridden value.
|
|
51
|
+
|
|
52
|
+
## 2. Serializers (Data Transformation)
|
|
53
|
+
|
|
54
|
+
Serializers map raw data paths to model properties:
|
|
55
|
+
|
|
56
|
+
```javascript
|
|
57
|
+
// test/sample/serializers/animal.js
|
|
58
|
+
import { Serializer } from '@stonyx/orm';
|
|
59
|
+
|
|
60
|
+
export default class AnimalSerializer extends Serializer {
|
|
61
|
+
map = {
|
|
62
|
+
// Nested path mapping
|
|
63
|
+
age: 'details.age',
|
|
64
|
+
size: 'details.c',
|
|
65
|
+
owner: 'details.location.owner',
|
|
66
|
+
|
|
67
|
+
// Custom transformation function
|
|
68
|
+
traits: ['details', ({ x:color }) => {
|
|
69
|
+
const traits = [{ id: 1, type: 'habitat', value: 'farm' }];
|
|
70
|
+
if (color) traits.push({ id: 2, type: 'color', value: color });
|
|
71
|
+
return traits;
|
|
72
|
+
}]
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
**Key Points:**
|
|
78
|
+
- `map` object defines field mappings
|
|
79
|
+
- Supports nested paths (`'details.age'`)
|
|
80
|
+
- Custom functions for complex transformations
|
|
81
|
+
- Handlers receive raw data subset
|
|
82
|
+
|
|
83
|
+
## 3. Custom Transforms
|
|
84
|
+
|
|
85
|
+
Transforms convert data types:
|
|
86
|
+
|
|
87
|
+
```javascript
|
|
88
|
+
// test/sample/transforms/animal.js
|
|
89
|
+
const codeEnumMap = { 'dog': 1, 'cat': 2, 'bird': 3 };
|
|
90
|
+
|
|
91
|
+
export default function(value) {
|
|
92
|
+
return codeEnumMap[value] || 0;
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
**Built-in Transforms:**
|
|
97
|
+
- Type: `boolean`, `number`, `float`, `string`, `date`, `timestamp`
|
|
98
|
+
- Math: `round`, `ceil`, `floor`
|
|
99
|
+
- String: `trim`, `uppercase`
|
|
100
|
+
- Utility: `passthrough`
|
|
101
|
+
|
|
102
|
+
## 4. CRUD Operations
|
|
103
|
+
|
|
104
|
+
```javascript
|
|
105
|
+
import { createRecord, updateRecord, store } from '@stonyx/orm';
|
|
106
|
+
|
|
107
|
+
// Create
|
|
108
|
+
createRecord('owner', { id: 'bob', age: 30 });
|
|
109
|
+
|
|
110
|
+
// Read
|
|
111
|
+
const owner = store.get('owner', 'bob');
|
|
112
|
+
const allOwners = store.get('owner');
|
|
113
|
+
|
|
114
|
+
// Update
|
|
115
|
+
updateRecord(owner, { age: 31 });
|
|
116
|
+
// Or direct: owner.age = 31;
|
|
117
|
+
|
|
118
|
+
// Delete
|
|
119
|
+
store.remove('owner', 'bob');
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## 5. Database Schema
|
|
123
|
+
|
|
124
|
+
The DB schema is a Model defining top-level collections:
|
|
125
|
+
|
|
126
|
+
```javascript
|
|
127
|
+
// test/sample/db-schema.js
|
|
128
|
+
import { Model, hasMany } from '@stonyx/orm';
|
|
129
|
+
|
|
130
|
+
export default class DBModel extends Model {
|
|
131
|
+
owners = hasMany('owner');
|
|
132
|
+
animals = hasMany('animal');
|
|
133
|
+
traits = hasMany('trait');
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## 6. Persistence
|
|
138
|
+
|
|
139
|
+
```javascript
|
|
140
|
+
import Orm from '@stonyx/orm';
|
|
141
|
+
|
|
142
|
+
// Save to file
|
|
143
|
+
await Orm.db.save();
|
|
144
|
+
|
|
145
|
+
// Data auto-serializes to JSON file
|
|
146
|
+
// Reload using createRecord with serialize:false, transform:false
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## 7. Access Control
|
|
150
|
+
|
|
151
|
+
```javascript
|
|
152
|
+
// test/sample/access/global-access.js
|
|
153
|
+
export default class GlobalAccess {
|
|
154
|
+
models = ['owner', 'animal']; // or '*' for all
|
|
155
|
+
|
|
156
|
+
access(request) {
|
|
157
|
+
// Deny specific access
|
|
158
|
+
if (request.url.endsWith('/owner/angela')) return false;
|
|
159
|
+
|
|
160
|
+
// Filter collections
|
|
161
|
+
if (request.url.endsWith('/owner')) {
|
|
162
|
+
return record => record.id !== 'angela';
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Grant CRUD permissions
|
|
166
|
+
return ['read', 'create', 'update', 'delete'];
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## 8. REST API (Auto-generated)
|
|
172
|
+
|
|
173
|
+
```javascript
|
|
174
|
+
// Endpoints auto-generated for models:
|
|
175
|
+
// GET /owners - List all
|
|
176
|
+
// GET /owners/:id - Get one
|
|
177
|
+
// POST /animals - Create
|
|
178
|
+
// PATCH /animals/:id - Update (attributes and/or relationships)
|
|
179
|
+
// DELETE /animals/:id - Delete
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
**PATCH supports both attributes and relationships:**
|
|
183
|
+
```javascript
|
|
184
|
+
// Update attributes only
|
|
185
|
+
PATCH /animals/1
|
|
186
|
+
{ data: { type: 'animal', attributes: { age: 5 } } }
|
|
187
|
+
|
|
188
|
+
// Update relationship only
|
|
189
|
+
PATCH /animals/1
|
|
190
|
+
{ data: { type: 'animal', relationships: { owner: { data: { type: 'owner', id: 'gina' } } } } }
|
|
191
|
+
|
|
192
|
+
// Update both
|
|
193
|
+
PATCH /animals/1
|
|
194
|
+
{ data: { type: 'animal', attributes: { age: 5 }, relationships: { owner: { data: { type: 'owner', id: 'gina' } } } } }
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## 9. Include Parameter (Sideloading)
|
|
198
|
+
|
|
199
|
+
GET endpoints support sideloading related records with **nested relationship traversal**:
|
|
200
|
+
|
|
201
|
+
```javascript
|
|
202
|
+
// Single-level includes
|
|
203
|
+
GET /animals/1?include=owner,traits
|
|
204
|
+
|
|
205
|
+
// Nested includes (NEW!)
|
|
206
|
+
GET /animals/1?include=owner.pets,owner.company
|
|
207
|
+
|
|
208
|
+
// Deep nesting (3+ levels)
|
|
209
|
+
GET /scenes/e001-s001?include=slides.dialogue.character
|
|
210
|
+
|
|
211
|
+
// Response structure (unchanged)
|
|
212
|
+
{
|
|
213
|
+
data: { type: 'animal', id: 1, attributes: {...}, relationships: {...} },
|
|
214
|
+
included: [
|
|
215
|
+
{ type: 'owner', id: 'angela', ... },
|
|
216
|
+
{ type: 'animal', id: 7, ... }, // owner's other pets
|
|
217
|
+
{ type: 'animal', id: 11, ... }, // owner's other pets
|
|
218
|
+
{ type: 'company', id: 'acme', ... } // owner's company (if requested)
|
|
219
|
+
]
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
**How Nested Includes Work:**
|
|
224
|
+
1. Query param parsed into path segments: `owner.pets` -> `[['owner'], ['owner', 'pets'], ['traits']]`
|
|
225
|
+
2. `traverseIncludePath()` recursively traverses relationships depth-first
|
|
226
|
+
3. Deduplication still by type+id (no duplicates in included array)
|
|
227
|
+
4. Gracefully handles null/missing relationships at any depth
|
|
228
|
+
5. Each included record gets full `toJSON()` representation
|
|
229
|
+
|
|
230
|
+
**Key Functions:**
|
|
231
|
+
- `parseInclude()` - Splits comma-separated includes and parses nested paths
|
|
232
|
+
- `traverseIncludePath()` - Recursively traverses relationship paths
|
|
233
|
+
- `collectIncludedRecords()` - Orchestrates traversal and deduplication
|
|
234
|
+
- All implemented in [src/orm-request.js](src/orm-request.js)
|
package/.github/workflows/ci.yml
CHANGED
|
@@ -2,35 +2,15 @@ name: CI
|
|
|
2
2
|
|
|
3
3
|
on:
|
|
4
4
|
pull_request:
|
|
5
|
-
branches:
|
|
6
|
-
- dev
|
|
7
|
-
- main
|
|
5
|
+
branches: [dev, main]
|
|
8
6
|
|
|
9
7
|
concurrency:
|
|
10
8
|
group: ci-${{ github.head_ref || github.ref }}
|
|
11
9
|
cancel-in-progress: true
|
|
12
10
|
|
|
11
|
+
permissions:
|
|
12
|
+
contents: read
|
|
13
|
+
|
|
13
14
|
jobs:
|
|
14
15
|
test:
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
steps:
|
|
18
|
-
- name: Checkout code
|
|
19
|
-
uses: actions/checkout@v3
|
|
20
|
-
|
|
21
|
-
- name: Setup pnpm
|
|
22
|
-
uses: pnpm/action-setup@v4
|
|
23
|
-
with:
|
|
24
|
-
version: 9
|
|
25
|
-
|
|
26
|
-
- name: Set up Node.js
|
|
27
|
-
uses: actions/setup-node@v3
|
|
28
|
-
with:
|
|
29
|
-
node-version: 24.13.0
|
|
30
|
-
cache: 'pnpm'
|
|
31
|
-
|
|
32
|
-
- name: Install dependencies
|
|
33
|
-
run: pnpm install --frozen-lockfile
|
|
34
|
-
|
|
35
|
-
- name: Run tests
|
|
36
|
-
run: pnpm test
|
|
16
|
+
uses: abofs/stonyx-workflows/.github/workflows/ci.yml@main
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
name: Publish to NPM
|
|
2
2
|
|
|
3
3
|
on:
|
|
4
|
-
|
|
4
|
+
repository_dispatch:
|
|
5
|
+
types: [cascade-publish]
|
|
5
6
|
workflow_dispatch:
|
|
6
7
|
inputs:
|
|
7
8
|
version-type:
|
|
@@ -9,7 +10,6 @@ on:
|
|
|
9
10
|
required: true
|
|
10
11
|
type: choice
|
|
11
12
|
options:
|
|
12
|
-
- alpha
|
|
13
13
|
- patch
|
|
14
14
|
- minor
|
|
15
15
|
- major
|
|
@@ -17,127 +17,35 @@ on:
|
|
|
17
17
|
description: 'Custom version (optional, overrides version-type)'
|
|
18
18
|
required: false
|
|
19
19
|
type: string
|
|
20
|
-
|
|
21
|
-
# Auto-publish alpha on PR
|
|
22
20
|
pull_request:
|
|
23
21
|
types: [opened, synchronize, reopened]
|
|
24
|
-
branches: [main
|
|
25
|
-
|
|
26
|
-
# Auto-publish stable on merge to main
|
|
22
|
+
branches: [main]
|
|
27
23
|
push:
|
|
28
24
|
branches: [main]
|
|
29
25
|
|
|
26
|
+
concurrency:
|
|
27
|
+
group: ${{ github.event_name == 'repository_dispatch' && 'cascade-update' || format('publish-{0}', github.ref) }}
|
|
28
|
+
cancel-in-progress: false
|
|
29
|
+
|
|
30
30
|
permissions:
|
|
31
31
|
contents: write
|
|
32
|
-
id-token: write
|
|
33
|
-
pull-requests: write
|
|
32
|
+
id-token: write
|
|
33
|
+
pull-requests: write
|
|
34
34
|
|
|
35
35
|
jobs:
|
|
36
36
|
publish:
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
});
|
|
37
|
+
if: "!contains(github.event.head_commit.message, '[skip ci]')"
|
|
38
|
+
uses: abofs/stonyx-workflows/.github/workflows/npm-publish.yml@main
|
|
39
|
+
with:
|
|
40
|
+
version-type: ${{ github.event.inputs.version-type }}
|
|
41
|
+
custom-version: ${{ github.event.inputs.custom-version }}
|
|
42
|
+
cascade-source: ${{ github.event.client_payload.source_package || '' }}
|
|
43
|
+
secrets: inherit
|
|
44
|
+
|
|
45
|
+
cascade:
|
|
46
|
+
needs: publish
|
|
47
|
+
uses: abofs/stonyx-workflows/.github/workflows/cascade.yml@main
|
|
48
|
+
with:
|
|
49
|
+
package-name: ${{ needs.publish.outputs.package-name }}
|
|
50
|
+
published-version: ${{ needs.publish.outputs.published-version }}
|
|
51
|
+
secrets: inherit
|