@stonyx/orm 0.3.2-beta.16 → 0.3.2-beta.161

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 (47) hide show
  1. package/README.md +36 -2
  2. package/config/environment.js +99 -12
  3. package/dist/commands.js +34 -0
  4. package/dist/dynamodb/connection.d.ts +31 -0
  5. package/dist/dynamodb/connection.js +28 -0
  6. package/dist/dynamodb/dynamodb-db.d.ts +142 -0
  7. package/dist/dynamodb/dynamodb-db.js +596 -0
  8. package/dist/dynamodb/operation-builder.d.ts +76 -0
  9. package/dist/dynamodb/operation-builder.js +116 -0
  10. package/dist/dynamodb/type-map.d.ts +31 -0
  11. package/dist/dynamodb/type-map.js +48 -0
  12. package/dist/main.js +10 -0
  13. package/dist/manage-record.js +34 -3
  14. package/dist/mysql/connection.d.ts +1 -0
  15. package/dist/mysql/mysql-db.d.ts +8 -0
  16. package/dist/mysql/mysql-db.js +44 -10
  17. package/dist/orm-request.js +7 -6
  18. package/dist/postgres/connection.d.ts +1 -0
  19. package/dist/postgres/connection.js +8 -6
  20. package/dist/postgres/postgres-db.d.ts +8 -0
  21. package/dist/postgres/postgres-db.js +44 -10
  22. package/dist/record.js +7 -5
  23. package/dist/relationships.js +1 -1
  24. package/dist/serializer.js +38 -2
  25. package/dist/store.d.ts +13 -1
  26. package/dist/store.js +65 -6
  27. package/dist/types/orm-types.d.ts +11 -0
  28. package/package.json +16 -7
  29. package/src/commands.ts +43 -0
  30. package/src/dynamodb/connection.ts +50 -0
  31. package/src/dynamodb/dynamodb-db.ts +811 -0
  32. package/src/dynamodb/operation-builder.ts +202 -0
  33. package/src/dynamodb/type-map.ts +54 -0
  34. package/src/main.ts +10 -0
  35. package/src/manage-record.ts +41 -9
  36. package/src/mysql/connection.ts +1 -0
  37. package/src/mysql/mysql-db.ts +44 -12
  38. package/src/orm-request.ts +8 -5
  39. package/src/postgres/connection.ts +10 -6
  40. package/src/postgres/postgres-db.ts +44 -12
  41. package/src/record.ts +8 -5
  42. package/src/relationships.ts +1 -1
  43. package/src/serializer.ts +39 -2
  44. package/src/store.ts +68 -6
  45. package/src/types/orm-types.ts +12 -0
  46. package/src/types/stonyx.d.ts +7 -1
  47. package/config/environment.ts +0 -91
package/src/serializer.ts CHANGED
@@ -94,8 +94,45 @@ export default class Serializer {
94
94
  const handlerOptions = { ...options, _relationshipKey: key };
95
95
  const childRecord = handler(record, data, handlerOptions);
96
96
 
97
- rec[key] = childRecord;
98
- relatedRecords[key] = childRecord;
97
+ // hasMany relationships use a getter so format()/toJSON() always read
98
+ // the live registry array instead of a stale snapshot captured at
99
+ // serialization time. This is critical when child records are created
100
+ // in a later async frame — the belongsTo inverse wiring pushes into
101
+ // the shared registry array, and the getter ensures the parent sees it.
102
+ const isHasMany = (handler as { __relationshipType?: string }).__relationshipType === 'hasMany';
103
+
104
+ if (isHasMany) {
105
+ // `childRecord` IS the shared registry array — define a getter that
106
+ // always dereferences through the same array reference.
107
+ const registryArray = childRecord as unknown[];
108
+ Object.defineProperty(rec, key, {
109
+ enumerable: true,
110
+ configurable: true,
111
+ get: () => registryArray,
112
+ set(v: unknown) { relatedRecords[key] = v; }
113
+ });
114
+ Object.defineProperty(relatedRecords, key, {
115
+ enumerable: true,
116
+ configurable: true,
117
+ get: () => registryArray,
118
+ set(v: unknown) { Object.defineProperty(relatedRecords, key, { value: v, writable: true, enumerable: true, configurable: true }); }
119
+ });
120
+ } else {
121
+ rec[key] = childRecord;
122
+ relatedRecords[key] = childRecord;
123
+
124
+ // Preserve the raw FK value in __data when the belongsTo handler
125
+ // couldn't resolve the target (e.g., memory:false model not loaded).
126
+ // This allows adapters to read the FK from __data as a fallback
127
+ // when __relationships[key] is null. Only store when `data` is a
128
+ // truthy non-object — i.e., a raw FK string/number that the handler
129
+ // attempted but failed to resolve. When `data` is null/undefined
130
+ // (optional empty relationship) we intentionally skip to preserve
131
+ // the existing behavior of not populating __data for empty FKs.
132
+ if (childRecord === null && data && typeof data !== 'object') {
133
+ parsedData[key] = data;
134
+ }
135
+ }
99
136
 
100
137
  continue;
101
138
  }
package/src/store.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import Orm, { relationships } from '@stonyx/orm';
2
- import { TYPES, getHasManyRegistry, getBelongsToRegistry, getPendingRegistry } from './relationships.js';
2
+ import { TYPES, getHasManyRegistry, getBelongsToRegistry, getPendingRegistry, getPendingBelongsToRegistry } from './relationships.js';
3
3
  import ViewResolver from './view-resolver.js';
4
4
 
5
5
  interface UnloadOptions {
@@ -64,7 +64,7 @@ export default class Store {
64
64
  get(key: string): Map<number | string, unknown> | undefined;
65
65
  get(key: string, id: number | string): unknown;
66
66
  get(key: string, id?: number | string): Map<number | string, unknown> | unknown | undefined {
67
- if (!id) return this.data.get(key);
67
+ if (id === undefined) return this.data.get(key);
68
68
 
69
69
  return this.data.get(key)?.get(id);
70
70
  }
@@ -170,14 +170,15 @@ export default class Store {
170
170
  this.data.set(key, value);
171
171
  }
172
172
 
173
- remove(key: string, id?: number | string): void {
173
+ remove(key: string, id?: number | string, options?: { _skipAutoPersist?: boolean }): void {
174
174
  // Guard: read-only views cannot have records removed
175
175
  if (Orm.instance?.isView?.(key)) {
176
176
  throw new Error(`Cannot remove records from read-only view '${key}'`);
177
177
  }
178
178
 
179
- // Auto-persist delete to SQL
180
- if (id && Orm.instance?.sqlDb) {
179
+ // Auto-persist delete to SQL (fire-and-forget) — skipped when the
180
+ // request path handles persist itself to avoid double-delete.
181
+ if (id && Orm.instance?.sqlDb && !options?._skipAutoPersist) {
181
182
  Orm.instance.sqlDb.persist('delete', key, { recordId: id }, {}).catch((err: unknown) => {
182
183
  Orm.instance.emitPersistError({
183
184
  operation: 'delete',
@@ -193,6 +194,44 @@ export default class Store {
193
194
  this.unloadAllRecords(key);
194
195
  }
195
196
 
197
+ /**
198
+ * Evict a record from the store with full relationship registry cleanup.
199
+ * The caller retains its reference to the returned record, which is the
200
+ * contract memory:false post-persist eviction relies on.
201
+ *
202
+ * @param registryId - The ID used when the record's relationships were
203
+ * registered. For SQL models with pending IDs, this is the original
204
+ * negative pending ID (before the adapter re-keyed to the real DB ID).
205
+ */
206
+ evictRecord(modelName: string, id: unknown, registryId?: unknown): void {
207
+ const modelStore = this.data.get(modelName);
208
+ if (!modelStore) return;
209
+
210
+ if (typeof id !== 'string' && typeof id !== 'number') return;
211
+ const raw = modelStore.get(id);
212
+ if (!raw || !isStoreRecord(raw)) return;
213
+
214
+ const visited = new Set([`${modelName}:${id}`]);
215
+
216
+ // Remove from hasMany arrays and nullify belongsTo references using current ID
217
+ // (the adapter updates record.id, so value-based matches need the current ID)
218
+ this._removeFromHasManyArrays(modelName, id, visited);
219
+ this._nullifyBelongsToReferences(modelName, id, visited);
220
+
221
+ // Clean up relationship registry entries using the registry key
222
+ // (belongsTo/hasMany registries were keyed by the ID at registration time,
223
+ // which may differ from the current ID if SQL persist re-keyed the record)
224
+ const cleanupId = registryId ?? id;
225
+ this._cleanupRelationshipRegistries(modelName, cleanupId);
226
+
227
+ // If registryId differs from id, also clean with current id as safety net
228
+ if (registryId !== undefined && registryId !== id) {
229
+ this._cleanupRelationshipRegistries(modelName, id);
230
+ }
231
+
232
+ modelStore.delete(id);
233
+ }
234
+
196
235
  unloadRecord(model: string, id: unknown, options: UnloadOptions = {}): void {
197
236
  const modelStore = this.data.get(model);
198
237
 
@@ -219,7 +258,6 @@ export default class Store {
219
258
  this._removeFromHasManyArrays(modelName, recordId, visited);
220
259
  this._nullifyBelongsToReferences(modelName, recordId, visited);
221
260
  this._cleanupRelationshipRegistries(modelName, recordId);
222
- recordToUnload.clean();
223
261
 
224
262
  this.data.get(modelName)?.delete(recordId as string | number);
225
263
  }
@@ -308,6 +346,30 @@ export default class Store {
308
346
 
309
347
  const pendingMap = getPendingRegistry().get(modelName);
310
348
  if (pendingMap) pendingMap.delete(recordId);
349
+
350
+ // Clean pendingBelongsTo entries in both directions
351
+ const pendingBelongsToMap = getPendingBelongsToRegistry();
352
+ if (pendingBelongsToMap) {
353
+ // Direction 1: evicted record was the TARGET others were waiting for
354
+ const targetEntries = pendingBelongsToMap.get(modelName);
355
+ if (targetEntries) targetEntries.delete(recordId);
356
+
357
+ // Direction 2: evicted record was the SOURCE with unresolved forward-references
358
+ for (const [, targetIdMap] of pendingBelongsToMap) {
359
+ for (const [targetId, entries] of targetIdMap) {
360
+ if (!Array.isArray(entries)) continue;
361
+ const filtered = entries.filter((e: unknown) => {
362
+ const entry = e as { sourceModelName?: string; relationshipId?: unknown };
363
+ return !(entry.sourceModelName === modelName && entry.relationshipId === recordId);
364
+ });
365
+ if (filtered.length === 0) {
366
+ targetIdMap.delete(targetId);
367
+ } else if (filtered.length < entries.length) {
368
+ targetIdMap.set(targetId, filtered);
369
+ }
370
+ }
371
+ }
372
+ }
311
373
  }
312
374
 
313
375
  /**
@@ -18,6 +18,7 @@ export interface OrmMysqlConfig {
18
18
  connectionLimit?: number;
19
19
  migrationsDir?: string;
20
20
  migrationsTable?: string;
21
+ autoMigrate?: boolean;
21
22
  [key: string]: unknown;
22
23
  }
23
24
 
@@ -30,6 +31,7 @@ export interface OrmPostgresConfig {
30
31
  connectionLimit?: number;
31
32
  migrationsDir?: string;
32
33
  migrationsTable?: string;
34
+ autoMigrate?: boolean;
33
35
  [key: string]: unknown;
34
36
  }
35
37
 
@@ -48,6 +50,13 @@ export interface OrmRestServerConfig {
48
50
  metaRoute: boolean;
49
51
  }
50
52
 
53
+ export interface OrmDynamoDBConfig {
54
+ region?: string;
55
+ endpoint?: string;
56
+ tablePrefix?: string;
57
+ [key: string]: unknown;
58
+ }
59
+
51
60
  export interface OrmSection {
52
61
  db: OrmDbConfig;
53
62
  paths: OrmPaths;
@@ -55,6 +64,9 @@ export interface OrmSection {
55
64
  mysql?: OrmMysqlConfig;
56
65
  postgres?: OrmPostgresConfig;
57
66
  timescale?: OrmPostgresConfig;
67
+ dynamodb?: OrmDynamoDBConfig;
68
+ logColor?: string;
69
+ logMethod?: string;
58
70
  [key: string]: unknown;
59
71
  }
60
72
 
@@ -5,7 +5,13 @@ declare module 'stonyx/config' {
5
5
  }
6
6
 
7
7
  declare module 'stonyx/log' {
8
- const log: Record<string, ((...args: unknown[]) => void) | undefined>;
8
+ interface Log {
9
+ db(message: string): void;
10
+ error(message: string, ...args: unknown[]): void;
11
+ defineType(type: string, setting: string, options?: Record<string, unknown> | null): void;
12
+ [key: string]: ((...args: unknown[]) => void) | undefined;
13
+ }
14
+ const log: Log;
9
15
  export default log;
10
16
  }
11
17
 
@@ -1,91 +0,0 @@
1
- const {
2
- ORM_ACCESS_PATH,
3
- ORM_MODEL_PATH,
4
- ORM_REST_ROUTE,
5
- ORM_SERIALIZER_PATH,
6
- ORM_TRANSFORM_PATH,
7
- ORM_VIEW_PATH,
8
- ORM_USE_REST_SERVER,
9
- DB_AUTO_SAVE,
10
- DB_FILE,
11
- DB_MODE,
12
- DB_DIRECTORY,
13
- DB_SCHEMA_PATH,
14
- DB_SAVE_INTERVAL,
15
- MYSQL_HOST,
16
- MYSQL_PORT,
17
- MYSQL_USER,
18
- MYSQL_PASSWORD,
19
- MYSQL_DATABASE,
20
- MYSQL_CONNECTION_LIMIT,
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,
36
- } = process.env;
37
-
38
- export default {
39
- logColor: 'white',
40
- logMethod: 'db',
41
-
42
- db: {
43
- autosave: DB_AUTO_SAVE ?? 'false', // 'true' (cron interval), 'false' (disabled), 'onUpdate' (save after each write op)
44
- file: DB_FILE ?? 'db.json',
45
- mode: DB_MODE ?? 'file', // 'file' (single db.json) or 'directory' (one file per collection)
46
- directory: DB_DIRECTORY ?? 'db', // directory name for collection files when mode is 'directory'
47
- saveInterval: DB_SAVE_INTERVAL ?? 60 * 60, // 1 hour
48
- schema: DB_SCHEMA_PATH ?? './config/db-schema.js'
49
- },
50
- paths: {
51
- access: ORM_ACCESS_PATH ?? './access', // Optional for restServer access hooks
52
- model: ORM_MODEL_PATH ?? './models',
53
- serializer: ORM_SERIALIZER_PATH ?? './serializers',
54
- transform: ORM_TRANSFORM_PATH ?? './transforms',
55
- view: ORM_VIEW_PATH ?? './views'
56
- },
57
- mysql: MYSQL_HOST ? {
58
- host: MYSQL_HOST ?? 'localhost',
59
- port: parseInt(MYSQL_PORT ?? '3306'),
60
- user: MYSQL_USER ?? 'root',
61
- password: MYSQL_PASSWORD ?? '',
62
- database: MYSQL_DATABASE ?? 'stonyx',
63
- connectionLimit: parseInt(MYSQL_CONNECTION_LIMIT ?? '10'),
64
- migrationsDir: MYSQL_MIGRATIONS_DIR ?? 'migrations',
65
- migrationsTable: '__migrations',
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,
87
- restServer: {
88
- enabled: ORM_USE_REST_SERVER ?? 'true', // Whether to load restServer for automatic route setup or
89
- route: ORM_REST_ROUTE ?? '/',
90
- }
91
- }