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

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 (149) hide show
  1. package/dist/aggregates.d.ts +21 -0
  2. package/dist/aggregates.js +90 -0
  3. package/dist/attr.d.ts +2 -0
  4. package/dist/attr.js +22 -0
  5. package/dist/belongs-to.d.ts +11 -0
  6. package/dist/belongs-to.js +58 -0
  7. package/dist/cli.d.ts +22 -0
  8. package/dist/cli.js +148 -0
  9. package/dist/commands.d.ts +7 -0
  10. package/dist/commands.js +146 -0
  11. package/dist/db.d.ts +21 -0
  12. package/dist/db.js +174 -0
  13. package/dist/exports/db.d.ts +7 -0
  14. package/{src → dist}/exports/db.js +2 -4
  15. package/dist/has-many.d.ts +11 -0
  16. package/dist/has-many.js +57 -0
  17. package/dist/hooks.d.ts +47 -0
  18. package/dist/hooks.js +106 -0
  19. package/dist/index.d.ts +14 -0
  20. package/dist/index.js +34 -0
  21. package/dist/main.d.ts +46 -0
  22. package/dist/main.js +178 -0
  23. package/dist/manage-record.d.ts +13 -0
  24. package/dist/manage-record.js +113 -0
  25. package/dist/meta-request.d.ts +6 -0
  26. package/dist/meta-request.js +52 -0
  27. package/dist/migrate.d.ts +2 -0
  28. package/dist/migrate.js +57 -0
  29. package/dist/model-property.d.ts +9 -0
  30. package/dist/model-property.js +29 -0
  31. package/dist/model.d.ts +15 -0
  32. package/dist/model.js +18 -0
  33. package/dist/mysql/connection.d.ts +14 -0
  34. package/dist/mysql/connection.js +24 -0
  35. package/dist/mysql/migration-generator.d.ts +45 -0
  36. package/dist/mysql/migration-generator.js +245 -0
  37. package/dist/mysql/migration-runner.d.ts +12 -0
  38. package/dist/mysql/migration-runner.js +83 -0
  39. package/dist/mysql/mysql-db.d.ts +100 -0
  40. package/dist/mysql/mysql-db.js +411 -0
  41. package/dist/mysql/query-builder.d.ts +10 -0
  42. package/dist/mysql/query-builder.js +44 -0
  43. package/dist/mysql/schema-introspector.d.ts +19 -0
  44. package/dist/mysql/schema-introspector.js +286 -0
  45. package/dist/mysql/type-map.d.ts +21 -0
  46. package/dist/mysql/type-map.js +36 -0
  47. package/dist/orm-request.d.ts +38 -0
  48. package/dist/orm-request.js +453 -0
  49. package/dist/plural-registry.d.ts +4 -0
  50. package/{src → dist}/plural-registry.js +3 -6
  51. package/dist/postgres/connection.d.ts +15 -0
  52. package/dist/postgres/connection.js +30 -0
  53. package/dist/postgres/migration-generator.d.ts +45 -0
  54. package/dist/postgres/migration-generator.js +257 -0
  55. package/dist/postgres/migration-runner.d.ts +10 -0
  56. package/dist/postgres/migration-runner.js +82 -0
  57. package/dist/postgres/postgres-db.d.ts +119 -0
  58. package/dist/postgres/postgres-db.js +473 -0
  59. package/dist/postgres/query-builder.d.ts +27 -0
  60. package/dist/postgres/query-builder.js +98 -0
  61. package/dist/postgres/schema-introspector.d.ts +29 -0
  62. package/dist/postgres/schema-introspector.js +309 -0
  63. package/dist/postgres/type-map.d.ts +23 -0
  64. package/dist/postgres/type-map.js +53 -0
  65. package/dist/record.d.ts +75 -0
  66. package/dist/record.js +115 -0
  67. package/dist/relationships.d.ts +10 -0
  68. package/dist/relationships.js +35 -0
  69. package/dist/serializer.d.ts +17 -0
  70. package/dist/serializer.js +130 -0
  71. package/dist/setup-rest-server.d.ts +1 -0
  72. package/dist/setup-rest-server.js +54 -0
  73. package/dist/standalone-db.d.ts +58 -0
  74. package/dist/standalone-db.js +142 -0
  75. package/dist/store.d.ts +62 -0
  76. package/dist/store.js +271 -0
  77. package/dist/timescale/query-builder.d.ts +41 -0
  78. package/dist/timescale/query-builder.js +87 -0
  79. package/dist/timescale/timescale-db.d.ts +44 -0
  80. package/dist/timescale/timescale-db.js +81 -0
  81. package/dist/transforms.d.ts +2 -0
  82. package/dist/transforms.js +17 -0
  83. package/dist/types/orm-types.d.ts +142 -0
  84. package/dist/types/orm-types.js +1 -0
  85. package/dist/utils.d.ts +5 -0
  86. package/dist/utils.js +13 -0
  87. package/dist/view-resolver.d.ts +8 -0
  88. package/dist/view-resolver.js +165 -0
  89. package/dist/view.d.ts +11 -0
  90. package/dist/view.js +18 -0
  91. package/package.json +34 -11
  92. package/src/{aggregates.js → aggregates.ts} +27 -13
  93. package/src/{attr.js → attr.ts} +2 -2
  94. package/src/{belongs-to.js → belongs-to.ts} +36 -17
  95. package/src/{cli.js → cli.ts} +17 -11
  96. package/src/{commands.js → commands.ts} +179 -170
  97. package/src/{db.js → db.ts} +35 -26
  98. package/src/exports/db.ts +7 -0
  99. package/src/has-many.ts +91 -0
  100. package/src/{hooks.js → hooks.ts} +23 -27
  101. package/src/{index.js → index.ts} +4 -4
  102. package/src/{main.js → main.ts} +59 -34
  103. package/src/{manage-record.js → manage-record.ts} +41 -22
  104. package/src/{meta-request.js → meta-request.ts} +17 -14
  105. package/src/{migrate.js → migrate.ts} +9 -9
  106. package/src/{model-property.js → model-property.ts} +12 -6
  107. package/src/{model.js → model.ts} +5 -4
  108. package/src/mysql/{connection.js → connection.ts} +43 -28
  109. package/src/mysql/{migration-generator.js → migration-generator.ts} +332 -286
  110. package/src/mysql/{migration-runner.js → migration-runner.ts} +116 -110
  111. package/src/mysql/{mysql-db.js → mysql-db.ts} +533 -473
  112. package/src/mysql/{query-builder.js → query-builder.ts} +69 -64
  113. package/src/mysql/{schema-introspector.js → schema-introspector.ts} +355 -325
  114. package/src/mysql/{type-map.js → type-map.ts} +42 -37
  115. package/src/{orm-request.js → orm-request.ts} +165 -95
  116. package/src/plural-registry.ts +12 -0
  117. package/src/postgres/{connection.js → connection.ts} +14 -5
  118. package/src/postgres/{migration-generator.js → migration-generator.ts} +82 -38
  119. package/src/postgres/{migration-runner.js → migration-runner.ts} +11 -10
  120. package/src/postgres/{postgres-db.js → postgres-db.ts} +195 -114
  121. package/src/postgres/{query-builder.js → query-builder.ts} +27 -28
  122. package/src/postgres/{schema-introspector.js → schema-introspector.ts} +87 -58
  123. package/src/postgres/{type-map.js → type-map.ts} +10 -6
  124. package/src/{record.js → record.ts} +73 -34
  125. package/src/relationships.ts +48 -0
  126. package/src/{serializer.js → serializer.ts} +44 -36
  127. package/src/{setup-rest-server.js → setup-rest-server.ts} +18 -13
  128. package/src/{standalone-db.js → standalone-db.ts} +33 -24
  129. package/src/{store.js → store.ts} +90 -68
  130. package/src/timescale/{query-builder.js → query-builder.ts} +33 -38
  131. package/src/timescale/timescale-db.ts +107 -0
  132. package/src/transforms.ts +20 -0
  133. package/src/types/mysql2.d.ts +30 -0
  134. package/src/types/orm-types.ts +146 -0
  135. package/src/types/pg.d.ts +28 -0
  136. package/src/types/stonyx-cron.d.ts +5 -0
  137. package/src/types/stonyx-events.d.ts +4 -0
  138. package/src/types/stonyx-rest-server.d.ts +11 -0
  139. package/src/types/stonyx-utils.d.ts +33 -0
  140. package/src/types/stonyx.d.ts +21 -0
  141. package/src/utils.ts +16 -0
  142. package/src/{view-resolver.js → view-resolver.ts} +53 -28
  143. package/src/view.ts +22 -0
  144. package/src/has-many.js +0 -68
  145. package/src/relationships.js +0 -43
  146. package/src/timescale/timescale-db.js +0 -111
  147. package/src/transforms.js +0 -20
  148. package/src/utils.js +0 -12
  149. package/src/view.js +0 -21
@@ -1,8 +1,50 @@
1
1
  import Orm, { relationships } from '@stonyx/orm';
2
- import { TYPES } from './relationships.js';
2
+ import { TYPES, getHasManyRegistry, getBelongsToRegistry, getPendingRegistry } from './relationships.js';
3
3
  import ViewResolver from './view-resolver.js';
4
4
 
5
+ interface UnloadOptions {
6
+ includeChildren?: boolean;
7
+ [key: string]: unknown;
8
+ }
9
+
10
+ interface UnloadQueueItem {
11
+ record: StoreRecord;
12
+ modelName: string;
13
+ recordId: unknown;
14
+ isRoot?: boolean;
15
+ depth?: number;
16
+ }
17
+
18
+ interface ChildInfo {
19
+ childRecord: StoreRecord;
20
+ relationshipKey: string;
21
+ type: string;
22
+ }
23
+
24
+ interface StoreRecord {
25
+ __model: { __name: string; [key: string]: unknown };
26
+ __data: Record<string, unknown>;
27
+ __relationships: Record<string, unknown>;
28
+ id: unknown;
29
+ clean(): void;
30
+ [key: string]: unknown;
31
+ }
32
+
5
33
  export default class Store {
34
+ static instance: Store | undefined;
35
+
36
+ data: Map<string, Map<number | string, unknown>> = new Map();
37
+
38
+ /**
39
+ * Set by Orm during init — resolves memory flag for a model name.
40
+ */
41
+ _memoryResolver: ((modelName: string) => boolean) | null = null;
42
+
43
+ /**
44
+ * Set by Orm during init — reference to the SQL adapter instance for on-demand queries.
45
+ */
46
+ _sqlDb: { findRecord(modelName: string, id: unknown): Promise<unknown>; findAll(modelName: string, conditions?: Record<string, unknown>): Promise<unknown[]> } | null = null;
47
+
6
48
  constructor() {
7
49
  if (Store.instance) return Store.instance;
8
50
  Store.instance = this;
@@ -15,7 +57,9 @@ export default class Store {
15
57
  * Returns the record if it exists in the in-memory store, undefined otherwise.
16
58
  * Does NOT query the database. For memory:false models, use find() instead.
17
59
  */
18
- get(key, id) {
60
+ get(key: string): Map<number | string, unknown> | undefined;
61
+ get(key: string, id: number | string): unknown;
62
+ get(key: string, id?: number | string): Map<number | string, unknown> | unknown | undefined {
19
63
  if (!id) return this.data.get(key);
20
64
 
21
65
  return this.data.get(key)?.get(id);
@@ -24,11 +68,8 @@ export default class Store {
24
68
  /**
25
69
  * Async authoritative read. Always queries the SQL database for memory: false models.
26
70
  * For memory: true models, returns from store (already loaded on boot).
27
- * @param {string} modelName - The model name
28
- * @param {string|number} id - The record ID
29
- * @returns {Promise<Record|undefined>}
30
71
  */
31
- async find(modelName, id) {
72
+ async find(modelName: string, id: number | string): Promise<unknown> {
32
73
  // For views in non-SQL mode, use view resolver
33
74
  if (Orm.instance?.isView?.(modelName) && !this._sqlDb) {
34
75
  const resolver = new ViewResolver(modelName);
@@ -52,11 +93,8 @@ export default class Store {
52
93
  /**
53
94
  * Async read for all records of a model. Always queries MySQL for memory: false models.
54
95
  * For memory: true models, returns from store.
55
- * @param {string} modelName - The model name
56
- * @param {Object} [conditions] - Optional WHERE conditions
57
- * @returns {Promise<Record[]>}
58
96
  */
59
- async findAll(modelName, conditions) {
97
+ async findAll(modelName: string, conditions?: Record<string, unknown>): Promise<unknown[]> {
60
98
  // For views in non-SQL mode, use view resolver
61
99
  if (Orm.instance?.isView?.(modelName) && !this._sqlDb) {
62
100
  const resolver = new ViewResolver(modelName);
@@ -64,8 +102,8 @@ export default class Store {
64
102
 
65
103
  if (!conditions || Object.keys(conditions).length === 0) return records;
66
104
 
67
- return records.filter(record =>
68
- Object.entries(conditions).every(([key, value]) => record.__data[key] === value)
105
+ return records.filter((record: unknown) =>
106
+ Object.entries(conditions).every(([key, value]) => (record as StoreRecord).__data[key] === value)
69
107
  );
70
108
  }
71
109
 
@@ -88,19 +126,16 @@ export default class Store {
88
126
 
89
127
  if (!conditions || Object.keys(conditions).length === 0) return records;
90
128
 
91
- return records.filter(record =>
92
- Object.entries(conditions).every(([key, value]) => record.__data[key] === value)
129
+ return records.filter((record: unknown) =>
130
+ Object.entries(conditions).every(([key, value]) => (record as StoreRecord).__data[key] === value)
93
131
  );
94
132
  }
95
133
 
96
134
  /**
97
135
  * Async query — always hits MySQL, never reads from memory cache.
98
136
  * Use for complex queries, aggregations, or when you need guaranteed freshness.
99
- * @param {string} modelName - The model name
100
- * @param {Object} conditions - WHERE conditions
101
- * @returns {Promise<Record[]>}
102
137
  */
103
- async query(modelName, conditions = {}) {
138
+ async query(modelName: string, conditions: Record<string, unknown> = {}): Promise<unknown[]> {
104
139
  if (this._sqlDb) {
105
140
  return this._sqlDb.findAll(modelName, conditions);
106
141
  }
@@ -113,37 +148,25 @@ export default class Store {
113
148
 
114
149
  if (Object.keys(conditions).length === 0) return records;
115
150
 
116
- return records.filter(record =>
117
- Object.entries(conditions).every(([key, value]) => record.__data[key] === value)
151
+ return records.filter((record: unknown) =>
152
+ Object.entries(conditions).every(([key, value]) => (record as StoreRecord).__data[key] === value)
118
153
  );
119
154
  }
120
155
 
121
- /**
122
- * Set by Orm during init — resolves memory flag for a model name.
123
- * @type {Function|null}
124
- */
125
- _memoryResolver = null;
126
-
127
- /**
128
- * Set by Orm during init — reference to the SQL adapter instance for on-demand queries.
129
- * @type {object|null}
130
- */
131
- _sqlDb = null;
132
-
133
156
  /**
134
157
  * Check if a model is configured for in-memory storage.
135
158
  * @private
136
159
  */
137
- _isMemoryModel(modelName) {
160
+ private _isMemoryModel(modelName: string): boolean {
138
161
  if (this._memoryResolver) return this._memoryResolver(modelName);
139
162
  return false; // default to non-memory if resolver not set yet
140
163
  }
141
164
 
142
- set(key, value) {
165
+ set(key: string, value: Map<number | string, unknown>): void {
143
166
  this.data.set(key, value);
144
167
  }
145
168
 
146
- remove(key, id) {
169
+ remove(key: string, id?: number | string): void {
147
170
  // Guard: read-only views cannot have records removed
148
171
  if (Orm.instance?.isView?.(key)) {
149
172
  throw new Error(`Cannot remove records from read-only view '${key}'`);
@@ -154,7 +177,7 @@ export default class Store {
154
177
  this.unloadAllRecords(key);
155
178
  }
156
179
 
157
- unloadRecord(model, id, options={}) {
180
+ unloadRecord(model: string, id: unknown, options: UnloadOptions = {}): void {
158
181
  const modelStore = this.data.get(model);
159
182
 
160
183
  if (!modelStore) {
@@ -162,7 +185,7 @@ export default class Store {
162
185
  return;
163
186
  }
164
187
 
165
- const record = modelStore.get(id);
188
+ const record = modelStore.get(id as string | number) as StoreRecord | undefined;
166
189
 
167
190
  if (!record) {
168
191
  console.warn(`[Store] Cannot unload record: ${model}:${id} not found in store`);
@@ -171,7 +194,7 @@ export default class Store {
171
194
 
172
195
  const { toUnload, visited } = options.includeChildren
173
196
  ? this._buildUnloadQueue(record, options)
174
- : { toUnload: [{ record, modelName: model, recordId: id }], visited: new Set([`${model}:${id}`]) };
197
+ : { toUnload: [{ record, modelName: model, recordId: id }] as UnloadQueueItem[], visited: new Set([`${model}:${id}`]) };
175
198
 
176
199
  for (const item of toUnload.reverse()) {
177
200
  const { record: recordToUnload, modelName, recordId } = item;
@@ -181,11 +204,11 @@ export default class Store {
181
204
  this._cleanupRelationshipRegistries(modelName, recordId);
182
205
  recordToUnload.clean();
183
206
 
184
- this.data.get(modelName).delete(recordId);
207
+ this.data.get(modelName)!.delete(recordId as string | number);
185
208
  }
186
209
  }
187
210
 
188
- unloadAllRecords(model, options={}) {
211
+ unloadAllRecords(model: string, options: UnloadOptions = {}): void {
189
212
  const modelStore = this.data.get(model);
190
213
 
191
214
  if (!modelStore) {
@@ -201,11 +224,11 @@ export default class Store {
201
224
  }
202
225
  }
203
226
 
204
- for (const relationshipType of TYPES) relationships.get(relationshipType).delete(model);
227
+ for (const relationshipType of TYPES) (relationships.get(relationshipType) as Map<string, unknown>).delete(model);
205
228
  }
206
229
 
207
- _removeFromHasManyArrays(modelName, recordId, visited) {
208
- const hasManyRegistry = relationships.get('hasMany');
230
+ private _removeFromHasManyArrays(modelName: string, recordId: unknown, visited: Set<string>): void {
231
+ const hasManyRegistry = getHasManyRegistry();
209
232
 
210
233
  for (const [sourceModel, targetModels] of hasManyRegistry) {
211
234
  const targetModelMap = targetModels.get(modelName);
@@ -217,30 +240,30 @@ export default class Store {
217
240
  // Don't modify arrays of records being deleted
218
241
  if (visited.has(sourceKey)) continue;
219
242
 
220
- const index = hasManyArray.findIndex(r => r && r.id === recordId);
243
+ const index = hasManyArray.findIndex(r => r && (r as StoreRecord).id === recordId);
221
244
  if (index !== -1) hasManyArray.splice(index, 1);
222
245
  }
223
246
  }
224
247
  }
225
248
 
226
- _nullifyBelongsToReferences(modelName, recordId, visited) {
227
- const belongsToRegistry = relationships.get('belongsTo');
249
+ private _nullifyBelongsToReferences(modelName: string, recordId: unknown, visited: Set<string>): void {
250
+ const belongsToRegistry = getBelongsToRegistry();
228
251
 
229
252
  for (const [sourceModel, targetModels] of belongsToRegistry) {
230
253
  const targetModelMap = targetModels.get(modelName);
231
254
  if (!targetModelMap) continue;
232
255
 
233
256
  for (const [sourceRecordId, belongsToRecord] of targetModelMap) {
234
- if (belongsToRecord && belongsToRecord.id === recordId) {
257
+ if (belongsToRecord && (belongsToRecord as StoreRecord).id === recordId) {
235
258
  const sourceKey = `${sourceModel}:${sourceRecordId}`;
236
259
 
237
260
  if (visited.has(sourceKey)) continue;
238
261
  targetModelMap.set(sourceRecordId, null);
239
262
 
240
- const sourceRecord = this.get(sourceModel, sourceRecordId);
263
+ const sourceRecord = this.get(sourceModel, sourceRecordId as string | number) as StoreRecord | undefined;
241
264
  if (sourceRecord && sourceRecord.__relationships) {
242
265
  for (const [key, value] of Object.entries(sourceRecord.__relationships)) {
243
- if (value && value.id === recordId) {
266
+ if (value && (value as StoreRecord).id === recordId) {
244
267
  sourceRecord.__relationships[key] = null;
245
268
  }
246
269
  }
@@ -250,18 +273,18 @@ export default class Store {
250
273
  }
251
274
  }
252
275
 
253
- _cleanupRelationshipRegistries(modelName, recordId) {
254
- const hasManyMap = relationships.get('hasMany').get(modelName);
276
+ private _cleanupRelationshipRegistries(modelName: string, recordId: unknown): void {
277
+ const hasManyMap = getHasManyRegistry().get(modelName);
255
278
  if (hasManyMap) {
256
279
  for (const [, recordMap] of hasManyMap) recordMap.delete(recordId);
257
280
  }
258
281
 
259
- const belongsToMap = relationships.get('belongsTo').get(modelName);
282
+ const belongsToMap = getBelongsToRegistry().get(modelName);
260
283
  if (belongsToMap) {
261
284
  for (const [, recordMap] of belongsToMap) recordMap.delete(recordId);
262
285
  }
263
286
 
264
- const pendingMap = relationships.get('pending').get(modelName);
287
+ const pendingMap = getPendingRegistry().get(modelName);
265
288
  if (pendingMap) pendingMap.delete(recordId);
266
289
  }
267
290
 
@@ -269,8 +292,8 @@ export default class Store {
269
292
  * Extracts hasMany and non-bidirectional belongsTo children from a record
270
293
  * @private
271
294
  */
272
- _getChildren(record) {
273
- const children = [];
295
+ private _getChildren(record: StoreRecord): ChildInfo[] {
296
+ const children: ChildInfo[] = [];
274
297
 
275
298
  if (!record.__relationships) return children;
276
299
 
@@ -278,30 +301,29 @@ export default class Store {
278
301
  // hasMany children - always include
279
302
  if (Array.isArray(value)) {
280
303
  for (const childRecord of value) {
281
- if (childRecord) children.push({ childRecord, relationshipKey: key, type: 'hasMany' });
304
+ if (childRecord) children.push({ childRecord: childRecord as StoreRecord, relationshipKey: key, type: 'hasMany' });
282
305
  }
283
306
  } else if (value && !this._isBidirectionalRelationship(
284
307
  record.__model.__name,
285
- value.__model.__name
308
+ (value as StoreRecord).__model.__name
286
309
  )) {
287
- children.push({ childRecord: value, relationshipKey: key, type: 'belongsTo' });
310
+ children.push({ childRecord: value as StoreRecord, relationshipKey: key, type: 'belongsTo' });
288
311
  }
289
312
  }
290
313
 
291
314
  return children;
292
315
  }
293
316
 
294
- _isBidirectionalRelationship(sourceModel, targetModel) {
295
- const hasManyRegistry = relationships.get('hasMany');
296
- const inverseMap = hasManyRegistry.get(targetModel)?.get(sourceModel);
317
+ private _isBidirectionalRelationship(sourceModel: string, targetModel: string): boolean {
318
+ const inverseMap = getHasManyRegistry().get(targetModel)?.get(sourceModel);
297
319
 
298
- return inverseMap && inverseMap.size > 0;
320
+ return !!inverseMap && inverseMap.size > 0;
299
321
  }
300
322
 
301
- _buildUnloadQueue(record, options) {
302
- const visited = new Set();
303
- const toUnload = [];
304
- const queue = [{
323
+ private _buildUnloadQueue(record: StoreRecord, options: UnloadOptions): { toUnload: UnloadQueueItem[]; visited: Set<string> } {
324
+ const visited = new Set<string>();
325
+ const toUnload: UnloadQueueItem[] = [];
326
+ const queue: UnloadQueueItem[] = [{
305
327
  record,
306
328
  modelName: record.__model.__name,
307
329
  recordId: record.id,
@@ -310,7 +332,7 @@ export default class Store {
310
332
  }];
311
333
 
312
334
  while (queue.length > 0) {
313
- const item = queue.shift();
335
+ const item = queue.shift()!;
314
336
  const key = `${item.modelName}:${item.recordId}`;
315
337
 
316
338
  if (visited.has(key)) continue;
@@ -328,7 +350,7 @@ export default class Store {
328
350
  modelName: childRecord.__model.__name,
329
351
  recordId: childRecord.id,
330
352
  isRoot: false,
331
- depth: item.depth + 1
353
+ depth: (item.depth ?? 0) + 1
332
354
  });
333
355
  }
334
356
  }
@@ -3,16 +3,35 @@ export { validateIdentifier, buildInsert, buildUpdate, buildDelete, buildSelect
3
3
 
4
4
  import { validateIdentifier } from '../postgres/query-builder.js';
5
5
 
6
+ interface QueryResult {
7
+ sql: string;
8
+ values: unknown[];
9
+ }
10
+
11
+ interface SqlResult {
12
+ sql: string;
13
+ }
14
+
15
+ interface HypertableOptions {
16
+ chunkInterval?: string;
17
+ }
18
+
19
+ interface TimeBucketOptions {
20
+ aggregates?: string[];
21
+ where?: Record<string, unknown>;
22
+ orderBy?: string;
23
+ limit?: number;
24
+ }
25
+
26
+ interface ContinuousAggregateOptions {
27
+ withNoData?: boolean;
28
+ }
29
+
6
30
  /**
7
31
  * Build a CREATE TABLE + hypertable conversion statement.
8
32
  * TimescaleDB hypertables are regular tables converted via create_hypertable().
9
- * @param {string} table - Table name
10
- * @param {string} timeColumn - The time-partitioning column (must be a timestamp type)
11
- * @param {Object} [options]
12
- * @param {string} [options.chunkInterval='7 days'] - Chunk time interval
13
- * @returns {{ sql: string, values: any[] }}
14
33
  */
15
- export function buildCreateHypertable(table, timeColumn, options = {}) {
34
+ export function buildCreateHypertable(table: string, timeColumn: string, options: HypertableOptions = {}): QueryResult {
16
35
  validateIdentifier(table, 'table name');
17
36
  validateIdentifier(timeColumn, 'column name');
18
37
 
@@ -25,32 +44,23 @@ export function buildCreateHypertable(table, timeColumn, options = {}) {
25
44
 
26
45
  /**
27
46
  * Build a time_bucket aggregation query.
28
- * @param {string} table - Table name
29
- * @param {string} timeColumn - Timestamp column to bucket
30
- * @param {string} bucketSize - Bucket interval (e.g. '1 hour', '5 minutes', '1 day')
31
- * @param {Object} [options]
32
- * @param {string[]} [options.aggregates] - Aggregate expressions (e.g. ['AVG("value") AS avg_value'])
33
- * @param {Object} [options.where] - WHERE conditions
34
- * @param {string} [options.orderBy='bucket'] - ORDER BY clause
35
- * @param {number} [options.limit] - LIMIT
36
- * @returns {{ sql: string, values: any[] }}
37
47
  */
38
- export function buildTimeBucket(table, timeColumn, bucketSize, options = {}) {
48
+ export function buildTimeBucket(table: string, timeColumn: string, bucketSize: string, options: TimeBucketOptions = {}): QueryResult {
39
49
  validateIdentifier(table, 'table name');
40
50
  validateIdentifier(timeColumn, 'column name');
41
51
 
42
52
  const { aggregates = [], where, orderBy = 'bucket', limit } = options;
43
- const values = [];
53
+ const values: unknown[] = [];
44
54
  let paramIndex = 1;
45
55
 
46
- const selectCols = [`time_bucket($${paramIndex++}, "${timeColumn}") AS bucket`];
56
+ const selectCols: string[] = [`time_bucket($${paramIndex++}, "${timeColumn}") AS bucket`];
47
57
  values.push(bucketSize);
48
58
 
49
59
  for (const agg of aggregates) {
50
60
  selectCols.push(agg);
51
61
  }
52
62
 
53
- let whereClauses = [];
63
+ const whereClauses: string[] = [];
54
64
  if (where) {
55
65
  for (const [k, v] of Object.entries(where)) {
56
66
  validateIdentifier(k, 'column name');
@@ -74,23 +84,15 @@ export function buildTimeBucket(table, timeColumn, bucketSize, options = {}) {
74
84
 
75
85
  /**
76
86
  * Build a continuous aggregate creation statement.
77
- * @param {string} viewName - Name for the continuous aggregate view
78
- * @param {string} table - Source hypertable
79
- * @param {string} timeColumn - Timestamp column
80
- * @param {string} bucketSize - Bucket interval
81
- * @param {string[]} aggregates - Aggregate expressions
82
- * @param {Object} [options]
83
- * @param {boolean} [options.withNoData=false] - Create without materializing data initially
84
- * @returns {{ sql: string }}
85
87
  */
86
- export function buildContinuousAggregate(viewName, table, timeColumn, bucketSize, aggregates, options = {}) {
88
+ export function buildContinuousAggregate(viewName: string, table: string, timeColumn: string, bucketSize: string, aggregates: string[], options: ContinuousAggregateOptions = {}): SqlResult {
87
89
  validateIdentifier(viewName, 'view name');
88
90
  validateIdentifier(table, 'table name');
89
91
  validateIdentifier(timeColumn, 'column name');
90
92
 
91
93
  const { withNoData = false } = options;
92
94
 
93
- const selectCols = [
95
+ const selectCols: string[] = [
94
96
  `time_bucket('${bucketSize}', "${timeColumn}") AS bucket`,
95
97
  ...aggregates,
96
98
  ];
@@ -104,11 +106,8 @@ export function buildContinuousAggregate(viewName, table, timeColumn, bucketSize
104
106
 
105
107
  /**
106
108
  * Build an ADD compression policy statement.
107
- * @param {string} table - Hypertable name
108
- * @param {string} compressAfter - Interval after which to compress (e.g. '7 days')
109
- * @returns {{ sql: string }}
110
109
  */
111
- export function buildCompressionPolicy(table, compressAfter) {
110
+ export function buildCompressionPolicy(table: string, compressAfter: string): SqlResult {
112
111
  validateIdentifier(table, 'table name');
113
112
 
114
113
  const sql = `SELECT add_compression_policy('"${table}"', INTERVAL '${compressAfter}', if_not_exists => TRUE)`;
@@ -118,12 +117,8 @@ export function buildCompressionPolicy(table, compressAfter) {
118
117
 
119
118
  /**
120
119
  * Build an ALTER TABLE to enable compression on a hypertable.
121
- * @param {string} table - Hypertable name
122
- * @param {string} segmentBy - Column to segment by (usually the non-time dimension)
123
- * @param {string} orderBy - Column to order compressed data by (usually the time column)
124
- * @returns {{ sql: string }}
125
120
  */
126
- export function buildEnableCompression(table, segmentBy, orderBy) {
121
+ export function buildEnableCompression(table: string, segmentBy?: string, orderBy?: string): SqlResult {
127
122
  validateIdentifier(table, 'table name');
128
123
 
129
124
  let opts = `timescaledb.compress`;
@@ -0,0 +1,107 @@
1
+ import PostgresDB from '../postgres/postgres-db.js';
2
+ import { isDbError } from '../utils.js';
3
+ import { buildCreateHypertable, buildTimeBucket, buildContinuousAggregate, buildCompressionPolicy, buildEnableCompression } from './query-builder.js';
4
+
5
+ interface HypertableOptions {
6
+ chunkInterval?: string;
7
+ }
8
+
9
+ interface TimeBucketOptions {
10
+ aggregates?: string[];
11
+ where?: Record<string, unknown>;
12
+ orderBy?: string;
13
+ limit?: number;
14
+ }
15
+
16
+ interface ContinuousAggregateOptions {
17
+ withNoData?: boolean;
18
+ }
19
+
20
+ interface CompressionOptions {
21
+ segmentBy?: string;
22
+ orderBy?: string;
23
+ }
24
+
25
+ export default class TimescaleDB extends PostgresDB {
26
+ static override extensions: string[] = ['timescaledb'];
27
+ static override configKey: string = 'timescale';
28
+
29
+ constructor(deps: Record<string, unknown> = {}) {
30
+ super({
31
+ ...deps,
32
+ buildCreateHypertable,
33
+ buildTimeBucket,
34
+ buildContinuousAggregate,
35
+ buildCompressionPolicy,
36
+ buildEnableCompression,
37
+ });
38
+ }
39
+
40
+ /**
41
+ * Convert a table to a TimescaleDB hypertable.
42
+ * Should be called after the table is created (e.g. after initial migration).
43
+ */
44
+ async createHypertable(modelName: string, timeColumn: string, options: HypertableOptions = {}): Promise<void> {
45
+ const schemas = this.deps.introspectModels();
46
+ const schema = schemas[modelName];
47
+ if (!schema) throw new Error(`Model '${modelName}' not found`);
48
+
49
+ const { sql } = (this.deps as unknown as { buildCreateHypertable: typeof buildCreateHypertable }).buildCreateHypertable(schema.table, timeColumn, options);
50
+ await this.requirePool().query(sql);
51
+ }
52
+
53
+ /**
54
+ * Query time-bucketed aggregations on a hypertable.
55
+ */
56
+ async timeBucket(modelName: string, timeColumn: string, bucketSize: string, options: TimeBucketOptions = {}): Promise<Record<string, unknown>[]> {
57
+ const schemas = this.deps.introspectModels();
58
+ const schema = schemas[modelName];
59
+ if (!schema) return [];
60
+
61
+ const { sql, values } = (this.deps as unknown as { buildTimeBucket: typeof buildTimeBucket }).buildTimeBucket(schema.table, timeColumn, bucketSize, options);
62
+
63
+ try {
64
+ const result = await this.requirePool().query(sql, values);
65
+ return result.rows;
66
+ } catch (error) {
67
+ if (isDbError(error) && error.code === '42P01') return [];
68
+ throw error;
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Create a continuous aggregate view on a hypertable.
74
+ */
75
+ async createContinuousAggregate(viewName: string, modelName: string, timeColumn: string, bucketSize: string, aggregates: string[], options: ContinuousAggregateOptions = {}): Promise<void> {
76
+ const schemas = this.deps.introspectModels();
77
+ const schema = schemas[modelName];
78
+ if (!schema) throw new Error(`Model '${modelName}' not found`);
79
+
80
+ const { sql } = (this.deps as unknown as { buildContinuousAggregate: typeof buildContinuousAggregate }).buildContinuousAggregate(viewName, schema.table, timeColumn, bucketSize, aggregates, options);
81
+ await this.requirePool().query(sql);
82
+ }
83
+
84
+ /**
85
+ * Enable compression on a hypertable.
86
+ */
87
+ async enableCompression(modelName: string, options: CompressionOptions = {}): Promise<void> {
88
+ const schemas = this.deps.introspectModels();
89
+ const schema = schemas[modelName];
90
+ if (!schema) throw new Error(`Model '${modelName}' not found`);
91
+
92
+ const { sql } = (this.deps as unknown as { buildEnableCompression: typeof buildEnableCompression }).buildEnableCompression(schema.table, options.segmentBy, options.orderBy);
93
+ await this.requirePool().query(sql);
94
+ }
95
+
96
+ /**
97
+ * Add a compression policy to a hypertable.
98
+ */
99
+ async addCompressionPolicy(modelName: string, compressAfter: string): Promise<void> {
100
+ const schemas = this.deps.introspectModels();
101
+ const schema = schemas[modelName];
102
+ if (!schema) throw new Error(`Model '${modelName}' not found`);
103
+
104
+ const { sql } = (this.deps as unknown as { buildCompressionPolicy: typeof buildCompressionPolicy }).buildCompressionPolicy(schema.table, compressAfter);
105
+ await this.requirePool().query(sql);
106
+ }
107
+ }
@@ -0,0 +1,20 @@
1
+ import { getTimestamp } from "@stonyx/utils/date";
2
+
3
+ const transforms: Record<string, (value: unknown) => unknown> = {
4
+ boolean: (value: unknown) => typeof value === 'string' ? (value as string).trim().toLowerCase() === 'true' : !!value,
5
+ date: (value: unknown) => value ? new Date(value as string | number) : null,
6
+ float: (value: unknown) => parseFloat(value as string),
7
+ number: (value: unknown) => parseInt(value as string),
8
+ passthrough: (value: unknown) => value,
9
+ string: (value: unknown) => String(value),
10
+ timestamp: (value: unknown) => getTimestamp(value),
11
+ trim: (value: unknown) => (value as string)?.trim(),
12
+ uppercase: (value: unknown) => (value as string)?.toUpperCase(),
13
+ };
14
+
15
+ // Math Proxies
16
+ (['ceil', 'floor', 'round'] as const).forEach(method => {
17
+ transforms[method] = (value: unknown) => Math[method](value as number);
18
+ });
19
+
20
+ export default transforms;
@@ -0,0 +1,30 @@
1
+ declare module 'mysql2/promise' {
2
+ interface PoolOptions {
3
+ host: string;
4
+ user: string;
5
+ password: string;
6
+ database: string;
7
+ port?: number;
8
+ [key: string]: unknown;
9
+ }
10
+
11
+ interface QueryResult {
12
+ [index: number]: unknown;
13
+ }
14
+
15
+ interface Pool {
16
+ execute(sql: string, params?: unknown[]): Promise<[unknown[], unknown]>;
17
+ query(sql: string, params?: unknown[]): Promise<[unknown[], unknown]>;
18
+ end(): Promise<void>;
19
+ getConnection(): Promise<PoolConnection>;
20
+ }
21
+
22
+ interface PoolConnection extends Pool {
23
+ release(): void;
24
+ beginTransaction(): Promise<void>;
25
+ commit(): Promise<void>;
26
+ rollback(): Promise<void>;
27
+ }
28
+
29
+ export function createPool(options: PoolOptions): Pool;
30
+ }