@stonyx/orm 0.2.1-beta.9 → 0.2.1-beta.90
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 +64 -6
- package/config/environment.js +37 -1
- 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 +62 -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 +291 -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 +474 -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 +261 -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 +314 -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/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 +142 -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 +57 -15
- 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.js → commands.ts} +179 -170
- package/src/{db.js → db.ts} +55 -29
- package/src/exports/db.ts +7 -0
- package/src/has-many.ts +92 -0
- package/src/{hooks.js → hooks.ts} +41 -27
- package/src/{index.js → index.ts} +11 -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.js → migrate.ts} +9 -9
- package/src/model-property.ts +35 -0
- package/src/model.ts +21 -0
- package/src/mysql/{connection.js → connection.ts} +43 -28
- package/src/mysql/migration-generator.ts +337 -0
- package/src/mysql/{migration-runner.js → migration-runner.ts} +121 -110
- package/src/mysql/mysql-db.ts +543 -0
- package/src/mysql/{query-builder.js → query-builder.ts} +69 -64
- package/src/mysql/schema-introspector.ts +358 -0
- package/src/mysql/{type-map.js → type-map.ts} +42 -37
- package/src/{orm-request.js → orm-request.ts} +186 -108
- package/src/plural-registry.ts +12 -0
- package/src/postgres/connection.ts +48 -0
- package/src/postgres/migration-generator.ts +348 -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 +386 -0
- package/src/postgres/type-map.ts +61 -0
- package/src/record.ts +186 -0
- package/src/relationships.ts +54 -0
- package/src/serializer.ts +161 -0
- package/src/{setup-rest-server.js → setup-rest-server.ts} +18 -16
- 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 +146 -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/code-style-rules.md +0 -44
- package/.claude/hooks.md +0 -250
- package/.claude/index.md +0 -279
- package/.claude/usage-patterns.md +0 -217
- package/.github/workflows/ci.yml +0 -16
- package/.github/workflows/publish.yml +0 -51
- package/improvements.md +0 -139
- package/project-structure.md +0 -343
- package/src/belongs-to.js +0 -63
- package/src/has-many.js +0 -61
- package/src/main.js +0 -148
- package/src/manage-record.js +0 -118
- package/src/model-property.js +0 -29
- package/src/model.js +0 -9
- package/src/mysql/migration-generator.js +0 -188
- package/src/mysql/mysql-db.js +0 -320
- package/src/mysql/schema-introspector.js +0 -158
- package/src/record.js +0 -127
- package/src/relationships.js +0 -43
- package/src/serializer.js +0 -138
- package/src/store.js +0 -211
- package/src/transforms.js +0 -20
- package/src/utils.js +0 -12
- package/test-events-setup.js +0 -41
- package/test-hooks-manual.js +0 -54
- package/test-hooks-with-logging.js +0 -52
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import Orm from '@stonyx/orm';
|
|
2
|
+
import { getPgType, getVectorType } from './type-map.js';
|
|
3
|
+
import { camelCaseToKebabCase } from '@stonyx/utils/string';
|
|
4
|
+
import { getPluralName } from '../plural-registry.js';
|
|
5
|
+
import { dbKey } from '../db.js';
|
|
6
|
+
import { AggregateProperty } from '../aggregates.js';
|
|
7
|
+
import ModelProperty from '../model-property.js';
|
|
8
|
+
function getRelationshipInfo(property) {
|
|
9
|
+
if (typeof property !== 'function')
|
|
10
|
+
return null;
|
|
11
|
+
const relType = property.__relationshipType;
|
|
12
|
+
const modelName = property.__relatedModelName || null;
|
|
13
|
+
if (relType === 'belongsTo')
|
|
14
|
+
return { type: 'belongsTo', modelName };
|
|
15
|
+
if (relType === 'hasMany')
|
|
16
|
+
return { type: 'hasMany', modelName };
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
function sanitizeTableName(name) {
|
|
20
|
+
return name.replace(/[-/]/g, '_');
|
|
21
|
+
}
|
|
22
|
+
export function introspectModels() {
|
|
23
|
+
const { models } = Orm.instance;
|
|
24
|
+
const schemas = {};
|
|
25
|
+
for (const [modelKey, modelClass] of Object.entries(models)) {
|
|
26
|
+
const name = camelCaseToKebabCase(modelKey.slice(0, -5));
|
|
27
|
+
if (name === dbKey)
|
|
28
|
+
continue;
|
|
29
|
+
const model = new modelClass(modelKey);
|
|
30
|
+
const columns = {};
|
|
31
|
+
const foreignKeys = {};
|
|
32
|
+
const relationships = { belongsTo: {}, hasMany: {} };
|
|
33
|
+
const vectorColumns = {};
|
|
34
|
+
let idType = 'number';
|
|
35
|
+
const transforms = Orm.instance.transforms;
|
|
36
|
+
for (const [key, property] of Object.entries(model)) {
|
|
37
|
+
if (key.startsWith('__'))
|
|
38
|
+
continue;
|
|
39
|
+
const relInfo = getRelationshipInfo(property);
|
|
40
|
+
if (relInfo?.type === 'belongsTo') {
|
|
41
|
+
relationships.belongsTo[key] = relInfo.modelName;
|
|
42
|
+
}
|
|
43
|
+
else if (relInfo?.type === 'hasMany') {
|
|
44
|
+
relationships.hasMany[key] = relInfo.modelName;
|
|
45
|
+
}
|
|
46
|
+
else if (property instanceof ModelProperty) {
|
|
47
|
+
const prop = property;
|
|
48
|
+
if (key === 'id') {
|
|
49
|
+
idType = prop.type;
|
|
50
|
+
}
|
|
51
|
+
else if (prop.type === 'vector') {
|
|
52
|
+
const dimensions = prop.dimensions || 1536;
|
|
53
|
+
columns[key] = getVectorType(dimensions);
|
|
54
|
+
vectorColumns[key] = dimensions;
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
columns[key] = getPgType(prop.type, transforms[prop.type]);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// Build foreign keys from belongsTo relationships
|
|
62
|
+
for (const [relName, targetModelName] of Object.entries(relationships.belongsTo)) {
|
|
63
|
+
if (!targetModelName)
|
|
64
|
+
continue;
|
|
65
|
+
const fkColumn = `${relName}_id`;
|
|
66
|
+
foreignKeys[fkColumn] = {
|
|
67
|
+
references: sanitizeTableName(getPluralName(targetModelName)),
|
|
68
|
+
column: 'id',
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
schemas[name] = {
|
|
72
|
+
table: sanitizeTableName(getPluralName(name)),
|
|
73
|
+
idType,
|
|
74
|
+
columns,
|
|
75
|
+
foreignKeys,
|
|
76
|
+
relationships,
|
|
77
|
+
vectorColumns,
|
|
78
|
+
memory: modelClass.memory === true,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
return schemas;
|
|
82
|
+
}
|
|
83
|
+
export function buildTableDDL(name, schema, allSchemas = {}) {
|
|
84
|
+
const { idType, columns, foreignKeys } = schema;
|
|
85
|
+
const table = sanitizeTableName(schema.table);
|
|
86
|
+
const lines = [];
|
|
87
|
+
// Primary key
|
|
88
|
+
if (idType === 'string') {
|
|
89
|
+
lines.push(' "id" VARCHAR(255) PRIMARY KEY');
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
lines.push(' "id" INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY');
|
|
93
|
+
}
|
|
94
|
+
// Attribute columns
|
|
95
|
+
for (const [col, pgType] of Object.entries(columns)) {
|
|
96
|
+
lines.push(` "${col}" ${pgType}`);
|
|
97
|
+
}
|
|
98
|
+
// Foreign key columns
|
|
99
|
+
for (const [fkCol, fkDef] of Object.entries(foreignKeys)) {
|
|
100
|
+
const refIdType = getReferencedIdType(fkDef.references, allSchemas);
|
|
101
|
+
lines.push(` "${fkCol}" ${refIdType}`);
|
|
102
|
+
}
|
|
103
|
+
// Timestamps
|
|
104
|
+
lines.push(' "created_at" TIMESTAMPTZ DEFAULT NOW()');
|
|
105
|
+
lines.push(' "updated_at" TIMESTAMPTZ DEFAULT NOW()');
|
|
106
|
+
// Foreign key constraints
|
|
107
|
+
for (const [fkCol, fkDef] of Object.entries(foreignKeys)) {
|
|
108
|
+
const refTable = sanitizeTableName(fkDef.references);
|
|
109
|
+
lines.push(` FOREIGN KEY ("${fkCol}") REFERENCES "${refTable}"("${fkDef.column}") ON DELETE SET NULL`);
|
|
110
|
+
}
|
|
111
|
+
return `CREATE TABLE IF NOT EXISTS "${table}" (\n${lines.join(',\n')}\n)`;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Build HNSW index DDL for vector columns on a model.
|
|
115
|
+
*/
|
|
116
|
+
export function buildVectorIndexDDL(name, schema) {
|
|
117
|
+
const table = sanitizeTableName(schema.table);
|
|
118
|
+
const statements = [];
|
|
119
|
+
for (const [col] of Object.entries(schema.vectorColumns || {})) {
|
|
120
|
+
statements.push(`CREATE INDEX IF NOT EXISTS "idx_${table}_${col}_hnsw" ON "${table}" USING hnsw ("${col}" vector_cosine_ops) WITH (m = 16, ef_construction = 200)`);
|
|
121
|
+
}
|
|
122
|
+
return statements;
|
|
123
|
+
}
|
|
124
|
+
function getReferencedIdType(tableName, allSchemas) {
|
|
125
|
+
for (const schema of Object.values(allSchemas)) {
|
|
126
|
+
if (schema.table === tableName) {
|
|
127
|
+
return schema.idType === 'string' ? 'VARCHAR(255)' : 'INTEGER';
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return 'INTEGER';
|
|
131
|
+
}
|
|
132
|
+
export function getTopologicalOrder(schemas) {
|
|
133
|
+
const visited = new Set();
|
|
134
|
+
const order = [];
|
|
135
|
+
function visit(name) {
|
|
136
|
+
if (visited.has(name))
|
|
137
|
+
return;
|
|
138
|
+
visited.add(name);
|
|
139
|
+
const schema = schemas[name];
|
|
140
|
+
if (!schema)
|
|
141
|
+
return;
|
|
142
|
+
// Visit dependencies (belongsTo targets) first
|
|
143
|
+
for (const targetModelName of Object.values(schema.relationships.belongsTo)) {
|
|
144
|
+
if (targetModelName)
|
|
145
|
+
visit(targetModelName);
|
|
146
|
+
}
|
|
147
|
+
order.push(name);
|
|
148
|
+
}
|
|
149
|
+
for (const name of Object.keys(schemas)) {
|
|
150
|
+
visit(name);
|
|
151
|
+
}
|
|
152
|
+
return order;
|
|
153
|
+
}
|
|
154
|
+
export function introspectViews() {
|
|
155
|
+
const orm = Orm.instance;
|
|
156
|
+
if (!orm.views)
|
|
157
|
+
return {};
|
|
158
|
+
const schemas = {};
|
|
159
|
+
for (const [viewKey, viewClass] of Object.entries(orm.views)) {
|
|
160
|
+
const name = camelCaseToKebabCase(viewKey.slice(0, -4)); // Remove 'View' suffix
|
|
161
|
+
const source = viewClass.source;
|
|
162
|
+
if (!source)
|
|
163
|
+
continue;
|
|
164
|
+
const model = new viewClass(name);
|
|
165
|
+
const columns = {};
|
|
166
|
+
const foreignKeys = {};
|
|
167
|
+
const aggregates = {};
|
|
168
|
+
const relationships = { belongsTo: {}, hasMany: {} };
|
|
169
|
+
for (const [key, property] of Object.entries(model)) {
|
|
170
|
+
if (key.startsWith('__'))
|
|
171
|
+
continue;
|
|
172
|
+
if (key === 'id')
|
|
173
|
+
continue;
|
|
174
|
+
if (property instanceof AggregateProperty) {
|
|
175
|
+
aggregates[key] = property;
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
const relInfo = getRelationshipInfo(property);
|
|
179
|
+
if (relInfo?.type === 'belongsTo') {
|
|
180
|
+
relationships.belongsTo[key] = relInfo.modelName;
|
|
181
|
+
if (relInfo.modelName) {
|
|
182
|
+
const fkColumn = `${key}_id`;
|
|
183
|
+
foreignKeys[fkColumn] = {
|
|
184
|
+
references: sanitizeTableName(getPluralName(relInfo.modelName)),
|
|
185
|
+
column: 'id',
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
else if (relInfo?.type === 'hasMany') {
|
|
190
|
+
relationships.hasMany[key] = relInfo.modelName;
|
|
191
|
+
}
|
|
192
|
+
else if (property instanceof ModelProperty) {
|
|
193
|
+
const transforms = Orm.instance.transforms;
|
|
194
|
+
const prop = property;
|
|
195
|
+
columns[key] = getPgType(prop.type, transforms[prop.type]);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
schemas[name] = {
|
|
199
|
+
viewName: sanitizeTableName(getPluralName(name)),
|
|
200
|
+
source,
|
|
201
|
+
groupBy: viewClass.groupBy || undefined,
|
|
202
|
+
columns,
|
|
203
|
+
foreignKeys,
|
|
204
|
+
aggregates,
|
|
205
|
+
relationships,
|
|
206
|
+
isView: true,
|
|
207
|
+
memory: false, // Views default to memory:false
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
return schemas;
|
|
211
|
+
}
|
|
212
|
+
export function buildViewDDL(name, viewSchema, modelSchemas = {}) {
|
|
213
|
+
if (!viewSchema.source) {
|
|
214
|
+
throw new Error(`View '${name}' must define a source model`);
|
|
215
|
+
}
|
|
216
|
+
const sourceModelName = viewSchema.source;
|
|
217
|
+
const sourceSchema = modelSchemas[sourceModelName];
|
|
218
|
+
const sourceTable = sanitizeTableName(sourceSchema
|
|
219
|
+
? sourceSchema.table
|
|
220
|
+
: getPluralName(sourceModelName));
|
|
221
|
+
const selectColumns = [];
|
|
222
|
+
const joins = [];
|
|
223
|
+
const hasAggregates = Object.keys(viewSchema.aggregates || {}).length > 0;
|
|
224
|
+
const groupByField = viewSchema.groupBy;
|
|
225
|
+
// ID column: groupBy field or source table PK
|
|
226
|
+
if (groupByField) {
|
|
227
|
+
selectColumns.push(`"${sourceTable}"."${groupByField}" AS "id"`);
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
selectColumns.push(`"${sourceTable}"."id" AS "id"`);
|
|
231
|
+
}
|
|
232
|
+
// Aggregate columns
|
|
233
|
+
for (const [key, aggProp] of Object.entries(viewSchema.aggregates || {})) {
|
|
234
|
+
// Use pgFunction if available, fall back to mysqlFunction
|
|
235
|
+
const fn = aggProp.pgFunction || aggProp.mysqlFunction;
|
|
236
|
+
if (aggProp.relationship === undefined) {
|
|
237
|
+
// Field-level aggregate (groupBy views)
|
|
238
|
+
if (aggProp.aggregateType === 'count') {
|
|
239
|
+
selectColumns.push(`COUNT(*) AS "${key}"`);
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
selectColumns.push(`${fn}("${sourceTable}"."${aggProp.field}") AS "${key}"`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
// Relationship aggregate
|
|
247
|
+
const relName = aggProp.relationship;
|
|
248
|
+
const relTable = sanitizeTableName(getPluralName(relName));
|
|
249
|
+
if (aggProp.aggregateType === 'count') {
|
|
250
|
+
selectColumns.push(`${fn}("${relTable}"."id") AS "${key}"`);
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
const field = aggProp.field;
|
|
254
|
+
selectColumns.push(`${fn}("${relTable}"."${field}") AS "${key}"`);
|
|
255
|
+
}
|
|
256
|
+
// Add LEFT JOIN for the relationship if not already added
|
|
257
|
+
const joinKey = `${relTable}`;
|
|
258
|
+
if (!joins.find(j => j.table === joinKey)) {
|
|
259
|
+
const fkColumn = `${sourceModelName}_id`;
|
|
260
|
+
joins.push({
|
|
261
|
+
table: relTable,
|
|
262
|
+
condition: `"${relTable}"."${fkColumn}" = "${sourceTable}"."id"`
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
// Regular columns
|
|
268
|
+
for (const [key] of Object.entries(viewSchema.columns || {})) {
|
|
269
|
+
selectColumns.push(`"${sourceTable}"."${key}" AS "${key}"`);
|
|
270
|
+
}
|
|
271
|
+
// Build JOIN clauses
|
|
272
|
+
const joinClauses = joins.map(j => `LEFT JOIN "${j.table}" ON ${j.condition}`).join('\n ');
|
|
273
|
+
// Build GROUP BY
|
|
274
|
+
let groupBy = '';
|
|
275
|
+
if (groupByField) {
|
|
276
|
+
groupBy = `\nGROUP BY "${sourceTable}"."${groupByField}"`;
|
|
277
|
+
}
|
|
278
|
+
else if (hasAggregates) {
|
|
279
|
+
groupBy = `\nGROUP BY "${sourceTable}"."id"`;
|
|
280
|
+
}
|
|
281
|
+
const viewName = sanitizeTableName(viewSchema.viewName);
|
|
282
|
+
const sql = `CREATE OR REPLACE VIEW "${viewName}" AS\nSELECT\n ${selectColumns.join(',\n ')}\nFROM "${sourceTable}"${joinClauses ? '\n ' + joinClauses : ''}${groupBy}`;
|
|
283
|
+
return sql;
|
|
284
|
+
}
|
|
285
|
+
export function viewSchemasToSnapshot(viewSchemas) {
|
|
286
|
+
const snapshot = {};
|
|
287
|
+
for (const [name, schema] of Object.entries(viewSchemas)) {
|
|
288
|
+
snapshot[name] = {
|
|
289
|
+
viewName: schema.viewName,
|
|
290
|
+
source: schema.source,
|
|
291
|
+
...(schema.groupBy ? { groupBy: schema.groupBy } : {}),
|
|
292
|
+
columns: { ...schema.columns },
|
|
293
|
+
foreignKeys: { ...schema.foreignKeys },
|
|
294
|
+
isView: true,
|
|
295
|
+
viewQuery: buildViewDDL(name, schema),
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
return snapshot;
|
|
299
|
+
}
|
|
300
|
+
export function schemasToSnapshot(schemas) {
|
|
301
|
+
const snapshot = {};
|
|
302
|
+
for (const [name, schema] of Object.entries(schemas)) {
|
|
303
|
+
snapshot[name] = {
|
|
304
|
+
table: schema.table,
|
|
305
|
+
idType: schema.idType,
|
|
306
|
+
columns: { ...schema.columns },
|
|
307
|
+
foreignKeys: { ...schema.foreignKeys },
|
|
308
|
+
...(schema.vectorColumns && Object.keys(schema.vectorColumns).length > 0
|
|
309
|
+
? { vectorColumns: { ...schema.vectorColumns } }
|
|
310
|
+
: {}),
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
return snapshot;
|
|
314
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
interface TransformFn {
|
|
2
|
+
pgType?: string;
|
|
3
|
+
mysqlType?: string;
|
|
4
|
+
(...args: unknown[]): unknown;
|
|
5
|
+
}
|
|
6
|
+
declare const typeMap: Record<string, string>;
|
|
7
|
+
/**
|
|
8
|
+
* Resolves a Stonyx ORM attribute type to a PostgreSQL column type.
|
|
9
|
+
*
|
|
10
|
+
* For built-in types, returns the mapped PostgreSQL type directly.
|
|
11
|
+
*
|
|
12
|
+
* For custom transforms (e.g. an `animal` transform that maps strings to ints):
|
|
13
|
+
* - If the transform function exports a `pgType` property, that value is used.
|
|
14
|
+
* - Otherwise, if `mysqlType` is defined, it is mapped to a PG equivalent.
|
|
15
|
+
* - Otherwise, defaults to JSONB. Values are JSON-stringified on write and
|
|
16
|
+
* JSON-parsed on read by PostgreSQL natively.
|
|
17
|
+
*/
|
|
18
|
+
export declare function getPgType(attrType: string, transformFn?: TransformFn): string;
|
|
19
|
+
/**
|
|
20
|
+
* Returns a vector column type for the given dimensions.
|
|
21
|
+
*/
|
|
22
|
+
export declare function getVectorType(dimensions: number): string;
|
|
23
|
+
export default typeMap;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
const typeMap = {
|
|
2
|
+
string: 'VARCHAR(255)',
|
|
3
|
+
number: 'INTEGER',
|
|
4
|
+
float: 'REAL',
|
|
5
|
+
boolean: 'BOOLEAN',
|
|
6
|
+
date: 'TIMESTAMPTZ',
|
|
7
|
+
timestamp: 'BIGINT',
|
|
8
|
+
passthrough: 'TEXT',
|
|
9
|
+
trim: 'VARCHAR(255)',
|
|
10
|
+
uppercase: 'VARCHAR(255)',
|
|
11
|
+
ceil: 'INTEGER',
|
|
12
|
+
floor: 'INTEGER',
|
|
13
|
+
round: 'INTEGER',
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Resolves a Stonyx ORM attribute type to a PostgreSQL column type.
|
|
17
|
+
*
|
|
18
|
+
* For built-in types, returns the mapped PostgreSQL type directly.
|
|
19
|
+
*
|
|
20
|
+
* For custom transforms (e.g. an `animal` transform that maps strings to ints):
|
|
21
|
+
* - If the transform function exports a `pgType` property, that value is used.
|
|
22
|
+
* - Otherwise, if `mysqlType` is defined, it is mapped to a PG equivalent.
|
|
23
|
+
* - Otherwise, defaults to JSONB. Values are JSON-stringified on write and
|
|
24
|
+
* JSON-parsed on read by PostgreSQL natively.
|
|
25
|
+
*/
|
|
26
|
+
export function getPgType(attrType, transformFn) {
|
|
27
|
+
if (typeMap[attrType])
|
|
28
|
+
return typeMap[attrType];
|
|
29
|
+
if (transformFn?.pgType)
|
|
30
|
+
return transformFn.pgType;
|
|
31
|
+
if (transformFn?.mysqlType)
|
|
32
|
+
return mysqlTypeToPg(transformFn.mysqlType);
|
|
33
|
+
return 'JSONB';
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Returns a vector column type for the given dimensions.
|
|
37
|
+
*/
|
|
38
|
+
export function getVectorType(dimensions) {
|
|
39
|
+
if (!Number.isInteger(dimensions) || dimensions < 1 || dimensions > 16000) {
|
|
40
|
+
throw new Error(`Invalid vector dimensions: ${dimensions}. Must be an integer between 1 and 16000.`);
|
|
41
|
+
}
|
|
42
|
+
return `vector(${dimensions})`;
|
|
43
|
+
}
|
|
44
|
+
function mysqlTypeToPg(mysqlType) {
|
|
45
|
+
const upper = mysqlType.toUpperCase();
|
|
46
|
+
if (upper === 'TINYINT(1)')
|
|
47
|
+
return 'BOOLEAN';
|
|
48
|
+
if (upper === 'INT' || upper === 'INT AUTO_INCREMENT')
|
|
49
|
+
return 'INTEGER';
|
|
50
|
+
if (upper === 'DATETIME')
|
|
51
|
+
return 'TIMESTAMPTZ';
|
|
52
|
+
if (upper === 'JSON')
|
|
53
|
+
return 'JSONB';
|
|
54
|
+
return mysqlType;
|
|
55
|
+
}
|
|
56
|
+
export default typeMap;
|
package/dist/record.d.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type Serializer from './serializer.js';
|
|
2
|
+
interface ToJSONOptions {
|
|
3
|
+
fields?: Set<string>;
|
|
4
|
+
baseUrl?: string;
|
|
5
|
+
}
|
|
6
|
+
interface SerializeOptions {
|
|
7
|
+
update?: boolean;
|
|
8
|
+
serialize?: boolean;
|
|
9
|
+
transform?: boolean;
|
|
10
|
+
[key: string]: unknown;
|
|
11
|
+
}
|
|
12
|
+
interface UnloadOptions {
|
|
13
|
+
[key: string]: unknown;
|
|
14
|
+
}
|
|
15
|
+
interface RelationshipLinks {
|
|
16
|
+
self: string;
|
|
17
|
+
related: string;
|
|
18
|
+
}
|
|
19
|
+
interface RelationshipEntry {
|
|
20
|
+
data: {
|
|
21
|
+
type: string;
|
|
22
|
+
id: unknown;
|
|
23
|
+
} | {
|
|
24
|
+
type: string;
|
|
25
|
+
id: unknown;
|
|
26
|
+
}[] | null;
|
|
27
|
+
links?: RelationshipLinks;
|
|
28
|
+
}
|
|
29
|
+
interface JSONAPIResult {
|
|
30
|
+
attributes: {
|
|
31
|
+
[key: string]: unknown;
|
|
32
|
+
};
|
|
33
|
+
relationships: {
|
|
34
|
+
[key: string]: RelationshipEntry;
|
|
35
|
+
};
|
|
36
|
+
id: unknown;
|
|
37
|
+
type: string;
|
|
38
|
+
links?: {
|
|
39
|
+
self: string;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export default class Record {
|
|
43
|
+
/** @private */
|
|
44
|
+
__data: {
|
|
45
|
+
[key: string]: unknown;
|
|
46
|
+
};
|
|
47
|
+
/** @private */
|
|
48
|
+
__relationships: {
|
|
49
|
+
[key: string]: unknown;
|
|
50
|
+
};
|
|
51
|
+
/** @private */
|
|
52
|
+
__serialized: boolean;
|
|
53
|
+
/** @private */
|
|
54
|
+
__model: {
|
|
55
|
+
__name: string;
|
|
56
|
+
[key: string]: unknown;
|
|
57
|
+
};
|
|
58
|
+
/** @private */
|
|
59
|
+
__serializer: Serializer;
|
|
60
|
+
[key: string]: unknown;
|
|
61
|
+
constructor(model: {
|
|
62
|
+
__name: string;
|
|
63
|
+
[key: string]: unknown;
|
|
64
|
+
}, serializer: Serializer);
|
|
65
|
+
serialize(rawData?: unknown, options?: SerializeOptions): {
|
|
66
|
+
[key: string]: unknown;
|
|
67
|
+
};
|
|
68
|
+
format(): {
|
|
69
|
+
[key: string]: unknown;
|
|
70
|
+
};
|
|
71
|
+
toJSON(options?: ToJSONOptions): JSONAPIResult;
|
|
72
|
+
unload(options?: UnloadOptions): void;
|
|
73
|
+
clean(): void;
|
|
74
|
+
}
|
|
75
|
+
export {};
|
package/dist/record.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { store } from '@stonyx/orm';
|
|
2
|
+
import { getComputedProperties } from "./serializer.js";
|
|
3
|
+
import { camelCaseToKebabCase } from '@stonyx/utils/string';
|
|
4
|
+
import { getPluralName } from './plural-registry.js';
|
|
5
|
+
export default class Record {
|
|
6
|
+
/** @private */
|
|
7
|
+
__data = {};
|
|
8
|
+
/** @private */
|
|
9
|
+
__relationships = {};
|
|
10
|
+
/** @private */
|
|
11
|
+
__serialized = false;
|
|
12
|
+
/** @private */
|
|
13
|
+
__model;
|
|
14
|
+
/** @private */
|
|
15
|
+
__serializer;
|
|
16
|
+
constructor(model, serializer) {
|
|
17
|
+
this.__model = model;
|
|
18
|
+
this.__serializer = serializer;
|
|
19
|
+
}
|
|
20
|
+
serialize(rawData, options = {}) {
|
|
21
|
+
const { __data: data } = this;
|
|
22
|
+
if (this.__serialized && !options.update) {
|
|
23
|
+
const relatedIds = {};
|
|
24
|
+
for (const [key, childRecord] of Object.entries(this.__relationships)) {
|
|
25
|
+
relatedIds[key] = Array.isArray(childRecord)
|
|
26
|
+
? childRecord.map((r) => r.id)
|
|
27
|
+
: childRecord?.id ?? null;
|
|
28
|
+
}
|
|
29
|
+
return { ...data, ...relatedIds };
|
|
30
|
+
}
|
|
31
|
+
const normalizedData = this.__serializer.normalize(rawData);
|
|
32
|
+
this.__serializer.setProperties(normalizedData, this, options);
|
|
33
|
+
return data;
|
|
34
|
+
}
|
|
35
|
+
// Similar to serialize, but preserves top level relationship records
|
|
36
|
+
format() {
|
|
37
|
+
if (!this.__serialized)
|
|
38
|
+
throw new Error('Record must be serialized before being converted to JSON');
|
|
39
|
+
const { __data: data } = this;
|
|
40
|
+
const records = {};
|
|
41
|
+
for (const [key, childRecord] of Object.entries(this.__relationships)) {
|
|
42
|
+
if (Array.isArray(childRecord)) {
|
|
43
|
+
// Deduplicate by record ID — keep last occurrence (latest data wins)
|
|
44
|
+
const seen = new Set();
|
|
45
|
+
const unique = [];
|
|
46
|
+
for (let i = childRecord.length - 1; i >= 0; i--) {
|
|
47
|
+
const r = childRecord[i];
|
|
48
|
+
if (!seen.has(r.id)) {
|
|
49
|
+
seen.add(r.id);
|
|
50
|
+
unique.push(r);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
unique.reverse();
|
|
54
|
+
records[key] = unique.map((r) => r.serialize());
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
records[key] = childRecord?.serialize() ?? null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return { ...data, ...records };
|
|
61
|
+
}
|
|
62
|
+
// Formats record for JSON API output
|
|
63
|
+
toJSON(options = {}) {
|
|
64
|
+
if (!this.__serialized)
|
|
65
|
+
throw new Error('Record must be serialized before being converted to JSON');
|
|
66
|
+
const { fields, baseUrl } = options;
|
|
67
|
+
const { __data: data } = this;
|
|
68
|
+
const modelName = this.__model.__name;
|
|
69
|
+
const pluralizedModelName = getPluralName(modelName);
|
|
70
|
+
const recordId = data.id;
|
|
71
|
+
const relationships = {};
|
|
72
|
+
const attributes = {};
|
|
73
|
+
for (const [key, value] of Object.entries(data)) {
|
|
74
|
+
if (key === 'id')
|
|
75
|
+
continue;
|
|
76
|
+
if (fields && !fields.has(key))
|
|
77
|
+
continue;
|
|
78
|
+
attributes[key] = value;
|
|
79
|
+
}
|
|
80
|
+
for (const [key, getter] of getComputedProperties(this.__model)) {
|
|
81
|
+
if (fields && !fields.has(key))
|
|
82
|
+
continue;
|
|
83
|
+
attributes[key] = getter.call(this);
|
|
84
|
+
}
|
|
85
|
+
for (const [key, childRecord] of Object.entries(this.__relationships)) {
|
|
86
|
+
if (fields && !fields.has(key))
|
|
87
|
+
continue;
|
|
88
|
+
const relationshipData = Array.isArray(childRecord)
|
|
89
|
+
? childRecord.map((r) => ({ type: r.__model.__name, id: r.id }))
|
|
90
|
+
: childRecord ? { type: childRecord.__model.__name, id: childRecord.id } : null;
|
|
91
|
+
// Dasherize the key for URL paths (e.g., accessLinks -> access-links)
|
|
92
|
+
const dasherizedKey = camelCaseToKebabCase(key);
|
|
93
|
+
relationships[dasherizedKey] = { data: relationshipData };
|
|
94
|
+
// Add links to relationship if baseUrl provided
|
|
95
|
+
if (baseUrl) {
|
|
96
|
+
relationships[dasherizedKey].links = {
|
|
97
|
+
self: `${baseUrl}/${pluralizedModelName}/${recordId}/relationships/${dasherizedKey}`,
|
|
98
|
+
related: `${baseUrl}/${pluralizedModelName}/${recordId}/${dasherizedKey}`
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const result = {
|
|
103
|
+
attributes,
|
|
104
|
+
relationships,
|
|
105
|
+
id: recordId,
|
|
106
|
+
type: modelName,
|
|
107
|
+
};
|
|
108
|
+
// Add resource links if baseUrl provided
|
|
109
|
+
if (baseUrl) {
|
|
110
|
+
result.links = {
|
|
111
|
+
self: `${baseUrl}/${pluralizedModelName}/${recordId}`
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
return result;
|
|
115
|
+
}
|
|
116
|
+
unload(options = {}) {
|
|
117
|
+
store.unloadRecord(this.__model.__name, this.id, options);
|
|
118
|
+
}
|
|
119
|
+
clean() {
|
|
120
|
+
try {
|
|
121
|
+
for (const key of Object.keys(this)) {
|
|
122
|
+
delete this[key];
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
// Ignore errors during cleanup, as some keys may not be deletable
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { HasManyMap, BelongsToMap, GlobalMap, PendingMap, PendingBelongsToMap } from './types/orm-types.js';
|
|
2
|
+
export declare function getRelationships(type: string, sourceModel: string, targetModel: string, relationshipId?: string): Map<unknown, unknown> | undefined;
|
|
3
|
+
export declare function getHasManyRelationships(sourceModel: string, targetModel: string): Map<unknown, unknown> | undefined;
|
|
4
|
+
/** Typed accessors for the relationship registry */
|
|
5
|
+
export declare function getHasManyRegistry(): HasManyMap;
|
|
6
|
+
export declare function getBelongsToRegistry(): BelongsToMap;
|
|
7
|
+
export declare function getGlobalRegistry(): GlobalMap;
|
|
8
|
+
export declare function getPendingRegistry(): PendingMap;
|
|
9
|
+
export declare function getPendingBelongsToRegistry(): PendingBelongsToMap;
|
|
10
|
+
export declare const TYPES: string[];
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { relationships } from '@stonyx/orm';
|
|
2
|
+
// TODO: Refactor mapping to remove a level of iteration
|
|
3
|
+
export function getRelationships(type, sourceModel, targetModel, relationshipId) {
|
|
4
|
+
let allRelationships = relationships.get(type);
|
|
5
|
+
if (!allRelationships) {
|
|
6
|
+
allRelationships = new Map();
|
|
7
|
+
relationships.set(type, allRelationships);
|
|
8
|
+
}
|
|
9
|
+
// create relationship map for this type of it doesn't already exist
|
|
10
|
+
if (!allRelationships.has(sourceModel))
|
|
11
|
+
allRelationships.set(sourceModel, new Map());
|
|
12
|
+
const modelRelationship = allRelationships.get(sourceModel);
|
|
13
|
+
if (!modelRelationship)
|
|
14
|
+
return undefined;
|
|
15
|
+
if (!modelRelationship.has(targetModel))
|
|
16
|
+
modelRelationship.set(targetModel, new Map());
|
|
17
|
+
const relationship = modelRelationship.get(targetModel);
|
|
18
|
+
// TODO: Determine whether already having id should be handled differently
|
|
19
|
+
//if (relationship.has(relationshipId)) return;
|
|
20
|
+
return relationship;
|
|
21
|
+
}
|
|
22
|
+
export function getHasManyRelationships(sourceModel, targetModel) {
|
|
23
|
+
return relationships.get('hasMany')?.get(sourceModel)?.get(targetModel);
|
|
24
|
+
}
|
|
25
|
+
/** Typed accessors for the relationship registry */
|
|
26
|
+
export function getHasManyRegistry() {
|
|
27
|
+
return relationships.get('hasMany');
|
|
28
|
+
}
|
|
29
|
+
export function getBelongsToRegistry() {
|
|
30
|
+
return relationships.get('belongsTo');
|
|
31
|
+
}
|
|
32
|
+
export function getGlobalRegistry() {
|
|
33
|
+
return relationships.get('global');
|
|
34
|
+
}
|
|
35
|
+
export function getPendingRegistry() {
|
|
36
|
+
return relationships.get('pending');
|
|
37
|
+
}
|
|
38
|
+
export function getPendingBelongsToRegistry() {
|
|
39
|
+
return relationships.get('pendingBelongsTo');
|
|
40
|
+
}
|
|
41
|
+
export const TYPES = ['global', 'hasMany', 'belongsTo', 'pending'];
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export default class Serializer {
|
|
2
|
+
map: Record<string, unknown>;
|
|
3
|
+
path: string;
|
|
4
|
+
model: Record<string, unknown>;
|
|
5
|
+
constructor(model: Record<string, unknown>);
|
|
6
|
+
/**
|
|
7
|
+
* This method populates the record's instance with instances of
|
|
8
|
+
* the ModelProperty object, while setting parsed values to the record's
|
|
9
|
+
* __data property, which represents the serialized version of the data
|
|
10
|
+
*/
|
|
11
|
+
setProperties(rawData: unknown, record: unknown, options: Record<string, unknown>): void;
|
|
12
|
+
/**
|
|
13
|
+
* OVERRIDE: This hook allows for data manipulation prior to serialization logic
|
|
14
|
+
*/
|
|
15
|
+
normalize(data: unknown): unknown;
|
|
16
|
+
}
|
|
17
|
+
export declare function getComputedProperties(classInstance: Record<string, unknown>): [string, PropertyDescriptor['get']][];
|