@stonyx/orm 0.2.1-beta.82 → 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 (150) hide show
  1. package/config/environment.js +17 -0
  2. package/dist/aggregates.d.ts +21 -0
  3. package/dist/aggregates.js +90 -0
  4. package/dist/attr.d.ts +2 -0
  5. package/dist/attr.js +22 -0
  6. package/dist/belongs-to.d.ts +11 -0
  7. package/dist/belongs-to.js +58 -0
  8. package/dist/cli.d.ts +22 -0
  9. package/dist/cli.js +148 -0
  10. package/dist/commands.d.ts +7 -0
  11. package/dist/commands.js +146 -0
  12. package/dist/db.d.ts +21 -0
  13. package/dist/db.js +174 -0
  14. package/dist/exports/db.d.ts +7 -0
  15. package/{src → dist}/exports/db.js +2 -4
  16. package/dist/has-many.d.ts +11 -0
  17. package/dist/has-many.js +57 -0
  18. package/dist/hooks.d.ts +47 -0
  19. package/dist/hooks.js +106 -0
  20. package/dist/index.d.ts +14 -0
  21. package/dist/index.js +34 -0
  22. package/dist/main.d.ts +46 -0
  23. package/dist/main.js +178 -0
  24. package/dist/manage-record.d.ts +13 -0
  25. package/dist/manage-record.js +113 -0
  26. package/dist/meta-request.d.ts +6 -0
  27. package/dist/meta-request.js +52 -0
  28. package/dist/migrate.d.ts +2 -0
  29. package/dist/migrate.js +57 -0
  30. package/dist/model-property.d.ts +9 -0
  31. package/dist/model-property.js +29 -0
  32. package/dist/model.d.ts +15 -0
  33. package/dist/model.js +18 -0
  34. package/dist/mysql/connection.d.ts +14 -0
  35. package/dist/mysql/connection.js +24 -0
  36. package/dist/mysql/migration-generator.d.ts +45 -0
  37. package/dist/mysql/migration-generator.js +245 -0
  38. package/dist/mysql/migration-runner.d.ts +12 -0
  39. package/dist/mysql/migration-runner.js +83 -0
  40. package/dist/mysql/mysql-db.d.ts +100 -0
  41. package/dist/mysql/mysql-db.js +411 -0
  42. package/dist/mysql/query-builder.d.ts +10 -0
  43. package/dist/mysql/query-builder.js +44 -0
  44. package/dist/mysql/schema-introspector.d.ts +19 -0
  45. package/dist/mysql/schema-introspector.js +286 -0
  46. package/dist/mysql/type-map.d.ts +21 -0
  47. package/dist/mysql/type-map.js +36 -0
  48. package/dist/orm-request.d.ts +38 -0
  49. package/dist/orm-request.js +453 -0
  50. package/dist/plural-registry.d.ts +4 -0
  51. package/{src → dist}/plural-registry.js +3 -6
  52. package/dist/postgres/connection.d.ts +15 -0
  53. package/dist/postgres/connection.js +30 -0
  54. package/dist/postgres/migration-generator.d.ts +45 -0
  55. package/dist/postgres/migration-generator.js +257 -0
  56. package/dist/postgres/migration-runner.d.ts +10 -0
  57. package/dist/postgres/migration-runner.js +82 -0
  58. package/dist/postgres/postgres-db.d.ts +119 -0
  59. package/dist/postgres/postgres-db.js +473 -0
  60. package/dist/postgres/query-builder.d.ts +27 -0
  61. package/dist/postgres/query-builder.js +98 -0
  62. package/dist/postgres/schema-introspector.d.ts +29 -0
  63. package/dist/postgres/schema-introspector.js +309 -0
  64. package/dist/postgres/type-map.d.ts +23 -0
  65. package/dist/postgres/type-map.js +53 -0
  66. package/dist/record.d.ts +75 -0
  67. package/dist/record.js +115 -0
  68. package/dist/relationships.d.ts +10 -0
  69. package/dist/relationships.js +35 -0
  70. package/dist/serializer.d.ts +17 -0
  71. package/dist/serializer.js +130 -0
  72. package/dist/setup-rest-server.d.ts +1 -0
  73. package/dist/setup-rest-server.js +54 -0
  74. package/dist/standalone-db.d.ts +58 -0
  75. package/dist/standalone-db.js +142 -0
  76. package/dist/store.d.ts +62 -0
  77. package/dist/store.js +271 -0
  78. package/dist/timescale/query-builder.d.ts +41 -0
  79. package/dist/timescale/query-builder.js +87 -0
  80. package/dist/timescale/timescale-db.d.ts +44 -0
  81. package/dist/timescale/timescale-db.js +81 -0
  82. package/dist/transforms.d.ts +2 -0
  83. package/dist/transforms.js +17 -0
  84. package/dist/types/orm-types.d.ts +142 -0
  85. package/dist/types/orm-types.js +1 -0
  86. package/dist/utils.d.ts +5 -0
  87. package/dist/utils.js +13 -0
  88. package/dist/view-resolver.d.ts +8 -0
  89. package/dist/view-resolver.js +165 -0
  90. package/dist/view.d.ts +11 -0
  91. package/dist/view.js +18 -0
  92. package/package.json +34 -11
  93. package/src/{aggregates.js → aggregates.ts} +27 -13
  94. package/src/{attr.js → attr.ts} +2 -2
  95. package/src/{belongs-to.js → belongs-to.ts} +36 -17
  96. package/src/{cli.js → cli.ts} +17 -11
  97. package/src/{commands.js → commands.ts} +179 -170
  98. package/src/{db.js → db.ts} +35 -26
  99. package/src/exports/db.ts +7 -0
  100. package/src/has-many.ts +91 -0
  101. package/src/{hooks.js → hooks.ts} +23 -27
  102. package/src/{index.js → index.ts} +4 -4
  103. package/src/{main.js → main.ts} +64 -34
  104. package/src/{manage-record.js → manage-record.ts} +41 -22
  105. package/src/{meta-request.js → meta-request.ts} +17 -14
  106. package/src/{migrate.js → migrate.ts} +9 -9
  107. package/src/{model-property.js → model-property.ts} +12 -6
  108. package/src/{model.js → model.ts} +5 -4
  109. package/src/mysql/{connection.js → connection.ts} +43 -28
  110. package/src/mysql/{migration-generator.js → migration-generator.ts} +332 -286
  111. package/src/mysql/{migration-runner.js → migration-runner.ts} +116 -110
  112. package/src/mysql/{mysql-db.js → mysql-db.ts} +533 -473
  113. package/src/mysql/{query-builder.js → query-builder.ts} +69 -64
  114. package/src/mysql/{schema-introspector.js → schema-introspector.ts} +355 -325
  115. package/src/mysql/{type-map.js → type-map.ts} +42 -37
  116. package/src/{orm-request.js → orm-request.ts} +165 -95
  117. package/src/plural-registry.ts +12 -0
  118. package/src/postgres/connection.ts +46 -0
  119. package/src/postgres/{migration-generator.js → migration-generator.ts} +82 -38
  120. package/src/postgres/{migration-runner.js → migration-runner.ts} +11 -10
  121. package/src/postgres/{postgres-db.js → postgres-db.ts} +199 -111
  122. package/src/postgres/{query-builder.js → query-builder.ts} +27 -28
  123. package/src/postgres/{schema-introspector.js → schema-introspector.ts} +87 -58
  124. package/src/postgres/{type-map.js → type-map.ts} +10 -6
  125. package/src/{record.js → record.ts} +73 -34
  126. package/src/relationships.ts +48 -0
  127. package/src/{serializer.js → serializer.ts} +44 -36
  128. package/src/{setup-rest-server.js → setup-rest-server.ts} +18 -13
  129. package/src/{standalone-db.js → standalone-db.ts} +33 -24
  130. package/src/{store.js → store.ts} +90 -68
  131. package/src/timescale/query-builder.ts +137 -0
  132. package/src/timescale/timescale-db.ts +107 -0
  133. package/src/transforms.ts +20 -0
  134. package/src/types/mysql2.d.ts +30 -0
  135. package/src/types/orm-types.ts +146 -0
  136. package/src/types/pg.d.ts +28 -0
  137. package/src/types/stonyx-cron.d.ts +5 -0
  138. package/src/types/stonyx-events.d.ts +4 -0
  139. package/src/types/stonyx-rest-server.d.ts +11 -0
  140. package/src/types/stonyx-utils.d.ts +33 -0
  141. package/src/types/stonyx.d.ts +21 -0
  142. package/src/utils.ts +16 -0
  143. package/src/{view-resolver.js → view-resolver.ts} +53 -28
  144. package/src/view.ts +22 -0
  145. package/src/has-many.js +0 -68
  146. package/src/postgres/connection.js +0 -30
  147. package/src/relationships.js +0 -43
  148. package/src/transforms.js +0 -20
  149. package/src/utils.js +0 -12
  150. 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
  }
@@ -0,0 +1,137 @@
1
+ // Re-export all base PostgreSQL query builders
2
+ export { validateIdentifier, buildInsert, buildUpdate, buildDelete, buildSelect } from '../postgres/query-builder.js';
3
+
4
+ import { validateIdentifier } from '../postgres/query-builder.js';
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
+
30
+ /**
31
+ * Build a CREATE TABLE + hypertable conversion statement.
32
+ * TimescaleDB hypertables are regular tables converted via create_hypertable().
33
+ */
34
+ export function buildCreateHypertable(table: string, timeColumn: string, options: HypertableOptions = {}): QueryResult {
35
+ validateIdentifier(table, 'table name');
36
+ validateIdentifier(timeColumn, 'column name');
37
+
38
+ const { chunkInterval = '7 days' } = options;
39
+
40
+ const sql = `SELECT create_hypertable('"${table}"', '${timeColumn}', chunk_time_interval => INTERVAL '${chunkInterval}', if_not_exists => TRUE)`;
41
+
42
+ return { sql, values: [] };
43
+ }
44
+
45
+ /**
46
+ * Build a time_bucket aggregation query.
47
+ */
48
+ export function buildTimeBucket(table: string, timeColumn: string, bucketSize: string, options: TimeBucketOptions = {}): QueryResult {
49
+ validateIdentifier(table, 'table name');
50
+ validateIdentifier(timeColumn, 'column name');
51
+
52
+ const { aggregates = [], where, orderBy = 'bucket', limit } = options;
53
+ const values: unknown[] = [];
54
+ let paramIndex = 1;
55
+
56
+ const selectCols: string[] = [`time_bucket($${paramIndex++}, "${timeColumn}") AS bucket`];
57
+ values.push(bucketSize);
58
+
59
+ for (const agg of aggregates) {
60
+ selectCols.push(agg);
61
+ }
62
+
63
+ const whereClauses: string[] = [];
64
+ if (where) {
65
+ for (const [k, v] of Object.entries(where)) {
66
+ validateIdentifier(k, 'column name');
67
+ whereClauses.push(`"${k}" = $${paramIndex++}`);
68
+ values.push(v);
69
+ }
70
+ }
71
+
72
+ const whereStr = whereClauses.length > 0 ? ` WHERE ${whereClauses.join(' AND ')}` : '';
73
+ const orderStr = orderBy ? ` ORDER BY ${orderBy}` : '';
74
+ let limitStr = '';
75
+ if (limit != null) {
76
+ limitStr = ` LIMIT $${paramIndex++}`;
77
+ values.push(limit);
78
+ }
79
+
80
+ const sql = `SELECT ${selectCols.join(', ')} FROM "${table}"${whereStr} GROUP BY bucket${orderStr}${limitStr}`;
81
+
82
+ return { sql, values };
83
+ }
84
+
85
+ /**
86
+ * Build a continuous aggregate creation statement.
87
+ */
88
+ export function buildContinuousAggregate(viewName: string, table: string, timeColumn: string, bucketSize: string, aggregates: string[], options: ContinuousAggregateOptions = {}): SqlResult {
89
+ validateIdentifier(viewName, 'view name');
90
+ validateIdentifier(table, 'table name');
91
+ validateIdentifier(timeColumn, 'column name');
92
+
93
+ const { withNoData = false } = options;
94
+
95
+ const selectCols: string[] = [
96
+ `time_bucket('${bucketSize}', "${timeColumn}") AS bucket`,
97
+ ...aggregates,
98
+ ];
99
+
100
+ const withClause = withNoData ? ' WITH NO DATA' : '';
101
+
102
+ const sql = `CREATE MATERIALIZED VIEW "${viewName}" WITH (timescaledb.continuous) AS SELECT ${selectCols.join(', ')} FROM "${table}" GROUP BY bucket${withClause}`;
103
+
104
+ return { sql };
105
+ }
106
+
107
+ /**
108
+ * Build an ADD compression policy statement.
109
+ */
110
+ export function buildCompressionPolicy(table: string, compressAfter: string): SqlResult {
111
+ validateIdentifier(table, 'table name');
112
+
113
+ const sql = `SELECT add_compression_policy('"${table}"', INTERVAL '${compressAfter}', if_not_exists => TRUE)`;
114
+
115
+ return { sql };
116
+ }
117
+
118
+ /**
119
+ * Build an ALTER TABLE to enable compression on a hypertable.
120
+ */
121
+ export function buildEnableCompression(table: string, segmentBy?: string, orderBy?: string): SqlResult {
122
+ validateIdentifier(table, 'table name');
123
+
124
+ let opts = `timescaledb.compress`;
125
+ if (segmentBy) {
126
+ validateIdentifier(segmentBy, 'column name');
127
+ opts += `, timescaledb.compress_segmentby = '"${segmentBy}"'`;
128
+ }
129
+ if (orderBy) {
130
+ validateIdentifier(orderBy, 'column name');
131
+ opts += `, timescaledb.compress_orderby = '"${orderBy}"'`;
132
+ }
133
+
134
+ const sql = `ALTER TABLE "${table}" SET (${opts})`;
135
+
136
+ return { sql };
137
+ }
@@ -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
+ }