@stonyx/orm 0.2.1-beta.84 → 0.2.1-beta.86

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.
@@ -42,7 +42,8 @@ export default function belongsTo(modelName) {
42
42
  }
43
43
  relationship.set(relationshipId, output || {});
44
44
  // Populate hasMany side if the relationship is defined
45
- const otherSide = hasManyRelationships.get(modelName)?.get(sourceModelName)?.get(output?.id);
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;
46
47
  if (otherSide) {
47
48
  otherSide.push(sourceRecord);
48
49
  // Remove pending queue if it was just fulfilled
package/dist/has-many.js CHANGED
@@ -36,8 +36,9 @@ export default function hasMany(modelName) {
36
36
  record = createRecord(modelName, elementData, options);
37
37
  }
38
38
  // Populate belongTo side if the relationship is defined
39
- const otherSide = getBelongsToRegistry()
40
- .get(modelName)?.get(sourceModelName)?.get(record.id);
39
+ const recordWithId = typeof record === 'object' && record !== null && 'id' in record ? record : undefined;
40
+ const otherSide = recordWithId ? getBelongsToRegistry()
41
+ .get(modelName)?.get(sourceModelName)?.get(recordWithId.id) : undefined;
41
42
  if (otherSide)
42
43
  Object.assign(otherSide, sourceRecord);
43
44
  return record;
package/dist/main.js CHANGED
@@ -66,7 +66,8 @@ export default class Orm {
66
66
  Orm.store.set(name, new Map());
67
67
  registerPluralName(name, exported);
68
68
  }
69
- return this[pluralize(lowerCaseType)][alias] = exported;
69
+ const collection = this[pluralize(lowerCaseType)];
70
+ return collection[alias] = exported;
70
71
  }, { ignoreAccessFailure: true, rawName: true, recursive: true, recursiveNaming: true });
71
72
  });
72
73
  // Wait for imports before db & rest server setup
@@ -22,8 +22,12 @@ export function createRecord(modelName, rawData = {}, userOptions = {}) {
22
22
  if (!modelStore)
23
23
  throw new Error(`Model store for '${modelName}' is not registered. Ensure the model is defined before creating records.`);
24
24
  assignRecordId(modelName, rawData);
25
- if (modelStore.has(rawData.id))
26
- return modelStore.get(rawData.id);
25
+ const existingRecord = modelStore.get(rawData.id);
26
+ if (existingRecord) {
27
+ // Update the existing record with new data so the last entry wins
28
+ updateRecord(existingRecord, rawData, { ...options, update: true });
29
+ return existingRecord;
30
+ }
27
31
  const recordClasses = orm.getRecordClasses(modelName);
28
32
  const modelClass = recordClasses.modelClass;
29
33
  const serializerClass = recordClasses.serializerClass;
@@ -123,7 +123,8 @@ export default class MysqlDB {
123
123
  const schema = schemas[modelName];
124
124
  const { sql, values } = this.deps.buildSelect(schema.table);
125
125
  try {
126
- const [rows] = await this.requirePool().execute(sql, values);
126
+ const result = await this.requirePool().execute(sql, values);
127
+ const rows = result[0];
127
128
  for (const row of rows) {
128
129
  const rawData = this._rowToRawData(row, schema);
129
130
  this.deps.createRecord(modelName, rawData, { isDbRecord: true, serialize: false, transform: false });
@@ -149,7 +150,8 @@ export default class MysqlDB {
149
150
  const schema = { table: viewSchema.viewName, columns: viewSchema.columns || {}, foreignKeys: viewSchema.foreignKeys || {} };
150
151
  const { sql, values } = this.deps.buildSelect(schema.table);
151
152
  try {
152
- const [rows] = await this.requirePool().execute(sql, values);
153
+ const result = await this.requirePool().execute(sql, values);
154
+ const rows = result[0];
153
155
  for (const row of rows) {
154
156
  const rawData = this._rowToRawData(row, schema);
155
157
  this.deps.createRecord(viewName, rawData, { isDbRecord: true, serialize: false, transform: false });
@@ -189,7 +191,8 @@ export default class MysqlDB {
189
191
  return undefined;
190
192
  const { sql, values } = this.deps.buildSelect(schema.table, { id });
191
193
  try {
192
- const [rows] = await this.requirePool().execute(sql, values);
194
+ const result = await this.requirePool().execute(sql, values);
195
+ const rows = result[0];
193
196
  if (rows.length === 0)
194
197
  return undefined;
195
198
  const rawData = this._rowToRawData(rows[0], schema);
@@ -223,7 +226,8 @@ export default class MysqlDB {
223
226
  return [];
224
227
  const { sql, values } = this.deps.buildSelect(schema.table, conditions);
225
228
  try {
226
- const [rows] = await this.requirePool().execute(sql, values);
229
+ const result = await this.requirePool().execute(sql, values);
230
+ const rows = result[0];
227
231
  const records = rows.map(row => {
228
232
  const rawData = this._rowToRawData(row, schema);
229
233
  return this.deps.createRecord(modelName, rawData, { isDbRecord: true, serialize: false, transform: false });
@@ -99,12 +99,14 @@ function traverseIncludePath(currentRecords, includePath, depth, seen, included)
99
99
  const type = relatedRecord.__model.__name;
100
100
  const id = relatedRecord.id;
101
101
  // Initialize Set for this type if needed
102
- if (!seen.has(type)) {
103
- seen.set(type, new Set());
102
+ let seenIds = seen.get(type);
103
+ if (!seenIds) {
104
+ seenIds = new Set();
105
+ seen.set(type, seenIds);
104
106
  }
105
107
  // Check if we've already seen this type+id combination
106
- if (!seen.get(type).has(id)) {
107
- seen.get(type).add(id);
108
+ if (!seenIds.has(id)) {
109
+ seenIds.add(id);
108
110
  included.push(relatedRecord);
109
111
  nextRecords.push(relatedRecord); // Prepare for next depth level
110
112
  }
@@ -150,9 +150,10 @@ export default class PostgresDB {
150
150
  this.deps.log.db(`Skipping memory load for view '${viewName}' (memory: false)`);
151
151
  continue;
152
152
  }
153
+ const sourceIdType = schemas[viewSchema.source]?.idType || 'number';
153
154
  const schema = {
154
155
  table: viewSchema.viewName,
155
- idType: 'number',
156
+ idType: sourceIdType,
156
157
  columns: viewSchema.columns || {},
157
158
  foreignKeys: (viewSchema.foreignKeys || {}),
158
159
  relationships: { belongsTo: {}, hasMany: {} },
@@ -194,9 +195,10 @@ export default class PostgresDB {
194
195
  const viewSchemas = this.deps.introspectViews();
195
196
  const viewSchema = viewSchemas[modelName];
196
197
  if (viewSchema) {
198
+ const sourceIdType = schemas[viewSchema.source]?.idType || 'number';
197
199
  schema = {
198
200
  table: viewSchema.viewName,
199
- idType: 'number',
201
+ idType: sourceIdType,
200
202
  columns: viewSchema.columns || {},
201
203
  foreignKeys: (viewSchema.foreignKeys || {}),
202
204
  relationships: { belongsTo: {}, hasMany: {} },
@@ -234,9 +236,10 @@ export default class PostgresDB {
234
236
  const viewSchemas = this.deps.introspectViews();
235
237
  const viewSchema = viewSchemas[modelName];
236
238
  if (viewSchema) {
239
+ const sourceIdType = schemas[viewSchema.source]?.idType || 'number';
237
240
  schema = {
238
241
  table: viewSchema.viewName,
239
- idType: 'number',
242
+ idType: sourceIdType,
240
243
  columns: viewSchema.columns || {},
241
244
  foreignKeys: (viewSchema.foreignKeys || {}),
242
245
  relationships: { belongsTo: {}, hasMany: {} },
package/dist/record.js CHANGED
@@ -39,9 +39,23 @@ export default class Record {
39
39
  const { __data: data } = this;
40
40
  const records = {};
41
41
  for (const [key, childRecord] of Object.entries(this.__relationships)) {
42
- records[key] = Array.isArray(childRecord)
43
- ? childRecord.map((r) => r.serialize())
44
- : childRecord?.serialize() ?? null;
42
+ if (Array.isArray(childRecord)) {
43
+ // Deduplicate by record ID — keep last occurrence (latest data wins)
44
+ const seen = new Set();
45
+ const unique = [];
46
+ for (let i = childRecord.length - 1; i >= 0; i--) {
47
+ const r = childRecord[i];
48
+ if (!seen.has(r.id)) {
49
+ seen.add(r.id);
50
+ unique.push(r);
51
+ }
52
+ }
53
+ unique.reverse();
54
+ records[key] = unique.map((r) => r.serialize());
55
+ }
56
+ else {
57
+ records[key] = childRecord?.serialize() ?? null;
58
+ }
45
59
  }
46
60
  return { ...data, ...records };
47
61
  }
@@ -1,7 +1,11 @@
1
1
  import { relationships } from '@stonyx/orm';
2
2
  // TODO: Refactor mapping to remove a level of iteration
3
3
  export function getRelationships(type, sourceModel, targetModel, relationshipId) {
4
- const allRelationships = relationships.get(type);
4
+ let allRelationships = relationships.get(type);
5
+ if (!allRelationships) {
6
+ allRelationships = new Map();
7
+ relationships.set(type, allRelationships);
8
+ }
5
9
  // create relationship map for this type of it doesn't already exist
6
10
  if (!allRelationships.has(sourceModel))
7
11
  allRelationships.set(sourceModel, new Map());
@@ -1,6 +1,12 @@
1
1
  import config from 'stonyx/config';
2
2
  import { get, makeArray } from '@stonyx/utils/object';
3
3
  const RESERVED_KEYS = ['__name'];
4
+ function isAggregateProperty(v) {
5
+ return typeof v === 'object' && v !== null && v.__kind === 'AggregateProperty';
6
+ }
7
+ function isModelProperty(v) {
8
+ return typeof v === 'object' && v !== null && v.__kind === 'ModelProperty';
9
+ }
4
10
  function searchQuery(query, array, key) {
5
11
  const result = makeArray(array).find((item) => {
6
12
  for (const [prop, value] of Object.entries(query)) {
@@ -78,13 +84,13 @@ export default class Serializer {
78
84
  continue;
79
85
  }
80
86
  // Aggregate property handling — use the rawData value, not the aggregate descriptor
81
- if (handler?.__kind === 'AggregateProperty') {
87
+ if (isAggregateProperty(handler)) {
82
88
  parsedData[key] = data;
83
89
  rec[key] = data;
84
90
  continue;
85
91
  }
86
92
  // Direct assignment handling
87
- if (handler?.__kind !== 'ModelProperty') {
93
+ if (!isModelProperty(handler)) {
88
94
  parsedData[key] = handler;
89
95
  rec[key] = handler;
90
96
  continue;
package/dist/store.js CHANGED
@@ -133,7 +133,7 @@ export default class Store {
133
133
  this._nullifyBelongsToReferences(modelName, recordId, visited);
134
134
  this._cleanupRelationshipRegistries(modelName, recordId);
135
135
  recordToUnload.clean();
136
- this.data.get(modelName).delete(recordId);
136
+ this.data.get(modelName)?.delete(recordId);
137
137
  }
138
138
  }
139
139
  unloadAllRecords(model, options = {}) {
@@ -19,6 +19,7 @@ export default class TimescaleDB extends PostgresDB {
19
19
  static extensions: string[];
20
20
  static configKey: string;
21
21
  constructor(deps?: Record<string, unknown>);
22
+ private get tsDeps();
22
23
  /**
23
24
  * Convert a table to a TimescaleDB hypertable.
24
25
  * Should be called after the table is created (e.g. after initial migration).
@@ -14,6 +14,9 @@ export default class TimescaleDB extends PostgresDB {
14
14
  buildEnableCompression,
15
15
  });
16
16
  }
17
+ get tsDeps() {
18
+ return this.deps;
19
+ }
17
20
  /**
18
21
  * Convert a table to a TimescaleDB hypertable.
19
22
  * Should be called after the table is created (e.g. after initial migration).
@@ -23,7 +26,7 @@ export default class TimescaleDB extends PostgresDB {
23
26
  const schema = schemas[modelName];
24
27
  if (!schema)
25
28
  throw new Error(`Model '${modelName}' not found`);
26
- const { sql } = this.deps.buildCreateHypertable(schema.table, timeColumn, options);
29
+ const { sql } = this.tsDeps.buildCreateHypertable(schema.table, timeColumn, options);
27
30
  await this.requirePool().query(sql);
28
31
  }
29
32
  /**
@@ -34,7 +37,7 @@ export default class TimescaleDB extends PostgresDB {
34
37
  const schema = schemas[modelName];
35
38
  if (!schema)
36
39
  return [];
37
- const { sql, values } = this.deps.buildTimeBucket(schema.table, timeColumn, bucketSize, options);
40
+ const { sql, values } = this.tsDeps.buildTimeBucket(schema.table, timeColumn, bucketSize, options);
38
41
  try {
39
42
  const result = await this.requirePool().query(sql, values);
40
43
  return result.rows;
@@ -53,7 +56,7 @@ export default class TimescaleDB extends PostgresDB {
53
56
  const schema = schemas[modelName];
54
57
  if (!schema)
55
58
  throw new Error(`Model '${modelName}' not found`);
56
- const { sql } = this.deps.buildContinuousAggregate(viewName, schema.table, timeColumn, bucketSize, aggregates, options);
59
+ const { sql } = this.tsDeps.buildContinuousAggregate(viewName, schema.table, timeColumn, bucketSize, aggregates, options);
57
60
  await this.requirePool().query(sql);
58
61
  }
59
62
  /**
@@ -64,7 +67,7 @@ export default class TimescaleDB extends PostgresDB {
64
67
  const schema = schemas[modelName];
65
68
  if (!schema)
66
69
  throw new Error(`Model '${modelName}' not found`);
67
- const { sql } = this.deps.buildEnableCompression(schema.table, options.segmentBy, options.orderBy);
70
+ const { sql } = this.tsDeps.buildEnableCompression(schema.table, options.segmentBy, options.orderBy);
68
71
  await this.requirePool().query(sql);
69
72
  }
70
73
  /**
@@ -75,7 +78,7 @@ export default class TimescaleDB extends PostgresDB {
75
78
  const schema = schemas[modelName];
76
79
  if (!schema)
77
80
  throw new Error(`Model '${modelName}' not found`);
78
- const { sql } = this.deps.buildCompressionPolicy(schema.table, compressAfter);
81
+ const { sql } = this.tsDeps.buildCompressionPolicy(schema.table, compressAfter);
79
82
  await this.requirePool().query(sql);
80
83
  }
81
84
  }
@@ -49,6 +49,8 @@ export default class ViewResolver {
49
49
  const rawData = { id: sourceRecord.id };
50
50
  // Compute aggregate fields from source record's relationships
51
51
  for (const [key, aggProp] of Object.entries(aggregateFields)) {
52
+ if (!aggProp.relationship)
53
+ continue;
52
54
  const relatedRecords = sourceRecord.__relationships?.[aggProp.relationship]
53
55
  || sourceRecord[aggProp.relationship];
54
56
  const relArray = Array.isArray(relatedRecords) ? relatedRecords : [];
@@ -112,6 +114,8 @@ export default class ViewResolver {
112
114
  }
113
115
  else {
114
116
  // Relationship aggregate — flatten related records across all group members
117
+ if (!aggProp.relationship)
118
+ continue;
115
119
  const allRelated = [];
116
120
  for (const record of groupRecords) {
117
121
  const relatedRecords = record.__relationships?.[aggProp.relationship]
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "stonyx-async",
5
5
  "stonyx-module"
6
6
  ],
7
- "version": "0.2.1-beta.84",
7
+ "version": "0.2.1-beta.86",
8
8
  "description": "",
9
9
  "main": "dist/index.js",
10
10
  "type": "module",
package/src/belongs-to.ts CHANGED
@@ -69,7 +69,8 @@ export default function belongsTo(modelName: string): RelationshipHandler {
69
69
  relationship.set(relationshipId, output || {});
70
70
 
71
71
  // Populate hasMany side if the relationship is defined
72
- const otherSide = hasManyRelationships.get(modelName)?.get(sourceModelName)?.get((output as SourceRecord)?.id) as unknown[] | undefined;
72
+ const outputRecord = typeof output === 'object' && output !== null && 'id' in output ? output as SourceRecord : undefined;
73
+ const otherSide = outputRecord ? hasManyRelationships.get(modelName)?.get(sourceModelName)?.get(outputRecord.id) as unknown[] | undefined : undefined;
73
74
 
74
75
  if (otherSide) {
75
76
  otherSide.push(sourceRecord);
package/src/has-many.ts CHANGED
@@ -44,7 +44,7 @@ export default function hasMany(modelName: string): RelationshipHandler {
44
44
  const modelStore = store.get(modelName);
45
45
  const pendingRelationshipQueue: PendingItem[] = [];
46
46
 
47
- const output: unknown[] = !rawData ? [] : (makeArray(rawData) as unknown[]).map((elementData: unknown) => {
47
+ const output: unknown[] = !rawData ? [] : makeArray(rawData).map((elementData: unknown) => {
48
48
  let record: unknown;
49
49
 
50
50
  if (typeof elementData !== 'object') {
@@ -66,8 +66,9 @@ export default function hasMany(modelName: string): RelationshipHandler {
66
66
  }
67
67
 
68
68
  // Populate belongTo side if the relationship is defined
69
- const otherSide = getBelongsToRegistry()
70
- .get(modelName)?.get(sourceModelName)?.get((record as SourceRecord).id);
69
+ const recordWithId = typeof record === 'object' && record !== null && 'id' in record ? record as SourceRecord : undefined;
70
+ const otherSide = recordWithId ? getBelongsToRegistry()
71
+ .get(modelName)?.get(sourceModelName)?.get(recordWithId.id) : undefined;
71
72
 
72
73
  if (otherSide) Object.assign(otherSide, sourceRecord);
73
74
 
package/src/main.ts CHANGED
@@ -98,7 +98,8 @@ export default class Orm {
98
98
  registerPluralName(name, exported as { pluralName?: string });
99
99
  }
100
100
 
101
- return (this as unknown as Record<string, Record<string, unknown>>)[pluralize(lowerCaseType)][alias] = exported;
101
+ const collection = this[pluralize(lowerCaseType) as keyof this] as Record<string, unknown>;
102
+ return collection[alias] = exported;
102
103
  }, { ignoreAccessFailure: true, rawName: true, recursive: true, recursiveNaming: true });
103
104
  });
104
105
 
@@ -43,7 +43,13 @@ export function createRecord(modelName: string, rawData: { [key: string]: unknow
43
43
  if (!modelStore) throw new Error(`Model store for '${modelName}' is not registered. Ensure the model is defined before creating records.`);
44
44
 
45
45
  assignRecordId(modelName, rawData);
46
- if (modelStore.has(rawData.id as number | string)) return modelStore.get(rawData.id as number | string)! as OrmRecord;
46
+ const existingRecord = modelStore.get(rawData.id as number | string);
47
+
48
+ if (existingRecord) {
49
+ // Update the existing record with new data so the last entry wins
50
+ updateRecord(existingRecord as OrmRecord, rawData, { ...options, update: true });
51
+ return existingRecord as OrmRecord;
52
+ }
47
53
 
48
54
  const recordClasses = orm.getRecordClasses(modelName);
49
55
  const modelClass = recordClasses.modelClass as (new (name: string) => { __name: string; [key: string]: unknown }) | undefined;
@@ -72,7 +72,7 @@ const defaultDeps: MysqlDBDeps = {
72
72
  introspectModels, introspectViews, getTopologicalOrder, schemasToSnapshot,
73
73
  loadLatestSnapshot, detectSchemaDrift,
74
74
  buildInsert, buildUpdate, buildDelete, buildSelect,
75
- createRecord, store: store as unknown as OrmStore, confirm, readFile, getPluralName,
75
+ createRecord, store: store as OrmStore, confirm, readFile, getPluralName,
76
76
  config, log, path
77
77
  };
78
78
 
@@ -199,7 +199,8 @@ export default class MysqlDB {
199
199
  const { sql, values } = this.deps.buildSelect(schema.table);
200
200
 
201
201
  try {
202
- const [rows] = await this.requirePool().execute(sql, values) as [Record<string, unknown>[], unknown];
202
+ const result = await this.requirePool().execute(sql, values);
203
+ const rows = result[0] as Record<string, unknown>[];
203
204
 
204
205
  for (const row of rows) {
205
206
  const rawData = this._rowToRawData(row, schema);
@@ -230,7 +231,8 @@ export default class MysqlDB {
230
231
  const { sql, values } = this.deps.buildSelect(schema.table);
231
232
 
232
233
  try {
233
- const [rows] = await this.requirePool().execute(sql, values) as [Record<string, unknown>[], unknown];
234
+ const result = await this.requirePool().execute(sql, values);
235
+ const rows = result[0] as Record<string, unknown>[];
234
236
 
235
237
  for (const row of rows) {
236
238
  const rawData = this._rowToRawData(row, schema);
@@ -275,7 +277,8 @@ export default class MysqlDB {
275
277
  const { sql, values } = this.deps.buildSelect(schema.table, { id });
276
278
 
277
279
  try {
278
- const [rows] = await this.requirePool().execute(sql, values) as [Record<string, unknown>[], unknown];
280
+ const result = await this.requirePool().execute(sql, values);
281
+ const rows = result[0] as Record<string, unknown>[];
279
282
 
280
283
  if (rows.length === 0) return undefined;
281
284
 
@@ -314,7 +317,8 @@ export default class MysqlDB {
314
317
  const { sql, values } = this.deps.buildSelect(schema.table, conditions);
315
318
 
316
319
  try {
317
- const [rows] = await this.requirePool().execute(sql, values) as [Record<string, unknown>[], unknown];
320
+ const result = await this.requirePool().execute(sql, values);
321
+ const rows = result[0] as Record<string, unknown>[];
318
322
 
319
323
  const records = rows.map(row => {
320
324
  const rawData = this._rowToRawData(row, schema!);
@@ -167,13 +167,15 @@ function traverseIncludePath(
167
167
  const id = relatedRecord.id as string | number;
168
168
 
169
169
  // Initialize Set for this type if needed
170
- if (!seen.has(type)) {
171
- seen.set(type, new Set());
170
+ let seenIds = seen.get(type);
171
+ if (!seenIds) {
172
+ seenIds = new Set();
173
+ seen.set(type, seenIds);
172
174
  }
173
175
 
174
176
  // Check if we've already seen this type+id combination
175
- if (!seen.get(type)!.has(id)) {
176
- seen.get(type)!.add(id);
177
+ if (!seenIds.has(id)) {
178
+ seenIds.add(id);
177
179
  included.push(relatedRecord);
178
180
  nextRecords.push(relatedRecord); // Prepare for next depth level
179
181
  } else if (depth < includePath.length - 1) {
@@ -235,9 +235,10 @@ export default class PostgresDB {
235
235
  continue;
236
236
  }
237
237
 
238
+ const sourceIdType = schemas[viewSchema.source]?.idType || 'number';
238
239
  const schema: ModelSchema = {
239
240
  table: viewSchema.viewName,
240
- idType: 'number',
241
+ idType: sourceIdType,
241
242
  columns: viewSchema.columns || {},
242
243
  foreignKeys: (viewSchema.foreignKeys || {}) as Record<string, ForeignKeyDef>,
243
244
  relationships: { belongsTo: {}, hasMany: {} },
@@ -283,9 +284,10 @@ export default class PostgresDB {
283
284
  const viewSchemas = this.deps.introspectViews();
284
285
  const viewSchema = viewSchemas[modelName];
285
286
  if (viewSchema) {
287
+ const sourceIdType = schemas[viewSchema.source]?.idType || 'number';
286
288
  schema = {
287
289
  table: viewSchema.viewName,
288
- idType: 'number',
290
+ idType: sourceIdType,
289
291
  columns: viewSchema.columns || {},
290
292
  foreignKeys: (viewSchema.foreignKeys || {}) as Record<string, ForeignKeyDef>,
291
293
  relationships: { belongsTo: {}, hasMany: {} },
@@ -328,9 +330,10 @@ export default class PostgresDB {
328
330
  const viewSchemas = this.deps.introspectViews();
329
331
  const viewSchema = viewSchemas[modelName];
330
332
  if (viewSchema) {
333
+ const sourceIdType = schemas[viewSchema.source]?.idType || 'number';
331
334
  schema = {
332
335
  table: viewSchema.viewName,
333
- idType: 'number',
336
+ idType: sourceIdType,
334
337
  columns: viewSchema.columns || {},
335
338
  foreignKeys: (viewSchema.foreignKeys || {}) as Record<string, ForeignKeyDef>,
336
339
  relationships: { belongsTo: {}, hasMany: {} },
@@ -423,7 +426,7 @@ export default class PostgresDB {
423
426
  * @private
424
427
  */
425
428
  private _evictIfNotMemory(modelName: string, record: OrmRecord): void {
426
- const storeRef = this.deps.store as unknown as {
429
+ const storeRef = this.deps.store as {
427
430
  _memoryResolver?: (name: string) => boolean;
428
431
  get?: (name: string) => Map<unknown, unknown> | undefined;
429
432
  data?: { get(name: string): Map<unknown, unknown> | undefined };
package/src/record.ts CHANGED
@@ -86,9 +86,24 @@ export default class Record {
86
86
  const records: { [key: string]: unknown } = {};
87
87
 
88
88
  for (const [key, childRecord] of Object.entries(this.__relationships)) {
89
- records[key] = Array.isArray(childRecord)
90
- ? childRecord.map((r: Record) => r.serialize())
91
- : (childRecord as Record)?.serialize() ?? null;
89
+ if (Array.isArray(childRecord)) {
90
+ // Deduplicate by record ID — keep last occurrence (latest data wins)
91
+ const seen = new Set<unknown>();
92
+ const unique: Record[] = [];
93
+
94
+ for (let i = childRecord.length - 1; i >= 0; i--) {
95
+ const r = childRecord[i] as Record;
96
+ if (!seen.has(r.id)) {
97
+ seen.add(r.id);
98
+ unique.push(r);
99
+ }
100
+ }
101
+
102
+ unique.reverse();
103
+ records[key] = unique.map((r: Record) => r.serialize());
104
+ } else {
105
+ records[key] = (childRecord as Record)?.serialize() ?? null;
106
+ }
92
107
  }
93
108
 
94
109
  return { ...data, ...records };
@@ -3,12 +3,17 @@ import type { HasManyMap, BelongsToMap, GlobalMap, PendingMap, PendingBelongsToM
3
3
 
4
4
  // TODO: Refactor mapping to remove a level of iteration
5
5
  export function getRelationships(type: string, sourceModel: string, targetModel: string, relationshipId?: string): Map<unknown, unknown> | undefined {
6
- const allRelationships = relationships.get(type) as Map<string, Map<string, Map<unknown, unknown>>> | undefined;
6
+ let allRelationships = relationships.get(type) as Map<string, Map<string, Map<unknown, unknown>>> | undefined;
7
+
8
+ if (!allRelationships) {
9
+ allRelationships = new Map();
10
+ relationships.set(type, allRelationships);
11
+ }
7
12
 
8
13
  // create relationship map for this type of it doesn't already exist
9
- if (!allRelationships!.has(sourceModel)) allRelationships!.set(sourceModel, new Map());
14
+ if (!allRelationships.has(sourceModel)) allRelationships.set(sourceModel, new Map());
10
15
 
11
- const modelRelationship = allRelationships!.get(sourceModel)!;
16
+ const modelRelationship = allRelationships.get(sourceModel)!;
12
17
 
13
18
  if (!modelRelationship.has(targetModel)) modelRelationship.set(targetModel, new Map());
14
19
 
package/src/serializer.ts CHANGED
@@ -5,6 +5,14 @@ import type ModelProperty from './model-property.js';
5
5
 
6
6
  const RESERVED_KEYS = ['__name'];
7
7
 
8
+ function isAggregateProperty(v: unknown): v is AggregateProperty {
9
+ return typeof v === 'object' && v !== null && (v as { __kind?: string }).__kind === 'AggregateProperty';
10
+ }
11
+
12
+ function isModelProperty(v: unknown): v is ModelProperty {
13
+ return typeof v === 'object' && v !== null && (v as { __kind?: string }).__kind === 'ModelProperty';
14
+ }
15
+
8
16
  function searchQuery(query: Record<string, unknown>, array: unknown, key?: string): unknown {
9
17
  const result = makeArray(array).find((item: unknown) => {
10
18
  for (const [prop, value] of Object.entries(query)) {
@@ -93,14 +101,14 @@ export default class Serializer {
93
101
  }
94
102
 
95
103
  // Aggregate property handling — use the rawData value, not the aggregate descriptor
96
- if ((handler as AggregateProperty)?.__kind === 'AggregateProperty') {
104
+ if (isAggregateProperty(handler)) {
97
105
  parsedData[key] = data;
98
106
  rec[key] = data;
99
107
  continue;
100
108
  }
101
109
 
102
110
  // Direct assignment handling
103
- if ((handler as ModelProperty)?.__kind !== 'ModelProperty') {
111
+ if (!isModelProperty(handler)) {
104
112
  parsedData[key] = handler;
105
113
  rec[key] = handler;
106
114
  continue;
package/src/store.ts CHANGED
@@ -204,7 +204,7 @@ export default class Store {
204
204
  this._cleanupRelationshipRegistries(modelName, recordId);
205
205
  recordToUnload.clean();
206
206
 
207
- this.data.get(modelName)!.delete(recordId as string | number);
207
+ this.data.get(modelName)?.delete(recordId as string | number);
208
208
  }
209
209
  }
210
210
 
@@ -22,6 +22,14 @@ interface CompressionOptions {
22
22
  orderBy?: string;
23
23
  }
24
24
 
25
+ interface TimescaleDeps {
26
+ buildCreateHypertable: typeof buildCreateHypertable;
27
+ buildTimeBucket: typeof buildTimeBucket;
28
+ buildContinuousAggregate: typeof buildContinuousAggregate;
29
+ buildEnableCompression: typeof buildEnableCompression;
30
+ buildCompressionPolicy: typeof buildCompressionPolicy;
31
+ }
32
+
25
33
  export default class TimescaleDB extends PostgresDB {
26
34
  static override extensions: string[] = ['timescaledb'];
27
35
  static override configKey: string = 'timescale';
@@ -37,6 +45,10 @@ export default class TimescaleDB extends PostgresDB {
37
45
  });
38
46
  }
39
47
 
48
+ private get tsDeps(): TimescaleDeps {
49
+ return this.deps as unknown as TimescaleDeps;
50
+ }
51
+
40
52
  /**
41
53
  * Convert a table to a TimescaleDB hypertable.
42
54
  * Should be called after the table is created (e.g. after initial migration).
@@ -46,7 +58,7 @@ export default class TimescaleDB extends PostgresDB {
46
58
  const schema = schemas[modelName];
47
59
  if (!schema) throw new Error(`Model '${modelName}' not found`);
48
60
 
49
- const { sql } = (this.deps as unknown as { buildCreateHypertable: typeof buildCreateHypertable }).buildCreateHypertable(schema.table, timeColumn, options);
61
+ const { sql } = this.tsDeps.buildCreateHypertable(schema.table, timeColumn, options);
50
62
  await this.requirePool().query(sql);
51
63
  }
52
64
 
@@ -58,7 +70,7 @@ export default class TimescaleDB extends PostgresDB {
58
70
  const schema = schemas[modelName];
59
71
  if (!schema) return [];
60
72
 
61
- const { sql, values } = (this.deps as unknown as { buildTimeBucket: typeof buildTimeBucket }).buildTimeBucket(schema.table, timeColumn, bucketSize, options);
73
+ const { sql, values } = this.tsDeps.buildTimeBucket(schema.table, timeColumn, bucketSize, options);
62
74
 
63
75
  try {
64
76
  const result = await this.requirePool().query(sql, values);
@@ -77,7 +89,7 @@ export default class TimescaleDB extends PostgresDB {
77
89
  const schema = schemas[modelName];
78
90
  if (!schema) throw new Error(`Model '${modelName}' not found`);
79
91
 
80
- const { sql } = (this.deps as unknown as { buildContinuousAggregate: typeof buildContinuousAggregate }).buildContinuousAggregate(viewName, schema.table, timeColumn, bucketSize, aggregates, options);
92
+ const { sql } = this.tsDeps.buildContinuousAggregate(viewName, schema.table, timeColumn, bucketSize, aggregates, options);
81
93
  await this.requirePool().query(sql);
82
94
  }
83
95
 
@@ -89,7 +101,7 @@ export default class TimescaleDB extends PostgresDB {
89
101
  const schema = schemas[modelName];
90
102
  if (!schema) throw new Error(`Model '${modelName}' not found`);
91
103
 
92
- const { sql } = (this.deps as unknown as { buildEnableCompression: typeof buildEnableCompression }).buildEnableCompression(schema.table, options.segmentBy, options.orderBy);
104
+ const { sql } = this.tsDeps.buildEnableCompression(schema.table, options.segmentBy, options.orderBy);
93
105
  await this.requirePool().query(sql);
94
106
  }
95
107
 
@@ -101,7 +113,7 @@ export default class TimescaleDB extends PostgresDB {
101
113
  const schema = schemas[modelName];
102
114
  if (!schema) throw new Error(`Model '${modelName}' not found`);
103
115
 
104
- const { sql } = (this.deps as unknown as { buildCompressionPolicy: typeof buildCompressionPolicy }).buildCompressionPolicy(schema.table, compressAfter);
116
+ const { sql } = this.tsDeps.buildCompressionPolicy(schema.table, compressAfter);
105
117
  await this.requirePool().query(sql);
106
118
  }
107
119
  }
@@ -73,8 +73,9 @@ export default class ViewResolver {
73
73
 
74
74
  // Compute aggregate fields from source record's relationships
75
75
  for (const [key, aggProp] of Object.entries(aggregateFields)) {
76
- const relatedRecords = sourceRecord.__relationships?.[aggProp.relationship!]
77
- || sourceRecord[aggProp.relationship!];
76
+ if (!aggProp.relationship) continue;
77
+ const relatedRecords = sourceRecord.__relationships?.[aggProp.relationship]
78
+ || sourceRecord[aggProp.relationship];
78
79
  const relArray = Array.isArray(relatedRecords) ? relatedRecords : [];
79
80
  rawData[key] = aggProp.compute(relArray);
80
81
  }
@@ -151,10 +152,11 @@ export default class ViewResolver {
151
152
  rawData[key] = aggProp.compute(groupRecords);
152
153
  } else {
153
154
  // Relationship aggregate — flatten related records across all group members
155
+ if (!aggProp.relationship) continue;
154
156
  const allRelated: unknown[] = [];
155
157
  for (const record of groupRecords) {
156
- const relatedRecords = record.__relationships?.[aggProp.relationship!]
157
- || record[aggProp.relationship!];
158
+ const relatedRecords = record.__relationships?.[aggProp.relationship]
159
+ || record[aggProp.relationship];
158
160
  if (Array.isArray(relatedRecords)) {
159
161
  allRelated.push(...relatedRecords);
160
162
  }