@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.
Files changed (171) hide show
  1. package/README.md +64 -6
  2. package/config/environment.js +37 -1
  3. package/dist/aggregates.d.ts +21 -0
  4. package/dist/aggregates.js +93 -0
  5. package/dist/attr.d.ts +2 -0
  6. package/dist/attr.js +22 -0
  7. package/dist/belongs-to.d.ts +11 -0
  8. package/dist/belongs-to.js +59 -0
  9. package/dist/cli.d.ts +22 -0
  10. package/dist/cli.js +148 -0
  11. package/dist/commands.d.ts +7 -0
  12. package/dist/commands.js +146 -0
  13. package/dist/db.d.ts +21 -0
  14. package/dist/db.js +180 -0
  15. package/dist/exports/db.d.ts +7 -0
  16. package/{src → dist}/exports/db.js +2 -4
  17. package/dist/has-many.d.ts +11 -0
  18. package/dist/has-many.js +58 -0
  19. package/dist/hooks.d.ts +62 -0
  20. package/dist/hooks.js +110 -0
  21. package/dist/index.d.ts +14 -0
  22. package/dist/index.js +34 -0
  23. package/dist/main.d.ts +46 -0
  24. package/dist/main.js +181 -0
  25. package/dist/manage-record.d.ts +13 -0
  26. package/dist/manage-record.js +123 -0
  27. package/dist/meta-request.d.ts +6 -0
  28. package/dist/meta-request.js +52 -0
  29. package/dist/migrate.d.ts +2 -0
  30. package/dist/migrate.js +57 -0
  31. package/dist/model-property.d.ts +9 -0
  32. package/dist/model-property.js +29 -0
  33. package/dist/model.d.ts +15 -0
  34. package/dist/model.js +18 -0
  35. package/dist/mysql/connection.d.ts +14 -0
  36. package/dist/mysql/connection.js +24 -0
  37. package/dist/mysql/migration-generator.d.ts +45 -0
  38. package/dist/mysql/migration-generator.js +254 -0
  39. package/dist/mysql/migration-runner.d.ts +12 -0
  40. package/dist/mysql/migration-runner.js +88 -0
  41. package/dist/mysql/mysql-db.d.ts +100 -0
  42. package/dist/mysql/mysql-db.js +425 -0
  43. package/dist/mysql/query-builder.d.ts +10 -0
  44. package/dist/mysql/query-builder.js +44 -0
  45. package/dist/mysql/schema-introspector.d.ts +19 -0
  46. package/dist/mysql/schema-introspector.js +291 -0
  47. package/dist/mysql/type-map.d.ts +21 -0
  48. package/dist/mysql/type-map.js +36 -0
  49. package/dist/orm-request.d.ts +38 -0
  50. package/dist/orm-request.js +474 -0
  51. package/dist/plural-registry.d.ts +4 -0
  52. package/dist/plural-registry.js +9 -0
  53. package/dist/postgres/connection.d.ts +15 -0
  54. package/dist/postgres/connection.js +32 -0
  55. package/dist/postgres/migration-generator.d.ts +45 -0
  56. package/dist/postgres/migration-generator.js +261 -0
  57. package/dist/postgres/migration-runner.d.ts +10 -0
  58. package/dist/postgres/migration-runner.js +87 -0
  59. package/dist/postgres/postgres-db.d.ts +119 -0
  60. package/dist/postgres/postgres-db.js +477 -0
  61. package/dist/postgres/query-builder.d.ts +27 -0
  62. package/dist/postgres/query-builder.js +98 -0
  63. package/dist/postgres/schema-introspector.d.ts +29 -0
  64. package/dist/postgres/schema-introspector.js +314 -0
  65. package/dist/postgres/type-map.d.ts +23 -0
  66. package/dist/postgres/type-map.js +56 -0
  67. package/dist/record.d.ts +75 -0
  68. package/dist/record.js +129 -0
  69. package/dist/relationships.d.ts +10 -0
  70. package/dist/relationships.js +41 -0
  71. package/dist/serializer.d.ts +17 -0
  72. package/dist/serializer.js +136 -0
  73. package/dist/setup-rest-server.d.ts +1 -0
  74. package/dist/setup-rest-server.js +52 -0
  75. package/dist/standalone-db.d.ts +58 -0
  76. package/dist/standalone-db.js +142 -0
  77. package/dist/store.d.ts +62 -0
  78. package/dist/store.js +286 -0
  79. package/dist/timescale/query-builder.d.ts +43 -0
  80. package/dist/timescale/query-builder.js +115 -0
  81. package/dist/timescale/timescale-db.d.ts +45 -0
  82. package/dist/timescale/timescale-db.js +84 -0
  83. package/dist/transforms.d.ts +2 -0
  84. package/dist/transforms.js +17 -0
  85. package/dist/types/orm-types.d.ts +142 -0
  86. package/dist/types/orm-types.js +1 -0
  87. package/dist/utils.d.ts +7 -0
  88. package/dist/utils.js +17 -0
  89. package/dist/view-resolver.d.ts +8 -0
  90. package/dist/view-resolver.js +171 -0
  91. package/dist/view.d.ts +11 -0
  92. package/dist/view.js +18 -0
  93. package/package.json +57 -15
  94. package/src/aggregates.ts +109 -0
  95. package/src/{attr.js → attr.ts} +2 -2
  96. package/src/belongs-to.ts +90 -0
  97. package/src/cli.ts +183 -0
  98. package/src/{commands.js → commands.ts} +179 -170
  99. package/src/{db.js → db.ts} +55 -29
  100. package/src/exports/db.ts +7 -0
  101. package/src/has-many.ts +92 -0
  102. package/src/{hooks.js → hooks.ts} +41 -27
  103. package/src/{index.js → index.ts} +11 -2
  104. package/src/main.ts +229 -0
  105. package/src/manage-record.ts +161 -0
  106. package/src/{meta-request.js → meta-request.ts} +17 -14
  107. package/src/{migrate.js → migrate.ts} +9 -9
  108. package/src/model-property.ts +35 -0
  109. package/src/model.ts +21 -0
  110. package/src/mysql/{connection.js → connection.ts} +43 -28
  111. package/src/mysql/migration-generator.ts +337 -0
  112. package/src/mysql/{migration-runner.js → migration-runner.ts} +121 -110
  113. package/src/mysql/mysql-db.ts +543 -0
  114. package/src/mysql/{query-builder.js → query-builder.ts} +69 -64
  115. package/src/mysql/schema-introspector.ts +358 -0
  116. package/src/mysql/{type-map.js → type-map.ts} +42 -37
  117. package/src/{orm-request.js → orm-request.ts} +186 -108
  118. package/src/plural-registry.ts +12 -0
  119. package/src/postgres/connection.ts +48 -0
  120. package/src/postgres/migration-generator.ts +348 -0
  121. package/src/postgres/migration-runner.ts +115 -0
  122. package/src/postgres/postgres-db.ts +616 -0
  123. package/src/postgres/query-builder.ts +148 -0
  124. package/src/postgres/schema-introspector.ts +386 -0
  125. package/src/postgres/type-map.ts +61 -0
  126. package/src/record.ts +186 -0
  127. package/src/relationships.ts +54 -0
  128. package/src/serializer.ts +161 -0
  129. package/src/{setup-rest-server.js → setup-rest-server.ts} +18 -16
  130. package/src/standalone-db.ts +185 -0
  131. package/src/store.ts +373 -0
  132. package/src/timescale/query-builder.ts +174 -0
  133. package/src/timescale/timescale-db.ts +119 -0
  134. package/src/transforms.ts +20 -0
  135. package/src/types/mysql2.d.ts +49 -0
  136. package/src/types/orm-types.ts +146 -0
  137. package/src/types/pg.d.ts +32 -0
  138. package/src/types/stonyx-cron.d.ts +5 -0
  139. package/src/types/stonyx-events.d.ts +4 -0
  140. package/src/types/stonyx-rest-server.d.ts +16 -0
  141. package/src/types/stonyx-utils.d.ts +33 -0
  142. package/src/types/stonyx.d.ts +21 -0
  143. package/src/utils.ts +22 -0
  144. package/src/view-resolver.ts +211 -0
  145. package/src/view.ts +22 -0
  146. package/.claude/code-style-rules.md +0 -44
  147. package/.claude/hooks.md +0 -250
  148. package/.claude/index.md +0 -279
  149. package/.claude/usage-patterns.md +0 -217
  150. package/.github/workflows/ci.yml +0 -16
  151. package/.github/workflows/publish.yml +0 -51
  152. package/improvements.md +0 -139
  153. package/project-structure.md +0 -343
  154. package/src/belongs-to.js +0 -63
  155. package/src/has-many.js +0 -61
  156. package/src/main.js +0 -148
  157. package/src/manage-record.js +0 -118
  158. package/src/model-property.js +0 -29
  159. package/src/model.js +0 -9
  160. package/src/mysql/migration-generator.js +0 -188
  161. package/src/mysql/mysql-db.js +0 -320
  162. package/src/mysql/schema-introspector.js +0 -158
  163. package/src/record.js +0 -127
  164. package/src/relationships.js +0 -43
  165. package/src/serializer.js +0 -138
  166. package/src/store.js +0 -211
  167. package/src/transforms.js +0 -20
  168. package/src/utils.js +0 -12
  169. package/test-events-setup.js +0 -41
  170. package/test-hooks-manual.js +0 -54
  171. package/test-hooks-with-logging.js +0 -52
package/README.md CHANGED
@@ -1,3 +1,7 @@
1
+ [![CI](https://github.com/abofs/stonyx-orm/actions/workflows/ci.yml/badge.svg)](https://github.com/abofs/stonyx-orm/actions/workflows/ci.yml)
2
+ [![npm version](https://img.shields.io/npm/v/@stonyx/orm.svg)](https://www.npmjs.com/package/@stonyx/orm)
3
+ [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](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 from `record.__data` or the record itself, providing access to the raw data structure
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 in context.record.__data) {
483
- if (context.oldState[key] !== context.record.__data[key]) {
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 [key, newValue] of Object.entries(context.record.__data || context.record)) {
523
- if (context.oldState[key] !== newValue) {
524
- changes[key] = { from: context.oldState[key], to: newValue };
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
  }
@@ -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
@@ -0,0 +1,2 @@
1
+ import ModelProperty from './model-property.js';
2
+ export default function attr(type?: string, defaultValue?: unknown): ModelProperty;
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();
@@ -0,0 +1,7 @@
1
+ interface Command {
2
+ description: string;
3
+ bootstrap: boolean;
4
+ run: (args?: string[]) => Promise<void>;
5
+ }
6
+ declare const commands: Record<string, Command>;
7
+ export default commands;