@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.
- package/dist/aggregates.d.ts +21 -0
- package/dist/aggregates.js +90 -0
- package/dist/attr.d.ts +2 -0
- package/dist/attr.js +22 -0
- package/dist/belongs-to.d.ts +11 -0
- package/dist/belongs-to.js +58 -0
- package/dist/cli.d.ts +22 -0
- package/dist/cli.js +148 -0
- package/dist/commands.d.ts +7 -0
- package/dist/commands.js +146 -0
- package/dist/db.d.ts +21 -0
- package/dist/db.js +174 -0
- package/dist/exports/db.d.ts +7 -0
- package/{src → dist}/exports/db.js +2 -4
- package/dist/has-many.d.ts +11 -0
- package/dist/has-many.js +57 -0
- package/dist/hooks.d.ts +47 -0
- package/dist/hooks.js +106 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +34 -0
- package/dist/main.d.ts +46 -0
- package/dist/main.js +178 -0
- package/dist/manage-record.d.ts +13 -0
- package/dist/manage-record.js +113 -0
- package/dist/meta-request.d.ts +6 -0
- package/dist/meta-request.js +52 -0
- package/dist/migrate.d.ts +2 -0
- package/dist/migrate.js +57 -0
- package/dist/model-property.d.ts +9 -0
- package/dist/model-property.js +29 -0
- package/dist/model.d.ts +15 -0
- package/dist/model.js +18 -0
- package/dist/mysql/connection.d.ts +14 -0
- package/dist/mysql/connection.js +24 -0
- package/dist/mysql/migration-generator.d.ts +45 -0
- package/dist/mysql/migration-generator.js +245 -0
- package/dist/mysql/migration-runner.d.ts +12 -0
- package/dist/mysql/migration-runner.js +83 -0
- package/dist/mysql/mysql-db.d.ts +100 -0
- package/dist/mysql/mysql-db.js +411 -0
- package/dist/mysql/query-builder.d.ts +10 -0
- package/dist/mysql/query-builder.js +44 -0
- package/dist/mysql/schema-introspector.d.ts +19 -0
- package/dist/mysql/schema-introspector.js +286 -0
- package/dist/mysql/type-map.d.ts +21 -0
- package/dist/mysql/type-map.js +36 -0
- package/dist/orm-request.d.ts +38 -0
- package/dist/orm-request.js +453 -0
- package/dist/plural-registry.d.ts +4 -0
- package/{src → dist}/plural-registry.js +3 -6
- package/dist/postgres/connection.d.ts +15 -0
- package/dist/postgres/connection.js +30 -0
- package/dist/postgres/migration-generator.d.ts +45 -0
- package/dist/postgres/migration-generator.js +257 -0
- package/dist/postgres/migration-runner.d.ts +10 -0
- package/dist/postgres/migration-runner.js +82 -0
- package/dist/postgres/postgres-db.d.ts +119 -0
- package/dist/postgres/postgres-db.js +473 -0
- package/dist/postgres/query-builder.d.ts +27 -0
- package/dist/postgres/query-builder.js +98 -0
- package/dist/postgres/schema-introspector.d.ts +29 -0
- package/dist/postgres/schema-introspector.js +309 -0
- package/dist/postgres/type-map.d.ts +23 -0
- package/dist/postgres/type-map.js +53 -0
- package/dist/record.d.ts +75 -0
- package/dist/record.js +115 -0
- package/dist/relationships.d.ts +10 -0
- package/dist/relationships.js +35 -0
- package/dist/serializer.d.ts +17 -0
- package/dist/serializer.js +130 -0
- package/dist/setup-rest-server.d.ts +1 -0
- package/dist/setup-rest-server.js +54 -0
- package/dist/standalone-db.d.ts +58 -0
- package/dist/standalone-db.js +142 -0
- package/dist/store.d.ts +62 -0
- package/dist/store.js +271 -0
- package/dist/timescale/query-builder.d.ts +41 -0
- package/dist/timescale/query-builder.js +87 -0
- package/dist/timescale/timescale-db.d.ts +44 -0
- package/dist/timescale/timescale-db.js +81 -0
- package/dist/transforms.d.ts +2 -0
- package/dist/transforms.js +17 -0
- package/dist/types/orm-types.d.ts +142 -0
- package/dist/types/orm-types.js +1 -0
- package/dist/utils.d.ts +5 -0
- package/dist/utils.js +13 -0
- package/dist/view-resolver.d.ts +8 -0
- package/dist/view-resolver.js +165 -0
- package/dist/view.d.ts +11 -0
- package/dist/view.js +18 -0
- package/package.json +34 -11
- package/src/{aggregates.js → aggregates.ts} +27 -13
- package/src/{attr.js → attr.ts} +2 -2
- package/src/{belongs-to.js → belongs-to.ts} +36 -17
- package/src/{cli.js → cli.ts} +17 -11
- package/src/{commands.js → commands.ts} +179 -170
- package/src/{db.js → db.ts} +35 -26
- package/src/exports/db.ts +7 -0
- package/src/has-many.ts +91 -0
- package/src/{hooks.js → hooks.ts} +23 -27
- package/src/{index.js → index.ts} +4 -4
- package/src/{main.js → main.ts} +59 -34
- package/src/{manage-record.js → manage-record.ts} +41 -22
- package/src/{meta-request.js → meta-request.ts} +17 -14
- package/src/{migrate.js → migrate.ts} +9 -9
- package/src/{model-property.js → model-property.ts} +12 -6
- package/src/{model.js → model.ts} +5 -4
- package/src/mysql/{connection.js → connection.ts} +43 -28
- package/src/mysql/{migration-generator.js → migration-generator.ts} +332 -286
- package/src/mysql/{migration-runner.js → migration-runner.ts} +116 -110
- package/src/mysql/{mysql-db.js → mysql-db.ts} +533 -473
- package/src/mysql/{query-builder.js → query-builder.ts} +69 -64
- package/src/mysql/{schema-introspector.js → schema-introspector.ts} +355 -325
- package/src/mysql/{type-map.js → type-map.ts} +42 -37
- package/src/{orm-request.js → orm-request.ts} +165 -95
- package/src/plural-registry.ts +12 -0
- package/src/postgres/{connection.js → connection.ts} +14 -5
- package/src/postgres/{migration-generator.js → migration-generator.ts} +82 -38
- package/src/postgres/{migration-runner.js → migration-runner.ts} +11 -10
- package/src/postgres/{postgres-db.js → postgres-db.ts} +195 -114
- package/src/postgres/{query-builder.js → query-builder.ts} +27 -28
- package/src/postgres/{schema-introspector.js → schema-introspector.ts} +87 -58
- package/src/postgres/{type-map.js → type-map.ts} +10 -6
- package/src/{record.js → record.ts} +73 -34
- package/src/relationships.ts +48 -0
- package/src/{serializer.js → serializer.ts} +44 -36
- package/src/{setup-rest-server.js → setup-rest-server.ts} +18 -13
- package/src/{standalone-db.js → standalone-db.ts} +33 -24
- package/src/{store.js → store.ts} +90 -68
- package/src/timescale/{query-builder.js → query-builder.ts} +33 -38
- package/src/timescale/timescale-db.ts +107 -0
- package/src/transforms.ts +20 -0
- package/src/types/mysql2.d.ts +30 -0
- package/src/types/orm-types.ts +146 -0
- package/src/types/pg.d.ts +28 -0
- package/src/types/stonyx-cron.d.ts +5 -0
- package/src/types/stonyx-events.d.ts +4 -0
- package/src/types/stonyx-rest-server.d.ts +11 -0
- package/src/types/stonyx-utils.d.ts +33 -0
- package/src/types/stonyx.d.ts +21 -0
- package/src/utils.ts +16 -0
- package/src/{view-resolver.js → view-resolver.ts} +53 -28
- package/src/view.ts +22 -0
- package/src/has-many.js +0 -68
- package/src/relationships.js +0 -43
- package/src/timescale/timescale-db.js +0 -111
- package/src/transforms.js +0 -20
- package/src/utils.js +0 -12
- 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
|
|
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)
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
+
}
|