@stonyx/orm 0.2.1-beta.9 → 0.2.1-beta.90
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/README.md +64 -6
- package/config/environment.js +37 -1
- package/dist/aggregates.d.ts +21 -0
- package/dist/aggregates.js +93 -0
- package/dist/attr.d.ts +2 -0
- package/dist/attr.js +22 -0
- package/dist/belongs-to.d.ts +11 -0
- package/dist/belongs-to.js +59 -0
- package/dist/cli.d.ts +22 -0
- package/dist/cli.js +148 -0
- package/dist/commands.d.ts +7 -0
- package/dist/commands.js +146 -0
- package/dist/db.d.ts +21 -0
- package/dist/db.js +180 -0
- package/dist/exports/db.d.ts +7 -0
- package/{src → dist}/exports/db.js +2 -4
- package/dist/has-many.d.ts +11 -0
- package/dist/has-many.js +58 -0
- package/dist/hooks.d.ts +62 -0
- package/dist/hooks.js +110 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +34 -0
- package/dist/main.d.ts +46 -0
- package/dist/main.js +181 -0
- package/dist/manage-record.d.ts +13 -0
- package/dist/manage-record.js +123 -0
- package/dist/meta-request.d.ts +6 -0
- package/dist/meta-request.js +52 -0
- package/dist/migrate.d.ts +2 -0
- package/dist/migrate.js +57 -0
- package/dist/model-property.d.ts +9 -0
- package/dist/model-property.js +29 -0
- package/dist/model.d.ts +15 -0
- package/dist/model.js +18 -0
- package/dist/mysql/connection.d.ts +14 -0
- package/dist/mysql/connection.js +24 -0
- package/dist/mysql/migration-generator.d.ts +45 -0
- package/dist/mysql/migration-generator.js +254 -0
- package/dist/mysql/migration-runner.d.ts +12 -0
- package/dist/mysql/migration-runner.js +88 -0
- package/dist/mysql/mysql-db.d.ts +100 -0
- package/dist/mysql/mysql-db.js +425 -0
- package/dist/mysql/query-builder.d.ts +10 -0
- package/dist/mysql/query-builder.js +44 -0
- package/dist/mysql/schema-introspector.d.ts +19 -0
- package/dist/mysql/schema-introspector.js +291 -0
- package/dist/mysql/type-map.d.ts +21 -0
- package/dist/mysql/type-map.js +36 -0
- package/dist/orm-request.d.ts +38 -0
- package/dist/orm-request.js +474 -0
- package/dist/plural-registry.d.ts +4 -0
- package/dist/plural-registry.js +9 -0
- package/dist/postgres/connection.d.ts +15 -0
- package/dist/postgres/connection.js +32 -0
- package/dist/postgres/migration-generator.d.ts +45 -0
- package/dist/postgres/migration-generator.js +261 -0
- package/dist/postgres/migration-runner.d.ts +10 -0
- package/dist/postgres/migration-runner.js +87 -0
- package/dist/postgres/postgres-db.d.ts +119 -0
- package/dist/postgres/postgres-db.js +477 -0
- package/dist/postgres/query-builder.d.ts +27 -0
- package/dist/postgres/query-builder.js +98 -0
- package/dist/postgres/schema-introspector.d.ts +29 -0
- package/dist/postgres/schema-introspector.js +314 -0
- package/dist/postgres/type-map.d.ts +23 -0
- package/dist/postgres/type-map.js +56 -0
- package/dist/record.d.ts +75 -0
- package/dist/record.js +129 -0
- package/dist/relationships.d.ts +10 -0
- package/dist/relationships.js +41 -0
- package/dist/serializer.d.ts +17 -0
- package/dist/serializer.js +136 -0
- package/dist/setup-rest-server.d.ts +1 -0
- package/dist/setup-rest-server.js +52 -0
- package/dist/standalone-db.d.ts +58 -0
- package/dist/standalone-db.js +142 -0
- package/dist/store.d.ts +62 -0
- package/dist/store.js +286 -0
- package/dist/timescale/query-builder.d.ts +43 -0
- package/dist/timescale/query-builder.js +115 -0
- package/dist/timescale/timescale-db.d.ts +45 -0
- package/dist/timescale/timescale-db.js +84 -0
- package/dist/transforms.d.ts +2 -0
- package/dist/transforms.js +17 -0
- package/dist/types/orm-types.d.ts +142 -0
- package/dist/types/orm-types.js +1 -0
- package/dist/utils.d.ts +7 -0
- package/dist/utils.js +17 -0
- package/dist/view-resolver.d.ts +8 -0
- package/dist/view-resolver.js +171 -0
- package/dist/view.d.ts +11 -0
- package/dist/view.js +18 -0
- package/package.json +57 -15
- package/src/aggregates.ts +109 -0
- package/src/{attr.js → attr.ts} +2 -2
- package/src/belongs-to.ts +90 -0
- package/src/cli.ts +183 -0
- package/src/{commands.js → commands.ts} +179 -170
- package/src/{db.js → db.ts} +55 -29
- package/src/exports/db.ts +7 -0
- package/src/has-many.ts +92 -0
- package/src/{hooks.js → hooks.ts} +41 -27
- package/src/{index.js → index.ts} +11 -2
- package/src/main.ts +229 -0
- package/src/manage-record.ts +161 -0
- package/src/{meta-request.js → meta-request.ts} +17 -14
- package/src/{migrate.js → migrate.ts} +9 -9
- package/src/model-property.ts +35 -0
- package/src/model.ts +21 -0
- package/src/mysql/{connection.js → connection.ts} +43 -28
- package/src/mysql/migration-generator.ts +337 -0
- package/src/mysql/{migration-runner.js → migration-runner.ts} +121 -110
- package/src/mysql/mysql-db.ts +543 -0
- package/src/mysql/{query-builder.js → query-builder.ts} +69 -64
- package/src/mysql/schema-introspector.ts +358 -0
- package/src/mysql/{type-map.js → type-map.ts} +42 -37
- package/src/{orm-request.js → orm-request.ts} +186 -108
- package/src/plural-registry.ts +12 -0
- package/src/postgres/connection.ts +48 -0
- package/src/postgres/migration-generator.ts +348 -0
- package/src/postgres/migration-runner.ts +115 -0
- package/src/postgres/postgres-db.ts +616 -0
- package/src/postgres/query-builder.ts +148 -0
- package/src/postgres/schema-introspector.ts +386 -0
- package/src/postgres/type-map.ts +61 -0
- package/src/record.ts +186 -0
- package/src/relationships.ts +54 -0
- package/src/serializer.ts +161 -0
- package/src/{setup-rest-server.js → setup-rest-server.ts} +18 -16
- package/src/standalone-db.ts +185 -0
- package/src/store.ts +373 -0
- package/src/timescale/query-builder.ts +174 -0
- package/src/timescale/timescale-db.ts +119 -0
- package/src/transforms.ts +20 -0
- package/src/types/mysql2.d.ts +49 -0
- package/src/types/orm-types.ts +146 -0
- package/src/types/pg.d.ts +32 -0
- package/src/types/stonyx-cron.d.ts +5 -0
- package/src/types/stonyx-events.d.ts +4 -0
- package/src/types/stonyx-rest-server.d.ts +16 -0
- package/src/types/stonyx-utils.d.ts +33 -0
- package/src/types/stonyx.d.ts +21 -0
- package/src/utils.ts +22 -0
- package/src/view-resolver.ts +211 -0
- package/src/view.ts +22 -0
- package/.claude/code-style-rules.md +0 -44
- package/.claude/hooks.md +0 -250
- package/.claude/index.md +0 -279
- package/.claude/usage-patterns.md +0 -217
- package/.github/workflows/ci.yml +0 -16
- package/.github/workflows/publish.yml +0 -51
- package/improvements.md +0 -139
- package/project-structure.md +0 -343
- package/src/belongs-to.js +0 -63
- package/src/has-many.js +0 -61
- package/src/main.js +0 -148
- package/src/manage-record.js +0 -118
- package/src/model-property.js +0 -29
- package/src/model.js +0 -9
- package/src/mysql/migration-generator.js +0 -188
- package/src/mysql/mysql-db.js +0 -320
- package/src/mysql/schema-introspector.js +0 -158
- package/src/record.js +0 -127
- package/src/relationships.js +0 -43
- package/src/serializer.js +0 -138
- package/src/store.js +0 -211
- package/src/transforms.js +0 -20
- package/src/utils.js +0 -12
- package/test-events-setup.js +0 -41
- package/test-hooks-manual.js +0 -54
- package/test-hooks-with-logging.js +0 -52
package/project-structure.md
DELETED
|
@@ -1,343 +0,0 @@
|
|
|
1
|
-
# Project Documentation: @stonyx/orm
|
|
2
|
-
|
|
3
|
-
> Last audited: 2026-02-09 | Auto-generated by project-docs-auditor
|
|
4
|
-
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
## Table of Contents
|
|
8
|
-
1. [Overview](#overview)
|
|
9
|
-
2. [Tech Stack](#tech-stack)
|
|
10
|
-
3. [Architecture](#architecture)
|
|
11
|
-
4. [Directory Map](#directory-map)
|
|
12
|
-
5. [Key Files Reference](#key-files-reference)
|
|
13
|
-
6. [Data Flow](#data-flow)
|
|
14
|
-
7. [API / Routes](#api--routes)
|
|
15
|
-
8. [Database & Models](#database--models)
|
|
16
|
-
9. [Authentication & Authorization](#authentication--authorization)
|
|
17
|
-
10. [Configuration & Environment](#configuration--environment)
|
|
18
|
-
11. [Development Guide](#development-guide)
|
|
19
|
-
12. [Testing](#testing)
|
|
20
|
-
13. [Deployment & CI/CD](#deployment--cicd)
|
|
21
|
-
14. [Conventions & Standards](#conventions--standards)
|
|
22
|
-
15. [Known Issues & Technical Debt](#known-issues--technical-debt)
|
|
23
|
-
|
|
24
|
-
---
|
|
25
|
-
|
|
26
|
-
## Overview
|
|
27
|
-
|
|
28
|
-
A lightweight ORM for the Stonyx framework that provides structured data modeling with type-safe attributes, bidirectional relationships, serializers for third-party data normalization, and automatic REST API generation. Supports both JSON file-based persistence and MySQL as storage backends.
|
|
29
|
-
|
|
30
|
-
## Tech Stack
|
|
31
|
-
|
|
32
|
-
| Category | Technology | Version | Notes |
|
|
33
|
-
|----------|-----------|---------|-------|
|
|
34
|
-
| Language | JavaScript (ES Modules) | Node 24.13.0 | `"type": "module"` in package.json |
|
|
35
|
-
| Framework | Stonyx | file ref | Core framework (peer dependency) |
|
|
36
|
-
| Database (JSON) | Built-in `DB` class | N/A | File-based JSON persistence |
|
|
37
|
-
| Database (SQL) | MySQL via mysql2 | ^3.0.0 | Optional peer dependency |
|
|
38
|
-
| Test Framework | QUnit | ^2.24.1 | Via `stonyx test` runner |
|
|
39
|
-
| Test Mocking | Sinon | ^21.0.0 | Stubs and spies |
|
|
40
|
-
| Package Manager | pnpm | N/A | Local `file:` references for sibling packages |
|
|
41
|
-
|
|
42
|
-
## Architecture
|
|
43
|
-
|
|
44
|
-
The ORM follows a **singleton + registry** pattern. A single `Orm` instance manages models, serializers, and transforms. Records are stored in an in-memory `Store` (nested Maps) and optionally persisted to JSON files or MySQL.
|
|
45
|
-
|
|
46
|
-
```
|
|
47
|
-
┌──────────────┐
|
|
48
|
-
│ Orm (main) │ Singleton, initializes everything
|
|
49
|
-
└──────┬───────┘
|
|
50
|
-
┌────────────┬────┴────┬────────────┐
|
|
51
|
-
v v v v
|
|
52
|
-
┌────────┐ ┌─────────┐ ┌────┐ ┌──────────────┐
|
|
53
|
-
│ Models │ │Serializers│ │ DB │ │ REST Server │
|
|
54
|
-
└───┬────┘ └────┬─────┘ └──┬─┘ │ Integration │
|
|
55
|
-
│ │ │ └──────┬───────┘
|
|
56
|
-
v v v v
|
|
57
|
-
┌────────┐ ┌──────────┐ ┌─────┐ ┌───────────┐
|
|
58
|
-
│ Record │──│ Store │ │JSON │ │OrmRequest │
|
|
59
|
-
│(instance)│ │(Map<Map>)│ │/MySQL│ │(CRUD+Hooks)│
|
|
60
|
-
└────────┘ └──────────┘ └─────┘ └───────────┘
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
**Key Design Patterns:**
|
|
64
|
-
- **Singleton**: Orm, Store, DB, MysqlDB classes
|
|
65
|
-
- **Proxy**: `attr()` wraps `ModelProperty` in a Proxy for type-safe get/set
|
|
66
|
-
- **Registry**: Relationships tracked in nested Maps (`hasMany`, `belongsTo`, `global`, `pending`, `pendingBelongsTo`)
|
|
67
|
-
- **Factory**: `createRecord()` instantiates records with serialization
|
|
68
|
-
- **Middleware**: Hook system with sequential before/after execution and halting capability
|
|
69
|
-
- **Convention over Configuration**: Auto-discovery of models, serializers, transforms, and access classes by filesystem directory
|
|
70
|
-
|
|
71
|
-
## Directory Map
|
|
72
|
-
|
|
73
|
-
```
|
|
74
|
-
stonyx-orm/
|
|
75
|
-
├── src/
|
|
76
|
-
│ ├── index.js # Public exports barrel
|
|
77
|
-
│ ├── main.js # Orm singleton class
|
|
78
|
-
│ ├── model.js # Base Model class
|
|
79
|
-
│ ├── record.js # Record instances (data + relationships)
|
|
80
|
-
│ ├── serializer.js # Base Serializer + computed properties
|
|
81
|
-
│ ├── store.js # In-memory store (Map<model, Map<id, Record>>)
|
|
82
|
-
│ ├── db.js # JSON file persistence layer
|
|
83
|
-
│ ├── attr.js # Attribute helper (Proxy-based)
|
|
84
|
-
│ ├── model-property.js # Transform handler for attributes
|
|
85
|
-
│ ├── transforms.js # Built-in transforms (boolean, number, string, etc.)
|
|
86
|
-
│ ├── has-many.js # One-to-many relationship handler
|
|
87
|
-
│ ├── belongs-to.js # Many-to-one relationship handler
|
|
88
|
-
│ ├── relationships.js # Relationship registry + helpers
|
|
89
|
-
│ ├── manage-record.js # createRecord / updateRecord
|
|
90
|
-
│ ├── hooks.js # Middleware hook registry (before/after)
|
|
91
|
-
│ ├── orm-request.js # REST CRUD handler with hooks + includes
|
|
92
|
-
│ ├── meta-request.js # Dev-only meta endpoint
|
|
93
|
-
│ ├── setup-rest-server.js # REST route registration
|
|
94
|
-
│ ├── migrate.js # JSON DB mode migration (file <-> directory)
|
|
95
|
-
│ ├── commands.js # CLI commands (db:migrate-*, etc.)
|
|
96
|
-
│ ├── utils.js # Pluralize wrapper for dasherized names
|
|
97
|
-
│ ├── exports/
|
|
98
|
-
│ │ └── db.js # Convenience re-export of DB instance
|
|
99
|
-
│ └── mysql/
|
|
100
|
-
│ ├── mysql-db.js # MySQL driver class (CRUD persistence)
|
|
101
|
-
│ ├── connection.js # mysql2 connection pool management
|
|
102
|
-
│ ├── query-builder.js # SQL query builders (INSERT/UPDATE/DELETE/SELECT)
|
|
103
|
-
│ ├── schema-introspector.js # Introspects models into MySQL schemas
|
|
104
|
-
│ ├── migration-generator.js # Generates .sql migration files from schema diffs
|
|
105
|
-
│ ├── migration-runner.js # Applies/rollbacks migrations with transactions
|
|
106
|
-
│ └── type-map.js # ORM attr types -> MySQL column types
|
|
107
|
-
├── config/
|
|
108
|
-
│ └── environment.js # Default ORM configuration
|
|
109
|
-
├── test/
|
|
110
|
-
│ ├── config/
|
|
111
|
-
│ │ └── environment.js # Test-specific config overrides
|
|
112
|
-
│ ├── integration/
|
|
113
|
-
│ │ ├── orm-test.js # Full pipeline integration tests
|
|
114
|
-
│ │ └── db-directory-test.js # Directory-mode DB tests
|
|
115
|
-
│ ├── unit/
|
|
116
|
-
│ │ ├── commands-test.js # CLI command structure tests
|
|
117
|
-
│ │ ├── create-record-test.js # Record creation tests
|
|
118
|
-
│ │ ├── relationships-test.js # Relationship wiring tests
|
|
119
|
-
│ │ ├── environment-test.js # Environment config tests
|
|
120
|
-
│ │ ├── orm-lifecycle-test.js # Startup/shutdown lifecycle tests
|
|
121
|
-
│ │ ├── mysql/
|
|
122
|
-
│ │ │ └── mysql-db-startup-test.js # MySQL startup behavior tests
|
|
123
|
-
│ │ ├── transforms/ # Per-transform unit tests
|
|
124
|
-
│ │ └── utils/
|
|
125
|
-
│ │ └── get-computed-properties-test.js
|
|
126
|
-
│ └── sample/ # Test fixtures
|
|
127
|
-
│ ├── models/ # Example model definitions
|
|
128
|
-
│ ├── serializers/ # Example serializers
|
|
129
|
-
│ ├── transforms/ # Custom transform functions
|
|
130
|
-
│ ├── access/ # Access control classes
|
|
131
|
-
│ ├── db-schema.js # Test DB schema
|
|
132
|
-
│ ├── constants.js # Test constants
|
|
133
|
-
│ └── payload.js # Sample raw + expected data
|
|
134
|
-
├── .claude/ # Claude AI assistant context docs
|
|
135
|
-
├── .github/workflows/
|
|
136
|
-
│ ├── ci.yml # PR CI pipeline
|
|
137
|
-
│ └── publish.yml # NPM publish workflow
|
|
138
|
-
├── package.json
|
|
139
|
-
├── .nvmrc # Node v24.13.0
|
|
140
|
-
├── .npmignore
|
|
141
|
-
├── .gitignore
|
|
142
|
-
└── LICENSE.md # Apache 2.0
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
## Key Files Reference
|
|
146
|
-
|
|
147
|
-
| File / Path | Purpose | Category |
|
|
148
|
-
|-------------|---------|----------|
|
|
149
|
-
| `src/main.js` | Orm singleton: initialization, model/serializer/transform loading, DB + REST setup | Core |
|
|
150
|
-
| `src/index.js` | Public API barrel export (Model, Serializer, attr, hooks, etc.) | Core |
|
|
151
|
-
| `src/store.js` | In-memory record storage with relationship-aware unload/cascade | Core |
|
|
152
|
-
| `src/record.js` | Record instances: serialize, format, toJSON (JSON:API), unload | Core |
|
|
153
|
-
| `src/serializer.js` | Base serializer: path mapping, property setup, computed properties | Serialization |
|
|
154
|
-
| `src/manage-record.js` | `createRecord` / `updateRecord` with pending relationship fulfillment | CRUD |
|
|
155
|
-
| `src/orm-request.js` | REST request handler: CRUD routes, hooks wrapper, includes, filters, fields | REST API |
|
|
156
|
-
| `src/hooks.js` | Middleware hook registry: before/after hooks with halting | Middleware |
|
|
157
|
-
| `src/db.js` | JSON file persistence: read, save, directory mode, auto-save via cron | Persistence |
|
|
158
|
-
| `src/mysql/mysql-db.js` | MySQL driver: load records on init, persist CRUD, migration prompts | Persistence |
|
|
159
|
-
| `src/mysql/schema-introspector.js` | Model introspection to MySQL schema + DDL generation | MySQL |
|
|
160
|
-
| `src/mysql/migration-generator.js` | Schema diff and `.sql` migration file generation | MySQL |
|
|
161
|
-
| `src/commands.js` | CLI commands for DB migrations (file/directory mode + MySQL) | CLI |
|
|
162
|
-
| `config/environment.js` | Default configuration with env var overrides | Config |
|
|
163
|
-
|
|
164
|
-
## Data Flow
|
|
165
|
-
|
|
166
|
-
**Typical REST request lifecycle:**
|
|
167
|
-
|
|
168
|
-
1. **Request arrives** at auto-generated route (e.g., `POST /animals`) via `@stonyx/rest-server`
|
|
169
|
-
2. **Access control** (`OrmRequest.auth`) calls the access class's `access()` method -- returns permissions, filter function, or denial
|
|
170
|
-
3. **Before hooks** run sequentially -- can halt the operation by returning a value (status code or response object)
|
|
171
|
-
4. **Handler executes** the CRUD operation (reads from / writes to the in-memory `Store`)
|
|
172
|
-
5. **MySQL persistence** (if configured) -- `MysqlDB.persist()` writes to MySQL after in-memory mutation
|
|
173
|
-
6. **After hooks** run sequentially with enriched context (record, response, oldState)
|
|
174
|
-
7. **Auto-save** (if `autosave: 'onUpdate'`) writes the entire store to the JSON file
|
|
175
|
-
8. **Response** returned as JSON:API format with optional `included` sideloaded relationships
|
|
176
|
-
|
|
177
|
-
**Record creation flow:**
|
|
178
|
-
|
|
179
|
-
1. `createRecord(modelName, rawData, options)` called
|
|
180
|
-
2. Auto-increment ID assigned if missing (or pending MySQL ID in MySQL mode)
|
|
181
|
-
3. Model + Serializer instantiated, Record wrapper created
|
|
182
|
-
4. `record.serialize()` maps raw data through serializer paths, applies transforms, wires relationships
|
|
183
|
-
5. Record stored in `Store` by ID
|
|
184
|
-
6. Global, pending hasMany, and pending belongsTo relationships fulfilled
|
|
185
|
-
|
|
186
|
-
## API / Routes
|
|
187
|
-
|
|
188
|
-
Auto-generated REST endpoints for each model with an access class:
|
|
189
|
-
|
|
190
|
-
| Method | Route Pattern | Operation | Notes |
|
|
191
|
-
|--------|--------------|-----------|-------|
|
|
192
|
-
| GET | `/:models` | List collection | Supports `?filter[path]=value`, `?fields[model]=attr1,attr2`, `?include=rel1,rel2.nested` |
|
|
193
|
-
| GET | `/:models/:id` | Get single record | Same query params as list; returns 404 if not found |
|
|
194
|
-
| POST | `/:models` | Create record | Body: `{ data: { type, id?, attributes } }`; 400 if no type; 409 if duplicate ID |
|
|
195
|
-
| PATCH | `/:models/:id` | Update record | Body: `{ data: { attributes } }`; updates only provided fields |
|
|
196
|
-
| DELETE | `/:models/:id` | Delete record | Removes from store; no response body |
|
|
197
|
-
| GET | `/:models/:id/:relationship` | Related resource | Returns full related records (array for hasMany, single for belongsTo) |
|
|
198
|
-
| GET | `/:models/:id/relationships/:relationship` | Relationship linkage | Returns JSON:API linkage objects with `links.self` and `links.related` |
|
|
199
|
-
|
|
200
|
-
**Include parameter** supports comma-separated relationships and dot-notation nesting: `?include=owner.pets,traits`
|
|
201
|
-
|
|
202
|
-
**Fields parameter** supports sparse fieldsets: `?fields[animals]=age,size`
|
|
203
|
-
|
|
204
|
-
**Filter parameter** supports dot-path filtering: `?filter[owner]=angela`
|
|
205
|
-
|
|
206
|
-
## Database & Models
|
|
207
|
-
|
|
208
|
-
### Model Definition
|
|
209
|
-
|
|
210
|
-
Models extend `Model` and define attributes with `attr(type)` and relationships with `hasMany(model)` / `belongsTo(model)`. Getters become computed properties.
|
|
211
|
-
|
|
212
|
-
### In-Memory Store
|
|
213
|
-
|
|
214
|
-
`Store.data` is a `Map<modelName, Map<recordId, Record>>`. O(1) lookup by model + ID.
|
|
215
|
-
|
|
216
|
-
### JSON File Persistence
|
|
217
|
-
|
|
218
|
-
Two modes configured via `db.mode`:
|
|
219
|
-
- **`'file'`** (default): Single `db.json` file
|
|
220
|
-
- **`'directory'`**: Per-collection files in a directory (e.g., `db/animals.json`)
|
|
221
|
-
|
|
222
|
-
Auto-save modes: `'true'` (cron interval), `'false'` (disabled), `'onUpdate'` (after each write operation)
|
|
223
|
-
|
|
224
|
-
### MySQL Persistence
|
|
225
|
-
|
|
226
|
-
Enabled when `MYSQL_HOST` env var is set. Uses mysql2 connection pool. On init, loads all records from MySQL into the in-memory store. CRUD operations persist to MySQL after in-memory mutation. Supports:
|
|
227
|
-
- Schema introspection from model definitions
|
|
228
|
-
- Migration generation via schema diffs (`.snapshot.json`)
|
|
229
|
-
- Migration apply/rollback with transactions
|
|
230
|
-
- Schema drift detection on startup
|
|
231
|
-
- Auto-increment ID re-keying after INSERT
|
|
232
|
-
|
|
233
|
-
### Relationship System
|
|
234
|
-
|
|
235
|
-
| Type | Handler | Storage | Resolution |
|
|
236
|
-
|------|---------|---------|------------|
|
|
237
|
-
| `hasMany` | `src/has-many.js` | Array of Records | Inline objects, ID references, or pending queue |
|
|
238
|
-
| `belongsTo` | `src/belongs-to.js` | Single Record or null | Inline object, ID reference, or pending queue |
|
|
239
|
-
|
|
240
|
-
Relationships are bidirectional: creating a `belongsTo` automatically populates the inverse `hasMany`, and vice versa. Pending relationships resolve when the target record is created later.
|
|
241
|
-
|
|
242
|
-
## Authentication & Authorization
|
|
243
|
-
|
|
244
|
-
Access control is defined via access classes in the configured `paths.access` directory:
|
|
245
|
-
|
|
246
|
-
```javascript
|
|
247
|
-
export default class GlobalAccess {
|
|
248
|
-
models = ['owner', 'animal']; // or '*' for all
|
|
249
|
-
access(request) {
|
|
250
|
-
// Return false → 403 Forbidden
|
|
251
|
-
// Return ['read', 'create', 'update', 'delete'] → allowed methods
|
|
252
|
-
// Return (record) => boolean → filter function for collections
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
```
|
|
256
|
-
|
|
257
|
-
Method mapping: `GET → 'read'`, `POST → 'create'`, `DELETE → 'delete'`, `PATCH → 'update'`
|
|
258
|
-
|
|
259
|
-
## Configuration & Environment
|
|
260
|
-
|
|
261
|
-
Located in `config/environment.js`. All values overridable via environment variables:
|
|
262
|
-
|
|
263
|
-
| Variable | Default | Description |
|
|
264
|
-
|----------|---------|-------------|
|
|
265
|
-
| `DB_AUTO_SAVE` | `'false'` | Auto-save mode: `'true'`, `'false'`, or `'onUpdate'` |
|
|
266
|
-
| `DB_FILE` | `'db.json'` | JSON file path |
|
|
267
|
-
| `DB_MODE` | `'file'` | `'file'` or `'directory'` |
|
|
268
|
-
| `DB_DIRECTORY` | `'db'` | Directory name for collection files |
|
|
269
|
-
| `DB_SAVE_INTERVAL` | `3600` | Auto-save interval in seconds |
|
|
270
|
-
| `DB_SCHEMA_PATH` | `'./config/db-schema.js'` | Path to DB schema |
|
|
271
|
-
| `ORM_MODEL_PATH` | `'./models'` | Models directory |
|
|
272
|
-
| `ORM_SERIALIZER_PATH` | `'./serializers'` | Serializers directory |
|
|
273
|
-
| `ORM_TRANSFORM_PATH` | `'./transforms'` | Transforms directory |
|
|
274
|
-
| `ORM_ACCESS_PATH` | `'./access'` | Access classes directory |
|
|
275
|
-
| `ORM_USE_REST_SERVER` | `'true'` | Enable auto REST route generation |
|
|
276
|
-
| `ORM_REST_ROUTE` | `'/'` | Base route for REST endpoints |
|
|
277
|
-
| `MYSQL_HOST` | undefined | Enables MySQL mode when set |
|
|
278
|
-
| `MYSQL_PORT` | `3306` | MySQL port |
|
|
279
|
-
| `MYSQL_USER` | `'root'` | MySQL user |
|
|
280
|
-
| `MYSQL_PASSWORD` | `''` | MySQL password |
|
|
281
|
-
| `MYSQL_DATABASE` | `'stonyx'` | MySQL database name |
|
|
282
|
-
| `MYSQL_CONNECTION_LIMIT` | `10` | Connection pool size |
|
|
283
|
-
| `MYSQL_MIGRATIONS_DIR` | `'migrations'` | Migration files directory |
|
|
284
|
-
|
|
285
|
-
## Development Guide
|
|
286
|
-
|
|
287
|
-
| Task | Command |
|
|
288
|
-
|------|---------|
|
|
289
|
-
| Install deps | `pnpm install` |
|
|
290
|
-
| Run tests | `stonyx test` (or `npm test`) |
|
|
291
|
-
| Node version | v24.13.0 (see `.nvmrc`) |
|
|
292
|
-
| Generate MySQL migration | `stonyx db:generate-migration <description>` |
|
|
293
|
-
| Apply MySQL migrations | `stonyx db:migrate` |
|
|
294
|
-
| Rollback MySQL migration | `stonyx db:migrate:rollback` |
|
|
295
|
-
| Migration status | `stonyx db:migrate:status` |
|
|
296
|
-
| Migrate DB file→directory | `stonyx db:migrate-to-directory` |
|
|
297
|
-
| Migrate DB directory→file | `stonyx db:migrate-to-file` |
|
|
298
|
-
|
|
299
|
-
**Local development** uses `file:../package-name` references in `package.json` for sibling Stonyx packages.
|
|
300
|
-
|
|
301
|
-
## Testing
|
|
302
|
-
|
|
303
|
-
| Aspect | Detail |
|
|
304
|
-
|--------|--------|
|
|
305
|
-
| Framework | QUnit via `stonyx test` |
|
|
306
|
-
| Mocking | Sinon |
|
|
307
|
-
| Unit tests | `test/unit/` — transforms, commands, relationships, environment, lifecycle, MySQL startup |
|
|
308
|
-
| Integration tests | `test/integration/` — full ORM pipeline, directory-mode DB |
|
|
309
|
-
| Test fixtures | `test/sample/` — models, serializers, transforms, access, payload, constants |
|
|
310
|
-
| Test config | `test/config/environment.js` |
|
|
311
|
-
|
|
312
|
-
## Deployment & CI/CD
|
|
313
|
-
|
|
314
|
-
| Pipeline | Trigger | Workflow |
|
|
315
|
-
|----------|---------|----------|
|
|
316
|
-
| CI | Pull requests to `dev` or `main` | Shared via `abofs/stonyx-workflows/.github/workflows/ci.yml@main` |
|
|
317
|
-
| Publish | Push to `main`, manual dispatch | Shared via `abofs/stonyx-workflows/.github/workflows/npm-publish.yml@main` |
|
|
318
|
-
|
|
319
|
-
Published to npm as `@stonyx/orm` with public access and provenance.
|
|
320
|
-
|
|
321
|
-
## Conventions & Standards
|
|
322
|
-
|
|
323
|
-
- **Files**: kebab-case (e.g., `phone-number.js`)
|
|
324
|
-
- **Models**: PascalCase + `Model` suffix (e.g., `PhoneNumberModel`)
|
|
325
|
-
- **Serializers**: PascalCase + `Serializer` suffix (e.g., `AnimalSerializer`)
|
|
326
|
-
- **Transforms**: Original filename as key (e.g., `animal.js` → `'animal'` transform)
|
|
327
|
-
- **Model names in code**: kebab-case (e.g., `'phone-number'`)
|
|
328
|
-
- **REST routes**: Pluralized, dasherized (e.g., `/phone-numbers`)
|
|
329
|
-
- **Relationship URLs**: Dasherized (e.g., `phoneNumbers` → `/phone-numbers`)
|
|
330
|
-
- **ES Modules**: All files use `import`/`export`
|
|
331
|
-
- **License headers**: Apache 2.0 in core source files
|
|
332
|
-
- **Git workflow**: Feature branches → `dev` → `main`
|
|
333
|
-
|
|
334
|
-
## Known Issues & Technical Debt
|
|
335
|
-
|
|
336
|
-
See [improvements.md](improvements.md) for detailed findings. Key items:
|
|
337
|
-
|
|
338
|
-
- `config/environment.js` contains a stray `console.log(MYSQL_HOST)` statement
|
|
339
|
-
- `getOrSet` utility is duplicated across `has-many.js` and `belongs-to.js`
|
|
340
|
-
- `getRelationshipInfo()` is duplicated across `orm-request.js`, `schema-introspector.js`, and `meta-request.js`
|
|
341
|
-
- `getCollectionKeys()` and `getDirPath()` are duplicated between `db.js` and `migrate.js`
|
|
342
|
-
- Package exports in `package.json` are missing `./commands` and `./hooks` entries (present in code but not in the outline docs)
|
|
343
|
-
- The outline doc (`/.claude/personal/outline.md`) references files that don't exist (`src/include-parser.js`, `src/include-collector.js`, `stonyx-bootstrap.cjs`) and lists version as `0.1.0` (actual: `0.2.1-beta.1`)
|
package/src/belongs-to.js
DELETED
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import { createRecord, relationships, store } from '@stonyx/orm';
|
|
2
|
-
import { getRelationships } from './relationships.js';
|
|
3
|
-
|
|
4
|
-
function getOrSet(map, key, defaultValue) {
|
|
5
|
-
if (!map.has(key)) map.set(key, defaultValue);
|
|
6
|
-
return map.get(key);
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export default function belongsTo(modelName) {
|
|
10
|
-
const hasManyRelationships = relationships.get('hasMany');
|
|
11
|
-
const pendingHasManyQueue = relationships.get('pending');
|
|
12
|
-
const pendingBelongsToQueue = relationships.get('pendingBelongsTo');
|
|
13
|
-
|
|
14
|
-
return (sourceRecord, rawData, options) => {
|
|
15
|
-
if (!rawData) return null;
|
|
16
|
-
|
|
17
|
-
const { __name: sourceModelName } = sourceRecord.__model;
|
|
18
|
-
const relationshipId = sourceRecord.id;
|
|
19
|
-
const relationshipKey = options._relationshipKey;
|
|
20
|
-
const relationship = getRelationships('belongsTo', sourceModelName, modelName, relationshipId);
|
|
21
|
-
const modelStore = store.get(modelName);
|
|
22
|
-
|
|
23
|
-
// Try to get existing record
|
|
24
|
-
const output = typeof rawData === 'object'
|
|
25
|
-
? createRecord(modelName, rawData, options)
|
|
26
|
-
: modelStore.get(rawData);
|
|
27
|
-
|
|
28
|
-
// If not found and is a string ID, register as pending
|
|
29
|
-
if (!output && typeof rawData !== 'object') {
|
|
30
|
-
const targetId = rawData;
|
|
31
|
-
|
|
32
|
-
// Register pending belongsTo
|
|
33
|
-
const modelPendingMap = getOrSet(pendingBelongsToQueue, modelName, new Map());
|
|
34
|
-
const targetPendingArray = getOrSet(modelPendingMap, targetId, []);
|
|
35
|
-
|
|
36
|
-
targetPendingArray.push({
|
|
37
|
-
sourceRecord,
|
|
38
|
-
sourceModelName,
|
|
39
|
-
relationshipKey,
|
|
40
|
-
relationshipId
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
relationship.set(relationshipId, null);
|
|
44
|
-
return null;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
relationship.set(relationshipId, output || {});
|
|
48
|
-
|
|
49
|
-
// Populate hasMany side if the relationship is defined
|
|
50
|
-
const otherSide = hasManyRelationships.get(modelName)?.get(sourceModelName)?.get(output?.id);
|
|
51
|
-
|
|
52
|
-
if (otherSide) {
|
|
53
|
-
otherSide.push(sourceRecord);
|
|
54
|
-
|
|
55
|
-
// Remove pending queue if it was just fulfilled
|
|
56
|
-
const pendingModelRelationships = pendingHasManyQueue.get(sourceModelName);
|
|
57
|
-
|
|
58
|
-
if (pendingModelRelationships) pendingModelRelationships.delete(relationshipId);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
return output;
|
|
62
|
-
}
|
|
63
|
-
}
|
package/src/has-many.js
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import { createRecord, relationships, store } from '@stonyx/orm';
|
|
2
|
-
import { getRelationships } from './relationships.js';
|
|
3
|
-
import { getOrSet, makeArray } from '@stonyx/utils/object';
|
|
4
|
-
import { dbKey } from './db.js';
|
|
5
|
-
|
|
6
|
-
function queuePendingRelationship(pendingRelationshipQueue, pendingRelationships, modelName, id) {
|
|
7
|
-
pendingRelationshipQueue.push({
|
|
8
|
-
pendingRelationship: getOrSet(pendingRelationships, modelName, new Map()),
|
|
9
|
-
id
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
return null;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export default function hasMany(modelName) {
|
|
16
|
-
const globalRelationships = relationships.get('global');
|
|
17
|
-
const pendingRelationships = relationships.get('pending');
|
|
18
|
-
|
|
19
|
-
return (sourceRecord, rawData, options) => {
|
|
20
|
-
const { __name: sourceModelName } = sourceRecord.__model;
|
|
21
|
-
const relationshipId = sourceRecord.id;
|
|
22
|
-
const relationship = getRelationships('hasMany', sourceModelName, modelName, relationshipId);
|
|
23
|
-
const modelStore = store.get(modelName);
|
|
24
|
-
const pendingRelationshipQueue = [];
|
|
25
|
-
|
|
26
|
-
const output = !rawData ? [] : makeArray(rawData).map(elementData => {
|
|
27
|
-
let record;
|
|
28
|
-
|
|
29
|
-
if (typeof elementData !== 'object') {
|
|
30
|
-
record = modelStore.get(elementData);
|
|
31
|
-
|
|
32
|
-
if (!record) {
|
|
33
|
-
return queuePendingRelationship(pendingRelationshipQueue, pendingRelationships, modelName, elementData);
|
|
34
|
-
}
|
|
35
|
-
} else {
|
|
36
|
-
if (elementData !== Object(elementData)) {
|
|
37
|
-
return queuePendingRelationship(pendingRelationshipQueue, pendingRelationships, modelName, elementData);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
record = createRecord(modelName, elementData, options);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// Populate belongTo side if the relationship is defined
|
|
44
|
-
const otherSide = relationships.get('belongsTo').get(modelName)?.get(sourceModelName)?.get(record.id);
|
|
45
|
-
|
|
46
|
-
if (otherSide) Object.assign(otherSide, sourceRecord);
|
|
47
|
-
|
|
48
|
-
return record;
|
|
49
|
-
}).filter(value => value);
|
|
50
|
-
|
|
51
|
-
relationship.set(relationshipId, output);
|
|
52
|
-
|
|
53
|
-
// Assign global relationship
|
|
54
|
-
if (options.global || sourceModelName === dbKey) getOrSet(globalRelationships, modelName, []).push(output);
|
|
55
|
-
|
|
56
|
-
// Assign pending relationships
|
|
57
|
-
for (const { pendingRelationship, id } of pendingRelationshipQueue) getOrSet(pendingRelationship, id, []).push(output);
|
|
58
|
-
|
|
59
|
-
return output;
|
|
60
|
-
}
|
|
61
|
-
}
|
package/src/main.js
DELETED
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Copyright 2025 Stone Costa
|
|
3
|
-
*
|
|
4
|
-
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
-
* you may not use this file except in compliance with the License.
|
|
6
|
-
* You may obtain a copy of the License at
|
|
7
|
-
*
|
|
8
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
-
*
|
|
10
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
-
* See the License for the specific language governing permissions and
|
|
14
|
-
* limitations under the License.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import DB from './db.js';
|
|
18
|
-
import config from 'stonyx/config';
|
|
19
|
-
import log from 'stonyx/log';
|
|
20
|
-
import { forEachFileImport } from '@stonyx/utils/file';
|
|
21
|
-
import { kebabCaseToPascalCase, pluralize } from '@stonyx/utils/string';
|
|
22
|
-
import setupRestServer from './setup-rest-server.js';
|
|
23
|
-
import baseTransforms from './transforms.js';
|
|
24
|
-
import Store from './store.js';
|
|
25
|
-
import Serializer from './serializer.js';
|
|
26
|
-
import { setup } from '@stonyx/events';
|
|
27
|
-
|
|
28
|
-
const defaultOptions = {
|
|
29
|
-
dbType: 'json'
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export default class Orm {
|
|
33
|
-
static initialized = false;
|
|
34
|
-
static relationships = new Map();
|
|
35
|
-
static store = new Store();
|
|
36
|
-
|
|
37
|
-
models = {};
|
|
38
|
-
serializers = {};
|
|
39
|
-
transforms = { ...baseTransforms };
|
|
40
|
-
warnings = new Set();
|
|
41
|
-
|
|
42
|
-
constructor(options={}) {
|
|
43
|
-
if (Orm.instance) return Orm.instance;
|
|
44
|
-
|
|
45
|
-
const { relationships } = Orm;
|
|
46
|
-
|
|
47
|
-
// Declare relationship maps
|
|
48
|
-
for (const key of ['hasMany', 'belongsTo', 'global', 'pending', 'pendingBelongsTo']) {
|
|
49
|
-
relationships.set(key, new Map());
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
this.options = { ...defaultOptions, ...options };
|
|
53
|
-
|
|
54
|
-
Orm.instance = this;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
async init() {
|
|
58
|
-
const { paths, restServer } = config.orm;
|
|
59
|
-
const promises = ['Model', 'Serializer', 'Transform'].map(type => {
|
|
60
|
-
const lowerCaseType = type.toLowerCase();
|
|
61
|
-
const path = paths[lowerCaseType];
|
|
62
|
-
|
|
63
|
-
if (!path) throw new Error(`Configuration Error: ORM path for "${type}" must be defined.`);
|
|
64
|
-
|
|
65
|
-
return forEachFileImport(path, (exported, { name }) => {
|
|
66
|
-
// Transforms keep their original name, everything else gets converted to PascalCase with the type suffix
|
|
67
|
-
const alias = type === 'Transform' ? name : `${kebabCaseToPascalCase(name)}${type}`;
|
|
68
|
-
|
|
69
|
-
if (type === 'Model') Orm.store.set(name, new Map());
|
|
70
|
-
|
|
71
|
-
return this[pluralize(lowerCaseType)][alias] = exported;
|
|
72
|
-
}, { ignoreAccessFailure: true, rawName: true, recursive: true, recursiveNaming: true });
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
// Wait for imports before db & rest server setup
|
|
76
|
-
await Promise.all(promises);
|
|
77
|
-
|
|
78
|
-
// Setup event names for hooks after models are loaded
|
|
79
|
-
const eventNames = [];
|
|
80
|
-
const operations = ['list', 'get', 'create', 'update', 'delete'];
|
|
81
|
-
const timings = ['before', 'after'];
|
|
82
|
-
|
|
83
|
-
for (const modelName of Orm.store.data.keys()) {
|
|
84
|
-
for (const timing of timings) {
|
|
85
|
-
for (const operation of operations) {
|
|
86
|
-
eventNames.push(`${timing}:${operation}:${modelName}`);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
setup(eventNames);
|
|
92
|
-
|
|
93
|
-
if (config.orm.mysql) {
|
|
94
|
-
const { default: MysqlDB } = await import('./mysql/mysql-db.js');
|
|
95
|
-
this.mysqlDb = new MysqlDB();
|
|
96
|
-
this.db = this.mysqlDb;
|
|
97
|
-
promises.push(this.mysqlDb.init());
|
|
98
|
-
} else if (this.options.dbType !== 'none') {
|
|
99
|
-
const db = new DB();
|
|
100
|
-
this.db = db;
|
|
101
|
-
|
|
102
|
-
promises.push(db.init());
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
if (restServer.enabled === 'true') {
|
|
106
|
-
promises.push(setupRestServer(restServer.route, paths.access, restServer.metaRoute));
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
Orm.ready = await Promise.all(promises);
|
|
110
|
-
Orm.initialized = true;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
async startup() {
|
|
114
|
-
if (this.mysqlDb) await this.mysqlDb.startup();
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
async shutdown() {
|
|
118
|
-
if (this.mysqlDb) await this.mysqlDb.shutdown();
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
static get db() {
|
|
122
|
-
if (!Orm.initialized) throw new Error('ORM has not been initialized yet');
|
|
123
|
-
|
|
124
|
-
return Orm.instance.db;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
getRecordClasses(modelName) {
|
|
128
|
-
const modelClassPrefix = kebabCaseToPascalCase(modelName);
|
|
129
|
-
|
|
130
|
-
return {
|
|
131
|
-
modelClass: this.models[`${modelClassPrefix}Model`],
|
|
132
|
-
serializerClass: this.serializers[`${modelClassPrefix}Serializer`] || Serializer
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// Queue warnings to avoid the same error from being logged in the same iteration
|
|
137
|
-
warn(message) {
|
|
138
|
-
this.warnings.add(message);
|
|
139
|
-
|
|
140
|
-
setTimeout(() => {
|
|
141
|
-
this.warnings.forEach(warning => log.warn(warning));
|
|
142
|
-
this.warnings.clear();
|
|
143
|
-
}, 0);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
export const store = Orm.store;
|
|
148
|
-
export const relationships = Orm.relationships;
|