@stonyx/orm 0.2.5-alpha.0 → 0.3.1
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/README.md +482 -15
- package/config/environment.js +63 -6
- package/dist/aggregates.d.ts +21 -0
- package/dist/aggregates.js +93 -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 +59 -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 +180 -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 +58 -0
- package/dist/hooks.d.ts +75 -0
- package/dist/hooks.js +110 -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 +181 -0
- package/dist/manage-record.d.ts +13 -0
- package/dist/manage-record.js +123 -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 +254 -0
- package/dist/mysql/migration-runner.d.ts +12 -0
- package/dist/mysql/migration-runner.js +88 -0
- package/dist/mysql/mysql-db.d.ts +100 -0
- package/dist/mysql/mysql-db.js +425 -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 +257 -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 +475 -0
- package/dist/plural-registry.d.ts +4 -0
- package/dist/plural-registry.js +9 -0
- package/dist/postgres/connection.d.ts +15 -0
- package/dist/postgres/connection.js +32 -0
- package/dist/postgres/migration-generator.d.ts +45 -0
- package/dist/postgres/migration-generator.js +280 -0
- package/dist/postgres/migration-runner.d.ts +10 -0
- package/dist/postgres/migration-runner.js +87 -0
- package/dist/postgres/postgres-db.d.ts +119 -0
- package/dist/postgres/postgres-db.js +477 -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 +296 -0
- package/dist/postgres/type-map.d.ts +23 -0
- package/dist/postgres/type-map.js +56 -0
- package/dist/record.d.ts +75 -0
- package/dist/record.js +129 -0
- package/dist/relationships.d.ts +10 -0
- package/dist/relationships.js +41 -0
- package/dist/schema-helpers.d.ts +20 -0
- package/dist/schema-helpers.js +48 -0
- package/dist/serializer.d.ts +17 -0
- package/dist/serializer.js +136 -0
- package/dist/setup-rest-server.d.ts +1 -0
- package/dist/setup-rest-server.js +52 -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 +286 -0
- package/dist/timescale/query-builder.d.ts +43 -0
- package/dist/timescale/query-builder.js +115 -0
- package/dist/timescale/timescale-db.d.ts +45 -0
- package/dist/timescale/timescale-db.js +84 -0
- package/dist/transforms.d.ts +2 -0
- package/dist/transforms.js +17 -0
- package/dist/types/orm-types.d.ts +153 -0
- package/dist/types/orm-types.js +1 -0
- package/dist/utils.d.ts +7 -0
- package/dist/utils.js +17 -0
- package/dist/view-resolver.d.ts +8 -0
- package/dist/view-resolver.js +171 -0
- package/dist/view.d.ts +11 -0
- package/dist/view.js +18 -0
- package/package.json +64 -11
- package/src/aggregates.ts +109 -0
- package/src/{attr.js → attr.ts} +2 -2
- package/src/belongs-to.ts +90 -0
- package/src/cli.ts +183 -0
- package/src/commands.ts +179 -0
- package/src/db.ts +232 -0
- package/src/exports/db.ts +7 -0
- package/src/has-many.ts +92 -0
- package/src/hooks.ts +151 -0
- package/src/{index.js → index.ts} +12 -2
- package/src/main.ts +229 -0
- package/src/manage-record.ts +161 -0
- package/src/{meta-request.js → meta-request.ts} +17 -14
- package/src/migrate.ts +72 -0
- package/src/model-property.ts +35 -0
- package/src/model.ts +21 -0
- package/src/mysql/connection.ts +43 -0
- package/src/mysql/migration-generator.ts +337 -0
- package/src/mysql/migration-runner.ts +121 -0
- package/src/mysql/mysql-db.ts +543 -0
- package/src/mysql/query-builder.ts +69 -0
- package/src/mysql/schema-introspector.ts +310 -0
- package/src/mysql/type-map.ts +42 -0
- package/src/orm-request.ts +582 -0
- package/src/plural-registry.ts +12 -0
- package/src/postgres/connection.ts +48 -0
- package/src/postgres/migration-generator.ts +370 -0
- package/src/postgres/migration-runner.ts +115 -0
- package/src/postgres/postgres-db.ts +616 -0
- package/src/postgres/query-builder.ts +148 -0
- package/src/postgres/schema-introspector.ts +360 -0
- package/src/postgres/type-map.ts +61 -0
- package/src/record.ts +186 -0
- package/src/relationships.ts +54 -0
- package/src/schema-helpers.ts +59 -0
- package/src/serializer.ts +161 -0
- package/src/setup-rest-server.ts +62 -0
- package/src/standalone-db.ts +185 -0
- package/src/store.ts +373 -0
- package/src/timescale/query-builder.ts +174 -0
- package/src/timescale/timescale-db.ts +119 -0
- package/src/transforms.ts +20 -0
- package/src/types/mysql2.d.ts +49 -0
- package/src/types/orm-types.ts +158 -0
- package/src/types/pg.d.ts +32 -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 +16 -0
- package/src/types/stonyx-utils.d.ts +33 -0
- package/src/types/stonyx.d.ts +21 -0
- package/src/utils.ts +22 -0
- package/src/view-resolver.ts +211 -0
- package/src/view.ts +22 -0
- package/.claude/project-structure.md +0 -578
- package/.github/workflows/ci.yml +0 -36
- package/.github/workflows/publish.yml +0 -143
- package/src/belongs-to.js +0 -63
- package/src/db.js +0 -80
- package/src/has-many.js +0 -61
- package/src/main.js +0 -119
- package/src/manage-record.js +0 -103
- package/src/model-property.js +0 -29
- package/src/model.js +0 -9
- package/src/orm-request.js +0 -249
- package/src/record.js +0 -100
- package/src/relationships.js +0 -43
- package/src/serializer.js +0 -138
- package/src/setup-rest-server.js +0 -57
- package/src/store.js +0 -211
- package/src/transforms.js +0 -20
- package/stonyx-bootstrap.cjs +0 -30
|
@@ -0,0 +1,174 @@
|
|
|
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
|
+
const SAFE_INTERVAL = /^\d+\s+(microsecond|millisecond|second|minute|hour|day|week|month|year)s?$/i;
|
|
7
|
+
|
|
8
|
+
export function validateInterval(interval: string, context: string = 'interval'): string {
|
|
9
|
+
if (!interval || typeof interval !== 'string' || !SAFE_INTERVAL.test(interval.trim())) {
|
|
10
|
+
throw new Error(`Invalid SQL ${context}: "${interval}". Intervals must match pattern like "7 days", "1 hour", "30 minutes".`);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return interval.trim();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const SAFE_AGGREGATE = /^(COUNT|SUM|AVG|MIN|MAX|FIRST|LAST)\s*\(\s*("?[a-zA-Z_][a-zA-Z0-9_]*"?|\*)\s*\)\s*(AS\s+"?[a-zA-Z_][a-zA-Z0-9_]*"?)?$/i;
|
|
17
|
+
|
|
18
|
+
export function validateAggregate(expr: string, context: string = 'aggregate'): string {
|
|
19
|
+
if (!expr || typeof expr !== 'string' || !SAFE_AGGREGATE.test(expr.trim())) {
|
|
20
|
+
throw new Error(`Invalid SQL ${context}: "${expr}". Aggregates must be simple function calls like "AVG(value) AS avg_value".`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return expr.trim();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface QueryResult {
|
|
27
|
+
sql: string;
|
|
28
|
+
values: unknown[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface SqlResult {
|
|
32
|
+
sql: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface HypertableOptions {
|
|
36
|
+
chunkInterval?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface TimeBucketOptions {
|
|
40
|
+
aggregates?: string[];
|
|
41
|
+
where?: Record<string, unknown>;
|
|
42
|
+
orderBy?: string;
|
|
43
|
+
limit?: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface ContinuousAggregateOptions {
|
|
47
|
+
withNoData?: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Build a CREATE TABLE + hypertable conversion statement.
|
|
52
|
+
* TimescaleDB hypertables are regular tables converted via create_hypertable().
|
|
53
|
+
*/
|
|
54
|
+
export function buildCreateHypertable(table: string, timeColumn: string, options: HypertableOptions = {}): QueryResult {
|
|
55
|
+
validateIdentifier(table, 'table name');
|
|
56
|
+
validateIdentifier(timeColumn, 'column name');
|
|
57
|
+
|
|
58
|
+
const { chunkInterval = '7 days' } = options;
|
|
59
|
+
validateInterval(chunkInterval, 'chunk interval');
|
|
60
|
+
|
|
61
|
+
const sql = `SELECT create_hypertable('"${table}"', '${timeColumn}', chunk_time_interval => INTERVAL '${chunkInterval}', if_not_exists => TRUE)`;
|
|
62
|
+
|
|
63
|
+
return { sql, values: [] };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Build a time_bucket aggregation query.
|
|
68
|
+
*/
|
|
69
|
+
export function buildTimeBucket(table: string, timeColumn: string, bucketSize: string, options: TimeBucketOptions = {}): QueryResult {
|
|
70
|
+
validateIdentifier(table, 'table name');
|
|
71
|
+
validateIdentifier(timeColumn, 'column name');
|
|
72
|
+
|
|
73
|
+
const { aggregates = [], where, orderBy = 'bucket', limit } = options;
|
|
74
|
+
const values: unknown[] = [];
|
|
75
|
+
let paramIndex = 1;
|
|
76
|
+
|
|
77
|
+
const selectCols: string[] = [`time_bucket($${paramIndex++}, "${timeColumn}") AS bucket`];
|
|
78
|
+
values.push(bucketSize);
|
|
79
|
+
|
|
80
|
+
for (const agg of aggregates) {
|
|
81
|
+
selectCols.push(validateAggregate(agg));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const whereClauses: string[] = [];
|
|
85
|
+
if (where) {
|
|
86
|
+
for (const [k, v] of Object.entries(where)) {
|
|
87
|
+
validateIdentifier(k, 'column name');
|
|
88
|
+
whereClauses.push(`"${k}" = $${paramIndex++}`);
|
|
89
|
+
values.push(v);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const whereStr = whereClauses.length > 0 ? ` WHERE ${whereClauses.join(' AND ')}` : '';
|
|
94
|
+
let orderStr = '';
|
|
95
|
+
if (orderBy) {
|
|
96
|
+
const parts = orderBy.trim().split(/\s+/);
|
|
97
|
+
const col = parts[0];
|
|
98
|
+
const dir = parts[1]?.toUpperCase();
|
|
99
|
+
|
|
100
|
+
validateIdentifier(col, 'ORDER BY column');
|
|
101
|
+
|
|
102
|
+
if (dir && dir !== 'ASC' && dir !== 'DESC') {
|
|
103
|
+
throw new Error(`Invalid ORDER BY direction: "${dir}". Must be ASC or DESC.`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
orderStr = ` ORDER BY "${col}"${dir ? ` ${dir}` : ''}`;
|
|
107
|
+
}
|
|
108
|
+
let limitStr = '';
|
|
109
|
+
if (limit != null) {
|
|
110
|
+
limitStr = ` LIMIT $${paramIndex++}`;
|
|
111
|
+
values.push(limit);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const sql = `SELECT ${selectCols.join(', ')} FROM "${table}"${whereStr} GROUP BY bucket${orderStr}${limitStr}`;
|
|
115
|
+
|
|
116
|
+
return { sql, values };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Build a continuous aggregate creation statement.
|
|
121
|
+
*/
|
|
122
|
+
export function buildContinuousAggregate(viewName: string, table: string, timeColumn: string, bucketSize: string, aggregates: string[], options: ContinuousAggregateOptions = {}): SqlResult {
|
|
123
|
+
validateIdentifier(viewName, 'view name');
|
|
124
|
+
validateIdentifier(table, 'table name');
|
|
125
|
+
validateIdentifier(timeColumn, 'column name');
|
|
126
|
+
|
|
127
|
+
const { withNoData = false } = options;
|
|
128
|
+
validateInterval(bucketSize, 'bucket size');
|
|
129
|
+
aggregates.forEach(agg => validateAggregate(agg));
|
|
130
|
+
|
|
131
|
+
const selectCols: string[] = [
|
|
132
|
+
`time_bucket('${bucketSize}', "${timeColumn}") AS bucket`,
|
|
133
|
+
...aggregates,
|
|
134
|
+
];
|
|
135
|
+
|
|
136
|
+
const withClause = withNoData ? ' WITH NO DATA' : '';
|
|
137
|
+
|
|
138
|
+
const sql = `CREATE MATERIALIZED VIEW "${viewName}" WITH (timescaledb.continuous) AS SELECT ${selectCols.join(', ')} FROM "${table}" GROUP BY bucket${withClause}`;
|
|
139
|
+
|
|
140
|
+
return { sql };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Build an ADD compression policy statement.
|
|
145
|
+
*/
|
|
146
|
+
export function buildCompressionPolicy(table: string, compressAfter: string): SqlResult {
|
|
147
|
+
validateIdentifier(table, 'table name');
|
|
148
|
+
validateInterval(compressAfter, 'compress after interval');
|
|
149
|
+
|
|
150
|
+
const sql = `SELECT add_compression_policy('"${table}"', INTERVAL '${compressAfter}', if_not_exists => TRUE)`;
|
|
151
|
+
|
|
152
|
+
return { sql };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Build an ALTER TABLE to enable compression on a hypertable.
|
|
157
|
+
*/
|
|
158
|
+
export function buildEnableCompression(table: string, segmentBy?: string, orderBy?: string): SqlResult {
|
|
159
|
+
validateIdentifier(table, 'table name');
|
|
160
|
+
|
|
161
|
+
let opts = `timescaledb.compress`;
|
|
162
|
+
if (segmentBy) {
|
|
163
|
+
validateIdentifier(segmentBy, 'column name');
|
|
164
|
+
opts += `, timescaledb.compress_segmentby = '"${segmentBy}"'`;
|
|
165
|
+
}
|
|
166
|
+
if (orderBy) {
|
|
167
|
+
validateIdentifier(orderBy, 'column name');
|
|
168
|
+
opts += `, timescaledb.compress_orderby = '"${orderBy}"'`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const sql = `ALTER TABLE "${table}" SET (${opts})`;
|
|
172
|
+
|
|
173
|
+
return { sql };
|
|
174
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
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
|
+
interface TimescaleDeps {
|
|
26
|
+
buildCreateHypertable: typeof buildCreateHypertable;
|
|
27
|
+
buildTimeBucket: typeof buildTimeBucket;
|
|
28
|
+
buildContinuousAggregate: typeof buildContinuousAggregate;
|
|
29
|
+
buildEnableCompression: typeof buildEnableCompression;
|
|
30
|
+
buildCompressionPolicy: typeof buildCompressionPolicy;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export default class TimescaleDB extends PostgresDB {
|
|
34
|
+
static override extensions: string[] = ['timescaledb'];
|
|
35
|
+
static override configKey: string = 'timescale';
|
|
36
|
+
|
|
37
|
+
constructor(deps: Record<string, unknown> = {}) {
|
|
38
|
+
super({
|
|
39
|
+
...deps,
|
|
40
|
+
buildCreateHypertable,
|
|
41
|
+
buildTimeBucket,
|
|
42
|
+
buildContinuousAggregate,
|
|
43
|
+
buildCompressionPolicy,
|
|
44
|
+
buildEnableCompression,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private get tsDeps(): TimescaleDeps {
|
|
49
|
+
return this.deps as unknown as TimescaleDeps;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Convert a table to a TimescaleDB hypertable.
|
|
54
|
+
* Should be called after the table is created (e.g. after initial migration).
|
|
55
|
+
*/
|
|
56
|
+
async createHypertable(modelName: string, timeColumn: string, options: HypertableOptions = {}): Promise<void> {
|
|
57
|
+
const schemas = this.deps.introspectModels();
|
|
58
|
+
const schema = schemas[modelName];
|
|
59
|
+
if (!schema) throw new Error(`Model '${modelName}' not found`);
|
|
60
|
+
|
|
61
|
+
const { sql } = this.tsDeps.buildCreateHypertable(schema.table, timeColumn, options);
|
|
62
|
+
await this.requirePool().query(sql);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Query time-bucketed aggregations on a hypertable.
|
|
67
|
+
*/
|
|
68
|
+
async timeBucket(modelName: string, timeColumn: string, bucketSize: string, options: TimeBucketOptions = {}): Promise<Record<string, unknown>[]> {
|
|
69
|
+
const schemas = this.deps.introspectModels();
|
|
70
|
+
const schema = schemas[modelName];
|
|
71
|
+
if (!schema) return [];
|
|
72
|
+
|
|
73
|
+
const { sql, values } = this.tsDeps.buildTimeBucket(schema.table, timeColumn, bucketSize, options);
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const result = await this.requirePool().query(sql, values);
|
|
77
|
+
return result.rows;
|
|
78
|
+
} catch (error) {
|
|
79
|
+
if (isDbError(error) && error.code === '42P01') return [];
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Create a continuous aggregate view on a hypertable.
|
|
86
|
+
*/
|
|
87
|
+
async createContinuousAggregate(viewName: string, modelName: string, timeColumn: string, bucketSize: string, aggregates: string[], options: ContinuousAggregateOptions = {}): 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.tsDeps.buildContinuousAggregate(viewName, schema.table, timeColumn, bucketSize, aggregates, options);
|
|
93
|
+
await this.requirePool().query(sql);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Enable compression on a hypertable.
|
|
98
|
+
*/
|
|
99
|
+
async enableCompression(modelName: string, options: CompressionOptions = {}): 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.tsDeps.buildEnableCompression(schema.table, options.segmentBy, options.orderBy);
|
|
105
|
+
await this.requirePool().query(sql);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Add a compression policy to a hypertable.
|
|
110
|
+
*/
|
|
111
|
+
async addCompressionPolicy(modelName: string, compressAfter: string): Promise<void> {
|
|
112
|
+
const schemas = this.deps.introspectModels();
|
|
113
|
+
const schema = schemas[modelName];
|
|
114
|
+
if (!schema) throw new Error(`Model '${modelName}' not found`);
|
|
115
|
+
|
|
116
|
+
const { sql } = this.tsDeps.buildCompressionPolicy(schema.table, compressAfter);
|
|
117
|
+
await this.requirePool().query(sql);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -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 as string | number | Date | undefined),
|
|
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,49 @@
|
|
|
1
|
+
declare module 'mysql2/promise' {
|
|
2
|
+
interface PoolOptions {
|
|
3
|
+
host: string;
|
|
4
|
+
user: string;
|
|
5
|
+
password: string;
|
|
6
|
+
database: string;
|
|
7
|
+
port?: number;
|
|
8
|
+
waitForConnections?: boolean;
|
|
9
|
+
connectionLimit?: number;
|
|
10
|
+
queueLimit?: number;
|
|
11
|
+
enableKeepAlive?: boolean;
|
|
12
|
+
keepAliveInitialDelay?: number;
|
|
13
|
+
[key: string]: string | number | boolean | undefined;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface ExecuteResult {
|
|
17
|
+
insertId: number;
|
|
18
|
+
affectedRows: number;
|
|
19
|
+
changedRows: number;
|
|
20
|
+
fieldCount: number;
|
|
21
|
+
info: string;
|
|
22
|
+
serverStatus: number;
|
|
23
|
+
warningStatus: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface FieldPacket {
|
|
27
|
+
name: string;
|
|
28
|
+
type: number;
|
|
29
|
+
length: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
type RowDataPacket = Record<string, string | number | boolean | null>;
|
|
33
|
+
|
|
34
|
+
interface Pool {
|
|
35
|
+
execute(sql: string, params?: unknown[]): Promise<[RowDataPacket[] | ExecuteResult, FieldPacket[]]>;
|
|
36
|
+
query(sql: string, params?: unknown[]): Promise<[RowDataPacket[] | ExecuteResult, FieldPacket[]]>;
|
|
37
|
+
end(): Promise<void>;
|
|
38
|
+
getConnection(): Promise<PoolConnection>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface PoolConnection extends Pool {
|
|
42
|
+
release(): void;
|
|
43
|
+
beginTransaction(): Promise<void>;
|
|
44
|
+
commit(): Promise<void>;
|
|
45
|
+
rollback(): Promise<void>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function createPool(options: PoolOptions): Pool;
|
|
49
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import type { AggregateProperty } from '../aggregates.js';
|
|
2
|
+
|
|
3
|
+
export interface OrmDbConfig {
|
|
4
|
+
file: string;
|
|
5
|
+
schema: string;
|
|
6
|
+
mode: string;
|
|
7
|
+
directory: string;
|
|
8
|
+
autosave: string;
|
|
9
|
+
saveInterval: string | number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface OrmMysqlConfig {
|
|
13
|
+
host: string;
|
|
14
|
+
port?: number;
|
|
15
|
+
user: string;
|
|
16
|
+
password: string;
|
|
17
|
+
database: string;
|
|
18
|
+
connectionLimit?: number;
|
|
19
|
+
migrationsDir?: string;
|
|
20
|
+
migrationsTable?: string;
|
|
21
|
+
[key: string]: unknown;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface OrmPostgresConfig {
|
|
25
|
+
host: string;
|
|
26
|
+
port: number;
|
|
27
|
+
user: string;
|
|
28
|
+
password: string;
|
|
29
|
+
database: string;
|
|
30
|
+
connectionLimit?: number;
|
|
31
|
+
migrationsDir?: string;
|
|
32
|
+
migrationsTable?: string;
|
|
33
|
+
[key: string]: unknown;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface OrmPaths {
|
|
37
|
+
model: string;
|
|
38
|
+
serializer: string;
|
|
39
|
+
transform: string;
|
|
40
|
+
view?: string;
|
|
41
|
+
access?: string;
|
|
42
|
+
[key: string]: string | undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface OrmRestServerConfig {
|
|
46
|
+
enabled: string;
|
|
47
|
+
route: string;
|
|
48
|
+
metaRoute: boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface OrmSection {
|
|
52
|
+
db: OrmDbConfig;
|
|
53
|
+
paths: OrmPaths;
|
|
54
|
+
restServer: OrmRestServerConfig;
|
|
55
|
+
mysql?: OrmMysqlConfig;
|
|
56
|
+
postgres?: OrmPostgresConfig;
|
|
57
|
+
timescale?: OrmPostgresConfig;
|
|
58
|
+
[key: string]: unknown;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface OrmConfig {
|
|
62
|
+
rootPath: string;
|
|
63
|
+
orm: OrmSection;
|
|
64
|
+
[key: string]: unknown;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface SourceRecord {
|
|
68
|
+
__model: { __name: string; [key: string]: unknown };
|
|
69
|
+
__data?: Record<string, unknown>;
|
|
70
|
+
__relationships?: Record<string, unknown>;
|
|
71
|
+
id: string | number;
|
|
72
|
+
[key: string]: unknown;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface OrmRecord {
|
|
76
|
+
id: string | number;
|
|
77
|
+
__model?: { __name: string };
|
|
78
|
+
__data: Record<string, unknown> & { id?: string | number; __pendingSqlId?: boolean };
|
|
79
|
+
__relationships: Record<string, unknown>;
|
|
80
|
+
toJSON?(options?: { fields?: Set<string>; baseUrl?: string }): Record<string, unknown>;
|
|
81
|
+
[key: string]: unknown;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface ForeignKeyDef {
|
|
85
|
+
references: string;
|
|
86
|
+
column: string;
|
|
87
|
+
}
|
|
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
|
+
|
|
99
|
+
export interface ModelSchema {
|
|
100
|
+
table: string;
|
|
101
|
+
idType: string;
|
|
102
|
+
columns: Record<string, string>;
|
|
103
|
+
foreignKeys: Record<string, ForeignKeyDef>;
|
|
104
|
+
relationships: {
|
|
105
|
+
belongsTo: Record<string, string | null>;
|
|
106
|
+
hasMany: Record<string, string | null>;
|
|
107
|
+
};
|
|
108
|
+
vectorColumns?: Record<string, number>;
|
|
109
|
+
hypertable?: HypertableConfig;
|
|
110
|
+
memory: boolean;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface ViewSchema {
|
|
114
|
+
viewName: string;
|
|
115
|
+
source: string;
|
|
116
|
+
groupBy?: string;
|
|
117
|
+
columns: Record<string, string>;
|
|
118
|
+
foreignKeys: Record<string, ForeignKeyDef>;
|
|
119
|
+
aggregates: Record<string, AggregateProperty>;
|
|
120
|
+
relationships: {
|
|
121
|
+
belongsTo: Record<string, string | null>;
|
|
122
|
+
hasMany: Record<string, string | null>;
|
|
123
|
+
};
|
|
124
|
+
isView: boolean;
|
|
125
|
+
memory: boolean;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Typed relationship registry maps.
|
|
130
|
+
* Each key in Orm.relationships stores a different nested Map structure.
|
|
131
|
+
*/
|
|
132
|
+
/** Relationship registry map types — source → target → recordId → value */
|
|
133
|
+
export type HasManyMap = Map<string, Map<string, Map<unknown, unknown[]>>>;
|
|
134
|
+
export type BelongsToMap = Map<string, Map<string, Map<unknown, unknown>>>;
|
|
135
|
+
export type GlobalMap = Map<string, unknown[][]>;
|
|
136
|
+
export type PendingMap = Map<string, Map<unknown, unknown[][]>>;
|
|
137
|
+
export type PendingBelongsToMap = Map<string, Map<unknown, unknown[]>>;
|
|
138
|
+
|
|
139
|
+
export interface RelationshipMaps {
|
|
140
|
+
hasMany: HasManyMap;
|
|
141
|
+
belongsTo: BelongsToMap;
|
|
142
|
+
global: GlobalMap;
|
|
143
|
+
pending: PendingMap;
|
|
144
|
+
pendingBelongsTo: PendingBelongsToMap;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export interface SnapshotEntry {
|
|
148
|
+
table?: string;
|
|
149
|
+
idType?: string;
|
|
150
|
+
columns?: Record<string, string>;
|
|
151
|
+
foreignKeys?: Record<string, ForeignKeyDef>;
|
|
152
|
+
vectorColumns?: Record<string, number>;
|
|
153
|
+
hypertable?: HypertableConfig;
|
|
154
|
+
isView?: boolean;
|
|
155
|
+
viewName?: string;
|
|
156
|
+
source?: string;
|
|
157
|
+
viewQuery?: string;
|
|
158
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
declare module 'pg' {
|
|
2
|
+
interface PoolConfig {
|
|
3
|
+
host?: string;
|
|
4
|
+
user?: string;
|
|
5
|
+
password?: string;
|
|
6
|
+
database?: string;
|
|
7
|
+
port?: number;
|
|
8
|
+
max?: number;
|
|
9
|
+
idleTimeoutMillis?: number;
|
|
10
|
+
connectionTimeoutMillis?: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type RowData = Record<string, string | number | boolean | null>;
|
|
14
|
+
|
|
15
|
+
interface QueryResult {
|
|
16
|
+
rows: RowData[];
|
|
17
|
+
rowCount: number;
|
|
18
|
+
fields?: { name: string; dataTypeID: number }[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class Pool {
|
|
22
|
+
constructor(config?: PoolConfig);
|
|
23
|
+
query(sql: string, params?: unknown[]): Promise<QueryResult>;
|
|
24
|
+
connect(): Promise<PoolClient>;
|
|
25
|
+
end(): Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class PoolClient {
|
|
29
|
+
query(sql: string, params?: unknown[]): Promise<QueryResult>;
|
|
30
|
+
release(): void;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
declare module '@stonyx/rest-server' {
|
|
2
|
+
export class Request {
|
|
3
|
+
constructor(...args: unknown[]);
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
interface RouteOptions {
|
|
7
|
+
name: string;
|
|
8
|
+
options?: { model: string; access: (request: unknown) => unknown } | Record<string, unknown>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default class RestServer {
|
|
12
|
+
static instance: RestServer;
|
|
13
|
+
static close(): void;
|
|
14
|
+
mountRoute(RequestClass: typeof Request, options: RouteOptions): void;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
declare module '@stonyx/utils/file' {
|
|
2
|
+
export function createFile(path: string, data: string | Record<string, unknown> | unknown[], options?: { json?: boolean }): Promise<void>;
|
|
3
|
+
export function createDirectory(path: string): Promise<void>;
|
|
4
|
+
export function updateFile(path: string, data: string | Record<string, unknown> | unknown[], options?: { json?: boolean }): Promise<void>;
|
|
5
|
+
export function readFile(path: string, options?: { json?: boolean; missingFileCallback?: () => Promise<Record<string, unknown>> }): Promise<string | Record<string, unknown> | unknown[]>;
|
|
6
|
+
export function fileExists(path: string): Promise<boolean>;
|
|
7
|
+
export function deleteDirectory(path: string): Promise<void>;
|
|
8
|
+
export function forEachFileImport(
|
|
9
|
+
path: string,
|
|
10
|
+
callback: (exported: Function | Record<string, unknown>, meta: { name: string }) => void | unknown,
|
|
11
|
+
options?: { ignoreAccessFailure?: boolean; rawName?: boolean; recursive?: boolean; recursiveNaming?: boolean }
|
|
12
|
+
): Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
declare module '@stonyx/utils/string' {
|
|
16
|
+
export function pluralize(word: string): string;
|
|
17
|
+
export function kebabCaseToPascalCase(str: string): string;
|
|
18
|
+
export function camelCaseToKebabCase(str: string): string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
declare module '@stonyx/utils/object' {
|
|
22
|
+
export function get(obj: Record<string, unknown>, path: string): unknown;
|
|
23
|
+
export function getOrSet<T>(map: Map<unknown, T>, key: unknown, defaultValue: T): T;
|
|
24
|
+
export function makeArray<T>(value: T | T[]): T[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
declare module '@stonyx/utils/date' {
|
|
28
|
+
export function getTimestamp(value?: string | number | Date): number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
declare module '@stonyx/utils/prompt' {
|
|
32
|
+
export function confirm(message: string): Promise<boolean>;
|
|
33
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
declare module 'stonyx/config' {
|
|
2
|
+
import type { OrmConfig } from './orm-types.js';
|
|
3
|
+
const config: OrmConfig;
|
|
4
|
+
export default config;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
declare module 'stonyx/log' {
|
|
8
|
+
const log: Record<string, ((...args: unknown[]) => void) | undefined>;
|
|
9
|
+
export default log;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
declare module 'stonyx' {
|
|
13
|
+
export function waitForModule(name: string): Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
declare module 'stonyx/test-helpers' {
|
|
17
|
+
export function setupIntegrationTests(hooks: {
|
|
18
|
+
before(fn: () => void | Promise<void>): void;
|
|
19
|
+
after(fn: () => void | Promise<void>): void;
|
|
20
|
+
}): void;
|
|
21
|
+
}
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { pluralize as basePluralize } from '@stonyx/utils/string';
|
|
2
|
+
import type { OrmRecord } from './types/orm-types.js';
|
|
3
|
+
|
|
4
|
+
export function isDbError(error: unknown): error is { code: string; message: string } {
|
|
5
|
+
return typeof error === 'object' && error !== null && 'code' in error && typeof (error as Record<string, unknown>).code === 'string' && 'message' in error && typeof (error as Record<string, unknown>).message === 'string';
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function isOrmRecord(value: unknown): value is OrmRecord {
|
|
9
|
+
return typeof value === 'object' && value !== null && '__data' in value && '__relationships' in value;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Wrapper to handle dasherized model names (e.g., "access-link" → "access-links")
|
|
13
|
+
export function pluralize(word: string): string {
|
|
14
|
+
if (word.includes('-')) {
|
|
15
|
+
const parts = word.split('-');
|
|
16
|
+
const last = parts.pop() as string;
|
|
17
|
+
const pluralizedLast = basePluralize(last);
|
|
18
|
+
return [...parts, pluralizedLast].join('-');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return basePluralize(word);
|
|
22
|
+
}
|