@stonyx/orm 0.2.1-beta.2 → 0.2.1-beta.4
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 +279 -0
- package/.claude/usage-patterns.md +217 -0
- package/.github/workflows/publish.yml +17 -1
- package/README.md +424 -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 +2 -1
- package/src/main.js +31 -2
- package/src/manage-record.js +19 -4
- package/src/migrate.js +72 -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 +320 -0
- package/src/mysql/query-builder.js +64 -0
- package/src/mysql/schema-introspector.js +158 -0
- package/src/mysql/type-map.js +37 -0
- package/src/orm-request.js +306 -52
- package/src/record.js +35 -8
- package/src/serializer.js +2 -2
- package/src/setup-rest-server.js +4 -1
- 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,44 @@
|
|
|
1
|
+
# Stonyx Code Style Rules
|
|
2
|
+
|
|
3
|
+
Strict prettier/eslint rules to apply across all Stonyx projects. These will be formalized into an ESLint/Prettier config once enough patterns are collected.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Rules
|
|
8
|
+
|
|
9
|
+
### 1. Destructure config objects in function signatures
|
|
10
|
+
|
|
11
|
+
When a function receives a config/options object and only uses specific properties, destructure them in the function signature rather than accessing them via dot notation in the body.
|
|
12
|
+
|
|
13
|
+
**Bad:**
|
|
14
|
+
```javascript
|
|
15
|
+
export async function getPool(mysqlConfig) {
|
|
16
|
+
pool = mysql.createPool({
|
|
17
|
+
host: mysqlConfig.host,
|
|
18
|
+
port: mysqlConfig.port,
|
|
19
|
+
user: mysqlConfig.user,
|
|
20
|
+
password: mysqlConfig.password,
|
|
21
|
+
database: mysqlConfig.database,
|
|
22
|
+
connectionLimit: mysqlConfig.connectionLimit,
|
|
23
|
+
// ...
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
**Good:**
|
|
29
|
+
```javascript
|
|
30
|
+
export async function getPool({ host, port, user, password, database, connectionLimit }) {
|
|
31
|
+
pool = mysql.createPool({
|
|
32
|
+
host,
|
|
33
|
+
port,
|
|
34
|
+
user,
|
|
35
|
+
password,
|
|
36
|
+
database,
|
|
37
|
+
connectionLimit,
|
|
38
|
+
// ...
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
**Source:** PR #14, `src/mysql/connection.js`
|
|
44
|
+
**ESLint rule (candidate):** `prefer-destructuring` (with custom config for function parameters)
|
package/.claude/hooks.md
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
# Middleware Hooks System
|
|
2
|
+
|
|
3
|
+
The ORM provides a powerful middleware-based hook system that allows custom logic before and after CRUD operations. **Before hooks can halt operations** by returning a value.
|
|
4
|
+
|
|
5
|
+
## Architecture
|
|
6
|
+
|
|
7
|
+
**Hook Registry**: [src/hooks.js](src/hooks.js) - Stores before/after hooks in Maps
|
|
8
|
+
**Integration**: [src/orm-request.js](src/orm-request.js) - `_withHooks()` wrapper executes hooks
|
|
9
|
+
**Exports**: [src/index.js](src/index.js) - Exports `beforeHook`, `afterHook`, `clearHook`, `clearAllHooks`
|
|
10
|
+
|
|
11
|
+
## API
|
|
12
|
+
|
|
13
|
+
### `beforeHook(operation, model, handler)`
|
|
14
|
+
|
|
15
|
+
Register a before hook that runs before the operation executes.
|
|
16
|
+
|
|
17
|
+
```javascript
|
|
18
|
+
import { beforeHook } from '@stonyx/orm';
|
|
19
|
+
|
|
20
|
+
beforeHook('create', 'animal', (context) => {
|
|
21
|
+
// Validate, transform, authorize...
|
|
22
|
+
if (invalid) {
|
|
23
|
+
return 400; // Halt with status code
|
|
24
|
+
}
|
|
25
|
+
// Return undefined to continue
|
|
26
|
+
});
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
**Handler return values:**
|
|
30
|
+
- `undefined` / no return - Operation continues
|
|
31
|
+
- **Any other value** - Halts operation and returns that value:
|
|
32
|
+
- Integer (e.g., `403`) - HTTP status code
|
|
33
|
+
- Object - JSON response body
|
|
34
|
+
|
|
35
|
+
**Returns:** Unregister function
|
|
36
|
+
|
|
37
|
+
### `afterHook(operation, model, handler)`
|
|
38
|
+
|
|
39
|
+
Register an after hook that runs after the operation completes.
|
|
40
|
+
|
|
41
|
+
```javascript
|
|
42
|
+
import { afterHook } from '@stonyx/orm';
|
|
43
|
+
|
|
44
|
+
afterHook('update', 'animal', (context) => {
|
|
45
|
+
console.log(`Updated animal ${context.record.id}`);
|
|
46
|
+
// After hooks cannot halt (operation already complete)
|
|
47
|
+
});
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**Returns:** Unregister function
|
|
51
|
+
|
|
52
|
+
### `clearHook(operation, model, [type])`
|
|
53
|
+
|
|
54
|
+
Clear registered hooks for a specific operation:model.
|
|
55
|
+
|
|
56
|
+
```javascript
|
|
57
|
+
import { clearHook } from '@stonyx/orm';
|
|
58
|
+
|
|
59
|
+
clearHook('create', 'animal'); // Clear both before and after
|
|
60
|
+
clearHook('create', 'animal', 'before'); // Clear only before hooks
|
|
61
|
+
clearHook('create', 'animal', 'after'); // Clear only after hooks
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### `clearAllHooks()`
|
|
65
|
+
|
|
66
|
+
Clear all registered hooks (useful for testing).
|
|
67
|
+
|
|
68
|
+
```javascript
|
|
69
|
+
import { clearAllHooks } from '@stonyx/orm';
|
|
70
|
+
|
|
71
|
+
afterEach(() => {
|
|
72
|
+
clearAllHooks();
|
|
73
|
+
});
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Operations
|
|
77
|
+
|
|
78
|
+
- `list` - GET collection (`/animals`)
|
|
79
|
+
- `get` - GET single record (`/animals/1`)
|
|
80
|
+
- `create` - POST new record (`/animals`)
|
|
81
|
+
- `update` - PATCH existing record (`/animals/1`)
|
|
82
|
+
- `delete` - DELETE record (`/animals/1`)
|
|
83
|
+
|
|
84
|
+
## Context Object
|
|
85
|
+
|
|
86
|
+
Each hook receives a context object:
|
|
87
|
+
|
|
88
|
+
```javascript
|
|
89
|
+
{
|
|
90
|
+
model: 'animal', // Model name
|
|
91
|
+
operation: 'create', // Operation type
|
|
92
|
+
request, // Express request object
|
|
93
|
+
params, // URL params (e.g., { id: 5 })
|
|
94
|
+
body, // Request body (POST/PATCH)
|
|
95
|
+
query, // Query parameters
|
|
96
|
+
state, // Request state (includes filter for access control)
|
|
97
|
+
|
|
98
|
+
// For update/delete operations:
|
|
99
|
+
oldState, // Deep copy of record BEFORE operation
|
|
100
|
+
|
|
101
|
+
// For after hooks only:
|
|
102
|
+
response, // Handler response
|
|
103
|
+
record, // Affected record (create/update/get)
|
|
104
|
+
records, // All records (list)
|
|
105
|
+
recordId, // Record ID (delete only, since record no longer exists)
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
**Notes:**
|
|
110
|
+
- `oldState` is captured via `JSON.parse(JSON.stringify())` before operation executes
|
|
111
|
+
- For delete operations, `recordId` is available since the record may no longer exist
|
|
112
|
+
- `oldState` enables precise field-level change detection
|
|
113
|
+
|
|
114
|
+
## Implementation Details
|
|
115
|
+
|
|
116
|
+
**Hook Wrapper** (`src/orm-request.js`):
|
|
117
|
+
|
|
118
|
+
```javascript
|
|
119
|
+
_withHooks(operation, handler) {
|
|
120
|
+
return async (request, state) => {
|
|
121
|
+
const context = { model, operation, request, params, body, query, state };
|
|
122
|
+
|
|
123
|
+
// Capture old state for update/delete
|
|
124
|
+
if (operation === 'update' || operation === 'delete') {
|
|
125
|
+
const existingRecord = store.get(this.model, getId(request.params));
|
|
126
|
+
if (existingRecord) {
|
|
127
|
+
context.oldState = JSON.parse(JSON.stringify(existingRecord.__data || existingRecord));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Run before hooks sequentially (can halt by returning a value)
|
|
132
|
+
for (const hook of getBeforeHooks(operation, this.model)) {
|
|
133
|
+
const result = await hook(context);
|
|
134
|
+
if (result !== undefined) {
|
|
135
|
+
return result; // Halt - return status/response
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Execute main handler
|
|
140
|
+
const response = await handler(request, state);
|
|
141
|
+
|
|
142
|
+
// Enrich context for after hooks
|
|
143
|
+
context.response = response;
|
|
144
|
+
context.record = /* fetched from store */;
|
|
145
|
+
context.records = /* for list operations */;
|
|
146
|
+
context.recordId = /* for delete operations */;
|
|
147
|
+
|
|
148
|
+
// Run after hooks sequentially
|
|
149
|
+
for (const hook of getAfterHooks(operation, this.model)) {
|
|
150
|
+
await hook(context);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return response;
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Usage Examples
|
|
159
|
+
|
|
160
|
+
### Validation (Halting)
|
|
161
|
+
|
|
162
|
+
```javascript
|
|
163
|
+
beforeHook('create', 'animal', (context) => {
|
|
164
|
+
const { age } = context.body.data.attributes;
|
|
165
|
+
if (age < 0) {
|
|
166
|
+
return 400; // Halt with Bad Request
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Custom Error Response
|
|
172
|
+
|
|
173
|
+
```javascript
|
|
174
|
+
beforeHook('delete', 'animal', (context) => {
|
|
175
|
+
const animal = store.get('animal', context.params.id);
|
|
176
|
+
if (animal.protected) {
|
|
177
|
+
return { errors: [{ detail: 'Cannot delete protected animals' }] };
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Change Detection with oldState
|
|
183
|
+
|
|
184
|
+
```javascript
|
|
185
|
+
afterHook('update', 'animal', (context) => {
|
|
186
|
+
if (!context.oldState) return;
|
|
187
|
+
|
|
188
|
+
// Detect specific field changes
|
|
189
|
+
if (context.oldState.owner !== context.record.owner) {
|
|
190
|
+
console.log(`Owner changed from ${context.oldState.owner} to ${context.record.owner}`);
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Audit Logging
|
|
196
|
+
|
|
197
|
+
```javascript
|
|
198
|
+
afterHook('update', 'animal', async (context) => {
|
|
199
|
+
const changes = {};
|
|
200
|
+
if (context.oldState) {
|
|
201
|
+
for (const [key, newValue] of Object.entries(context.record.__data)) {
|
|
202
|
+
if (context.oldState[key] !== newValue) {
|
|
203
|
+
changes[key] = { from: context.oldState[key], to: newValue };
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
await auditLog.create({
|
|
209
|
+
operation: 'update',
|
|
210
|
+
model: context.model,
|
|
211
|
+
recordId: context.record.id,
|
|
212
|
+
changes // { age: { from: 2, to: 3 } }
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### Delete Auditing
|
|
218
|
+
|
|
219
|
+
```javascript
|
|
220
|
+
afterHook('delete', 'animal', async (context) => {
|
|
221
|
+
await auditLog.create({
|
|
222
|
+
operation: 'delete',
|
|
223
|
+
model: context.model,
|
|
224
|
+
recordId: context.recordId,
|
|
225
|
+
deletedData: context.oldState // Full snapshot
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
## Key Differences from Event-Based System
|
|
231
|
+
|
|
232
|
+
| Feature | Event-Based (Old) | Middleware-Based (Current) |
|
|
233
|
+
|---------|-------------------|---------------------------|
|
|
234
|
+
| Execution | Parallel (fire-and-forget) | Sequential |
|
|
235
|
+
| Can halt operation | No | Yes (return any value) |
|
|
236
|
+
| Error handling | Isolated (logged) | Propagated (halts operation) |
|
|
237
|
+
| Middleware order | Not guaranteed | Registration order |
|
|
238
|
+
| Context modification | Not reliable | Reliable (sequential) |
|
|
239
|
+
| API | `subscribe('before:create:animal')` | `beforeHook('create', 'animal')` |
|
|
240
|
+
|
|
241
|
+
## Testing
|
|
242
|
+
|
|
243
|
+
**Location**: `test/integration/orm-test.js`
|
|
244
|
+
**Coverage**: Comprehensive hook tests including:
|
|
245
|
+
- Before/after hooks for all operations
|
|
246
|
+
- Halting with status codes
|
|
247
|
+
- Halting with custom response objects
|
|
248
|
+
- Sequential execution order
|
|
249
|
+
- Unsubscribe functionality
|
|
250
|
+
- clearHook functionality
|
package/.claude/index.md
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
# Stonyx-ORM Guide for Claude
|
|
2
|
+
|
|
3
|
+
## Detailed Guides
|
|
4
|
+
|
|
5
|
+
- [Usage Patterns](usage-patterns.md) — Model definitions, serializers, transforms, CRUD, DB schema, persistence, access control, REST API, and include parameters
|
|
6
|
+
- [Middleware Hooks System](hooks.md) — Before/after hooks for CRUD operations, halting, context object, change detection, and testing
|
|
7
|
+
- [Code Style Rules](code-style-rules.md) — Strict prettier/eslint rules to apply across all Stonyx projects
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Project Overview
|
|
12
|
+
|
|
13
|
+
**stonyx-orm** is a lightweight Object-Relational Mapping (ORM) library designed specifically for the Stonyx framework. It provides structured data modeling, relationship management, serialization, and persistence to JSON files, with optional REST API integration.
|
|
14
|
+
|
|
15
|
+
## Core Problem It Solves
|
|
16
|
+
|
|
17
|
+
1. **Data Modeling**: Clean, type-safe model definitions with attributes and relationships
|
|
18
|
+
2. **Data Serialization**: Transforms messy third-party data into structured model instances
|
|
19
|
+
3. **Relationship Management**: Automatic bidirectional relationships (hasMany, belongsTo)
|
|
20
|
+
4. **Data Persistence**: File-based JSON storage with auto-save
|
|
21
|
+
5. **REST API Generation**: Auto-generated RESTful endpoints with access control
|
|
22
|
+
6. **Data Transformation**: Custom type conversion and formatting
|
|
23
|
+
7. **Middleware Hooks**: Before/after hooks for all CRUD operations with halting capability
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Architecture Overview
|
|
28
|
+
|
|
29
|
+
### Key Components
|
|
30
|
+
|
|
31
|
+
1. **Orm** ([src/main.js](src/main.js)) - Singleton that initializes and manages the entire system
|
|
32
|
+
2. **Store** ([src/store.js](src/store.js)) - In-memory storage (nested Maps: `Map<modelName, Map<recordId, record>>`)
|
|
33
|
+
3. **Model** ([src/model.js](src/model.js)) - Base class for all models
|
|
34
|
+
4. **Record** ([src/record.js](src/record.js)) - Individual model instances
|
|
35
|
+
5. **Serializer** ([src/serializer.js](src/serializer.js)) - Maps raw data to model format
|
|
36
|
+
6. **DB** ([src/db.js](src/db.js)) - JSON file persistence layer
|
|
37
|
+
7. **Relationships** ([src/has-many.js](src/has-many.js), [src/belongs-to.js](src/belongs-to.js)) - Relationship handlers
|
|
38
|
+
8. **Include Logic** (inline in [src/orm-request.js](src/orm-request.js)) - Parses include query params, traverses relationships, collects and deduplicates included records
|
|
39
|
+
9. **Hooks** ([src/hooks.js](src/hooks.js)) - Middleware-based hook registry for CRUD lifecycle
|
|
40
|
+
10. **MySQL Driver** ([src/mysql/mysql-db.js](src/mysql/mysql-db.js)) - MySQL persistence, migrations, schema introspection. Loads records in topological order. `_rowToRawData()` converts TINYINT(1) → boolean, remaps FK columns, strips timestamps.
|
|
41
|
+
|
|
42
|
+
### Project Structure
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
stonyx-orm/
|
|
46
|
+
├── src/
|
|
47
|
+
│ ├── index.js # Main exports (includes hook functions)
|
|
48
|
+
│ ├── main.js # Orm class
|
|
49
|
+
│ ├── model.js # Base Model
|
|
50
|
+
│ ├── record.js # Record instances
|
|
51
|
+
│ ├── serializer.js # Base Serializer
|
|
52
|
+
│ ├── store.js # In-memory storage
|
|
53
|
+
│ ├── db.js # JSON persistence
|
|
54
|
+
│ ├── attr.js # Attribute helper (Proxy-based)
|
|
55
|
+
│ ├── has-many.js # One-to-many relationships
|
|
56
|
+
│ ├── belongs-to.js # Many-to-one relationships
|
|
57
|
+
│ ├── relationships.js # Relationship registry
|
|
58
|
+
│ ├── manage-record.js # createRecord/updateRecord
|
|
59
|
+
│ ├── model-property.js # Transform handler
|
|
60
|
+
│ ├── transforms.js # Built-in transforms
|
|
61
|
+
│ ├── hooks.js # Middleware hook registry
|
|
62
|
+
│ ├── setup-rest-server.js # REST integration
|
|
63
|
+
│ ├── orm-request.js # CRUD request handler with hooks + includes
|
|
64
|
+
│ ├── meta-request.js # Meta endpoint (dev only)
|
|
65
|
+
│ ├── migrate.js # JSON DB mode migration (file <-> directory)
|
|
66
|
+
│ ├── commands.js # CLI commands (db:migrate-*, etc.)
|
|
67
|
+
│ ├── utils.js # Pluralize wrapper for dasherized names
|
|
68
|
+
│ ├── exports/
|
|
69
|
+
│ │ └── db.js # Convenience re-export of DB instance
|
|
70
|
+
│ └── mysql/
|
|
71
|
+
│ ├── mysql-db.js # MySQL driver (CRUD persistence, record loading)
|
|
72
|
+
│ ├── connection.js # mysql2 connection pool
|
|
73
|
+
│ ├── query-builder.js # SQL builders (INSERT/UPDATE/DELETE/SELECT) with identifier validation
|
|
74
|
+
│ ├── schema-introspector.js # Model-to-MySQL schema introspection
|
|
75
|
+
│ ├── migration-generator.js # Schema diff and .sql migration generation
|
|
76
|
+
│ ├── migration-runner.js # Migration apply/rollback with transactions
|
|
77
|
+
│ └── type-map.js # ORM attr types -> MySQL column types (supports custom transform mysqlType)
|
|
78
|
+
├── config/
|
|
79
|
+
│ └── environment.js # Default configuration
|
|
80
|
+
├── test/
|
|
81
|
+
│ ├── integration/ # Integration tests
|
|
82
|
+
│ ├── unit/ # Unit tests
|
|
83
|
+
│ └── sample/ # Test fixtures
|
|
84
|
+
│ ├── models/ # Example models
|
|
85
|
+
│ ├── serializers/ # Example serializers
|
|
86
|
+
│ ├── transforms/ # Custom transforms
|
|
87
|
+
│ ├── access/ # Access control
|
|
88
|
+
│ ├── db-schema.js # DB schema
|
|
89
|
+
│ └── payload.js # Test data
|
|
90
|
+
└── package.json
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Configuration
|
|
96
|
+
|
|
97
|
+
Located in [config/environment.js](config/environment.js), overridable via environment variables:
|
|
98
|
+
|
|
99
|
+
```javascript
|
|
100
|
+
config.orm = {
|
|
101
|
+
paths: {
|
|
102
|
+
model: './models',
|
|
103
|
+
serializer: './serializers',
|
|
104
|
+
transform: './transforms',
|
|
105
|
+
access: './access'
|
|
106
|
+
},
|
|
107
|
+
db: {
|
|
108
|
+
autosave: 'false',
|
|
109
|
+
file: 'db.json',
|
|
110
|
+
mode: 'file', // 'file' (single db.json) or 'directory' (one file per collection)
|
|
111
|
+
directory: 'db', // directory name for collection files when mode is 'directory'
|
|
112
|
+
saveInterval: 3600,
|
|
113
|
+
schema: './config/db-schema.js'
|
|
114
|
+
},
|
|
115
|
+
restServer: {
|
|
116
|
+
enabled: 'true',
|
|
117
|
+
route: '/'
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Storage Modes
|
|
125
|
+
|
|
126
|
+
The ORM supports two storage modes, configured via `db.mode`:
|
|
127
|
+
|
|
128
|
+
- **`'file'`** (default): All data is stored in a single `db.json` file.
|
|
129
|
+
- **`'directory'`**: Each collection is stored as a separate file in the configured directory — `{directory}/{collection}.json` (e.g., `db/animals.json`, `db/owners.json`). The main `db.json` is kept as a skeleton with empty arrays.
|
|
130
|
+
|
|
131
|
+
**Migration CLI commands:**
|
|
132
|
+
- `stonyx-db-file-to-directory` — Splits a single `db.json` into per-collection files in the directory.
|
|
133
|
+
- `stonyx-db-directory-to-file` — Merges per-collection files back into a single `db.json`.
|
|
134
|
+
|
|
135
|
+
**Mode validation:** On startup, the ORM warns if the configured mode doesn't match the actual file state (e.g., mode is `'file'` but a `db/` directory exists, or mode is `'directory'` but no directory is found).
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Design Patterns
|
|
140
|
+
|
|
141
|
+
1. **Singleton**: Orm, Store, DB classes
|
|
142
|
+
2. **Proxy**: `attr()` uses Proxies for type-safe access
|
|
143
|
+
3. **Registry**: Relationships in nested Maps
|
|
144
|
+
4. **Factory**: `createRecord()` function
|
|
145
|
+
5. **Observer**: Auto-save via Cron
|
|
146
|
+
6. **Middleware**: Hook system with halting capability
|
|
147
|
+
7. **Convention over Configuration**: Auto-discovery by naming
|
|
148
|
+
|
|
149
|
+
**Naming Conventions:**
|
|
150
|
+
- Models: `{PascalCase}Model` (e.g., `AnimalModel`)
|
|
151
|
+
- Serializers: `{PascalCase}Serializer` (e.g., `AnimalSerializer`)
|
|
152
|
+
- Transforms: Original filename (e.g., `animal.js`)
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## Testing
|
|
157
|
+
|
|
158
|
+
**Test Runner**: QUnit via `stonyx test` (auto-bootstraps and runs `test/**/*-test.js`)
|
|
159
|
+
|
|
160
|
+
**Test Structure:**
|
|
161
|
+
- **Integration**: [test/integration/orm-test.js](test/integration/orm-test.js) - Full pipeline test
|
|
162
|
+
- **Unit**: [test/unit/transforms/](test/unit/transforms/) - Transform tests
|
|
163
|
+
- **Sample**: [test/sample/](test/sample/) - Test fixtures
|
|
164
|
+
|
|
165
|
+
**Key Test Data:**
|
|
166
|
+
- [test/sample/payload.js](test/sample/payload.js) - Raw vs serialized data
|
|
167
|
+
- Demonstrates transformation from messy external data to clean models
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## Critical Files for Common Tasks
|
|
172
|
+
|
|
173
|
+
**Understanding Core Behavior:**
|
|
174
|
+
- [src/main.js](src/main.js) - Initialization flow
|
|
175
|
+
- [src/store.js](src/store.js) - Record storage/retrieval
|
|
176
|
+
- [src/manage-record.js](src/manage-record.js) - CRUD operations
|
|
177
|
+
|
|
178
|
+
**Understanding Relationships:**
|
|
179
|
+
- [src/relationships.js](src/relationships.js) - Registry system
|
|
180
|
+
- [src/has-many.js](src/has-many.js) - One-to-many logic
|
|
181
|
+
- [src/belongs-to.js](src/belongs-to.js) - Many-to-one logic
|
|
182
|
+
|
|
183
|
+
**Understanding Data Flow:**
|
|
184
|
+
- [src/serializer.js](src/serializer.js) - Raw → Model mapping
|
|
185
|
+
- [src/model-property.js](src/model-property.js) - Transform application
|
|
186
|
+
- [src/transforms.js](src/transforms.js) - Built-in transforms
|
|
187
|
+
|
|
188
|
+
**Understanding REST API:**
|
|
189
|
+
- [src/setup-rest-server.js](src/setup-rest-server.js) - Endpoint registration
|
|
190
|
+
- [src/orm-request.js](src/orm-request.js) - Request handling with hooks
|
|
191
|
+
|
|
192
|
+
**Understanding Hooks:**
|
|
193
|
+
- [src/hooks.js](src/hooks.js) - Hook registry (beforeHooks, afterHooks Maps)
|
|
194
|
+
- [src/orm-request.js](src/orm-request.js) - `_withHooks()` wrapper
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
## Key Insights
|
|
199
|
+
|
|
200
|
+
**Strengths:**
|
|
201
|
+
- Zero-config REST API generation
|
|
202
|
+
- Clean declarative model definitions
|
|
203
|
+
- Automatic relationship management
|
|
204
|
+
- File-based (no database setup needed)
|
|
205
|
+
- Flexible serialization for messy data
|
|
206
|
+
- Middleware hooks with halting capability
|
|
207
|
+
|
|
208
|
+
**Use Cases:**
|
|
209
|
+
- Rapid prototyping
|
|
210
|
+
- Small to medium applications
|
|
211
|
+
- Third-party API consumption with normalization
|
|
212
|
+
- Development/testing environments
|
|
213
|
+
- Applications needing quick REST APIs
|
|
214
|
+
|
|
215
|
+
**Dependencies:**
|
|
216
|
+
- `stonyx` - Main framework (peer)
|
|
217
|
+
- `@stonyx/utils` - File/string utilities
|
|
218
|
+
- `@stonyx/events` - Pub/sub event system (event names initialized on startup; hooks use separate middleware-based registry)
|
|
219
|
+
- `@stonyx/cron` - Scheduled tasks (used by DB for auto-save)
|
|
220
|
+
- `@stonyx/rest-server` - REST API
|
|
221
|
+
- `mysql2` - Optional peer dependency for MySQL mode
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
## Quick Reference
|
|
226
|
+
|
|
227
|
+
**Import the ORM:**
|
|
228
|
+
```javascript
|
|
229
|
+
import {
|
|
230
|
+
Orm, Model, Serializer, attr, hasMany, belongsTo,
|
|
231
|
+
createRecord, updateRecord, store,
|
|
232
|
+
beforeHook, afterHook, clearHook, clearAllHooks
|
|
233
|
+
} from '@stonyx/orm';
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
**Initialize:**
|
|
237
|
+
```javascript
|
|
238
|
+
const orm = new Orm({ dbType: 'json' });
|
|
239
|
+
await orm.init();
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
**Access Database:**
|
|
243
|
+
```javascript
|
|
244
|
+
await Orm.db.save();
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
**Common Operations:**
|
|
248
|
+
```javascript
|
|
249
|
+
// Create
|
|
250
|
+
const record = createRecord('modelName', data);
|
|
251
|
+
|
|
252
|
+
// Read
|
|
253
|
+
const record = store.get('modelName', id);
|
|
254
|
+
const all = store.get('modelName');
|
|
255
|
+
|
|
256
|
+
// Update
|
|
257
|
+
updateRecord(record, newData);
|
|
258
|
+
|
|
259
|
+
// Delete
|
|
260
|
+
store.remove('modelName', id);
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
**Register Hooks:**
|
|
264
|
+
```javascript
|
|
265
|
+
// Before hook (can halt)
|
|
266
|
+
const unsubscribe = beforeHook('create', 'animal', (ctx) => {
|
|
267
|
+
if (invalid) return 400;
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// After hook
|
|
271
|
+
afterHook('update', 'animal', (ctx) => {
|
|
272
|
+
console.log('Updated:', ctx.record.id);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
// Cleanup
|
|
276
|
+
unsubscribe(); // Remove specific hook
|
|
277
|
+
clearHook('create', 'animal'); // Clear all hooks for operation
|
|
278
|
+
clearAllHooks(); // Clear everything
|
|
279
|
+
```
|