@stonyx/orm 0.2.1-beta.91 → 0.2.1-beta.93

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.
@@ -1,4 +1,5 @@
1
1
  import { introspectModels, introspectViews, buildTableDDL, buildViewDDL, buildVectorIndexDDL, schemasToSnapshot, viewSchemasToSnapshot, getTopologicalOrder } from './schema-introspector.js';
2
+ import { buildCreateHypertable, buildEnableCompression, buildCompressionPolicy } from '../timescale/query-builder.js';
2
3
  import { readFile, createFile, createDirectory, fileExists } from '@stonyx/utils/file';
3
4
  import path from 'path';
4
5
  import config from 'stonyx/config';
@@ -41,6 +42,24 @@ export async function generateMigration(description = 'migration', configKey = '
41
42
  }
42
43
  downStatements.unshift(`DROP TABLE IF EXISTS "${schemas[name].table}" CASCADE;`);
43
44
  }
45
+ // Hypertable conversion + compression (TimescaleDB only)
46
+ if (configKey === 'timescale') {
47
+ for (const name of addedOrdered) {
48
+ const schema = schemas[name];
49
+ if (!schema.hypertable)
50
+ continue;
51
+ const { timeColumn, chunkInterval } = schema.hypertable;
52
+ upStatements.push(buildCreateHypertable(schema.table, timeColumn, { chunkInterval }).sql + ';');
53
+ if (schema.hypertable.compress) {
54
+ const { segmentBy, orderBy, after } = schema.hypertable.compress;
55
+ upStatements.push(buildEnableCompression(schema.table, segmentBy, orderBy).sql + ';');
56
+ if (after) {
57
+ upStatements.push(buildCompressionPolicy(schema.table, after).sql + ';');
58
+ }
59
+ }
60
+ downStatements.unshift('-- Hypertable conversion is not reversible; table drop handles cleanup');
61
+ }
62
+ }
44
63
  // Removed tables (warn only, commented out)
45
64
  for (const name of diff.removedModels) {
46
65
  upStatements.push(`-- WARNING: Model '${name}' was removed. Uncomment to drop table:`);
@@ -1,4 +1,4 @@
1
- import type { ForeignKeyDef, ModelSchema, ViewSchema } from '../types/orm-types.js';
1
+ import type { ForeignKeyDef, HypertableConfig, ModelSchema, ViewSchema } from '../types/orm-types.js';
2
2
  interface ViewSnapshotEntry {
3
3
  viewName: string;
4
4
  source: string;
@@ -14,6 +14,7 @@ interface ModelSnapshotEntry {
14
14
  columns: Record<string, string>;
15
15
  foreignKeys: Record<string, ForeignKeyDef>;
16
16
  vectorColumns?: Record<string, number>;
17
+ hypertable?: HypertableConfig;
17
18
  }
18
19
  export declare function introspectModels(): Record<string, ModelSchema>;
19
20
  export declare function buildTableDDL(name: string, schema: ModelSchema, allSchemas?: Record<string, ModelSchema>): string;
@@ -55,6 +55,7 @@ export function introspectModels() {
55
55
  column: 'id',
56
56
  };
57
57
  }
58
+ const hypertable = modelClass.hypertable;
58
59
  schemas[name] = {
59
60
  table: sanitizeTableName(getPluralName(name)),
60
61
  idType,
@@ -62,19 +63,24 @@ export function introspectModels() {
62
63
  foreignKeys,
63
64
  relationships,
64
65
  vectorColumns,
66
+ hypertable: hypertable || undefined,
65
67
  memory: modelClass.memory === true,
66
68
  };
67
69
  }
68
70
  return schemas;
69
71
  }
70
72
  export function buildTableDDL(name, schema, allSchemas = {}) {
71
- const { idType, columns, foreignKeys } = schema;
73
+ const { idType, columns, foreignKeys, hypertable } = schema;
72
74
  const table = sanitizeTableName(schema.table);
73
75
  const lines = [];
76
+ const useCompositePK = hypertable && idType !== 'string';
74
77
  // Primary key
75
78
  if (idType === 'string') {
76
79
  lines.push(' "id" VARCHAR(255) PRIMARY KEY');
77
80
  }
81
+ else if (useCompositePK) {
82
+ lines.push(' "id" INTEGER GENERATED ALWAYS AS IDENTITY');
83
+ }
78
84
  else {
79
85
  lines.push(' "id" INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY');
80
86
  }
@@ -88,13 +94,22 @@ export function buildTableDDL(name, schema, allSchemas = {}) {
88
94
  lines.push(` "${fkCol}" ${refIdType}`);
89
95
  }
90
96
  // Timestamps
91
- lines.push(' "created_at" TIMESTAMPTZ DEFAULT NOW()');
97
+ if (useCompositePK) {
98
+ lines.push(' "created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW()');
99
+ }
100
+ else {
101
+ lines.push(' "created_at" TIMESTAMPTZ DEFAULT NOW()');
102
+ }
92
103
  lines.push(' "updated_at" TIMESTAMPTZ DEFAULT NOW()');
93
104
  // Foreign key constraints
94
105
  for (const [fkCol, fkDef] of Object.entries(foreignKeys)) {
95
106
  const refTable = sanitizeTableName(fkDef.references);
96
107
  lines.push(` FOREIGN KEY ("${fkCol}") REFERENCES "${refTable}"("${fkDef.column}") ON DELETE SET NULL`);
97
108
  }
109
+ // Composite primary key for hypertable models
110
+ if (useCompositePK) {
111
+ lines.push(` PRIMARY KEY ("id", "${hypertable.timeColumn}")`);
112
+ }
98
113
  return `CREATE TABLE IF NOT EXISTS "${table}" (\n${lines.join(',\n')}\n)`;
99
114
  }
100
115
  /**
@@ -274,6 +289,7 @@ export function schemasToSnapshot(schemas) {
274
289
  ...(schema.vectorColumns && Object.keys(schema.vectorColumns).length > 0
275
290
  ? { vectorColumns: { ...schema.vectorColumns } }
276
291
  : {}),
292
+ ...(schema.hypertable ? { hypertable: schema.hypertable } : {}),
277
293
  };
278
294
  }
279
295
  return snapshot;
@@ -86,6 +86,15 @@ export interface ForeignKeyDef {
86
86
  references: string;
87
87
  column: string;
88
88
  }
89
+ export interface HypertableConfig {
90
+ timeColumn: string;
91
+ chunkInterval?: string;
92
+ compress?: {
93
+ segmentBy?: string;
94
+ orderBy?: string;
95
+ after?: string;
96
+ };
97
+ }
89
98
  export interface ModelSchema {
90
99
  table: string;
91
100
  idType: string;
@@ -96,6 +105,7 @@ export interface ModelSchema {
96
105
  hasMany: Record<string, string | null>;
97
106
  };
98
107
  vectorColumns?: Record<string, number>;
108
+ hypertable?: HypertableConfig;
99
109
  memory: boolean;
100
110
  }
101
111
  export interface ViewSchema {
@@ -135,6 +145,7 @@ export interface SnapshotEntry {
135
145
  columns?: Record<string, string>;
136
146
  foreignKeys?: Record<string, ForeignKeyDef>;
137
147
  vectorColumns?: Record<string, number>;
148
+ hypertable?: HypertableConfig;
138
149
  isView?: boolean;
139
150
  viewName?: string;
140
151
  source?: string;
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "stonyx-async",
5
5
  "stonyx-module"
6
6
  ],
7
- "version": "0.2.1-beta.91",
7
+ "version": "0.2.1-beta.93",
8
8
  "description": "",
9
9
  "main": "dist/index.js",
10
10
  "type": "module",
package/src/db.ts CHANGED
@@ -115,7 +115,7 @@ export default class DB {
115
115
  const { autosave, saveInterval } = config.orm.db;
116
116
 
117
117
  store.set(dbKey, new Map());
118
- Orm.instance.models[`${dbKey}Model`] = await this.getSchema();
118
+ (Orm.instance as Orm).models[`${dbKey}Model`] = await this.getSchema();
119
119
 
120
120
  await this.validateMode();
121
121
  this.record = await this.getRecord();
@@ -1,4 +1,5 @@
1
1
  import { introspectModels, introspectViews, buildTableDDL, buildViewDDL, buildVectorIndexDDL, schemasToSnapshot, viewSchemasToSnapshot, getTopologicalOrder } from './schema-introspector.js';
2
+ import { buildCreateHypertable, buildEnableCompression, buildCompressionPolicy } from '../timescale/query-builder.js';
2
3
  import { readFile, createFile, createDirectory, fileExists } from '@stonyx/utils/file';
3
4
  import path from 'path';
4
5
  import config from 'stonyx/config';
@@ -97,6 +98,27 @@ export async function generateMigration(description: string = 'migration', confi
97
98
  downStatements.unshift(`DROP TABLE IF EXISTS "${schemas[name].table}" CASCADE;`);
98
99
  }
99
100
 
101
+ // Hypertable conversion + compression (TimescaleDB only)
102
+ if (configKey === 'timescale') {
103
+ for (const name of addedOrdered) {
104
+ const schema = schemas[name];
105
+ if (!schema.hypertable) continue;
106
+
107
+ const { timeColumn, chunkInterval } = schema.hypertable;
108
+ upStatements.push(buildCreateHypertable(schema.table, timeColumn, { chunkInterval }).sql + ';');
109
+
110
+ if (schema.hypertable.compress) {
111
+ const { segmentBy, orderBy, after } = schema.hypertable.compress;
112
+ upStatements.push(buildEnableCompression(schema.table, segmentBy, orderBy).sql + ';');
113
+ if (after) {
114
+ upStatements.push(buildCompressionPolicy(schema.table, after).sql + ';');
115
+ }
116
+ }
117
+
118
+ downStatements.unshift('-- Hypertable conversion is not reversible; table drop handles cleanup');
119
+ }
120
+ }
121
+
100
122
  // Removed tables (warn only, commented out)
101
123
  for (const name of diff.removedModels) {
102
124
  upStatements.push(`-- WARNING: Model '${name}' was removed. Uncomment to drop table:`);
@@ -5,7 +5,7 @@ import { getPluralName } from '../plural-registry.js';
5
5
  import { dbKey } from '../db.js';
6
6
  import { AggregateProperty } from '../aggregates.js';
7
7
  import { getRelationshipInfo, sanitizeTableName } from '../schema-helpers.js';
8
- import type { ForeignKeyDef, ModelSchema, ViewSchema } from '../types/orm-types.js';
8
+ import type { ForeignKeyDef, HypertableConfig, ModelSchema, ViewSchema } from '../types/orm-types.js';
9
9
  import ModelProperty from '../model-property.js';
10
10
 
11
11
  interface ViewSnapshotEntry {
@@ -24,6 +24,7 @@ interface ModelSnapshotEntry {
24
24
  columns: Record<string, string>;
25
25
  foreignKeys: Record<string, ForeignKeyDef>;
26
26
  vectorColumns?: Record<string, number>;
27
+ hypertable?: HypertableConfig;
27
28
  }
28
29
 
29
30
  interface JoinDef {
@@ -82,6 +83,8 @@ export function introspectModels(): Record<string, ModelSchema> {
82
83
  };
83
84
  }
84
85
 
86
+ const hypertable = (modelClass as { hypertable?: HypertableConfig }).hypertable;
87
+
85
88
  schemas[name] = {
86
89
  table: sanitizeTableName(getPluralName(name)),
87
90
  idType,
@@ -89,6 +92,7 @@ export function introspectModels(): Record<string, ModelSchema> {
89
92
  foreignKeys,
90
93
  relationships,
91
94
  vectorColumns,
95
+ hypertable: hypertable || undefined,
92
96
  memory: (modelClass as { memory?: boolean }).memory === true,
93
97
  };
94
98
  }
@@ -97,13 +101,16 @@ export function introspectModels(): Record<string, ModelSchema> {
97
101
  }
98
102
 
99
103
  export function buildTableDDL(name: string, schema: ModelSchema, allSchemas: Record<string, ModelSchema> = {}): string {
100
- const { idType, columns, foreignKeys } = schema;
104
+ const { idType, columns, foreignKeys, hypertable } = schema;
101
105
  const table = sanitizeTableName(schema.table);
102
106
  const lines: string[] = [];
107
+ const useCompositePK = hypertable && idType !== 'string';
103
108
 
104
109
  // Primary key
105
110
  if (idType === 'string') {
106
111
  lines.push(' "id" VARCHAR(255) PRIMARY KEY');
112
+ } else if (useCompositePK) {
113
+ lines.push(' "id" INTEGER GENERATED ALWAYS AS IDENTITY');
107
114
  } else {
108
115
  lines.push(' "id" INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY');
109
116
  }
@@ -120,7 +127,11 @@ export function buildTableDDL(name: string, schema: ModelSchema, allSchemas: Rec
120
127
  }
121
128
 
122
129
  // Timestamps
123
- lines.push(' "created_at" TIMESTAMPTZ DEFAULT NOW()');
130
+ if (useCompositePK) {
131
+ lines.push(' "created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW()');
132
+ } else {
133
+ lines.push(' "created_at" TIMESTAMPTZ DEFAULT NOW()');
134
+ }
124
135
  lines.push(' "updated_at" TIMESTAMPTZ DEFAULT NOW()');
125
136
 
126
137
  // Foreign key constraints
@@ -129,6 +140,11 @@ export function buildTableDDL(name: string, schema: ModelSchema, allSchemas: Rec
129
140
  lines.push(` FOREIGN KEY ("${fkCol}") REFERENCES "${refTable}"("${fkDef.column}") ON DELETE SET NULL`);
130
141
  }
131
142
 
143
+ // Composite primary key for hypertable models
144
+ if (useCompositePK) {
145
+ lines.push(` PRIMARY KEY ("id", "${hypertable.timeColumn}")`);
146
+ }
147
+
132
148
  return `CREATE TABLE IF NOT EXISTS "${table}" (\n${lines.join(',\n')}\n)`;
133
149
  }
134
150
 
@@ -336,6 +352,7 @@ export function schemasToSnapshot(schemas: Record<string, ModelSchema>): Record<
336
352
  ...(schema.vectorColumns && Object.keys(schema.vectorColumns).length > 0
337
353
  ? { vectorColumns: { ...schema.vectorColumns } }
338
354
  : {}),
355
+ ...(schema.hypertable ? { hypertable: schema.hypertable } : {}),
339
356
  };
340
357
  }
341
358
 
@@ -31,9 +31,9 @@ export default async function(route: string, accessPath: string, metaRoute: bool
31
31
  for (const model of models === '*' ? availableModels : models) {
32
32
  if (model === dbKey) continue;
33
33
  if (!store.data.has(model)) throw new Error(`Unable to define access for Invalid Model "${model}". Model does not exist`);
34
- if (accessFiles[model]) throw new Error(`Access for model "${model}" has already been defined by another access class.`);
34
+ if (accessFiles![model]) throw new Error(`Access for model "${model}" has already been defined by another access class.`);
35
35
 
36
- accessFiles[model] = accessInstance.access;
36
+ accessFiles![model] = accessInstance.access;
37
37
  }
38
38
  });
39
39
  } catch (error) {
@@ -47,7 +47,7 @@ export default async function(route: string, accessPath: string, metaRoute: bool
47
47
  const name = route === '/' ? 'index' : (route[0] === '/' ? route.slice(1) : route);
48
48
 
49
49
  // Configure endpoints for models and views with access configuration
50
- for (const [model, access] of Object.entries(accessFiles)) {
50
+ for (const [model, access] of Object.entries(accessFiles!)) {
51
51
  const pluralizedModel = getPluralName(model);
52
52
  const modelName = name === 'index' ? pluralizedModel : `${name}/${pluralizedModel}`;
53
53
  RestServer.instance.mountRoute(OrmRequest, { name: modelName, options: { model, access } });
package/src/store.ts CHANGED
@@ -315,9 +315,9 @@ export default class Store {
315
315
  }
316
316
  } else if (value && isStoreRecord(value) && value.__model && !this._isBidirectionalRelationship(
317
317
  record.__model.__name,
318
- value.__model.__name
318
+ (value as StoreRecord).__model.__name
319
319
  )) {
320
- children.push({ childRecord: value, relationshipKey: key, type: 'belongsTo' });
320
+ children.push({ childRecord: value as StoreRecord, relationshipKey: key, type: 'belongsTo' });
321
321
  }
322
322
  }
323
323
 
@@ -86,6 +86,16 @@ export interface ForeignKeyDef {
86
86
  column: string;
87
87
  }
88
88
 
89
+ export interface HypertableConfig {
90
+ timeColumn: string;
91
+ chunkInterval?: string;
92
+ compress?: {
93
+ segmentBy?: string;
94
+ orderBy?: string;
95
+ after?: string;
96
+ };
97
+ }
98
+
89
99
  export interface ModelSchema {
90
100
  table: string;
91
101
  idType: string;
@@ -96,6 +106,7 @@ export interface ModelSchema {
96
106
  hasMany: Record<string, string | null>;
97
107
  };
98
108
  vectorColumns?: Record<string, number>;
109
+ hypertable?: HypertableConfig;
99
110
  memory: boolean;
100
111
  }
101
112
 
@@ -139,6 +150,7 @@ export interface SnapshotEntry {
139
150
  columns?: Record<string, string>;
140
151
  foreignKeys?: Record<string, ForeignKeyDef>;
141
152
  vectorColumns?: Record<string, number>;
153
+ hypertable?: HypertableConfig;
142
154
  isView?: boolean;
143
155
  viewName?: string;
144
156
  source?: string;
@@ -156,8 +156,8 @@ export default class ViewResolver {
156
156
  if (!aggProp.relationship) continue;
157
157
  const allRelated: unknown[] = [];
158
158
  for (const record of groupRecords) {
159
- const relatedRecords = record.__relationships?.[aggProp.relationship]
160
- || record[aggProp.relationship];
159
+ const relatedRecords = record.__relationships?.[aggProp.relationship!]
160
+ || record[aggProp.relationship!];
161
161
  if (Array.isArray(relatedRecords)) {
162
162
  allRelated.push(...relatedRecords);
163
163
  }