@stonyx/orm 0.3.2-alpha.2 → 0.3.2-alpha.21

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 +35 -2
  2. package/config/environment.js +8 -0
  3. package/dist/commands.js +34 -0
  4. package/dist/dynamodb/connection.d.ts +30 -0
  5. package/dist/dynamodb/connection.js +28 -0
  6. package/dist/dynamodb/dynamodb-db.d.ts +131 -0
  7. package/dist/dynamodb/dynamodb-db.js +583 -0
  8. package/dist/dynamodb/operation-builder.d.ts +76 -0
  9. package/dist/dynamodb/operation-builder.js +109 -0
  10. package/dist/dynamodb/type-map.d.ts +31 -0
  11. package/dist/dynamodb/type-map.js +48 -0
  12. package/dist/index.d.ts +1 -0
  13. package/dist/index.js +4 -4
  14. package/dist/main.d.ts +12 -12
  15. package/dist/main.js +28 -42
  16. package/dist/manage-record.d.ts +1 -0
  17. package/dist/manage-record.js +66 -4
  18. package/dist/mysql/mysql-db.d.ts +1 -0
  19. package/dist/mysql/mysql-db.js +4 -2
  20. package/dist/orm-request.js +4 -4
  21. package/dist/postgres/connection.d.ts +1 -0
  22. package/dist/postgres/connection.js +8 -6
  23. package/dist/postgres/postgres-db.d.ts +1 -0
  24. package/dist/postgres/postgres-db.js +20 -8
  25. package/dist/relationships.js +1 -1
  26. package/dist/serializer.js +27 -2
  27. package/dist/store.d.ts +10 -0
  28. package/dist/store.js +71 -1
  29. package/dist/types/orm-types.d.ts +8 -0
  30. package/package.json +17 -7
  31. package/src/commands.ts +43 -0
  32. package/src/dynamodb/connection.ts +49 -0
  33. package/src/dynamodb/dynamodb-db.ts +797 -0
  34. package/src/dynamodb/operation-builder.ts +188 -0
  35. package/src/dynamodb/type-map.ts +54 -0
  36. package/src/index.ts +5 -4
  37. package/src/main.ts +36 -50
  38. package/src/manage-record.ts +72 -4
  39. package/src/mysql/mysql-db.ts +5 -2
  40. package/src/orm-request.ts +4 -4
  41. package/src/postgres/connection.ts +10 -6
  42. package/src/postgres/postgres-db.ts +19 -8
  43. package/src/relationships.ts +1 -1
  44. package/src/serializer.ts +27 -2
  45. package/src/store.ts +75 -1
  46. package/src/types/orm-types.ts +9 -0
  47. package/src/types/stonyx.d.ts +7 -1
@@ -51,4 +51,4 @@ export function getPendingBelongsToRegistry(): PendingBelongsToMap {
51
51
  return relationships.get('pendingBelongsTo') as PendingBelongsToMap;
52
52
  }
53
53
 
54
- export const TYPES: string[] = ['global', 'hasMany', 'belongsTo', 'pending'];
54
+ export const TYPES: string[] = ['global', 'hasMany', 'belongsTo', 'pending', 'pendingBelongsTo'];
package/src/serializer.ts CHANGED
@@ -94,8 +94,33 @@ 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
+ }
99
124
 
100
125
  continue;
101
126
  }
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 {
@@ -176,11 +176,61 @@ export default class Store {
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) {
181
+ Orm.instance.sqlDb.persist('delete', key, { recordId: id }, {}).catch((err: unknown) => {
182
+ Orm.instance.emitPersistError({
183
+ operation: 'delete',
184
+ modelName: key,
185
+ recordId: id,
186
+ error: err instanceof Error ? err : new Error(String(err)),
187
+ });
188
+ });
189
+ }
190
+
179
191
  if (id) return this.unloadRecord(key, id);
180
192
 
181
193
  this.unloadAllRecords(key);
182
194
  }
183
195
 
196
+ /**
197
+ * Evict a record from the store with full relationship registry cleanup,
198
+ * WITHOUT calling record.clean(). This preserves the caller's reference
199
+ * to the returned record (used by memory:false post-persist eviction).
200
+ *
201
+ * @param registryId - The ID used when the record's relationships were
202
+ * registered. For SQL models with pending IDs, this is the original
203
+ * negative pending ID (before the adapter re-keyed to the real DB ID).
204
+ */
205
+ evictRecord(modelName: string, id: unknown, registryId?: unknown): void {
206
+ const modelStore = this.data.get(modelName);
207
+ if (!modelStore) return;
208
+
209
+ if (typeof id !== 'string' && typeof id !== 'number') return;
210
+ const raw = modelStore.get(id);
211
+ if (!raw || !isStoreRecord(raw)) return;
212
+
213
+ const visited = new Set([`${modelName}:${id}`]);
214
+
215
+ // Remove from hasMany arrays and nullify belongsTo references using current ID
216
+ // (the adapter updates record.id, so value-based matches need the current ID)
217
+ this._removeFromHasManyArrays(modelName, id, visited);
218
+ this._nullifyBelongsToReferences(modelName, id, visited);
219
+
220
+ // Clean up relationship registry entries using the registry key
221
+ // (belongsTo/hasMany registries were keyed by the ID at registration time,
222
+ // which may differ from the current ID if SQL persist re-keyed the record)
223
+ const cleanupId = registryId ?? id;
224
+ this._cleanupRelationshipRegistries(modelName, cleanupId);
225
+
226
+ // If registryId differs from id, also clean with current id as safety net
227
+ if (registryId !== undefined && registryId !== id) {
228
+ this._cleanupRelationshipRegistries(modelName, id);
229
+ }
230
+
231
+ modelStore.delete(id);
232
+ }
233
+
184
234
  unloadRecord(model: string, id: unknown, options: UnloadOptions = {}): void {
185
235
  const modelStore = this.data.get(model);
186
236
 
@@ -296,6 +346,30 @@ export default class Store {
296
346
 
297
347
  const pendingMap = getPendingRegistry().get(modelName);
298
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
+ }
299
373
  }
300
374
 
301
375
  /**
@@ -48,6 +48,12 @@ export interface OrmRestServerConfig {
48
48
  metaRoute: boolean;
49
49
  }
50
50
 
51
+ export interface OrmDynamoDBConfig {
52
+ region?: string;
53
+ endpoint?: string;
54
+ [key: string]: unknown;
55
+ }
56
+
51
57
  export interface OrmSection {
52
58
  db: OrmDbConfig;
53
59
  paths: OrmPaths;
@@ -55,6 +61,9 @@ export interface OrmSection {
55
61
  mysql?: OrmMysqlConfig;
56
62
  postgres?: OrmPostgresConfig;
57
63
  timescale?: OrmPostgresConfig;
64
+ dynamodb?: OrmDynamoDBConfig;
65
+ logColor?: string;
66
+ logMethod?: string;
58
67
  [key: string]: unknown;
59
68
  }
60
69
 
@@ -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