@stonyx/orm 0.3.2-beta.301 → 0.3.2-beta.302

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.
Files changed (2) hide show
  1. package/README.md +53 -14
  2. 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. The ORM loads all records into memory on startup and persists CRUD operations to MySQL automatically. Supports schema-aware migration generation, apply, rollback, and drift detection.
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 can automatically register REST routes using your access classes.
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
- import setupRestServer from '@stonyx/orm/setup-rest-server';
362
-
363
- await setupRestServer('/', './access');
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
- beforeHook('delete', 'animal', (context) => {
718
- const animal = store.get('animal', context.request.recordId);
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
- beforeHook('update', 'animal', (context) => {
750
- context.state.previousOwnerId = store.get('animal', context.request.recordId)?.owner?.id;
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
- beforeHook('delete', 'animal', (context) => {
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.get('animal', context.request.recordId);
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
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "stonyx-async",
5
5
  "stonyx-module"
6
6
  ],
7
- "version": "0.3.2-beta.301",
7
+ "version": "0.3.2-beta.302",
8
8
  "description": "",
9
9
  "main": "dist/index.js",
10
10
  "type": "module",