@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.
@@ -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)
@@ -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
@@ -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
+ ```