@stonyx/orm 0.2.1-beta.40 → 0.2.1-beta.42
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 +21 -0
- package/package.json +6 -5
- package/scripts/setup-test-db.sh +21 -0
- package/src/belongs-to.js +4 -1
- package/src/has-many.js +4 -1
- package/src/mysql/schema-introspector.js +32 -28
package/README.md
CHANGED
|
@@ -223,6 +223,27 @@ Set the `MYSQL_HOST` environment variable to enable MySQL persistence. The ORM l
|
|
|
223
223
|
| `stonyx db:migrate:rollback` | Rollback the most recent migration |
|
|
224
224
|
| `stonyx db:migrate:status` | Show migration status |
|
|
225
225
|
|
|
226
|
+
### Running MySQL Tests
|
|
227
|
+
|
|
228
|
+
The ORM includes integration tests that run against a real MySQL database. These are optional — all other tests work without MySQL.
|
|
229
|
+
|
|
230
|
+
**One-time setup:**
|
|
231
|
+
|
|
232
|
+
```bash
|
|
233
|
+
# Requires local MySQL 8.0+ running
|
|
234
|
+
./scripts/setup-test-db.sh
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
This creates a `stonyx_orm_test` database with a `stonyx_test` user. Safe to re-run.
|
|
238
|
+
|
|
239
|
+
**Running tests:**
|
|
240
|
+
|
|
241
|
+
```bash
|
|
242
|
+
npm test
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
MySQL integration tests run automatically when MySQL is available. In CI (where `CI=true`), they skip gracefully.
|
|
246
|
+
|
|
226
247
|
## REST Server Integration
|
|
227
248
|
|
|
228
249
|
The ORM can automatically register REST routes using your access classes.
|
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"stonyx-async",
|
|
5
5
|
"stonyx-module"
|
|
6
6
|
],
|
|
7
|
-
"version": "0.2.1-beta.
|
|
7
|
+
"version": "0.2.1-beta.42",
|
|
8
8
|
"description": "",
|
|
9
9
|
"main": "src/main.js",
|
|
10
10
|
"type": "module",
|
|
@@ -33,13 +33,13 @@
|
|
|
33
33
|
},
|
|
34
34
|
"homepage": "https://github.com/abofs/stonyx-orm#readme",
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"stonyx": "0.2.
|
|
36
|
+
"@stonyx/cron": "0.2.1-beta.15",
|
|
37
37
|
"@stonyx/events": "0.1.1-beta.7",
|
|
38
|
-
"
|
|
38
|
+
"stonyx": "0.2.3-beta.6"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
|
-
"
|
|
42
|
-
"
|
|
41
|
+
"@stonyx/rest-server": ">=0.2.1-beta.14",
|
|
42
|
+
"mysql2": "^3.0.0"
|
|
43
43
|
},
|
|
44
44
|
"peerDependenciesMeta": {
|
|
45
45
|
"mysql2": {
|
|
@@ -52,6 +52,7 @@
|
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@stonyx/rest-server": "0.2.1-beta.19",
|
|
54
54
|
"@stonyx/utils": "0.2.3-beta.5",
|
|
55
|
+
"mysql2": "^3.20.0",
|
|
55
56
|
"qunit": "^2.24.1",
|
|
56
57
|
"sinon": "^21.0.0"
|
|
57
58
|
},
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# scripts/setup-test-db.sh
|
|
3
|
+
# Creates the stonyx_orm_test database and stonyx_test user.
|
|
4
|
+
# Idempotent — safe to re-run. Requires local MySQL with root access.
|
|
5
|
+
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
|
|
8
|
+
echo "Setting up stonyx-orm test database..."
|
|
9
|
+
|
|
10
|
+
mysql -u root <<SQL
|
|
11
|
+
CREATE DATABASE IF NOT EXISTS stonyx_orm_test CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
|
12
|
+
CREATE USER IF NOT EXISTS 'stonyx_test'@'localhost' IDENTIFIED BY 'stonyx_test';
|
|
13
|
+
GRANT ALL PRIVILEGES ON stonyx_orm_test.* TO 'stonyx_test'@'localhost';
|
|
14
|
+
FLUSH PRIVILEGES;
|
|
15
|
+
SQL
|
|
16
|
+
|
|
17
|
+
echo ""
|
|
18
|
+
echo "Done! Test database 'stonyx_orm_test' is ready."
|
|
19
|
+
echo "User: stonyx_test / Password: stonyx_test"
|
|
20
|
+
echo ""
|
|
21
|
+
echo "Run tests with: npm test"
|
package/src/belongs-to.js
CHANGED
|
@@ -11,7 +11,7 @@ export default function belongsTo(modelName) {
|
|
|
11
11
|
const pendingHasManyQueue = relationships.get('pending');
|
|
12
12
|
const pendingBelongsToQueue = relationships.get('pendingBelongsTo');
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
const fn = (sourceRecord, rawData, options) => {
|
|
15
15
|
if (!rawData) return null;
|
|
16
16
|
|
|
17
17
|
const { __name: sourceModelName } = sourceRecord.__model;
|
|
@@ -60,4 +60,7 @@ export default function belongsTo(modelName) {
|
|
|
60
60
|
|
|
61
61
|
return output;
|
|
62
62
|
}
|
|
63
|
+
|
|
64
|
+
Object.defineProperty(fn, '__relatedModelName', { value: modelName });
|
|
65
|
+
return fn;
|
|
63
66
|
}
|
package/src/has-many.js
CHANGED
|
@@ -16,7 +16,7 @@ export default function hasMany(modelName) {
|
|
|
16
16
|
const globalRelationships = relationships.get('global');
|
|
17
17
|
const pendingRelationships = relationships.get('pending');
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
const fn = (sourceRecord, rawData, options) => {
|
|
20
20
|
const { __name: sourceModelName } = sourceRecord.__model;
|
|
21
21
|
const relationshipId = sourceRecord.id;
|
|
22
22
|
const relationship = getRelationships('hasMany', sourceModelName, modelName, relationshipId);
|
|
@@ -58,4 +58,7 @@ export default function hasMany(modelName) {
|
|
|
58
58
|
|
|
59
59
|
return output;
|
|
60
60
|
}
|
|
61
|
+
|
|
62
|
+
Object.defineProperty(fn, '__relatedModelName', { value: modelName });
|
|
63
|
+
return fn;
|
|
61
64
|
}
|
|
@@ -8,13 +8,18 @@ import { AggregateProperty } from '../aggregates.js';
|
|
|
8
8
|
function getRelationshipInfo(property) {
|
|
9
9
|
if (typeof property !== 'function') return null;
|
|
10
10
|
const fnStr = property.toString();
|
|
11
|
+
const modelName = property.__relatedModelName || null;
|
|
11
12
|
|
|
12
|
-
if (fnStr.includes(`getRelationships('belongsTo',`)) return 'belongsTo';
|
|
13
|
-
if (fnStr.includes(`getRelationships('hasMany',`)) return 'hasMany';
|
|
13
|
+
if (fnStr.includes(`getRelationships('belongsTo',`)) return { type: 'belongsTo', modelName };
|
|
14
|
+
if (fnStr.includes(`getRelationships('hasMany',`)) return { type: 'hasMany', modelName };
|
|
14
15
|
|
|
15
16
|
return null;
|
|
16
17
|
}
|
|
17
18
|
|
|
19
|
+
function sanitizeTableName(name) {
|
|
20
|
+
return name.replace(/[-/]/g, '_');
|
|
21
|
+
}
|
|
22
|
+
|
|
18
23
|
export function introspectModels() {
|
|
19
24
|
const { models } = Orm.instance;
|
|
20
25
|
const schemas = {};
|
|
@@ -35,12 +40,12 @@ export function introspectModels() {
|
|
|
35
40
|
for (const [key, property] of Object.entries(model)) {
|
|
36
41
|
if (key.startsWith('__')) continue;
|
|
37
42
|
|
|
38
|
-
const
|
|
43
|
+
const relInfo = getRelationshipInfo(property);
|
|
39
44
|
|
|
40
|
-
if (
|
|
41
|
-
relationships.belongsTo[key] =
|
|
42
|
-
} else if (
|
|
43
|
-
relationships.hasMany[key] =
|
|
45
|
+
if (relInfo?.type === 'belongsTo') {
|
|
46
|
+
relationships.belongsTo[key] = relInfo.modelName;
|
|
47
|
+
} else if (relInfo?.type === 'hasMany') {
|
|
48
|
+
relationships.hasMany[key] = relInfo.modelName;
|
|
44
49
|
} else if (property?.constructor?.name === 'ModelProperty') {
|
|
45
50
|
if (key === 'id') {
|
|
46
51
|
idType = property.type;
|
|
@@ -51,17 +56,16 @@ export function introspectModels() {
|
|
|
51
56
|
}
|
|
52
57
|
|
|
53
58
|
// Build foreign keys from belongsTo relationships
|
|
54
|
-
for (const relName of Object.
|
|
55
|
-
const modelName = camelCaseToKebabCase(relName);
|
|
59
|
+
for (const [relName, targetModelName] of Object.entries(relationships.belongsTo)) {
|
|
56
60
|
const fkColumn = `${relName}_id`;
|
|
57
61
|
foreignKeys[fkColumn] = {
|
|
58
|
-
references: getPluralName(
|
|
62
|
+
references: sanitizeTableName(getPluralName(targetModelName)),
|
|
59
63
|
column: 'id',
|
|
60
64
|
};
|
|
61
65
|
}
|
|
62
66
|
|
|
63
67
|
schemas[name] = {
|
|
64
|
-
table: getPluralName(name),
|
|
68
|
+
table: sanitizeTableName(getPluralName(name)),
|
|
65
69
|
idType,
|
|
66
70
|
columns,
|
|
67
71
|
foreignKeys,
|
|
@@ -74,7 +78,8 @@ export function introspectModels() {
|
|
|
74
78
|
}
|
|
75
79
|
|
|
76
80
|
export function buildTableDDL(name, schema, allSchemas = {}) {
|
|
77
|
-
const {
|
|
81
|
+
const { idType, columns, foreignKeys } = schema;
|
|
82
|
+
const table = sanitizeTableName(schema.table);
|
|
78
83
|
const lines = [];
|
|
79
84
|
|
|
80
85
|
// Primary key
|
|
@@ -101,7 +106,8 @@ export function buildTableDDL(name, schema, allSchemas = {}) {
|
|
|
101
106
|
|
|
102
107
|
// Foreign key constraints
|
|
103
108
|
for (const [fkCol, fkDef] of Object.entries(foreignKeys)) {
|
|
104
|
-
|
|
109
|
+
const refTable = sanitizeTableName(fkDef.references);
|
|
110
|
+
lines.push(` FOREIGN KEY (\`${fkCol}\`) REFERENCES \`${refTable}\`(\`${fkDef.column}\`) ON DELETE SET NULL`);
|
|
105
111
|
}
|
|
106
112
|
|
|
107
113
|
return `CREATE TABLE IF NOT EXISTS \`${table}\` (\n${lines.join(',\n')}\n)`;
|
|
@@ -131,8 +137,8 @@ export function getTopologicalOrder(schemas) {
|
|
|
131
137
|
if (!schema) return;
|
|
132
138
|
|
|
133
139
|
// Visit dependencies (belongsTo targets) first
|
|
134
|
-
for (const
|
|
135
|
-
visit(
|
|
140
|
+
for (const targetModelName of Object.values(schema.relationships.belongsTo)) {
|
|
141
|
+
visit(targetModelName);
|
|
136
142
|
}
|
|
137
143
|
|
|
138
144
|
order.push(name);
|
|
@@ -172,18 +178,17 @@ export function introspectViews() {
|
|
|
172
178
|
continue;
|
|
173
179
|
}
|
|
174
180
|
|
|
175
|
-
const
|
|
181
|
+
const relInfo = getRelationshipInfo(property);
|
|
176
182
|
|
|
177
|
-
if (
|
|
178
|
-
relationships.belongsTo[key] =
|
|
179
|
-
const modelName = camelCaseToKebabCase(key);
|
|
183
|
+
if (relInfo?.type === 'belongsTo') {
|
|
184
|
+
relationships.belongsTo[key] = relInfo.modelName;
|
|
180
185
|
const fkColumn = `${key}_id`;
|
|
181
186
|
foreignKeys[fkColumn] = {
|
|
182
|
-
references: getPluralName(modelName),
|
|
187
|
+
references: sanitizeTableName(getPluralName(relInfo.modelName)),
|
|
183
188
|
column: 'id',
|
|
184
189
|
};
|
|
185
|
-
} else if (
|
|
186
|
-
relationships.hasMany[key] =
|
|
190
|
+
} else if (relInfo?.type === 'hasMany') {
|
|
191
|
+
relationships.hasMany[key] = relInfo.modelName;
|
|
187
192
|
} else if (property?.constructor?.name === 'ModelProperty') {
|
|
188
193
|
const transforms = Orm.instance.transforms;
|
|
189
194
|
columns[key] = getMysqlType(property.type, transforms[property.type]);
|
|
@@ -191,7 +196,7 @@ export function introspectViews() {
|
|
|
191
196
|
}
|
|
192
197
|
|
|
193
198
|
schemas[name] = {
|
|
194
|
-
viewName: getPluralName(name),
|
|
199
|
+
viewName: sanitizeTableName(getPluralName(name)),
|
|
195
200
|
source,
|
|
196
201
|
groupBy: viewClass.groupBy || undefined,
|
|
197
202
|
columns,
|
|
@@ -213,9 +218,9 @@ export function buildViewDDL(name, viewSchema, modelSchemas = {}) {
|
|
|
213
218
|
|
|
214
219
|
const sourceModelName = viewSchema.source;
|
|
215
220
|
const sourceSchema = modelSchemas[sourceModelName];
|
|
216
|
-
const sourceTable = sourceSchema
|
|
221
|
+
const sourceTable = sanitizeTableName(sourceSchema
|
|
217
222
|
? sourceSchema.table
|
|
218
|
-
: getPluralName(sourceModelName);
|
|
223
|
+
: getPluralName(sourceModelName));
|
|
219
224
|
|
|
220
225
|
const selectColumns = [];
|
|
221
226
|
const joins = [];
|
|
@@ -241,8 +246,7 @@ export function buildViewDDL(name, viewSchema, modelSchemas = {}) {
|
|
|
241
246
|
} else {
|
|
242
247
|
// Relationship aggregate
|
|
243
248
|
const relName = aggProp.relationship;
|
|
244
|
-
const
|
|
245
|
-
const relTable = getPluralName(relModelName);
|
|
249
|
+
const relTable = sanitizeTableName(getPluralName(relName));
|
|
246
250
|
|
|
247
251
|
if (aggProp.aggregateType === 'count') {
|
|
248
252
|
selectColumns.push(`${aggProp.mysqlFunction}(\`${relTable}\`.\`id\`) AS \`${key}\``);
|
|
@@ -281,7 +285,7 @@ export function buildViewDDL(name, viewSchema, modelSchemas = {}) {
|
|
|
281
285
|
groupBy = `\nGROUP BY \`${sourceTable}\`.\`id\``;
|
|
282
286
|
}
|
|
283
287
|
|
|
284
|
-
const viewName = viewSchema.viewName;
|
|
288
|
+
const viewName = sanitizeTableName(viewSchema.viewName);
|
|
285
289
|
const sql = `CREATE OR REPLACE VIEW \`${viewName}\` AS\nSELECT\n ${selectColumns.join(',\n ')}\nFROM \`${sourceTable}\`${joinClauses ? '\n ' + joinClauses : ''}${groupBy}`;
|
|
286
290
|
|
|
287
291
|
return sql;
|