@stonyx/orm 0.2.1-beta.9 → 0.2.1-beta.91
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 +75 -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 +257 -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 +475 -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 +28 -0
- package/dist/postgres/schema-introspector.js +280 -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/schema-helpers.d.ts +20 -0
- package/dist/schema-helpers.js +48 -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.ts +151 -0
- 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 +310 -0
- package/src/mysql/{type-map.js → type-map.ts} +42 -37
- package/src/{orm-request.js → orm-request.ts} +187 -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 +343 -0
- package/src/postgres/type-map.ts +61 -0
- package/src/record.ts +186 -0
- package/src/relationships.ts +54 -0
- package/src/schema-helpers.ts +59 -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/hooks.js +0 -124
- 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/README.md
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
[](https://github.com/abofs/stonyx-orm/actions/workflows/ci.yml)
|
|
2
|
+
[](https://www.npmjs.com/package/@stonyx/orm)
|
|
3
|
+
[](https://opensource.org/licenses/Apache-2.0)
|
|
4
|
+
|
|
1
5
|
# @stonyx/orm
|
|
2
6
|
|
|
3
7
|
A lightweight ORM for Stonyx projects, featuring model definitions, serializers, relationships, transforms, and optional REST server integration.
|
|
@@ -13,6 +17,23 @@ A lightweight ORM for Stonyx projects, featuring model definitions, serializers,
|
|
|
13
17
|
- **REST Server Integration**: Automatic route setup with customizable access control.
|
|
14
18
|
- **Lifecycle Hooks**: Middleware-based before/after hooks for validation, authorization, side effects, and auditing.
|
|
15
19
|
|
|
20
|
+
## Public API vs Internals
|
|
21
|
+
|
|
22
|
+
Records use a proxy that exposes model attributes as direct properties. Always use direct property access for reading and writing field values:
|
|
23
|
+
|
|
24
|
+
```js
|
|
25
|
+
// Correct: read/write via the proxy
|
|
26
|
+
const age = record.age;
|
|
27
|
+
record.age = 5;
|
|
28
|
+
|
|
29
|
+
// Correct: iterate fields using the record directly
|
|
30
|
+
for (const key of Object.keys(record.serialize())) {
|
|
31
|
+
console.log(key, record[key]);
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
All properties prefixed with `__` (`__data`, `__relationships`, `__model`, `__serializer`, `__serialized`) are **internal implementation details** and must not be accessed by consumer code. Bypassing the proxy by reading or writing `__data` directly skips type transforms and change tracking, which can lead to silent data corruption.
|
|
36
|
+
|
|
16
37
|
## Installation
|
|
17
38
|
|
|
18
39
|
```bash
|
|
@@ -109,6 +130,22 @@ export default class OwnerModel extends Model {
|
|
|
109
130
|
}
|
|
110
131
|
```
|
|
111
132
|
|
|
133
|
+
### Overriding Plural Names
|
|
134
|
+
|
|
135
|
+
By default, model names are auto-pluralized for REST routes, JSON:API URLs, and DB table names (e.g., `animal` → `animals`). When auto-pluralization produces the wrong result, override it with `static pluralName`:
|
|
136
|
+
|
|
137
|
+
```js
|
|
138
|
+
import { Model, attr } from '@stonyx/orm';
|
|
139
|
+
|
|
140
|
+
export default class PersonModel extends Model {
|
|
141
|
+
static pluralName = 'people';
|
|
142
|
+
|
|
143
|
+
name = attr('string');
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
The override is picked up automatically during ORM initialization. All routes, JSON:API type references, and MySQL table names will use the overridden value.
|
|
148
|
+
|
|
112
149
|
## Serializers
|
|
113
150
|
|
|
114
151
|
Based on the following sample payload structure which represents a poorly structure third-party data source:
|
|
@@ -207,6 +244,27 @@ Set the `MYSQL_HOST` environment variable to enable MySQL persistence. The ORM l
|
|
|
207
244
|
| `stonyx db:migrate:rollback` | Rollback the most recent migration |
|
|
208
245
|
| `stonyx db:migrate:status` | Show migration status |
|
|
209
246
|
|
|
247
|
+
### Running MySQL Tests
|
|
248
|
+
|
|
249
|
+
The ORM includes integration tests that run against a real MySQL database. These are optional — all other tests work without MySQL.
|
|
250
|
+
|
|
251
|
+
**One-time setup:**
|
|
252
|
+
|
|
253
|
+
```bash
|
|
254
|
+
# Requires local MySQL 8.0+ running
|
|
255
|
+
./scripts/setup-test-db.sh
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
This creates a `stonyx_orm_test` database with a `stonyx_test` user. Safe to re-run.
|
|
259
|
+
|
|
260
|
+
**Running tests:**
|
|
261
|
+
|
|
262
|
+
```bash
|
|
263
|
+
npm test
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
MySQL integration tests run automatically when MySQL is available. In CI (where `CI=true`), they skip gracefully.
|
|
267
|
+
|
|
210
268
|
## REST Server Integration
|
|
211
269
|
|
|
212
270
|
The ORM can automatically register REST routes using your access classes.
|
|
@@ -376,7 +434,7 @@ Each hook receives a context object with comprehensive information:
|
|
|
376
434
|
- It contains a deep copy of the record's state **before** the operation executes (captured before the `before` hook fires)
|
|
377
435
|
- The deep copy is created via JSON serialization (`JSON.parse(JSON.stringify())`) to ensure complete isolation
|
|
378
436
|
- For `delete` operations, `recordId` is provided in after hooks since the record may no longer exist in the store
|
|
379
|
-
- `oldState` is captured
|
|
437
|
+
- `oldState` is captured as a deep copy of the record's data before the operation, providing access to the previous field values
|
|
380
438
|
|
|
381
439
|
### Usage Examples
|
|
382
440
|
|
|
@@ -479,8 +537,8 @@ afterHook('update', 'animal', async (context) => {
|
|
|
479
537
|
|
|
480
538
|
// Track multiple field changes
|
|
481
539
|
const changedFields = [];
|
|
482
|
-
for (const key
|
|
483
|
-
if (context.oldState[key] !== context.record
|
|
540
|
+
for (const key of Object.keys(context.oldState)) {
|
|
541
|
+
if (context.oldState[key] !== context.record[key]) {
|
|
484
542
|
changedFields.push(key);
|
|
485
543
|
}
|
|
486
544
|
}
|
|
@@ -519,9 +577,9 @@ afterHook('update', 'animal', async (context) => {
|
|
|
519
577
|
// Compare oldState with current record to capture exact changes
|
|
520
578
|
const changes = {};
|
|
521
579
|
if (context.oldState) {
|
|
522
|
-
for (const
|
|
523
|
-
if (context.oldState[key] !==
|
|
524
|
-
changes[key] = { from: context.oldState[key], to:
|
|
580
|
+
for (const key of Object.keys(context.oldState)) {
|
|
581
|
+
if (context.oldState[key] !== context.record[key]) {
|
|
582
|
+
changes[key] = { from: context.oldState[key], to: context.record[key] };
|
|
525
583
|
}
|
|
526
584
|
}
|
|
527
585
|
}
|
package/config/environment.js
CHANGED
|
@@ -4,6 +4,7 @@ const {
|
|
|
4
4
|
ORM_REST_ROUTE,
|
|
5
5
|
ORM_SERIALIZER_PATH,
|
|
6
6
|
ORM_TRANSFORM_PATH,
|
|
7
|
+
ORM_VIEW_PATH,
|
|
7
8
|
ORM_USE_REST_SERVER,
|
|
8
9
|
DB_AUTO_SAVE,
|
|
9
10
|
DB_FILE,
|
|
@@ -18,6 +19,20 @@ const {
|
|
|
18
19
|
MYSQL_DATABASE,
|
|
19
20
|
MYSQL_CONNECTION_LIMIT,
|
|
20
21
|
MYSQL_MIGRATIONS_DIR,
|
|
22
|
+
PG_HOST,
|
|
23
|
+
PG_PORT,
|
|
24
|
+
PG_USER,
|
|
25
|
+
PG_PASSWORD,
|
|
26
|
+
PG_DATABASE,
|
|
27
|
+
PG_CONNECTION_LIMIT,
|
|
28
|
+
PG_MIGRATIONS_DIR,
|
|
29
|
+
TIMESCALE_HOST,
|
|
30
|
+
TIMESCALE_PORT,
|
|
31
|
+
TIMESCALE_USER,
|
|
32
|
+
TIMESCALE_PASSWORD,
|
|
33
|
+
TIMESCALE_DATABASE,
|
|
34
|
+
TIMESCALE_CONNECTION_LIMIT,
|
|
35
|
+
TIMESCALE_MIGRATIONS_DIR,
|
|
21
36
|
} = process.env;
|
|
22
37
|
|
|
23
38
|
export default {
|
|
@@ -36,7 +51,8 @@ export default {
|
|
|
36
51
|
access: ORM_ACCESS_PATH ?? './access', // Optional for restServer access hooks
|
|
37
52
|
model: ORM_MODEL_PATH ?? './models',
|
|
38
53
|
serializer: ORM_SERIALIZER_PATH ?? './serializers',
|
|
39
|
-
transform: ORM_TRANSFORM_PATH ?? './transforms'
|
|
54
|
+
transform: ORM_TRANSFORM_PATH ?? './transforms',
|
|
55
|
+
view: ORM_VIEW_PATH ?? './views'
|
|
40
56
|
},
|
|
41
57
|
mysql: MYSQL_HOST ? {
|
|
42
58
|
host: MYSQL_HOST ?? 'localhost',
|
|
@@ -48,6 +64,26 @@ export default {
|
|
|
48
64
|
migrationsDir: MYSQL_MIGRATIONS_DIR ?? 'migrations',
|
|
49
65
|
migrationsTable: '__migrations',
|
|
50
66
|
} : undefined,
|
|
67
|
+
postgres: PG_HOST ? {
|
|
68
|
+
host: PG_HOST ?? 'localhost',
|
|
69
|
+
port: parseInt(PG_PORT ?? '5432'),
|
|
70
|
+
user: PG_USER ?? 'postgres',
|
|
71
|
+
password: PG_PASSWORD ?? '',
|
|
72
|
+
database: PG_DATABASE ?? 'stonyx',
|
|
73
|
+
connectionLimit: parseInt(PG_CONNECTION_LIMIT ?? '10'),
|
|
74
|
+
migrationsDir: PG_MIGRATIONS_DIR ?? 'migrations',
|
|
75
|
+
migrationsTable: '__migrations',
|
|
76
|
+
} : undefined,
|
|
77
|
+
timescale: TIMESCALE_HOST ? {
|
|
78
|
+
host: TIMESCALE_HOST ?? 'localhost',
|
|
79
|
+
port: parseInt(TIMESCALE_PORT ?? '5432'),
|
|
80
|
+
user: TIMESCALE_USER ?? 'postgres',
|
|
81
|
+
password: TIMESCALE_PASSWORD ?? '',
|
|
82
|
+
database: TIMESCALE_DATABASE ?? 'stonyx',
|
|
83
|
+
connectionLimit: parseInt(TIMESCALE_CONNECTION_LIMIT ?? '10'),
|
|
84
|
+
migrationsDir: TIMESCALE_MIGRATIONS_DIR ?? 'migrations',
|
|
85
|
+
migrationsTable: '__migrations',
|
|
86
|
+
} : undefined,
|
|
51
87
|
restServer: {
|
|
52
88
|
enabled: ORM_USE_REST_SERVER ?? 'true', // Whether to load restServer for automatic route setup or
|
|
53
89
|
route: ORM_REST_ROUTE ?? '/',
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
type AggregateType = 'count' | 'avg' | 'sum' | 'min' | 'max';
|
|
2
|
+
interface AggregateRecord {
|
|
3
|
+
__data?: Record<string, unknown>;
|
|
4
|
+
[key: string]: unknown;
|
|
5
|
+
}
|
|
6
|
+
export declare class AggregateProperty {
|
|
7
|
+
readonly __kind: "AggregateProperty";
|
|
8
|
+
readonly aggregateType: AggregateType;
|
|
9
|
+
readonly relationship: string | undefined;
|
|
10
|
+
readonly field: string | undefined;
|
|
11
|
+
readonly mysqlFunction: string;
|
|
12
|
+
readonly resultType: 'float' | 'number';
|
|
13
|
+
constructor(aggregateType: AggregateType, relationship?: string, field?: string);
|
|
14
|
+
compute(relatedRecords: AggregateRecord[]): number | null;
|
|
15
|
+
}
|
|
16
|
+
export declare function count(relationship: string): AggregateProperty;
|
|
17
|
+
export declare function avg(relationshipOrField: string, field?: string): AggregateProperty;
|
|
18
|
+
export declare function sum(relationshipOrField: string, field?: string): AggregateProperty;
|
|
19
|
+
export declare function min(relationshipOrField: string, field?: string): AggregateProperty;
|
|
20
|
+
export declare function max(relationshipOrField: string, field?: string): AggregateProperty;
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
export class AggregateProperty {
|
|
2
|
+
__kind = 'AggregateProperty';
|
|
3
|
+
aggregateType;
|
|
4
|
+
relationship;
|
|
5
|
+
field;
|
|
6
|
+
mysqlFunction;
|
|
7
|
+
resultType;
|
|
8
|
+
constructor(aggregateType, relationship, field) {
|
|
9
|
+
this.aggregateType = aggregateType;
|
|
10
|
+
this.relationship = relationship;
|
|
11
|
+
this.field = field;
|
|
12
|
+
this.mysqlFunction = aggregateType.toUpperCase();
|
|
13
|
+
this.resultType = aggregateType === 'avg' ? 'float' : 'number';
|
|
14
|
+
}
|
|
15
|
+
compute(relatedRecords) {
|
|
16
|
+
if (!relatedRecords || !Array.isArray(relatedRecords) || relatedRecords.length === 0) {
|
|
17
|
+
if (this.aggregateType === 'min' || this.aggregateType === 'max')
|
|
18
|
+
return null;
|
|
19
|
+
return 0;
|
|
20
|
+
}
|
|
21
|
+
if (this.aggregateType === 'count')
|
|
22
|
+
return relatedRecords.length;
|
|
23
|
+
const field = this.field;
|
|
24
|
+
if (!field)
|
|
25
|
+
return null;
|
|
26
|
+
switch (this.aggregateType) {
|
|
27
|
+
case 'sum':
|
|
28
|
+
return relatedRecords.reduce((acc, record) => {
|
|
29
|
+
const val = parseFloat(record?.__data?.[field] ?? record?.[field]);
|
|
30
|
+
return acc + (isNaN(val) ? 0 : val);
|
|
31
|
+
}, 0);
|
|
32
|
+
case 'avg': {
|
|
33
|
+
let sum = 0;
|
|
34
|
+
let count = 0;
|
|
35
|
+
for (const record of relatedRecords) {
|
|
36
|
+
const val = parseFloat(record?.__data?.[field] ?? record?.[field]);
|
|
37
|
+
if (!isNaN(val)) {
|
|
38
|
+
sum += val;
|
|
39
|
+
count++;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return count === 0 ? 0 : sum / count;
|
|
43
|
+
}
|
|
44
|
+
case 'min': {
|
|
45
|
+
let min = null;
|
|
46
|
+
for (const record of relatedRecords) {
|
|
47
|
+
const val = parseFloat(record?.__data?.[field] ?? record?.[field]);
|
|
48
|
+
if (!isNaN(val) && (min === null || val < min))
|
|
49
|
+
min = val;
|
|
50
|
+
}
|
|
51
|
+
return min;
|
|
52
|
+
}
|
|
53
|
+
case 'max': {
|
|
54
|
+
let max = null;
|
|
55
|
+
for (const record of relatedRecords) {
|
|
56
|
+
const val = parseFloat(record?.__data?.[field] ?? record?.[field]);
|
|
57
|
+
if (!isNaN(val) && (max === null || val > max))
|
|
58
|
+
max = val;
|
|
59
|
+
}
|
|
60
|
+
return max;
|
|
61
|
+
}
|
|
62
|
+
default:
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
export function count(relationship) {
|
|
68
|
+
return new AggregateProperty('count', relationship);
|
|
69
|
+
}
|
|
70
|
+
export function avg(relationshipOrField, field) {
|
|
71
|
+
if (field !== undefined) {
|
|
72
|
+
return new AggregateProperty('avg', relationshipOrField, field);
|
|
73
|
+
}
|
|
74
|
+
return new AggregateProperty('avg', undefined, relationshipOrField);
|
|
75
|
+
}
|
|
76
|
+
export function sum(relationshipOrField, field) {
|
|
77
|
+
if (field !== undefined) {
|
|
78
|
+
return new AggregateProperty('sum', relationshipOrField, field);
|
|
79
|
+
}
|
|
80
|
+
return new AggregateProperty('sum', undefined, relationshipOrField);
|
|
81
|
+
}
|
|
82
|
+
export function min(relationshipOrField, field) {
|
|
83
|
+
if (field !== undefined) {
|
|
84
|
+
return new AggregateProperty('min', relationshipOrField, field);
|
|
85
|
+
}
|
|
86
|
+
return new AggregateProperty('min', undefined, relationshipOrField);
|
|
87
|
+
}
|
|
88
|
+
export function max(relationshipOrField, field) {
|
|
89
|
+
if (field !== undefined) {
|
|
90
|
+
return new AggregateProperty('max', relationshipOrField, field);
|
|
91
|
+
}
|
|
92
|
+
return new AggregateProperty('max', undefined, relationshipOrField);
|
|
93
|
+
}
|
package/dist/attr.d.ts
ADDED
package/dist/attr.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import ModelProperty from './model-property.js';
|
|
2
|
+
export default function attr(type, defaultValue) {
|
|
3
|
+
const modelProp = new ModelProperty(type, defaultValue);
|
|
4
|
+
return new Proxy(modelProp, {
|
|
5
|
+
get(target, prop, receiver) {
|
|
6
|
+
if (prop === 'valueOf' || prop === 'toString') {
|
|
7
|
+
return () => target.value;
|
|
8
|
+
}
|
|
9
|
+
if (prop in target) {
|
|
10
|
+
return Reflect.get(target, prop, receiver);
|
|
11
|
+
}
|
|
12
|
+
return target.value;
|
|
13
|
+
},
|
|
14
|
+
set(target, prop, value, receiver) {
|
|
15
|
+
if (prop === 'value') {
|
|
16
|
+
target.value = value;
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
return Reflect.set(target, prop, value, receiver);
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { SourceRecord } from './types/orm-types.js';
|
|
2
|
+
interface BelongsToOptions {
|
|
3
|
+
_relationshipKey?: string;
|
|
4
|
+
[key: string]: unknown;
|
|
5
|
+
}
|
|
6
|
+
type RelationshipHandler = ((sourceRecord: SourceRecord, rawData: unknown, options: BelongsToOptions) => unknown) & {
|
|
7
|
+
__relatedModelName: string;
|
|
8
|
+
__relationshipType: 'belongsTo';
|
|
9
|
+
};
|
|
10
|
+
export default function belongsTo(modelName: string): RelationshipHandler;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { createRecord, store } from '@stonyx/orm';
|
|
2
|
+
import { getRelationships, getHasManyRegistry, getPendingRegistry, getPendingBelongsToRegistry } from './relationships.js';
|
|
3
|
+
function getOrSet(map, key, defaultValue) {
|
|
4
|
+
if (!map.has(key))
|
|
5
|
+
map.set(key, defaultValue);
|
|
6
|
+
return map.get(key);
|
|
7
|
+
}
|
|
8
|
+
export default function belongsTo(modelName) {
|
|
9
|
+
const hasManyRelationships = getHasManyRegistry();
|
|
10
|
+
const pendingHasManyQueue = getPendingRegistry();
|
|
11
|
+
const pendingBelongsToQueue = getPendingBelongsToRegistry();
|
|
12
|
+
const fn = (sourceRecord, rawData, options) => {
|
|
13
|
+
if (!rawData)
|
|
14
|
+
return null;
|
|
15
|
+
const { __name: sourceModelName } = sourceRecord.__model;
|
|
16
|
+
const relationshipId = sourceRecord.id;
|
|
17
|
+
const relationshipKey = options._relationshipKey;
|
|
18
|
+
const relationship = getRelationships('belongsTo', sourceModelName, modelName, relationshipId);
|
|
19
|
+
const modelStore = store.get(modelName);
|
|
20
|
+
// Try to get existing record
|
|
21
|
+
let output;
|
|
22
|
+
if (typeof rawData === 'object') {
|
|
23
|
+
output = createRecord(modelName, rawData, options);
|
|
24
|
+
}
|
|
25
|
+
else if (modelStore) {
|
|
26
|
+
output = 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
|
+
// Register pending belongsTo
|
|
32
|
+
const modelPendingMap = getOrSet(pendingBelongsToQueue, modelName, new Map());
|
|
33
|
+
const targetPendingArray = getOrSet(modelPendingMap, targetId, []);
|
|
34
|
+
targetPendingArray.push({
|
|
35
|
+
sourceRecord,
|
|
36
|
+
sourceModelName,
|
|
37
|
+
relationshipKey,
|
|
38
|
+
relationshipId
|
|
39
|
+
});
|
|
40
|
+
relationship.set(relationshipId, null);
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
relationship.set(relationshipId, output || {});
|
|
44
|
+
// Populate hasMany side if the relationship is defined
|
|
45
|
+
const outputRecord = typeof output === 'object' && output !== null && 'id' in output ? output : undefined;
|
|
46
|
+
const otherSide = outputRecord ? hasManyRelationships.get(modelName)?.get(sourceModelName)?.get(outputRecord.id) : undefined;
|
|
47
|
+
if (otherSide) {
|
|
48
|
+
otherSide.push(sourceRecord);
|
|
49
|
+
// Remove pending queue if it was just fulfilled
|
|
50
|
+
const pendingModelRelationships = pendingHasManyQueue.get(sourceModelName);
|
|
51
|
+
if (pendingModelRelationships)
|
|
52
|
+
pendingModelRelationships.delete(relationshipId);
|
|
53
|
+
}
|
|
54
|
+
return output;
|
|
55
|
+
};
|
|
56
|
+
Object.defineProperty(fn, '__relatedModelName', { value: modelName });
|
|
57
|
+
Object.defineProperty(fn, '__relationshipType', { value: 'belongsTo' });
|
|
58
|
+
return fn;
|
|
59
|
+
}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Standalone CLI for ORM database operations.
|
|
4
|
+
*
|
|
5
|
+
* Performs CRUD operations on the JSON database without requiring
|
|
6
|
+
* the full Stonyx bootstrap. Supports both file and directory modes.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* stonyx-orm create <collection> <json-data>
|
|
10
|
+
* stonyx-orm list <collection>
|
|
11
|
+
* stonyx-orm get <collection> <id>
|
|
12
|
+
* stonyx-orm delete <collection> <id>
|
|
13
|
+
*
|
|
14
|
+
* Configuration (environment variables):
|
|
15
|
+
* DB_MODE -- 'file' or 'directory' (default: 'directory')
|
|
16
|
+
* DB_PATH -- Path to db.json (default: 'db.json')
|
|
17
|
+
* DB_DIRECTORY -- Directory name for collection files (default: 'db')
|
|
18
|
+
*
|
|
19
|
+
* Configuration (CLI flag):
|
|
20
|
+
* --config <path> -- Path to a JSON config file with { mode, dbPath, directory }
|
|
21
|
+
*/
|
|
22
|
+
export {};
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Standalone CLI for ORM database operations.
|
|
4
|
+
*
|
|
5
|
+
* Performs CRUD operations on the JSON database without requiring
|
|
6
|
+
* the full Stonyx bootstrap. Supports both file and directory modes.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* stonyx-orm create <collection> <json-data>
|
|
10
|
+
* stonyx-orm list <collection>
|
|
11
|
+
* stonyx-orm get <collection> <id>
|
|
12
|
+
* stonyx-orm delete <collection> <id>
|
|
13
|
+
*
|
|
14
|
+
* Configuration (environment variables):
|
|
15
|
+
* DB_MODE -- 'file' or 'directory' (default: 'directory')
|
|
16
|
+
* DB_PATH -- Path to db.json (default: 'db.json')
|
|
17
|
+
* DB_DIRECTORY -- Directory name for collection files (default: 'db')
|
|
18
|
+
*
|
|
19
|
+
* Configuration (CLI flag):
|
|
20
|
+
* --config <path> -- Path to a JSON config file with { mode, dbPath, directory }
|
|
21
|
+
*/
|
|
22
|
+
import StandaloneDB from './standalone-db.js';
|
|
23
|
+
import fs from 'fs/promises';
|
|
24
|
+
const USAGE = `Usage: stonyx-orm <command> [options]
|
|
25
|
+
|
|
26
|
+
Commands:
|
|
27
|
+
create <collection> <json-data> Create a record
|
|
28
|
+
list <collection> List all records
|
|
29
|
+
get <collection> <id> Get a record by ID
|
|
30
|
+
delete <collection> <id> Delete a record by ID
|
|
31
|
+
|
|
32
|
+
Options:
|
|
33
|
+
--config <path> Path to JSON config file
|
|
34
|
+
--help Show this help message
|
|
35
|
+
|
|
36
|
+
Environment variables:
|
|
37
|
+
DB_MODE 'file' or 'directory' (default: 'directory')
|
|
38
|
+
DB_PATH Path to db.json (default: 'db.json')
|
|
39
|
+
DB_DIRECTORY Directory name for collection files (default: 'db')`;
|
|
40
|
+
async function loadConfig(args) {
|
|
41
|
+
const config = {};
|
|
42
|
+
// Check for --config flag
|
|
43
|
+
const configIndex = args.indexOf('--config');
|
|
44
|
+
if (configIndex !== -1 && args[configIndex + 1]) {
|
|
45
|
+
const configPath = args[configIndex + 1];
|
|
46
|
+
try {
|
|
47
|
+
const content = await fs.readFile(configPath, 'utf-8');
|
|
48
|
+
Object.assign(config, JSON.parse(content));
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
console.error(`Error reading config file '${configPath}': ${err instanceof Error ? err.message : String(err)}`);
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
// Remove --config and its value from args
|
|
55
|
+
args.splice(configIndex, 2);
|
|
56
|
+
}
|
|
57
|
+
// Environment variables override config file, config file overrides defaults
|
|
58
|
+
return {
|
|
59
|
+
mode: (process.env.DB_MODE || config.mode || 'directory'),
|
|
60
|
+
dbPath: process.env.DB_PATH || config.dbPath || 'db.json',
|
|
61
|
+
directory: process.env.DB_DIRECTORY || config.directory || 'db',
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function parseArgs(argv) {
|
|
65
|
+
// Strip node binary and script path
|
|
66
|
+
const args = argv.slice(2);
|
|
67
|
+
if (args.includes('--help') || args.includes('-h') || args.length === 0) {
|
|
68
|
+
console.log(USAGE);
|
|
69
|
+
process.exit(0);
|
|
70
|
+
}
|
|
71
|
+
return args;
|
|
72
|
+
}
|
|
73
|
+
async function run() {
|
|
74
|
+
const args = parseArgs(process.argv);
|
|
75
|
+
const config = await loadConfig(args);
|
|
76
|
+
const db = new StandaloneDB(config);
|
|
77
|
+
const [command, collection, ...rest] = args;
|
|
78
|
+
if (!command) {
|
|
79
|
+
console.error('Error: No command specified.\n');
|
|
80
|
+
console.log(USAGE);
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
if (!collection && command !== '--help') {
|
|
84
|
+
console.error(`Error: No collection specified for '${command}' command.\n`);
|
|
85
|
+
console.log(USAGE);
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
switch (command) {
|
|
90
|
+
case 'list': {
|
|
91
|
+
const records = await db.list(collection);
|
|
92
|
+
console.log(JSON.stringify(records, null, 2));
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
case 'get': {
|
|
96
|
+
const id = rest[0];
|
|
97
|
+
if (!id) {
|
|
98
|
+
console.error("Error: 'get' command requires an <id> argument.");
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
const record = await db.get(collection, id);
|
|
102
|
+
if (!record) {
|
|
103
|
+
console.error(`Record with id '${id}' not found in '${collection}'.`);
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
console.log(JSON.stringify(record, null, 2));
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
case 'create': {
|
|
110
|
+
const jsonStr = rest.join(' ');
|
|
111
|
+
if (!jsonStr) {
|
|
112
|
+
console.error("Error: 'create' command requires <json-data> argument.");
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
let data;
|
|
116
|
+
try {
|
|
117
|
+
data = JSON.parse(jsonStr);
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
console.error(`Error: Invalid JSON data: ${jsonStr}`);
|
|
121
|
+
process.exit(1);
|
|
122
|
+
}
|
|
123
|
+
const created = await db.create(collection, data);
|
|
124
|
+
console.log(JSON.stringify(created, null, 2));
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
case 'delete': {
|
|
128
|
+
const deleteId = rest[0];
|
|
129
|
+
if (!deleteId) {
|
|
130
|
+
console.error("Error: 'delete' command requires an <id> argument.");
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
|
133
|
+
const removed = await db.delete(collection, deleteId);
|
|
134
|
+
console.log(JSON.stringify(removed, null, 2));
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
default:
|
|
138
|
+
console.error(`Error: Unknown command '${command}'.\n`);
|
|
139
|
+
console.log(USAGE);
|
|
140
|
+
process.exit(1);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
catch (err) {
|
|
144
|
+
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
145
|
+
process.exit(1);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
run();
|