@stonyx/orm 0.2.1-beta.2 → 0.2.1-beta.21

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,281 @@
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
+ │ ├── plural-registry.js # Plural name registry (populated at init, supports Model.pluralName overrides)
69
+ │ ├── exports/
70
+ │ │ └── db.js # Convenience re-export of DB instance
71
+ │ └── mysql/
72
+ │ ├── mysql-db.js # MySQL driver (CRUD persistence, record loading)
73
+ │ ├── connection.js # mysql2 connection pool
74
+ │ ├── query-builder.js # SQL builders (INSERT/UPDATE/DELETE/SELECT) with identifier validation
75
+ │ ├── schema-introspector.js # Model-to-MySQL schema introspection
76
+ │ ├── migration-generator.js # Schema diff and .sql migration generation
77
+ │ ├── migration-runner.js # Migration apply/rollback with transactions
78
+ │ └── type-map.js # ORM attr types -> MySQL column types (supports custom transform mysqlType)
79
+ ├── config/
80
+ │ └── environment.js # Default configuration
81
+ ├── test/
82
+ │ ├── integration/ # Integration tests
83
+ │ ├── unit/ # Unit tests
84
+ │ └── sample/ # Test fixtures
85
+ │ ├── models/ # Example models
86
+ │ ├── serializers/ # Example serializers
87
+ │ ├── transforms/ # Custom transforms
88
+ │ ├── access/ # Access control
89
+ │ ├── db-schema.js # DB schema
90
+ │ └── payload.js # Test data
91
+ └── package.json
92
+ ```
93
+
94
+ ---
95
+
96
+ ## Configuration
97
+
98
+ Located in [config/environment.js](config/environment.js), overridable via environment variables:
99
+
100
+ ```javascript
101
+ config.orm = {
102
+ paths: {
103
+ model: './models',
104
+ serializer: './serializers',
105
+ transform: './transforms',
106
+ access: './access'
107
+ },
108
+ db: {
109
+ autosave: 'false',
110
+ file: 'db.json',
111
+ mode: 'file', // 'file' (single db.json) or 'directory' (one file per collection)
112
+ directory: 'db', // directory name for collection files when mode is 'directory'
113
+ saveInterval: 3600,
114
+ schema: './config/db-schema.js'
115
+ },
116
+ restServer: {
117
+ enabled: 'true',
118
+ route: '/'
119
+ }
120
+ }
121
+ ```
122
+
123
+ ---
124
+
125
+ ## Storage Modes
126
+
127
+ The ORM supports two storage modes, configured via `db.mode`:
128
+
129
+ - **`'file'`** (default): All data is stored in a single `db.json` file.
130
+ - **`'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.
131
+
132
+ **Migration CLI commands:**
133
+ - `stonyx-db-file-to-directory` — Splits a single `db.json` into per-collection files in the directory.
134
+ - `stonyx-db-directory-to-file` — Merges per-collection files back into a single `db.json`.
135
+
136
+ **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).
137
+
138
+ ---
139
+
140
+ ## Design Patterns
141
+
142
+ 1. **Singleton**: Orm, Store, DB classes
143
+ 2. **Proxy**: `attr()` uses Proxies for type-safe access
144
+ 3. **Registry**: Relationships in nested Maps
145
+ 4. **Factory**: `createRecord()` function
146
+ 5. **Observer**: Auto-save via Cron
147
+ 6. **Middleware**: Hook system with halting capability
148
+ 7. **Convention over Configuration**: Auto-discovery by naming
149
+
150
+ **Naming Conventions:**
151
+ - Models: `{PascalCase}Model` (e.g., `AnimalModel`)
152
+ - Serializers: `{PascalCase}Serializer` (e.g., `AnimalSerializer`)
153
+ - Transforms: Original filename (e.g., `animal.js`)
154
+ - Plural names: Auto-pluralized by default (e.g., `animal` → `animals`). Override with `static pluralName` on the model class (e.g., `static pluralName = 'people'`). All call sites use `getPluralName()` from the plural registry, **not** `pluralize()` directly.
155
+
156
+ ---
157
+
158
+ ## Testing
159
+
160
+ **Test Runner**: QUnit via `stonyx test` (auto-bootstraps and runs `test/**/*-test.js`)
161
+
162
+ **Test Structure:**
163
+ - **Integration**: [test/integration/orm-test.js](test/integration/orm-test.js) - Full pipeline test
164
+ - **Unit**: [test/unit/transforms/](test/unit/transforms/) - Transform tests
165
+ - **Sample**: [test/sample/](test/sample/) - Test fixtures
166
+
167
+ **Key Test Data:**
168
+ - [test/sample/payload.js](test/sample/payload.js) - Raw vs serialized data
169
+ - Demonstrates transformation from messy external data to clean models
170
+
171
+ ---
172
+
173
+ ## Critical Files for Common Tasks
174
+
175
+ **Understanding Core Behavior:**
176
+ - [src/main.js](src/main.js) - Initialization flow
177
+ - [src/store.js](src/store.js) - Record storage/retrieval
178
+ - [src/manage-record.js](src/manage-record.js) - CRUD operations
179
+
180
+ **Understanding Relationships:**
181
+ - [src/relationships.js](src/relationships.js) - Registry system
182
+ - [src/has-many.js](src/has-many.js) - One-to-many logic
183
+ - [src/belongs-to.js](src/belongs-to.js) - Many-to-one logic
184
+
185
+ **Understanding Data Flow:**
186
+ - [src/serializer.js](src/serializer.js) - Raw → Model mapping
187
+ - [src/model-property.js](src/model-property.js) - Transform application
188
+ - [src/transforms.js](src/transforms.js) - Built-in transforms
189
+
190
+ **Understanding REST API:**
191
+ - [src/setup-rest-server.js](src/setup-rest-server.js) - Endpoint registration
192
+ - [src/orm-request.js](src/orm-request.js) - Request handling with hooks
193
+
194
+ **Understanding Hooks:**
195
+ - [src/hooks.js](src/hooks.js) - Hook registry (beforeHooks, afterHooks Maps)
196
+ - [src/orm-request.js](src/orm-request.js) - `_withHooks()` wrapper
197
+
198
+ ---
199
+
200
+ ## Key Insights
201
+
202
+ **Strengths:**
203
+ - Zero-config REST API generation
204
+ - Clean declarative model definitions
205
+ - Automatic relationship management
206
+ - File-based (no database setup needed)
207
+ - Flexible serialization for messy data
208
+ - Middleware hooks with halting capability
209
+
210
+ **Use Cases:**
211
+ - Rapid prototyping
212
+ - Small to medium applications
213
+ - Third-party API consumption with normalization
214
+ - Development/testing environments
215
+ - Applications needing quick REST APIs
216
+
217
+ **Dependencies:**
218
+ - `stonyx` - Main framework (peer)
219
+ - `@stonyx/utils` - File/string utilities
220
+ - `@stonyx/events` - Pub/sub event system (event names initialized on startup; hooks use separate middleware-based registry)
221
+ - `@stonyx/cron` - Scheduled tasks (used by DB for auto-save)
222
+ - `@stonyx/rest-server` - REST API
223
+ - `mysql2` - Optional peer dependency for MySQL mode
224
+
225
+ ---
226
+
227
+ ## Quick Reference
228
+
229
+ **Import the ORM:**
230
+ ```javascript
231
+ import {
232
+ Orm, Model, Serializer, attr, hasMany, belongsTo,
233
+ createRecord, updateRecord, store,
234
+ beforeHook, afterHook, clearHook, clearAllHooks
235
+ } from '@stonyx/orm';
236
+ ```
237
+
238
+ **Initialize:**
239
+ ```javascript
240
+ const orm = new Orm({ dbType: 'json' });
241
+ await orm.init();
242
+ ```
243
+
244
+ **Access Database:**
245
+ ```javascript
246
+ await Orm.db.save();
247
+ ```
248
+
249
+ **Common Operations:**
250
+ ```javascript
251
+ // Create
252
+ const record = createRecord('modelName', data);
253
+
254
+ // Read
255
+ const record = store.get('modelName', id);
256
+ const all = store.get('modelName');
257
+
258
+ // Update
259
+ updateRecord(record, newData);
260
+
261
+ // Delete
262
+ store.remove('modelName', id);
263
+ ```
264
+
265
+ **Register Hooks:**
266
+ ```javascript
267
+ // Before hook (can halt)
268
+ const unsubscribe = beforeHook('create', 'animal', (ctx) => {
269
+ if (invalid) return 400;
270
+ });
271
+
272
+ // After hook
273
+ afterHook('update', 'animal', (ctx) => {
274
+ console.log('Updated:', ctx.record.id);
275
+ });
276
+
277
+ // Cleanup
278
+ unsubscribe(); // Remove specific hook
279
+ clearHook('create', 'animal'); // Clear all hooks for operation
280
+ clearAllHooks(); // Clear everything
281
+ ```