@stonyx/orm 0.3.2-beta.301 → 0.3.2-beta.303
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 +53 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ A lightweight ORM for Stonyx projects, featuring model definitions, serializers,
|
|
|
9
9
|
|
|
10
10
|
## Highlights
|
|
11
11
|
|
|
12
|
-
- **Automatic Loading**: Models, serializers, transforms, and access classes are auto-registered from their configured directories.
|
|
12
|
+
- **Automatic Loading**: Models, serializers, transforms, views and access classes are auto-registered from their configured directories. Models, serializers, transforms and views are also loaded from subdirectories and named after their path (`models/admin/user-role.ts` registers `admin/user-role`); access classes are loaded only from the top level of `paths.access`, and one in a subdirectory is silently never loaded.
|
|
13
13
|
- **Models**: Define attributes with type-safe proxies (`attr`) and relationships (`hasMany`, `belongsTo`).
|
|
14
14
|
- **Serializers**: Map raw data into model-friendly structures, including nested properties.
|
|
15
15
|
- **Transforms**: Apply custom transformations on data values automatically.
|
|
@@ -46,6 +46,21 @@ not put them on disk, and none of them is loaded unless your configuration asks
|
|
|
46
46
|
for it. Add only the ones you actually use — see
|
|
47
47
|
[Optional peer dependencies](#optional-peer-dependencies).
|
|
48
48
|
|
|
49
|
+
## Exports
|
|
50
|
+
|
|
51
|
+
| Import path | What it exports |
|
|
52
|
+
| :--- | :--- |
|
|
53
|
+
| `@stonyx/orm` | `Orm` (default), `store`, `relationships`, the base classes `Model`, `View`, `Serializer`, the aggregate helpers `count`, `avg`, `sum`, `min`, `max` (see [docs/views.md](https://github.com/abofs/stonyx-orm/blob/dev/docs/views.md)), the helpers listed in [Exported Helpers](#exported-helpers), and the types `OrmAccessRequest` and `PersistErrorDetail` |
|
|
54
|
+
| `@stonyx/orm/db` | the live DB instance (default), `data` (its record) and `saveDB()`. It reads `Orm.db` when it is imported, so import it only after the ORM has initialized |
|
|
55
|
+
| `@stonyx/orm/standalone-db` | `StandaloneDB` (default): reads and writes the JSON db files directly, without booting Stonyx or the ORM |
|
|
56
|
+
| `@stonyx/orm/migrate` | `fileToDirectory()`, `directoryToFile()`: convert the JSON db between file and directory mode |
|
|
57
|
+
| `@stonyx/orm/commands` | the `db:*` command map (default) that the Stonyx CLI loads; see [Database (DB) Integration](#database-db-integration) |
|
|
58
|
+
| `@stonyx/orm/hooks` | `beforeHook`, `afterHook`, `clearHook`, `clearAllHooks`, `getBeforeHooks`, `getAfterHooks`, and the `HookContext` type |
|
|
59
|
+
|
|
60
|
+
## Samples
|
|
61
|
+
|
|
62
|
+
[`test/sample/`](https://github.com/abofs/stonyx-orm/tree/dev/test/sample) is the canonical usage reference: models, serializers, transforms, views and an access class that the test suite loads as a running app, so they stay correct. It is not included in the npm package.
|
|
63
|
+
|
|
49
64
|
## Usage example
|
|
50
65
|
|
|
51
66
|
This module is part of the **Stonyx framework**. To use it, first configure the `restServer` key in your `environment.js` file:
|
|
@@ -298,7 +313,7 @@ In directory mode, each collection is stored as `{directory}/{collection}.json`
|
|
|
298
313
|
|
|
299
314
|
### MySQL Mode
|
|
300
315
|
|
|
301
|
-
Set the `MYSQL_HOST` environment variable to enable MySQL persistence.
|
|
316
|
+
Set the `MYSQL_HOST` environment variable to enable MySQL persistence. At startup the ORM loads into memory only the records of models declared `static memory = true`; every other model (the default is `memory = false`) is read from MySQL on demand by `store.find()` / `store.findAll()`, and the synchronous `store.get()` does not see it (see [docs/internals.md](https://github.com/abofs/stonyx-orm/blob/dev/docs/internals.md#store-reads-and-the-memory-flag)). A record read that way resolves a `belongsTo` only when the related record is already in memory; otherwise the relationship is `null`. CRUD operations are persisted to MySQL automatically. Supports schema-aware migration generation, apply, rollback, and drift detection.
|
|
302
317
|
|
|
303
318
|
| Command | Description |
|
|
304
319
|
|---------|-------------|
|
|
@@ -310,7 +325,7 @@ Set the `MYSQL_HOST` environment variable to enable MySQL persistence. The ORM l
|
|
|
310
325
|
|
|
311
326
|
### DynamoDB Mode
|
|
312
327
|
|
|
313
|
-
Set the `DYNAMODB_REGION` environment variable to enable DynamoDB persistence. Tables are created with PAY_PER_REQUEST (on-demand) billing. Global Secondary Indexes (GSIs) are auto-provisioned at startup based on model `belongsTo` relationships — each FK column gets a GSI. `findAll()` with conditions routes to a GSI Query when the condition key matches a GSI partition key; non-indexed attribute conditions fall back to Scan + FilterExpression (expensive for large tables). ULID generation replaces auto-increment for numeric-ID models.
|
|
328
|
+
Set the `DYNAMODB_REGION` environment variable to enable DynamoDB persistence. Tables are created with PAY_PER_REQUEST (on-demand) billing. Global Secondary Indexes (GSIs) are auto-provisioned at startup based on model `belongsTo` relationships — each FK column gets a GSI. `findAll()` with conditions routes to a GSI Query when the condition key matches a GSI partition key; non-indexed attribute conditions fall back to Scan + FilterExpression (expensive for large tables). ULID generation replaces auto-increment for numeric-ID models. The `memory` flag works as in [MySQL Mode](#mysql-mode): only `static memory = true` models are loaded at startup, every other model is read on demand by `store.find()` / `store.findAll()` and is not seen by `store.get()`, and a record read that way resolves a `belongsTo` only when the related record is already in memory.
|
|
314
329
|
|
|
315
330
|
```javascript
|
|
316
331
|
dynamodb: {
|
|
@@ -355,12 +370,20 @@ MySQL integration tests run automatically when MySQL is available. In CI (where
|
|
|
355
370
|
|
|
356
371
|
## REST Server Integration
|
|
357
372
|
|
|
358
|
-
The ORM
|
|
373
|
+
The ORM registers REST routes for the models your access classes name. It does this itself during initialization when `restServer.enabled` is `'true'`; there is no setup function to import or call. Configure it in `config/environment.js`:
|
|
359
374
|
|
|
360
375
|
```js
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
376
|
+
export default {
|
|
377
|
+
orm: {
|
|
378
|
+
paths: {
|
|
379
|
+
access: './access' // directory of access classes
|
|
380
|
+
},
|
|
381
|
+
restServer: {
|
|
382
|
+
enabled: 'true', // requires @stonyx/rest-server, see Optional peer dependencies
|
|
383
|
+
route: '/' // mount prefix for the generated routes
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
};
|
|
364
387
|
```
|
|
365
388
|
|
|
366
389
|
Access classes define models and provide custom filtering/authorization logic:
|
|
@@ -419,6 +442,11 @@ unrecognised spelling falls through to whatever your method returns next, so
|
|
|
419
442
|
prefer one class per model. A class may still list several models in `models`
|
|
420
443
|
when they share one rule.
|
|
421
444
|
|
|
445
|
+
`access()` covers a model's own routes only: records of that model served
|
|
446
|
+
through another model's `?include=`, `/:id/<relationship>` or
|
|
447
|
+
`/:id/relationships/<relationship>` routes are never
|
|
448
|
+
checked against it. See [docs/security.md](https://github.com/abofs/stonyx-orm/blob/dev/docs/security.md).
|
|
449
|
+
|
|
422
450
|
**Numeric ids: authorize on `request.recordId`, not on `request.params.id`.**
|
|
423
451
|
`params.id` is raw text from the client. When it looks numeric the ORM coerces
|
|
424
452
|
it *before* it resolves the record, so `7`, `007`, `7.0`, `7.9`, `7e0`, `0x7`,
|
|
@@ -616,6 +644,7 @@ GET /animals/1
|
|
|
616
644
|
#### Limitations
|
|
617
645
|
|
|
618
646
|
- Only available on GET endpoints (not POST/PATCH)
|
|
647
|
+
- Included records are authorized only by the requested model's `access()`, not by their own model's, and `include` has no built-in cap on depth or on the number of paths; a long `include` blocks the server while it is walked. [docs/security.md](https://github.com/abofs/stonyx-orm/blob/dev/docs/security.md) has the measurements and an `access()` check that caps both.
|
|
619
648
|
|
|
620
649
|
## Lifecycle Hooks
|
|
621
650
|
|
|
@@ -714,8 +743,12 @@ beforeHook('create', 'animal', (context) => {
|
|
|
714
743
|
// resolved the record by. `context.params.id` is the raw client text and is a
|
|
715
744
|
// different value for every alias of the same id, so a lookup keyed on it
|
|
716
745
|
// finds nothing on a numeric-id model (abofs/stonyx-orm#270).
|
|
717
|
-
|
|
718
|
-
|
|
746
|
+
// `store.find` rather than `store.get`: with a database adapter (MySQL,
|
|
747
|
+
// Postgres, TimescaleDB or DynamoDB) a model without `static memory = true` is
|
|
748
|
+
// not in memory, and `get` returns undefined.
|
|
749
|
+
// Before hooks are awaited, so an async hook works here.
|
|
750
|
+
beforeHook('delete', 'animal', async (context) => {
|
|
751
|
+
const animal = await store.find('animal', context.request.recordId);
|
|
719
752
|
if (animal.protected) {
|
|
720
753
|
return { errors: [{ detail: 'Cannot delete protected animals' }] };
|
|
721
754
|
}
|
|
@@ -746,8 +779,10 @@ beforeHook('create', 'owner', (context) => {
|
|
|
746
779
|
// Send notification after animal is adopted.
|
|
747
780
|
// `oldState` holds attributes only, and a belongsTo is the related Record, not
|
|
748
781
|
// its id: capture the previous owner's id before the update, compare ids after.
|
|
749
|
-
|
|
750
|
-
|
|
782
|
+
// With a database adapter (MySQL, Postgres, TimescaleDB or DynamoDB) the owner
|
|
783
|
+
// resolves only when it is in memory (see MySQL Mode).
|
|
784
|
+
beforeHook('update', 'animal', async (context) => {
|
|
785
|
+
context.state.previousOwnerId = (await store.find('animal', context.request.recordId))?.owner?.id;
|
|
751
786
|
});
|
|
752
787
|
|
|
753
788
|
afterHook('update', 'animal', async (context) => {
|
|
@@ -816,10 +851,14 @@ afterHook('delete', 'animal', async (context) => {
|
|
|
816
851
|
// same value `access()` is handed. Authorizing on `context.params.id` instead
|
|
817
852
|
// looks up the raw client text: on a numeric-id model that lookup returns
|
|
818
853
|
// `undefined` for EVERY spelling, `animal.owner` throws, and the check never
|
|
819
|
-
// runs. Measured; abofs/stonyx-orm#270.
|
|
820
|
-
|
|
854
|
+
// runs. Measured; abofs/stonyx-orm#270. `store.find`, not `store.get`: with a
|
|
855
|
+
// database adapter (MySQL, Postgres, TimescaleDB or DynamoDB) a model without
|
|
856
|
+
// `static memory = true` is not in memory, and `animal.owner` resolves only when
|
|
857
|
+
// the owner is in memory too, and is `null` otherwise, so this check then
|
|
858
|
+
// refuses the owner as well (see MySQL Mode).
|
|
859
|
+
beforeHook('delete', 'animal', async (context) => {
|
|
821
860
|
const user = context.state.currentUser;
|
|
822
|
-
const animal = store.
|
|
861
|
+
const animal = await store.find('animal', context.request.recordId);
|
|
823
862
|
|
|
824
863
|
// `animal.owner` is the related Record, so compare its id. Refuse an unowned
|
|
825
864
|
// animal outright: `animal.owner?.id !== user.id` lets a user with no id
|