@getstrata/core 0.5.100 → 0.7.3
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/CHANGELOG.md +67 -32
- package/README.md +16 -35
- package/dist/core/auth/abilityCatalog.d.ts +2 -2
- package/dist/core/auth/basicAuthGuard.d.ts +9 -0
- package/dist/core/auth/guard.d.ts +6 -0
- package/dist/core/auth/jwt.d.ts +19 -0
- package/dist/core/auth/jwtGuard.d.ts +14 -0
- package/dist/core/auth/tokenAbilityChecker.d.ts +5 -0
- package/dist/core/cache/tags.d.ts +6 -0
- package/dist/core/contracts/authUserDirectory.d.ts +4 -0
- package/dist/core/database/baseRepository.d.ts +5 -1
- package/dist/core/database/dialect.d.ts +18 -0
- package/dist/core/database/factory.d.ts +1 -0
- package/dist/core/database/index.d.ts +11 -3
- package/dist/core/database/model.d.ts +21 -2
- package/dist/core/database/mysqlConnection.d.ts +12 -0
- package/dist/core/database/namedConnections.d.ts +15 -0
- package/dist/core/database/relationQuery.d.ts +22 -3
- package/dist/core/database/relationships.d.ts +22 -2
- package/dist/core/database/repositoryQuery.d.ts +5 -1
- package/dist/core/database/sqliteConnection.d.ts +7 -0
- package/dist/core/http/loginThrottleMiddleware.d.ts +5 -2
- package/dist/core/http/resources.d.ts +2 -2
- package/dist/core/http/response.d.ts +2 -1
- package/dist/core/http/statelessAuth.d.ts +8 -0
- package/dist/core/http/throttleResponse.d.ts +2 -0
- package/dist/core/runtime/frontendMode.d.ts +10 -2
- package/dist/entries/auth/basicAuthGuard.js +137 -0
- package/dist/entries/auth/jwt.js +135 -0
- package/dist/entries/auth/jwtGuard.js +203 -0
- package/dist/entries/auth/sessionGuard.js +3 -21
- package/dist/entries/auth/tokenAbilityChecker.js +24 -0
- package/dist/entries/cache/tags.js +7 -1
- package/dist/entries/database/connectionContext.js +1 -0
- package/dist/entries/database/dialect.js +1 -0
- package/dist/entries/database/factory.js +5 -4
- package/dist/entries/database/model.js +189 -33
- package/dist/entries/database/mysqlConnection.js +35 -0
- package/dist/entries/database/namedConnections.js +1 -0
- package/dist/entries/database/query.js +28 -15
- package/dist/entries/database/relationships.js +43 -6
- package/dist/entries/database/repositoryQuery.js +142 -73
- package/dist/entries/database/schema.js +28 -15
- package/dist/entries/database/sqliteConnection.js +34 -0
- package/dist/entries/facades.js +1 -1
- package/dist/entries/http/contentNegotiation.js +5 -2
- package/dist/entries/http/csrfMiddleware.js +45 -0
- package/dist/entries/http/loginThrottleMiddleware.js +246 -7
- package/dist/entries/http/memoryThrottleMiddleware.js +208 -6
- package/dist/entries/http/requireAbilityMiddleware.js +35 -11
- package/dist/entries/http/requirePasswordConfirmMiddleware.js +5 -2
- package/dist/entries/http/requireVerifiedMiddleware.js +5 -2
- package/dist/entries/http/requireWebAuthMiddleware.js +12 -3
- package/dist/entries/http/resources.js +4 -1
- package/dist/entries/http/response.js +46 -12
- package/dist/entries/http/statelessAuth.js +48 -0
- package/dist/entries/http/throttleMiddleware.js +208 -6
- package/dist/entries/http/webErrorResponse.js +35 -11
- package/dist/entries/http/webFormRequest.js +5 -2
- package/dist/entries/mail/mailer.js +1 -1
- package/dist/entries/openapi/generator.js +48 -5
- package/dist/entries/runtime/frontendMode.js +39 -10
- package/dist/framework/public-api.d.ts +11 -1
- package/dist/index.js +1068 -250
- package/package.json +56 -5
|
@@ -23,11 +23,12 @@ function pivotTableName(leftTable, rightTable) {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
// ../../src/core/database/query.ts
|
|
26
|
+
import { currentSqlDialect } from "@getstrata/core/database/dialect";
|
|
26
27
|
function quoteIdentifier(identifier) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
return
|
|
28
|
+
return currentSqlDialect().quoteIdentifier(identifier);
|
|
29
|
+
}
|
|
30
|
+
function returningSuffix(columns) {
|
|
31
|
+
return currentSqlDialect().returningClause(columns);
|
|
31
32
|
}
|
|
32
33
|
function qualifyColumn(tableName, column) {
|
|
33
34
|
return `${quoteIdentifier(tableName)}.${quoteIdentifier(column)}`;
|
|
@@ -57,7 +58,7 @@ function isQueryOperator(value) {
|
|
|
57
58
|
}
|
|
58
59
|
function pushParam(values, value) {
|
|
59
60
|
values.push(value);
|
|
60
|
-
return
|
|
61
|
+
return currentSqlDialect().placeholder(values.length);
|
|
61
62
|
}
|
|
62
63
|
function buildInClause(column, values, params) {
|
|
63
64
|
if (values.length === 0) {
|
|
@@ -97,9 +98,12 @@ function buildOperatorClauses(column, operator, params) {
|
|
|
97
98
|
clauses.push(`${column} <= ${pushParam(params, operator.lte)}`);
|
|
98
99
|
}
|
|
99
100
|
if (operator.ilike !== undefined) {
|
|
100
|
-
clauses.push(`${column}
|
|
101
|
+
clauses.push(`${column} ${currentSqlDialect().ilikeOperator()} ${pushParam(params, operator.ilike)}`);
|
|
101
102
|
}
|
|
102
103
|
if (operator.tsMatch !== undefined) {
|
|
104
|
+
if (currentSqlDialect().driver !== "pgsql") {
|
|
105
|
+
throw new Error("Full-text search (tsMatch) is only available on PostgreSQL.");
|
|
106
|
+
}
|
|
103
107
|
clauses.push(`${column} @@ plainto_tsquery('english', ${pushParam(params, operator.tsMatch)})`);
|
|
104
108
|
}
|
|
105
109
|
return clauses;
|
|
@@ -298,7 +302,7 @@ function buildSelectList(table, select, params = []) {
|
|
|
298
302
|
return item.as ? `${column2} AS ${quoteIdentifier(item.as)}` : column2;
|
|
299
303
|
}
|
|
300
304
|
if (item.kind === "literalText") {
|
|
301
|
-
return `${pushParam(params, item.value)}
|
|
305
|
+
return `${currentSqlDialect().castToText(pushParam(params, item.value))} AS ${quoteIdentifier(item.as)}`;
|
|
302
306
|
}
|
|
303
307
|
const column = qualifyColumn(item.table, item.column);
|
|
304
308
|
const placeholder = pushParam(params, item.query);
|
|
@@ -390,7 +394,7 @@ function buildInsertQuery(table, values) {
|
|
|
390
394
|
const placeholders = entries.map(([, value]) => pushParam(params, value)).join(", ");
|
|
391
395
|
const returningColumns = buildReturningColumns(table);
|
|
392
396
|
return {
|
|
393
|
-
text: `INSERT INTO ${quoteIdentifier(table.name)} (${columns}) VALUES (${placeholders})
|
|
397
|
+
text: `INSERT INTO ${quoteIdentifier(table.name)} (${columns}) VALUES (${placeholders})${returningSuffix(returningColumns)}`,
|
|
394
398
|
params
|
|
395
399
|
};
|
|
396
400
|
}
|
|
@@ -409,7 +413,7 @@ function buildUpdateQuery(table, id, changes) {
|
|
|
409
413
|
appendSoftDeleteScope(table, {}, scopeClauses);
|
|
410
414
|
const scopeSuffix = scopeClauses.length > 0 ? ` AND ${scopeClauses.join(" AND ")}` : "";
|
|
411
415
|
return {
|
|
412
|
-
text: `UPDATE ${quoteIdentifier(table.name)} SET ${setClause} WHERE ${quoteIdentifier(table.primaryKey)} = ${primaryKeyPlaceholder}${scopeSuffix}
|
|
416
|
+
text: `UPDATE ${quoteIdentifier(table.name)} SET ${setClause} WHERE ${quoteIdentifier(table.primaryKey)} = ${primaryKeyPlaceholder}${scopeSuffix}${returningSuffix(returningColumns)}`,
|
|
413
417
|
params
|
|
414
418
|
};
|
|
415
419
|
}
|
|
@@ -422,9 +426,12 @@ function buildSoftDeleteByIdQuery(table, id, deletedAt) {
|
|
|
422
426
|
const scopeClauses = [];
|
|
423
427
|
appendSoftDeleteScope(table, {}, scopeClauses);
|
|
424
428
|
const scopeSuffix = scopeClauses.length > 0 ? ` AND ${scopeClauses.join(" AND ")}` : "";
|
|
429
|
+
const params = [];
|
|
430
|
+
const deletedAtPlaceholder = pushParam(params, deletedAt);
|
|
431
|
+
const idPlaceholder = pushParam(params, id);
|
|
425
432
|
return {
|
|
426
|
-
text: `UPDATE ${quoteIdentifier(table.name)} SET ${quoteIdentifier(deletedAtColumn)} = $
|
|
427
|
-
params
|
|
433
|
+
text: `UPDATE ${quoteIdentifier(table.name)} SET ${quoteIdentifier(deletedAtColumn)} = ${deletedAtPlaceholder} WHERE ${quoteIdentifier(table.primaryKey)} = ${idPlaceholder}${scopeSuffix}${returningSuffix(returningColumns)}`,
|
|
434
|
+
params
|
|
428
435
|
};
|
|
429
436
|
}
|
|
430
437
|
function buildRestoreByIdQuery(table, id) {
|
|
@@ -433,15 +440,21 @@ function buildRestoreByIdQuery(table, id) {
|
|
|
433
440
|
throw new Error(`Table ${table.name} does not support soft deletes.`);
|
|
434
441
|
}
|
|
435
442
|
const returningColumns = buildReturningColumns(table);
|
|
443
|
+
const params = [];
|
|
444
|
+
const deletedAtPlaceholder = pushParam(params, null);
|
|
445
|
+
const idPlaceholder = pushParam(params, id);
|
|
436
446
|
return {
|
|
437
|
-
text: `UPDATE ${quoteIdentifier(table.name)} SET ${quoteIdentifier(deletedAtColumn)} = $
|
|
438
|
-
params
|
|
447
|
+
text: `UPDATE ${quoteIdentifier(table.name)} SET ${quoteIdentifier(deletedAtColumn)} = ${deletedAtPlaceholder} WHERE ${quoteIdentifier(table.primaryKey)} = ${idPlaceholder} AND ${qualifyColumn(table.name, deletedAtColumn)} IS NOT NULL${returningSuffix(returningColumns)}`,
|
|
448
|
+
params
|
|
439
449
|
};
|
|
440
450
|
}
|
|
441
451
|
function buildDeleteByIdQuery(table, id) {
|
|
452
|
+
const params = [];
|
|
453
|
+
const idPlaceholder = pushParam(params, id);
|
|
454
|
+
const returning = returningSuffix(`${quoteIdentifier(table.primaryKey)} AS ${quoteIdentifier("deleted_id")}`);
|
|
442
455
|
return {
|
|
443
|
-
text: `DELETE FROM ${quoteIdentifier(table.name)} WHERE ${quoteIdentifier(table.primaryKey)} = $
|
|
444
|
-
params
|
|
456
|
+
text: `DELETE FROM ${quoteIdentifier(table.name)} WHERE ${quoteIdentifier(table.primaryKey)} = ${idPlaceholder}${returning}`,
|
|
457
|
+
params
|
|
445
458
|
};
|
|
446
459
|
}
|
|
447
460
|
|
|
@@ -547,11 +560,7 @@ class HasManyRelationQuery {
|
|
|
547
560
|
return this.create(related);
|
|
548
561
|
}
|
|
549
562
|
async createMany(records) {
|
|
550
|
-
|
|
551
|
-
for (const attributes of records) {
|
|
552
|
-
created.push(await this.create(attributes));
|
|
553
|
-
}
|
|
554
|
-
return created;
|
|
563
|
+
return Promise.all(records.map((attributes) => this.create(attributes)));
|
|
555
564
|
}
|
|
556
565
|
}
|
|
557
566
|
|
|
@@ -775,7 +784,8 @@ class BelongsToManyRelationQuery {
|
|
|
775
784
|
return;
|
|
776
785
|
}
|
|
777
786
|
const list = Array.isArray(ids) ? ids : [ids];
|
|
778
|
-
|
|
787
|
+
const placeholders = list.map((_, index) => `$${index + 2}`).join(", ");
|
|
788
|
+
await this.connection().unsafe(`DELETE FROM ${this.relation.pivotTable} WHERE ${String(this.relation.foreignPivotKey)} = $1 AND ${String(this.relation.relatedPivotKey)} IN (${placeholders})`, [parentId, ...list]);
|
|
779
789
|
}
|
|
780
790
|
async sync(ids) {
|
|
781
791
|
await this.detach();
|
|
@@ -957,6 +967,65 @@ class MorphToRelationQuery {
|
|
|
957
967
|
}
|
|
958
968
|
}
|
|
959
969
|
|
|
970
|
+
class HasManyThroughRelationQuery {
|
|
971
|
+
parent;
|
|
972
|
+
related;
|
|
973
|
+
relation;
|
|
974
|
+
kind = "hasManyThrough";
|
|
975
|
+
extraWhere = {};
|
|
976
|
+
extraOptions = {};
|
|
977
|
+
constructor(parent, related, relation) {
|
|
978
|
+
this.parent = parent;
|
|
979
|
+
this.related = related;
|
|
980
|
+
this.relation = relation;
|
|
981
|
+
}
|
|
982
|
+
where(where) {
|
|
983
|
+
this.extraWhere = { ...this.extraWhere, ...where };
|
|
984
|
+
return this;
|
|
985
|
+
}
|
|
986
|
+
orderBy(orderBy) {
|
|
987
|
+
this.extraOptions = { ...this.extraOptions, orderBy };
|
|
988
|
+
return this;
|
|
989
|
+
}
|
|
990
|
+
limit(limit) {
|
|
991
|
+
this.extraOptions = { ...this.extraOptions, limit };
|
|
992
|
+
return this;
|
|
993
|
+
}
|
|
994
|
+
applyEagerLoad(query, alias) {
|
|
995
|
+
query.withHasManyThrough(alias, this.relation, this.related.repository(), this.extraOptions);
|
|
996
|
+
}
|
|
997
|
+
hydrateEager(row, alias) {
|
|
998
|
+
const value = row[alias] ?? [];
|
|
999
|
+
const rows = Array.isArray(value) ? value : [];
|
|
1000
|
+
return rows.map((item) => this.related.newFromRecord(item));
|
|
1001
|
+
}
|
|
1002
|
+
toExistsClause(parentTable) {
|
|
1003
|
+
const farTable = this.related.repository().getTable().name;
|
|
1004
|
+
const extra = buildAdvancedWhereClause(farTable, this.extraWhere, [], []);
|
|
1005
|
+
const extraSql = extra.clause.replace(/^ WHERE /, "");
|
|
1006
|
+
const sql = `SELECT 1 FROM ${quoteIdentifier(farTable)} INNER JOIN ${quoteIdentifier(this.relation.throughTable)} ON ${qualifyColumn(this.relation.throughTable, this.relation.secondLocalKey)} = ${qualifyColumn(farTable, this.relation.secondKey)} WHERE ${qualifyColumn(this.relation.throughTable, this.relation.firstKey)} = ${qualifyColumn(parentTable, this.relation.localKey)}${extraSql ? ` AND ${extraSql}` : ""}`;
|
|
1007
|
+
return { sql, params: extra.params };
|
|
1008
|
+
}
|
|
1009
|
+
async get() {
|
|
1010
|
+
const rows = await this.related.repository().withConnection(this.parent.getRepository().getConnection()).findHasManyThrough(this.parent.get(this.relation.localKey), this.relation, {
|
|
1011
|
+
...this.extraOptions,
|
|
1012
|
+
where: this.extraWhere
|
|
1013
|
+
});
|
|
1014
|
+
return rows.map((row) => this.related.newFromRecord(row));
|
|
1015
|
+
}
|
|
1016
|
+
async first() {
|
|
1017
|
+
const rows = await this.limit(1).get();
|
|
1018
|
+
return rows[0] ?? null;
|
|
1019
|
+
}
|
|
1020
|
+
async count() {
|
|
1021
|
+
const rows = await this.get();
|
|
1022
|
+
return rows.length;
|
|
1023
|
+
}
|
|
1024
|
+
then(onfulfilled, onrejected) {
|
|
1025
|
+
return thenGet(() => this.get(), onfulfilled, onrejected);
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
|
|
960
1029
|
// ../../src/core/database/relationships.ts
|
|
961
1030
|
function hasMany(definition) {
|
|
962
1031
|
return {
|
|
@@ -976,6 +1045,13 @@ function belongsTo(definition) {
|
|
|
976
1045
|
...definition
|
|
977
1046
|
};
|
|
978
1047
|
}
|
|
1048
|
+
function hasManyThrough(definition) {
|
|
1049
|
+
return {
|
|
1050
|
+
type: "hasManyThrough",
|
|
1051
|
+
throughParentKey: "__through_parent_id",
|
|
1052
|
+
...definition
|
|
1053
|
+
};
|
|
1054
|
+
}
|
|
979
1055
|
function belongsToMany(definition) {
|
|
980
1056
|
return {
|
|
981
1057
|
type: "belongsToMany",
|
|
@@ -997,6 +1073,19 @@ function relationMatchKey(value) {
|
|
|
997
1073
|
}
|
|
998
1074
|
return String(value);
|
|
999
1075
|
}
|
|
1076
|
+
var relationLookupCache = new WeakMap;
|
|
1077
|
+
function indexedRelationLookup(map) {
|
|
1078
|
+
const cached = relationLookupCache.get(map);
|
|
1079
|
+
if (cached) {
|
|
1080
|
+
return cached;
|
|
1081
|
+
}
|
|
1082
|
+
const indexed = new Map;
|
|
1083
|
+
for (const [existing, value] of map) {
|
|
1084
|
+
indexed.set(relationMatchKey(existing), value);
|
|
1085
|
+
}
|
|
1086
|
+
relationLookupCache.set(map, indexed);
|
|
1087
|
+
return indexed;
|
|
1088
|
+
}
|
|
1000
1089
|
function getByRelationKey(map, key) {
|
|
1001
1090
|
if (map.has(key)) {
|
|
1002
1091
|
return map.get(key);
|
|
@@ -1005,12 +1094,7 @@ function getByRelationKey(map, key) {
|
|
|
1005
1094
|
if (want === "") {
|
|
1006
1095
|
return;
|
|
1007
1096
|
}
|
|
1008
|
-
|
|
1009
|
-
if (relationMatchKey(existing) === want) {
|
|
1010
|
-
return value;
|
|
1011
|
-
}
|
|
1012
|
-
}
|
|
1013
|
-
return;
|
|
1097
|
+
return indexedRelationLookup(map).get(want);
|
|
1014
1098
|
}
|
|
1015
1099
|
function indexHasManyRelation(parents, children, relation) {
|
|
1016
1100
|
const groups = new Map;
|
|
@@ -1149,6 +1233,26 @@ function indexMorphOneRelation(parents, children, relation) {
|
|
|
1149
1233
|
}
|
|
1150
1234
|
return result;
|
|
1151
1235
|
}
|
|
1236
|
+
function indexHasManyThroughRelation(parents, children, relation) {
|
|
1237
|
+
const throughKey = relation.throughParentKey ?? "__through_parent_id";
|
|
1238
|
+
const grouped = new Map;
|
|
1239
|
+
for (const child of children) {
|
|
1240
|
+
const key = relationMatchKey(child[throughKey]);
|
|
1241
|
+
if (key === "") {
|
|
1242
|
+
continue;
|
|
1243
|
+
}
|
|
1244
|
+
const existing = grouped.get(key) ?? [];
|
|
1245
|
+
const { [throughKey]: _through, ...far } = child;
|
|
1246
|
+
existing.push(far);
|
|
1247
|
+
grouped.set(key, existing);
|
|
1248
|
+
}
|
|
1249
|
+
const result = new Map;
|
|
1250
|
+
for (const parent of parents) {
|
|
1251
|
+
const key = relationMatchKey(parent[relation.localKey]);
|
|
1252
|
+
result.set(parent[relation.localKey], grouped.get(key) ?? []);
|
|
1253
|
+
}
|
|
1254
|
+
return result;
|
|
1255
|
+
}
|
|
1152
1256
|
function indexMorphToRelation(children, parentsByType, relation) {
|
|
1153
1257
|
const result = new Map;
|
|
1154
1258
|
for (const child of children) {
|
|
@@ -1205,12 +1309,12 @@ async function eagerLoadOnModels(models, paths) {
|
|
|
1205
1309
|
}
|
|
1206
1310
|
grouped.set(head, existing);
|
|
1207
1311
|
}
|
|
1208
|
-
|
|
1312
|
+
await Promise.all([...grouped.entries()].map(async ([head, nested]) => {
|
|
1209
1313
|
const unloaded = models.filter((model) => model.loaded(head) === undefined);
|
|
1210
1314
|
if (unloaded.length > 0) {
|
|
1211
1315
|
const first = unloaded[0];
|
|
1212
1316
|
if (!first) {
|
|
1213
|
-
|
|
1317
|
+
return;
|
|
1214
1318
|
}
|
|
1215
1319
|
const method = first[head];
|
|
1216
1320
|
if (typeof method !== "function") {
|
|
@@ -1226,14 +1330,14 @@ async function eagerLoadOnModels(models, paths) {
|
|
|
1226
1330
|
}
|
|
1227
1331
|
}
|
|
1228
1332
|
if (nested.length === 0) {
|
|
1229
|
-
|
|
1333
|
+
return;
|
|
1230
1334
|
}
|
|
1231
1335
|
const children = models.flatMap((model) => {
|
|
1232
1336
|
const loaded = model.loaded(head);
|
|
1233
1337
|
return Array.isArray(loaded) ? loaded : loaded ? [loaded] : [];
|
|
1234
1338
|
});
|
|
1235
1339
|
await eagerLoadOnModels(children.filter(isLoadableModel), nested);
|
|
1236
|
-
}
|
|
1340
|
+
}));
|
|
1237
1341
|
}
|
|
1238
1342
|
async function loadNested(model, path) {
|
|
1239
1343
|
await eagerLoadOnModels([model], [path]);
|
|
@@ -1493,18 +1597,30 @@ class ModelQuery {
|
|
|
1493
1597
|
this.query.withMorphTo(...args);
|
|
1494
1598
|
return this;
|
|
1495
1599
|
}
|
|
1600
|
+
withHasManyThrough(...args) {
|
|
1601
|
+
this.query.withHasManyThrough(...args);
|
|
1602
|
+
return this;
|
|
1603
|
+
}
|
|
1604
|
+
withTrashed() {
|
|
1605
|
+
this.query.withTrashed();
|
|
1606
|
+
return this;
|
|
1607
|
+
}
|
|
1608
|
+
onlyTrashed() {
|
|
1609
|
+
this.query.onlyTrashed();
|
|
1610
|
+
return this;
|
|
1611
|
+
}
|
|
1496
1612
|
async get() {
|
|
1497
1613
|
const statics = modelStatics(this.modelClass);
|
|
1498
1614
|
const rows = await this.query.get();
|
|
1499
1615
|
const models = [];
|
|
1500
1616
|
for (const row of rows) {
|
|
1501
1617
|
const model = statics.newFromRecord(row, true);
|
|
1502
|
-
await runObservers(model, "retrieved");
|
|
1503
1618
|
for (const { name, relationQuery } of this.eager) {
|
|
1504
1619
|
model.setLoaded(name, relationQuery.hydrateEager(row, name));
|
|
1505
1620
|
}
|
|
1506
1621
|
models.push(model);
|
|
1507
1622
|
}
|
|
1623
|
+
await Promise.all(models.map((model) => runObservers(model, "retrieved")));
|
|
1508
1624
|
const nested = this.eager.filter((item) => item.path.includes(".")).map((item) => item.path);
|
|
1509
1625
|
await eagerLoadOnModels(models.filter(isLoadableModel), nested);
|
|
1510
1626
|
return models;
|
|
@@ -1700,6 +1816,31 @@ class Model {
|
|
|
1700
1816
|
static with(...relations) {
|
|
1701
1817
|
return Model.query.call(this).with(...relations);
|
|
1702
1818
|
}
|
|
1819
|
+
static withTrashed() {
|
|
1820
|
+
return Model.query.call(this).withTrashed();
|
|
1821
|
+
}
|
|
1822
|
+
static onlyTrashed() {
|
|
1823
|
+
return Model.query.call(this).onlyTrashed();
|
|
1824
|
+
}
|
|
1825
|
+
static async chunk(count, callback) {
|
|
1826
|
+
const statics = modelStatics(this);
|
|
1827
|
+
ensureBooted(this);
|
|
1828
|
+
await resolveModelRepository(this).chunk(count, async (rows) => {
|
|
1829
|
+
return await callback(rows.map((row) => statics.newFromRecord(row, true)));
|
|
1830
|
+
});
|
|
1831
|
+
}
|
|
1832
|
+
static async cursorPaginate(options) {
|
|
1833
|
+
const statics = modelStatics(this);
|
|
1834
|
+
ensureBooted(this);
|
|
1835
|
+
const page = await resolveModelRepository(this).cursorPaginate({
|
|
1836
|
+
perPage: options.perPage,
|
|
1837
|
+
cursor: options.cursor
|
|
1838
|
+
});
|
|
1839
|
+
return {
|
|
1840
|
+
data: page.data.map((row) => statics.newFromRecord(row, true)),
|
|
1841
|
+
meta: page.meta
|
|
1842
|
+
};
|
|
1843
|
+
}
|
|
1703
1844
|
static whereHas(name, constrain) {
|
|
1704
1845
|
return Model.query.call(this).whereHas(name, constrain);
|
|
1705
1846
|
}
|
|
@@ -1884,6 +2025,20 @@ class Model {
|
|
|
1884
2025
|
ownerKey: ownerKey ?? relatedTable.primaryKey
|
|
1885
2026
|
}));
|
|
1886
2027
|
}
|
|
2028
|
+
hasManyThrough(related, through, firstKey, secondKey, localKey, secondLocalKey) {
|
|
2029
|
+
const table = this.repository.getTable();
|
|
2030
|
+
const relatedClass = resolveRelated(related);
|
|
2031
|
+
const throughClass = resolveRelated(through);
|
|
2032
|
+
const throughTable = throughClass.repository().getTable();
|
|
2033
|
+
return new HasManyThroughRelationQuery(this, relatedClass, hasManyThrough({
|
|
2034
|
+
name: relatedClass.repository().getTable().name,
|
|
2035
|
+
throughTable: throughTable.name,
|
|
2036
|
+
localKey: localKey ?? table.primaryKey,
|
|
2037
|
+
firstKey: firstKey ?? foreignKeyFromTable(table.name),
|
|
2038
|
+
secondLocalKey: secondLocalKey ?? throughTable.primaryKey,
|
|
2039
|
+
secondKey: secondKey ?? foreignKeyFromTable(throughTable.name)
|
|
2040
|
+
}));
|
|
2041
|
+
}
|
|
1887
2042
|
belongsToMany(related, pivotTable, foreignPivotKey, relatedPivotKey) {
|
|
1888
2043
|
const table = this.repository.getTable();
|
|
1889
2044
|
const relatedClass = resolveRelated(related);
|
|
@@ -1922,7 +2077,7 @@ class Model {
|
|
|
1922
2077
|
morphTo(relatedByType, morphName, typeKey, idKey) {
|
|
1923
2078
|
const resolvedName = morphName ?? inferRelationMethodName("morphTo");
|
|
1924
2079
|
if (!resolvedName) {
|
|
1925
|
-
throw new Error(`${this.constructor.name}.morphTo() needs an explicit morph name (
|
|
2080
|
+
throw new Error(`${this.constructor.name}.morphTo() needs an explicit morph name (the calling method name is not available at runtime).`);
|
|
1926
2081
|
}
|
|
1927
2082
|
const resolvedMap = Object.fromEntries(Object.entries(relatedByType).map(([type, related]) => [type, resolveRelated(related)]));
|
|
1928
2083
|
return new MorphToRelationQuery(this, resolvedMap, morphTo({
|
|
@@ -1975,6 +2130,7 @@ export {
|
|
|
1975
2130
|
BelongsToManyRelationQuery,
|
|
1976
2131
|
BelongsToRelationQuery,
|
|
1977
2132
|
HasManyRelationQuery,
|
|
2133
|
+
HasManyThroughRelationQuery,
|
|
1978
2134
|
HasOneRelationQuery,
|
|
1979
2135
|
Model,
|
|
1980
2136
|
ModelQuery,
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// ../../src/core/database/mysqlConnection.ts
|
|
3
|
+
import mysql from "mysql2/promise";
|
|
4
|
+
function rowsFromResult(result) {
|
|
5
|
+
if (Array.isArray(result)) {
|
|
6
|
+
return result;
|
|
7
|
+
}
|
|
8
|
+
if (result && typeof result === "object") {
|
|
9
|
+
return [result];
|
|
10
|
+
}
|
|
11
|
+
return [];
|
|
12
|
+
}
|
|
13
|
+
function createMysqlConnectionFromPool(pool) {
|
|
14
|
+
return {
|
|
15
|
+
async unsafe(query, params = []) {
|
|
16
|
+
const [result] = await pool.execute(query, [...params]);
|
|
17
|
+
return rowsFromResult(result);
|
|
18
|
+
},
|
|
19
|
+
async close() {
|
|
20
|
+
if (typeof pool.end === "function") {
|
|
21
|
+
await pool.end();
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function createMysqlConnection(url) {
|
|
27
|
+
if (!url.trim()) {
|
|
28
|
+
throw new Error("MYSQL_URL is not configured. Set url before creating a MySQL pool.");
|
|
29
|
+
}
|
|
30
|
+
return createMysqlConnectionFromPool(mysql.createPool(url));
|
|
31
|
+
}
|
|
32
|
+
export {
|
|
33
|
+
createMysqlConnection,
|
|
34
|
+
createMysqlConnectionFromPool
|
|
35
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../../index.js";
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// ../../src/core/database/query.ts
|
|
3
|
+
import { currentSqlDialect } from "@getstrata/core/database/dialect";
|
|
3
4
|
function quoteIdentifier(identifier) {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
return
|
|
5
|
+
return currentSqlDialect().quoteIdentifier(identifier);
|
|
6
|
+
}
|
|
7
|
+
function returningSuffix(columns) {
|
|
8
|
+
return currentSqlDialect().returningClause(columns);
|
|
8
9
|
}
|
|
9
10
|
function qualifyColumn(tableName, column) {
|
|
10
11
|
return `${quoteIdentifier(tableName)}.${quoteIdentifier(column)}`;
|
|
@@ -34,7 +35,7 @@ function isQueryOperator(value) {
|
|
|
34
35
|
}
|
|
35
36
|
function pushParam(values, value) {
|
|
36
37
|
values.push(value);
|
|
37
|
-
return
|
|
38
|
+
return currentSqlDialect().placeholder(values.length);
|
|
38
39
|
}
|
|
39
40
|
function buildInClause(column, values, params) {
|
|
40
41
|
if (values.length === 0) {
|
|
@@ -74,9 +75,12 @@ function buildOperatorClauses(column, operator, params) {
|
|
|
74
75
|
clauses.push(`${column} <= ${pushParam(params, operator.lte)}`);
|
|
75
76
|
}
|
|
76
77
|
if (operator.ilike !== undefined) {
|
|
77
|
-
clauses.push(`${column}
|
|
78
|
+
clauses.push(`${column} ${currentSqlDialect().ilikeOperator()} ${pushParam(params, operator.ilike)}`);
|
|
78
79
|
}
|
|
79
80
|
if (operator.tsMatch !== undefined) {
|
|
81
|
+
if (currentSqlDialect().driver !== "pgsql") {
|
|
82
|
+
throw new Error("Full-text search (tsMatch) is only available on PostgreSQL.");
|
|
83
|
+
}
|
|
80
84
|
clauses.push(`${column} @@ plainto_tsquery('english', ${pushParam(params, operator.tsMatch)})`);
|
|
81
85
|
}
|
|
82
86
|
return clauses;
|
|
@@ -275,7 +279,7 @@ function buildSelectList(table, select, params = []) {
|
|
|
275
279
|
return item.as ? `${column2} AS ${quoteIdentifier(item.as)}` : column2;
|
|
276
280
|
}
|
|
277
281
|
if (item.kind === "literalText") {
|
|
278
|
-
return `${pushParam(params, item.value)}
|
|
282
|
+
return `${currentSqlDialect().castToText(pushParam(params, item.value))} AS ${quoteIdentifier(item.as)}`;
|
|
279
283
|
}
|
|
280
284
|
const column = qualifyColumn(item.table, item.column);
|
|
281
285
|
const placeholder = pushParam(params, item.query);
|
|
@@ -367,7 +371,7 @@ function buildInsertQuery(table, values) {
|
|
|
367
371
|
const placeholders = entries.map(([, value]) => pushParam(params, value)).join(", ");
|
|
368
372
|
const returningColumns = buildReturningColumns(table);
|
|
369
373
|
return {
|
|
370
|
-
text: `INSERT INTO ${quoteIdentifier(table.name)} (${columns}) VALUES (${placeholders})
|
|
374
|
+
text: `INSERT INTO ${quoteIdentifier(table.name)} (${columns}) VALUES (${placeholders})${returningSuffix(returningColumns)}`,
|
|
371
375
|
params
|
|
372
376
|
};
|
|
373
377
|
}
|
|
@@ -386,7 +390,7 @@ function buildUpdateQuery(table, id, changes) {
|
|
|
386
390
|
appendSoftDeleteScope(table, {}, scopeClauses);
|
|
387
391
|
const scopeSuffix = scopeClauses.length > 0 ? ` AND ${scopeClauses.join(" AND ")}` : "";
|
|
388
392
|
return {
|
|
389
|
-
text: `UPDATE ${quoteIdentifier(table.name)} SET ${setClause} WHERE ${quoteIdentifier(table.primaryKey)} = ${primaryKeyPlaceholder}${scopeSuffix}
|
|
393
|
+
text: `UPDATE ${quoteIdentifier(table.name)} SET ${setClause} WHERE ${quoteIdentifier(table.primaryKey)} = ${primaryKeyPlaceholder}${scopeSuffix}${returningSuffix(returningColumns)}`,
|
|
390
394
|
params
|
|
391
395
|
};
|
|
392
396
|
}
|
|
@@ -399,9 +403,12 @@ function buildSoftDeleteByIdQuery(table, id, deletedAt) {
|
|
|
399
403
|
const scopeClauses = [];
|
|
400
404
|
appendSoftDeleteScope(table, {}, scopeClauses);
|
|
401
405
|
const scopeSuffix = scopeClauses.length > 0 ? ` AND ${scopeClauses.join(" AND ")}` : "";
|
|
406
|
+
const params = [];
|
|
407
|
+
const deletedAtPlaceholder = pushParam(params, deletedAt);
|
|
408
|
+
const idPlaceholder = pushParam(params, id);
|
|
402
409
|
return {
|
|
403
|
-
text: `UPDATE ${quoteIdentifier(table.name)} SET ${quoteIdentifier(deletedAtColumn)} = $
|
|
404
|
-
params
|
|
410
|
+
text: `UPDATE ${quoteIdentifier(table.name)} SET ${quoteIdentifier(deletedAtColumn)} = ${deletedAtPlaceholder} WHERE ${quoteIdentifier(table.primaryKey)} = ${idPlaceholder}${scopeSuffix}${returningSuffix(returningColumns)}`,
|
|
411
|
+
params
|
|
405
412
|
};
|
|
406
413
|
}
|
|
407
414
|
function buildRestoreByIdQuery(table, id) {
|
|
@@ -410,15 +417,21 @@ function buildRestoreByIdQuery(table, id) {
|
|
|
410
417
|
throw new Error(`Table ${table.name} does not support soft deletes.`);
|
|
411
418
|
}
|
|
412
419
|
const returningColumns = buildReturningColumns(table);
|
|
420
|
+
const params = [];
|
|
421
|
+
const deletedAtPlaceholder = pushParam(params, null);
|
|
422
|
+
const idPlaceholder = pushParam(params, id);
|
|
413
423
|
return {
|
|
414
|
-
text: `UPDATE ${quoteIdentifier(table.name)} SET ${quoteIdentifier(deletedAtColumn)} = $
|
|
415
|
-
params
|
|
424
|
+
text: `UPDATE ${quoteIdentifier(table.name)} SET ${quoteIdentifier(deletedAtColumn)} = ${deletedAtPlaceholder} WHERE ${quoteIdentifier(table.primaryKey)} = ${idPlaceholder} AND ${qualifyColumn(table.name, deletedAtColumn)} IS NOT NULL${returningSuffix(returningColumns)}`,
|
|
425
|
+
params
|
|
416
426
|
};
|
|
417
427
|
}
|
|
418
428
|
function buildDeleteByIdQuery(table, id) {
|
|
429
|
+
const params = [];
|
|
430
|
+
const idPlaceholder = pushParam(params, id);
|
|
431
|
+
const returning = returningSuffix(`${quoteIdentifier(table.primaryKey)} AS ${quoteIdentifier("deleted_id")}`);
|
|
419
432
|
return {
|
|
420
|
-
text: `DELETE FROM ${quoteIdentifier(table.name)} WHERE ${quoteIdentifier(table.primaryKey)} = $
|
|
421
|
-
params
|
|
433
|
+
text: `DELETE FROM ${quoteIdentifier(table.name)} WHERE ${quoteIdentifier(table.primaryKey)} = ${idPlaceholder}${returning}`,
|
|
434
|
+
params
|
|
422
435
|
};
|
|
423
436
|
}
|
|
424
437
|
export {
|
|
@@ -18,6 +18,13 @@ function belongsTo(definition) {
|
|
|
18
18
|
...definition
|
|
19
19
|
};
|
|
20
20
|
}
|
|
21
|
+
function hasManyThrough(definition) {
|
|
22
|
+
return {
|
|
23
|
+
type: "hasManyThrough",
|
|
24
|
+
throughParentKey: "__through_parent_id",
|
|
25
|
+
...definition
|
|
26
|
+
};
|
|
27
|
+
}
|
|
21
28
|
function belongsToMany(definition) {
|
|
22
29
|
return {
|
|
23
30
|
type: "belongsToMany",
|
|
@@ -39,6 +46,19 @@ function relationMatchKey(value) {
|
|
|
39
46
|
}
|
|
40
47
|
return String(value);
|
|
41
48
|
}
|
|
49
|
+
var relationLookupCache = new WeakMap;
|
|
50
|
+
function indexedRelationLookup(map) {
|
|
51
|
+
const cached = relationLookupCache.get(map);
|
|
52
|
+
if (cached) {
|
|
53
|
+
return cached;
|
|
54
|
+
}
|
|
55
|
+
const indexed = new Map;
|
|
56
|
+
for (const [existing, value] of map) {
|
|
57
|
+
indexed.set(relationMatchKey(existing), value);
|
|
58
|
+
}
|
|
59
|
+
relationLookupCache.set(map, indexed);
|
|
60
|
+
return indexed;
|
|
61
|
+
}
|
|
42
62
|
function getByRelationKey(map, key) {
|
|
43
63
|
if (map.has(key)) {
|
|
44
64
|
return map.get(key);
|
|
@@ -47,12 +67,7 @@ function getByRelationKey(map, key) {
|
|
|
47
67
|
if (want === "") {
|
|
48
68
|
return;
|
|
49
69
|
}
|
|
50
|
-
|
|
51
|
-
if (relationMatchKey(existing) === want) {
|
|
52
|
-
return value;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
return;
|
|
70
|
+
return indexedRelationLookup(map).get(want);
|
|
56
71
|
}
|
|
57
72
|
function indexHasManyRelation(parents, children, relation) {
|
|
58
73
|
const groups = new Map;
|
|
@@ -191,6 +206,26 @@ function indexMorphOneRelation(parents, children, relation) {
|
|
|
191
206
|
}
|
|
192
207
|
return result;
|
|
193
208
|
}
|
|
209
|
+
function indexHasManyThroughRelation(parents, children, relation) {
|
|
210
|
+
const throughKey = relation.throughParentKey ?? "__through_parent_id";
|
|
211
|
+
const grouped = new Map;
|
|
212
|
+
for (const child of children) {
|
|
213
|
+
const key = relationMatchKey(child[throughKey]);
|
|
214
|
+
if (key === "") {
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
const existing = grouped.get(key) ?? [];
|
|
218
|
+
const { [throughKey]: _through, ...far } = child;
|
|
219
|
+
existing.push(far);
|
|
220
|
+
grouped.set(key, existing);
|
|
221
|
+
}
|
|
222
|
+
const result = new Map;
|
|
223
|
+
for (const parent of parents) {
|
|
224
|
+
const key = relationMatchKey(parent[relation.localKey]);
|
|
225
|
+
result.set(parent[relation.localKey], grouped.get(key) ?? []);
|
|
226
|
+
}
|
|
227
|
+
return result;
|
|
228
|
+
}
|
|
194
229
|
function indexMorphToRelation(children, parentsByType, relation) {
|
|
195
230
|
const result = new Map;
|
|
196
231
|
for (const child of children) {
|
|
@@ -211,10 +246,12 @@ export {
|
|
|
211
246
|
belongsToMany,
|
|
212
247
|
getByRelationKey,
|
|
213
248
|
hasMany,
|
|
249
|
+
hasManyThrough,
|
|
214
250
|
hasOne,
|
|
215
251
|
indexBelongsToManyRelation,
|
|
216
252
|
indexBelongsToRelation,
|
|
217
253
|
indexHasManyRelation,
|
|
254
|
+
indexHasManyThroughRelation,
|
|
218
255
|
indexHasOneRelation,
|
|
219
256
|
indexMorphManyRelation,
|
|
220
257
|
indexMorphOneRelation,
|