@stonyx/orm 0.2.1-beta.83 → 0.2.1-beta.85
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 +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 +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 +58 -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 +179 -0
- package/dist/manage-record.d.ts +13 -0
- package/dist/manage-record.js +114 -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 +415 -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 +455 -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 +476 -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 +39 -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 +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 +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 +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 +169 -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.ts +90 -0
- 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 +92 -0
- package/src/{hooks.js → hooks.ts} +23 -27
- package/src/{index.js → index.ts} +4 -4
- package/src/{main.js → main.ts} +60 -34
- package/src/{manage-record.js → manage-record.ts} +42 -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} +537 -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} +169 -97
- 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} +198 -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 +53 -0
- package/src/{serializer.js → serializer.ts} +52 -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 +119 -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} +51 -24
- package/src/view.ts +22 -0
- package/src/belongs-to.js +0 -70
- 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,6 +1,21 @@
|
|
|
1
|
+
interface QueryResult {
|
|
2
|
+
sql: string;
|
|
3
|
+
values: unknown[];
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
interface VectorSearchOptions {
|
|
7
|
+
limit?: number;
|
|
8
|
+
where?: Record<string, unknown>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface HybridSearchOptions {
|
|
12
|
+
limit?: number;
|
|
13
|
+
where?: Record<string, unknown>;
|
|
14
|
+
}
|
|
15
|
+
|
|
1
16
|
const SAFE_IDENTIFIER = /^[a-zA-Z_][a-zA-Z0-9_-]*$/;
|
|
2
17
|
|
|
3
|
-
export function validateIdentifier(name, context = 'identifier') {
|
|
18
|
+
export function validateIdentifier(name: string, context: string = 'identifier'): string {
|
|
4
19
|
if (!name || typeof name !== 'string' || !SAFE_IDENTIFIER.test(name)) {
|
|
5
20
|
throw new Error(`Invalid SQL ${context}: "${name}". Identifiers must match ${SAFE_IDENTIFIER}`);
|
|
6
21
|
}
|
|
@@ -8,7 +23,7 @@ export function validateIdentifier(name, context = 'identifier') {
|
|
|
8
23
|
return name;
|
|
9
24
|
}
|
|
10
25
|
|
|
11
|
-
export function buildInsert(table, data) {
|
|
26
|
+
export function buildInsert(table: string, data: Record<string, unknown>): QueryResult {
|
|
12
27
|
validateIdentifier(table, 'table name');
|
|
13
28
|
|
|
14
29
|
const keys = Object.keys(data);
|
|
@@ -22,21 +37,21 @@ export function buildInsert(table, data) {
|
|
|
22
37
|
return { sql, values };
|
|
23
38
|
}
|
|
24
39
|
|
|
25
|
-
export function buildUpdate(table, id, data) {
|
|
40
|
+
export function buildUpdate(table: string, id: unknown, data: Record<string, unknown>): QueryResult {
|
|
26
41
|
validateIdentifier(table, 'table name');
|
|
27
42
|
|
|
28
43
|
const keys = Object.keys(data);
|
|
29
44
|
keys.forEach(k => validateIdentifier(k, 'column name'));
|
|
30
45
|
|
|
31
46
|
const setClauses = keys.map((k, i) => `"${k}" = $${i + 1}`);
|
|
32
|
-
const values = [...keys.map(k => data[k]), id];
|
|
47
|
+
const values: unknown[] = [...keys.map(k => data[k]), id];
|
|
33
48
|
|
|
34
49
|
const sql = `UPDATE "${table}" SET ${setClauses.join(', ')} WHERE "id" = $${keys.length + 1}`;
|
|
35
50
|
|
|
36
51
|
return { sql, values };
|
|
37
52
|
}
|
|
38
53
|
|
|
39
|
-
export function buildDelete(table, id) {
|
|
54
|
+
export function buildDelete(table: string, id: unknown): QueryResult {
|
|
40
55
|
validateIdentifier(table, 'table name');
|
|
41
56
|
|
|
42
57
|
return {
|
|
@@ -45,7 +60,7 @@ export function buildDelete(table, id) {
|
|
|
45
60
|
};
|
|
46
61
|
}
|
|
47
62
|
|
|
48
|
-
export function buildSelect(table, conditions) {
|
|
63
|
+
export function buildSelect(table: string, conditions?: Record<string, unknown>): QueryResult {
|
|
49
64
|
validateIdentifier(table, 'table name');
|
|
50
65
|
|
|
51
66
|
if (!conditions || Object.keys(conditions).length === 0) {
|
|
@@ -65,20 +80,13 @@ export function buildSelect(table, conditions) {
|
|
|
65
80
|
|
|
66
81
|
/**
|
|
67
82
|
* Build a vector similarity search query using cosine distance (<=>).
|
|
68
|
-
* @param {string} table - Table name
|
|
69
|
-
* @param {string} vectorColumn - Name of the vector column
|
|
70
|
-
* @param {number[]} queryVector - The query vector
|
|
71
|
-
* @param {Object} [options]
|
|
72
|
-
* @param {number} [options.limit=10] - Number of results to return
|
|
73
|
-
* @param {Object} [options.where] - Additional WHERE conditions
|
|
74
|
-
* @returns {{ sql: string, values: any[] }}
|
|
75
83
|
*/
|
|
76
|
-
export function buildVectorSearch(table, vectorColumn, queryVector, options = {}) {
|
|
84
|
+
export function buildVectorSearch(table: string, vectorColumn: string, queryVector: number[], options: VectorSearchOptions = {}): QueryResult {
|
|
77
85
|
validateIdentifier(table, 'table name');
|
|
78
86
|
validateIdentifier(vectorColumn, 'column name');
|
|
79
87
|
|
|
80
88
|
const { limit = 10, where } = options;
|
|
81
|
-
const values = [];
|
|
89
|
+
const values: unknown[] = [];
|
|
82
90
|
let paramIndex = 1;
|
|
83
91
|
|
|
84
92
|
// Vector parameter as a formatted string for pgvector
|
|
@@ -86,7 +94,7 @@ export function buildVectorSearch(table, vectorColumn, queryVector, options = {}
|
|
|
86
94
|
values.push(vectorStr);
|
|
87
95
|
const vectorParam = `$${paramIndex++}`;
|
|
88
96
|
|
|
89
|
-
|
|
97
|
+
const whereClauses: string[] = [];
|
|
90
98
|
if (where) {
|
|
91
99
|
for (const [k, v] of Object.entries(where)) {
|
|
92
100
|
validateIdentifier(k, 'column name');
|
|
@@ -106,23 +114,14 @@ export function buildVectorSearch(table, vectorColumn, queryVector, options = {}
|
|
|
106
114
|
/**
|
|
107
115
|
* Build a hybrid search query combining vector similarity with text filtering.
|
|
108
116
|
* Uses cosine distance for vector ranking and ILIKE for text matching.
|
|
109
|
-
* @param {string} table - Table name
|
|
110
|
-
* @param {string} vectorColumn - Vector column name
|
|
111
|
-
* @param {number[]} queryVector - The query vector
|
|
112
|
-
* @param {string} textColumn - Column to search text in
|
|
113
|
-
* @param {string} textQuery - Text to search for
|
|
114
|
-
* @param {Object} [options]
|
|
115
|
-
* @param {number} [options.limit=10]
|
|
116
|
-
* @param {Object} [options.where] - Additional WHERE conditions
|
|
117
|
-
* @returns {{ sql: string, values: any[] }}
|
|
118
117
|
*/
|
|
119
|
-
export function buildHybridSearch(table, vectorColumn, queryVector, textColumn, textQuery, options = {}) {
|
|
118
|
+
export function buildHybridSearch(table: string, vectorColumn: string, queryVector: number[], textColumn: string, textQuery: string, options: HybridSearchOptions = {}): QueryResult {
|
|
120
119
|
validateIdentifier(table, 'table name');
|
|
121
120
|
validateIdentifier(vectorColumn, 'column name');
|
|
122
121
|
validateIdentifier(textColumn, 'column name');
|
|
123
122
|
|
|
124
123
|
const { limit = 10, where } = options;
|
|
125
|
-
const values = [];
|
|
124
|
+
const values: unknown[] = [];
|
|
126
125
|
let paramIndex = 1;
|
|
127
126
|
|
|
128
127
|
const vectorStr = `[${queryVector.join(',')}]`;
|
|
@@ -132,7 +131,7 @@ export function buildHybridSearch(table, vectorColumn, queryVector, textColumn,
|
|
|
132
131
|
values.push(`%${textQuery}%`);
|
|
133
132
|
const textParam = `$${paramIndex++}`;
|
|
134
133
|
|
|
135
|
-
|
|
134
|
+
const whereClauses: string[] = [`"${textColumn}" ILIKE ${textParam}`];
|
|
136
135
|
if (where) {
|
|
137
136
|
for (const [k, v] of Object.entries(where)) {
|
|
138
137
|
validateIdentifier(k, 'column name');
|
|
@@ -4,39 +4,69 @@ import { camelCaseToKebabCase } from '@stonyx/utils/string';
|
|
|
4
4
|
import { getPluralName } from '../plural-registry.js';
|
|
5
5
|
import { dbKey } from '../db.js';
|
|
6
6
|
import { AggregateProperty } from '../aggregates.js';
|
|
7
|
+
import type { ForeignKeyDef, ModelSchema, ViewSchema } from '../types/orm-types.js';
|
|
8
|
+
import ModelProperty from '../model-property.js';
|
|
7
9
|
|
|
8
|
-
|
|
10
|
+
interface RelationshipInfo {
|
|
11
|
+
type: 'belongsTo' | 'hasMany';
|
|
12
|
+
modelName: string | null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface ViewSnapshotEntry {
|
|
16
|
+
viewName: string;
|
|
17
|
+
source: string;
|
|
18
|
+
groupBy?: string;
|
|
19
|
+
columns: Record<string, string>;
|
|
20
|
+
foreignKeys: Record<string, ForeignKeyDef>;
|
|
21
|
+
isView: boolean;
|
|
22
|
+
viewQuery: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface ModelSnapshotEntry {
|
|
26
|
+
table: string;
|
|
27
|
+
idType: string;
|
|
28
|
+
columns: Record<string, string>;
|
|
29
|
+
foreignKeys: Record<string, ForeignKeyDef>;
|
|
30
|
+
vectorColumns?: Record<string, number>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface JoinDef {
|
|
34
|
+
table: string;
|
|
35
|
+
condition: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function getRelationshipInfo(property: unknown): RelationshipInfo | null {
|
|
9
39
|
if (typeof property !== 'function') return null;
|
|
10
|
-
const
|
|
11
|
-
const modelName = property.__relatedModelName || null;
|
|
40
|
+
const relType = (property as { __relationshipType?: string }).__relationshipType;
|
|
41
|
+
const modelName = (property as { __relatedModelName?: string }).__relatedModelName || null;
|
|
12
42
|
|
|
13
|
-
if (
|
|
14
|
-
if (
|
|
43
|
+
if (relType === 'belongsTo') return { type: 'belongsTo', modelName };
|
|
44
|
+
if (relType === 'hasMany') return { type: 'hasMany', modelName };
|
|
15
45
|
|
|
16
46
|
return null;
|
|
17
47
|
}
|
|
18
48
|
|
|
19
|
-
function sanitizeTableName(name) {
|
|
49
|
+
function sanitizeTableName(name: string): string {
|
|
20
50
|
return name.replace(/[-/]/g, '_');
|
|
21
51
|
}
|
|
22
52
|
|
|
23
|
-
export function introspectModels() {
|
|
24
|
-
const { models } = Orm.instance;
|
|
25
|
-
const schemas = {};
|
|
53
|
+
export function introspectModels(): Record<string, ModelSchema> {
|
|
54
|
+
const { models } = Orm.instance as { models: Record<string, unknown> };
|
|
55
|
+
const schemas: Record<string, ModelSchema> = {};
|
|
26
56
|
|
|
27
57
|
for (const [modelKey, modelClass] of Object.entries(models)) {
|
|
28
58
|
const name = camelCaseToKebabCase(modelKey.slice(0, -5));
|
|
29
59
|
|
|
30
60
|
if (name === dbKey) continue;
|
|
31
61
|
|
|
32
|
-
const model = new modelClass(modelKey);
|
|
33
|
-
const columns = {};
|
|
34
|
-
const foreignKeys = {};
|
|
35
|
-
const relationships = { belongsTo: {}, hasMany: {} };
|
|
36
|
-
const vectorColumns = {};
|
|
62
|
+
const model = new (modelClass as new (key: string) => Record<string, unknown>)(modelKey);
|
|
63
|
+
const columns: Record<string, string> = {};
|
|
64
|
+
const foreignKeys: Record<string, ForeignKeyDef> = {};
|
|
65
|
+
const relationships: { belongsTo: Record<string, string | null>; hasMany: Record<string, string | null> } = { belongsTo: {}, hasMany: {} };
|
|
66
|
+
const vectorColumns: Record<string, number> = {};
|
|
37
67
|
let idType = 'number';
|
|
38
68
|
|
|
39
|
-
const transforms = Orm.instance.transforms;
|
|
69
|
+
const transforms = (Orm.instance as { transforms: Record<string, unknown> }).transforms;
|
|
40
70
|
|
|
41
71
|
for (const [key, property] of Object.entries(model)) {
|
|
42
72
|
if (key.startsWith('__')) continue;
|
|
@@ -47,15 +77,16 @@ export function introspectModels() {
|
|
|
47
77
|
relationships.belongsTo[key] = relInfo.modelName;
|
|
48
78
|
} else if (relInfo?.type === 'hasMany') {
|
|
49
79
|
relationships.hasMany[key] = relInfo.modelName;
|
|
50
|
-
} else if (property
|
|
80
|
+
} else if (property instanceof ModelProperty) {
|
|
81
|
+
const prop = property as { type: string; dimensions?: number };
|
|
51
82
|
if (key === 'id') {
|
|
52
|
-
idType =
|
|
53
|
-
} else if (
|
|
54
|
-
const dimensions =
|
|
83
|
+
idType = prop.type;
|
|
84
|
+
} else if (prop.type === 'vector') {
|
|
85
|
+
const dimensions = prop.dimensions || 1536;
|
|
55
86
|
columns[key] = getVectorType(dimensions);
|
|
56
87
|
vectorColumns[key] = dimensions;
|
|
57
88
|
} else {
|
|
58
|
-
columns[key] = getPgType(
|
|
89
|
+
columns[key] = getPgType(prop.type, transforms[prop.type] as undefined);
|
|
59
90
|
}
|
|
60
91
|
}
|
|
61
92
|
}
|
|
@@ -64,7 +95,7 @@ export function introspectModels() {
|
|
|
64
95
|
for (const [relName, targetModelName] of Object.entries(relationships.belongsTo)) {
|
|
65
96
|
const fkColumn = `${relName}_id`;
|
|
66
97
|
foreignKeys[fkColumn] = {
|
|
67
|
-
references: sanitizeTableName(getPluralName(targetModelName)),
|
|
98
|
+
references: sanitizeTableName(getPluralName(targetModelName!)),
|
|
68
99
|
column: 'id',
|
|
69
100
|
};
|
|
70
101
|
}
|
|
@@ -76,17 +107,17 @@ export function introspectModels() {
|
|
|
76
107
|
foreignKeys,
|
|
77
108
|
relationships,
|
|
78
109
|
vectorColumns,
|
|
79
|
-
memory: modelClass.memory === true,
|
|
110
|
+
memory: (modelClass as { memory?: boolean }).memory === true,
|
|
80
111
|
};
|
|
81
112
|
}
|
|
82
113
|
|
|
83
114
|
return schemas;
|
|
84
115
|
}
|
|
85
116
|
|
|
86
|
-
export function buildTableDDL(name, schema, allSchemas = {}) {
|
|
117
|
+
export function buildTableDDL(name: string, schema: ModelSchema, allSchemas: Record<string, ModelSchema> = {}): string {
|
|
87
118
|
const { idType, columns, foreignKeys } = schema;
|
|
88
119
|
const table = sanitizeTableName(schema.table);
|
|
89
|
-
const lines = [];
|
|
120
|
+
const lines: string[] = [];
|
|
90
121
|
|
|
91
122
|
// Primary key
|
|
92
123
|
if (idType === 'string') {
|
|
@@ -121,13 +152,10 @@ export function buildTableDDL(name, schema, allSchemas = {}) {
|
|
|
121
152
|
|
|
122
153
|
/**
|
|
123
154
|
* Build HNSW index DDL for vector columns on a model.
|
|
124
|
-
* @param {string} name - Model name
|
|
125
|
-
* @param {Object} schema - Model schema with vectorColumns
|
|
126
|
-
* @returns {string[]} Array of CREATE INDEX statements
|
|
127
155
|
*/
|
|
128
|
-
export function buildVectorIndexDDL(name, schema) {
|
|
156
|
+
export function buildVectorIndexDDL(name: string, schema: ModelSchema): string[] {
|
|
129
157
|
const table = sanitizeTableName(schema.table);
|
|
130
|
-
const statements = [];
|
|
158
|
+
const statements: string[] = [];
|
|
131
159
|
|
|
132
160
|
for (const [col] of Object.entries(schema.vectorColumns || {})) {
|
|
133
161
|
statements.push(
|
|
@@ -138,7 +166,7 @@ export function buildVectorIndexDDL(name, schema) {
|
|
|
138
166
|
return statements;
|
|
139
167
|
}
|
|
140
168
|
|
|
141
|
-
function getReferencedIdType(tableName, allSchemas) {
|
|
169
|
+
function getReferencedIdType(tableName: string, allSchemas: Record<string, ModelSchema>): string {
|
|
142
170
|
for (const schema of Object.values(allSchemas)) {
|
|
143
171
|
if (schema.table === tableName) {
|
|
144
172
|
return schema.idType === 'string' ? 'VARCHAR(255)' : 'INTEGER';
|
|
@@ -148,11 +176,11 @@ function getReferencedIdType(tableName, allSchemas) {
|
|
|
148
176
|
return 'INTEGER';
|
|
149
177
|
}
|
|
150
178
|
|
|
151
|
-
export function getTopologicalOrder(schemas) {
|
|
152
|
-
const visited = new Set();
|
|
153
|
-
const order = [];
|
|
179
|
+
export function getTopologicalOrder(schemas: Record<string, ModelSchema>): string[] {
|
|
180
|
+
const visited = new Set<string>();
|
|
181
|
+
const order: string[] = [];
|
|
154
182
|
|
|
155
|
-
function visit(name) {
|
|
183
|
+
function visit(name: string): void {
|
|
156
184
|
if (visited.has(name)) return;
|
|
157
185
|
visited.add(name);
|
|
158
186
|
|
|
@@ -161,7 +189,7 @@ export function getTopologicalOrder(schemas) {
|
|
|
161
189
|
|
|
162
190
|
// Visit dependencies (belongsTo targets) first
|
|
163
191
|
for (const targetModelName of Object.values(schema.relationships.belongsTo)) {
|
|
164
|
-
visit(targetModelName);
|
|
192
|
+
visit(targetModelName!);
|
|
165
193
|
}
|
|
166
194
|
|
|
167
195
|
order.push(name);
|
|
@@ -174,23 +202,23 @@ export function getTopologicalOrder(schemas) {
|
|
|
174
202
|
return order;
|
|
175
203
|
}
|
|
176
204
|
|
|
177
|
-
export function introspectViews() {
|
|
178
|
-
const orm = Orm.instance;
|
|
205
|
+
export function introspectViews(): Record<string, ViewSchema> {
|
|
206
|
+
const orm = Orm.instance as { views?: Record<string, unknown> };
|
|
179
207
|
if (!orm.views) return {};
|
|
180
208
|
|
|
181
|
-
const schemas = {};
|
|
209
|
+
const schemas: Record<string, ViewSchema> = {};
|
|
182
210
|
|
|
183
211
|
for (const [viewKey, viewClass] of Object.entries(orm.views)) {
|
|
184
212
|
const name = camelCaseToKebabCase(viewKey.slice(0, -4)); // Remove 'View' suffix
|
|
185
213
|
|
|
186
|
-
const source = viewClass.source;
|
|
214
|
+
const source = (viewClass as { source?: string }).source;
|
|
187
215
|
if (!source) continue;
|
|
188
216
|
|
|
189
|
-
const model = new viewClass(name);
|
|
190
|
-
const columns = {};
|
|
191
|
-
const foreignKeys = {};
|
|
192
|
-
const aggregates = {};
|
|
193
|
-
const relationships = { belongsTo: {}, hasMany: {} };
|
|
217
|
+
const model = new (viewClass as new (name: string) => Record<string, unknown>)(name);
|
|
218
|
+
const columns: Record<string, string> = {};
|
|
219
|
+
const foreignKeys: Record<string, ForeignKeyDef> = {};
|
|
220
|
+
const aggregates: Record<string, AggregateProperty> = {};
|
|
221
|
+
const relationships: { belongsTo: Record<string, string | null>; hasMany: Record<string, string | null> } = { belongsTo: {}, hasMany: {} };
|
|
194
222
|
|
|
195
223
|
for (const [key, property] of Object.entries(model)) {
|
|
196
224
|
if (key.startsWith('__')) continue;
|
|
@@ -207,34 +235,35 @@ export function introspectViews() {
|
|
|
207
235
|
relationships.belongsTo[key] = relInfo.modelName;
|
|
208
236
|
const fkColumn = `${key}_id`;
|
|
209
237
|
foreignKeys[fkColumn] = {
|
|
210
|
-
references: sanitizeTableName(getPluralName(relInfo.modelName)),
|
|
238
|
+
references: sanitizeTableName(getPluralName(relInfo.modelName!)),
|
|
211
239
|
column: 'id',
|
|
212
240
|
};
|
|
213
241
|
} else if (relInfo?.type === 'hasMany') {
|
|
214
242
|
relationships.hasMany[key] = relInfo.modelName;
|
|
215
|
-
} else if (property
|
|
216
|
-
const transforms = Orm.instance.transforms;
|
|
217
|
-
|
|
243
|
+
} else if (property instanceof ModelProperty) {
|
|
244
|
+
const transforms = (Orm.instance as { transforms: Record<string, unknown> }).transforms;
|
|
245
|
+
const prop = property as { type: string };
|
|
246
|
+
columns[key] = getPgType(prop.type, transforms[prop.type] as undefined);
|
|
218
247
|
}
|
|
219
248
|
}
|
|
220
249
|
|
|
221
250
|
schemas[name] = {
|
|
222
251
|
viewName: sanitizeTableName(getPluralName(name)),
|
|
223
252
|
source,
|
|
224
|
-
groupBy: viewClass.groupBy || undefined,
|
|
253
|
+
groupBy: (viewClass as { groupBy?: string }).groupBy || undefined,
|
|
225
254
|
columns,
|
|
226
255
|
foreignKeys,
|
|
227
256
|
aggregates,
|
|
228
257
|
relationships,
|
|
229
258
|
isView: true,
|
|
230
|
-
memory:
|
|
259
|
+
memory: false, // Views default to memory:false
|
|
231
260
|
};
|
|
232
261
|
}
|
|
233
262
|
|
|
234
263
|
return schemas;
|
|
235
264
|
}
|
|
236
265
|
|
|
237
|
-
export function buildViewDDL(name, viewSchema, modelSchemas = {}) {
|
|
266
|
+
export function buildViewDDL(name: string, viewSchema: ViewSchema, modelSchemas: Record<string, ModelSchema> = {}): string {
|
|
238
267
|
if (!viewSchema.source) {
|
|
239
268
|
throw new Error(`View '${name}' must define a source model`);
|
|
240
269
|
}
|
|
@@ -245,8 +274,8 @@ export function buildViewDDL(name, viewSchema, modelSchemas = {}) {
|
|
|
245
274
|
? sourceSchema.table
|
|
246
275
|
: getPluralName(sourceModelName));
|
|
247
276
|
|
|
248
|
-
const selectColumns = [];
|
|
249
|
-
const joins = [];
|
|
277
|
+
const selectColumns: string[] = [];
|
|
278
|
+
const joins: JoinDef[] = [];
|
|
250
279
|
const hasAggregates = Object.keys(viewSchema.aggregates || {}).length > 0;
|
|
251
280
|
const groupByField = viewSchema.groupBy;
|
|
252
281
|
|
|
@@ -260,7 +289,7 @@ export function buildViewDDL(name, viewSchema, modelSchemas = {}) {
|
|
|
260
289
|
// Aggregate columns
|
|
261
290
|
for (const [key, aggProp] of Object.entries(viewSchema.aggregates || {})) {
|
|
262
291
|
// Use pgFunction if available, fall back to mysqlFunction
|
|
263
|
-
const fn = aggProp.pgFunction || aggProp.mysqlFunction;
|
|
292
|
+
const fn = (aggProp as AggregateProperty & { pgFunction?: string }).pgFunction || aggProp.mysqlFunction;
|
|
264
293
|
|
|
265
294
|
if (aggProp.relationship === undefined) {
|
|
266
295
|
// Field-level aggregate (groupBy views)
|
|
@@ -317,8 +346,8 @@ export function buildViewDDL(name, viewSchema, modelSchemas = {}) {
|
|
|
317
346
|
return sql;
|
|
318
347
|
}
|
|
319
348
|
|
|
320
|
-
export function viewSchemasToSnapshot(viewSchemas) {
|
|
321
|
-
const snapshot = {};
|
|
349
|
+
export function viewSchemasToSnapshot(viewSchemas: Record<string, ViewSchema>): Record<string, ViewSnapshotEntry> {
|
|
350
|
+
const snapshot: Record<string, ViewSnapshotEntry> = {};
|
|
322
351
|
|
|
323
352
|
for (const [name, schema] of Object.entries(viewSchemas)) {
|
|
324
353
|
snapshot[name] = {
|
|
@@ -335,8 +364,8 @@ export function viewSchemasToSnapshot(viewSchemas) {
|
|
|
335
364
|
return snapshot;
|
|
336
365
|
}
|
|
337
366
|
|
|
338
|
-
export function schemasToSnapshot(schemas) {
|
|
339
|
-
const snapshot = {};
|
|
367
|
+
export function schemasToSnapshot(schemas: Record<string, ModelSchema>): Record<string, ModelSnapshotEntry> {
|
|
368
|
+
const snapshot: Record<string, ModelSnapshotEntry> = {};
|
|
340
369
|
|
|
341
370
|
for (const [name, schema] of Object.entries(schemas)) {
|
|
342
371
|
snapshot[name] = {
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
interface TransformFn {
|
|
2
|
+
pgType?: string;
|
|
3
|
+
mysqlType?: string;
|
|
4
|
+
(...args: unknown[]): unknown;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const typeMap: Record<string, string> = {
|
|
2
8
|
string: 'VARCHAR(255)',
|
|
3
9
|
number: 'INTEGER',
|
|
4
10
|
float: 'REAL',
|
|
@@ -24,7 +30,7 @@ const typeMap = {
|
|
|
24
30
|
* - Otherwise, defaults to JSONB. Values are JSON-stringified on write and
|
|
25
31
|
* JSON-parsed on read by PostgreSQL natively.
|
|
26
32
|
*/
|
|
27
|
-
export function getPgType(attrType, transformFn) {
|
|
33
|
+
export function getPgType(attrType: string, transformFn?: TransformFn): string {
|
|
28
34
|
if (typeMap[attrType]) return typeMap[attrType];
|
|
29
35
|
if (transformFn?.pgType) return transformFn.pgType;
|
|
30
36
|
if (transformFn?.mysqlType) return mysqlTypeToPg(transformFn.mysqlType);
|
|
@@ -34,14 +40,12 @@ export function getPgType(attrType, transformFn) {
|
|
|
34
40
|
|
|
35
41
|
/**
|
|
36
42
|
* Returns a vector column type for the given dimensions.
|
|
37
|
-
* @param {number} dimensions - Vector dimensionality (e.g. 768, 1536)
|
|
38
|
-
* @returns {string}
|
|
39
43
|
*/
|
|
40
|
-
export function getVectorType(dimensions) {
|
|
44
|
+
export function getVectorType(dimensions: number): string {
|
|
41
45
|
return `vector(${dimensions})`;
|
|
42
46
|
}
|
|
43
47
|
|
|
44
|
-
function mysqlTypeToPg(mysqlType) {
|
|
48
|
+
function mysqlTypeToPg(mysqlType: string): string {
|
|
45
49
|
const upper = mysqlType.toUpperCase();
|
|
46
50
|
if (upper === 'TINYINT(1)') return 'BOOLEAN';
|
|
47
51
|
if (upper === 'INT' || upper === 'INT AUTO_INCREMENT') return 'INTEGER';
|
|
@@ -1,33 +1,72 @@
|
|
|
1
|
-
import { store } from '
|
|
1
|
+
import { store } from '@stonyx/orm';
|
|
2
2
|
import { getComputedProperties } from "./serializer.js";
|
|
3
3
|
import { camelCaseToKebabCase } from '@stonyx/utils/string';
|
|
4
4
|
import { getPluralName } from './plural-registry.js';
|
|
5
|
+
import type Serializer from './serializer.js';
|
|
6
|
+
|
|
7
|
+
interface ToJSONOptions {
|
|
8
|
+
fields?: Set<string>;
|
|
9
|
+
baseUrl?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface SerializeOptions {
|
|
13
|
+
update?: boolean;
|
|
14
|
+
serialize?: boolean;
|
|
15
|
+
transform?: boolean;
|
|
16
|
+
[key: string]: unknown;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface UnloadOptions {
|
|
20
|
+
[key: string]: unknown;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface RelationshipLinks {
|
|
24
|
+
self: string;
|
|
25
|
+
related: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface RelationshipEntry {
|
|
29
|
+
data: { type: string; id: unknown } | { type: string; id: unknown }[] | null;
|
|
30
|
+
links?: RelationshipLinks;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface JSONAPIResult {
|
|
34
|
+
attributes: { [key: string]: unknown };
|
|
35
|
+
relationships: { [key: string]: RelationshipEntry };
|
|
36
|
+
id: unknown;
|
|
37
|
+
type: string;
|
|
38
|
+
links?: { self: string };
|
|
39
|
+
}
|
|
40
|
+
|
|
5
41
|
export default class Record {
|
|
6
42
|
/** @private */
|
|
7
|
-
__data = {};
|
|
43
|
+
__data: { [key: string]: unknown } = {};
|
|
8
44
|
/** @private */
|
|
9
|
-
__relationships = {};
|
|
45
|
+
__relationships: { [key: string]: unknown } = {};
|
|
10
46
|
/** @private */
|
|
11
47
|
__serialized = false;
|
|
48
|
+
/** @private */
|
|
49
|
+
__model: { __name: string; [key: string]: unknown };
|
|
50
|
+
/** @private */
|
|
51
|
+
__serializer: Serializer;
|
|
52
|
+
|
|
53
|
+
[key: string]: unknown;
|
|
12
54
|
|
|
13
|
-
constructor(model, serializer) {
|
|
14
|
-
/** @private */
|
|
55
|
+
constructor(model: { __name: string; [key: string]: unknown }, serializer: Serializer) {
|
|
15
56
|
this.__model = model;
|
|
16
|
-
/** @private */
|
|
17
57
|
this.__serializer = serializer;
|
|
18
|
-
|
|
19
58
|
}
|
|
20
59
|
|
|
21
|
-
serialize(rawData, options={}) {
|
|
22
|
-
const { __data:data } = this;
|
|
23
|
-
|
|
60
|
+
serialize(rawData?: unknown, options: SerializeOptions = {}): { [key: string]: unknown } {
|
|
61
|
+
const { __data: data } = this;
|
|
62
|
+
|
|
24
63
|
if (this.__serialized && !options.update) {
|
|
25
|
-
const relatedIds = {};
|
|
64
|
+
const relatedIds: { [key: string]: unknown } = {};
|
|
26
65
|
|
|
27
|
-
for (const [
|
|
28
|
-
relatedIds[key] = Array.isArray(childRecord)
|
|
29
|
-
|
|
30
|
-
|
|
66
|
+
for (const [key, childRecord] of Object.entries(this.__relationships)) {
|
|
67
|
+
relatedIds[key] = Array.isArray(childRecord)
|
|
68
|
+
? childRecord.map((r: Record) => r.id)
|
|
69
|
+
: (childRecord as Record)?.id ?? null;
|
|
31
70
|
}
|
|
32
71
|
|
|
33
72
|
return { ...data, ...relatedIds };
|
|
@@ -40,32 +79,32 @@ export default class Record {
|
|
|
40
79
|
}
|
|
41
80
|
|
|
42
81
|
// Similar to serialize, but preserves top level relationship records
|
|
43
|
-
format() {
|
|
82
|
+
format(): { [key: string]: unknown } {
|
|
44
83
|
if (!this.__serialized) throw new Error('Record must be serialized before being converted to JSON');
|
|
45
|
-
|
|
46
|
-
const { __data:data } = this;
|
|
47
|
-
const records = {};
|
|
48
|
-
|
|
49
|
-
for (const [
|
|
50
|
-
records[key] = Array.isArray(childRecord)
|
|
51
|
-
|
|
52
|
-
|
|
84
|
+
|
|
85
|
+
const { __data: data } = this;
|
|
86
|
+
const records: { [key: string]: unknown } = {};
|
|
87
|
+
|
|
88
|
+
for (const [key, childRecord] of Object.entries(this.__relationships)) {
|
|
89
|
+
records[key] = Array.isArray(childRecord)
|
|
90
|
+
? childRecord.map((r: Record) => r.serialize())
|
|
91
|
+
: (childRecord as Record)?.serialize() ?? null;
|
|
53
92
|
}
|
|
54
93
|
|
|
55
94
|
return { ...data, ...records };
|
|
56
95
|
}
|
|
57
96
|
|
|
58
97
|
// Formats record for JSON API output
|
|
59
|
-
toJSON(options = {}) {
|
|
98
|
+
toJSON(options: ToJSONOptions = {}): JSONAPIResult {
|
|
60
99
|
if (!this.__serialized) throw new Error('Record must be serialized before being converted to JSON');
|
|
61
100
|
|
|
62
101
|
const { fields, baseUrl } = options;
|
|
63
|
-
const { __data:data } = this;
|
|
102
|
+
const { __data: data } = this;
|
|
64
103
|
const modelName = this.__model.__name;
|
|
65
104
|
const pluralizedModelName = getPluralName(modelName);
|
|
66
105
|
const recordId = data.id;
|
|
67
|
-
const relationships = {};
|
|
68
|
-
const attributes = {};
|
|
106
|
+
const relationships: { [key: string]: RelationshipEntry } = {};
|
|
107
|
+
const attributes: { [key: string]: unknown } = {};
|
|
69
108
|
|
|
70
109
|
for (const [key, value] of Object.entries(data)) {
|
|
71
110
|
if (key === 'id') continue;
|
|
@@ -75,15 +114,15 @@ export default class Record {
|
|
|
75
114
|
|
|
76
115
|
for (const [key, getter] of getComputedProperties(this.__model)) {
|
|
77
116
|
if (fields && !fields.has(key)) continue;
|
|
78
|
-
attributes[key] = getter.call(this);
|
|
117
|
+
attributes[key] = (getter as () => unknown).call(this);
|
|
79
118
|
}
|
|
80
119
|
|
|
81
120
|
for (const [key, childRecord] of Object.entries(this.__relationships)) {
|
|
82
121
|
if (fields && !fields.has(key)) continue;
|
|
83
122
|
|
|
84
123
|
const relationshipData = Array.isArray(childRecord)
|
|
85
|
-
? childRecord.map(r => ({ type: r.__model.__name, id: r.id }))
|
|
86
|
-
: childRecord ? { type: childRecord.__model.__name, id: childRecord.id } : null;
|
|
124
|
+
? childRecord.map((r: Record) => ({ type: r.__model.__name, id: r.id }))
|
|
125
|
+
: childRecord ? { type: (childRecord as Record).__model.__name, id: (childRecord as Record).id } : null;
|
|
87
126
|
|
|
88
127
|
// Dasherize the key for URL paths (e.g., accessLinks -> access-links)
|
|
89
128
|
const dasherizedKey = camelCaseToKebabCase(key);
|
|
@@ -99,7 +138,7 @@ export default class Record {
|
|
|
99
138
|
}
|
|
100
139
|
}
|
|
101
140
|
|
|
102
|
-
const result = {
|
|
141
|
+
const result: JSONAPIResult = {
|
|
103
142
|
attributes,
|
|
104
143
|
relationships,
|
|
105
144
|
id: recordId,
|
|
@@ -116,11 +155,11 @@ export default class Record {
|
|
|
116
155
|
return result;
|
|
117
156
|
}
|
|
118
157
|
|
|
119
|
-
unload(options={}) {
|
|
158
|
+
unload(options: UnloadOptions = {}): void {
|
|
120
159
|
store.unloadRecord(this.__model.__name, this.id, options);
|
|
121
160
|
}
|
|
122
161
|
|
|
123
|
-
clean() {
|
|
162
|
+
clean(): void {
|
|
124
163
|
try {
|
|
125
164
|
for (const key of Object.keys(this)) {
|
|
126
165
|
delete this[key];
|