@getstrata/core 0.5.101 → 0.7.4
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/dialect.d.ts +18 -0
- package/dist/core/database/factory.d.ts +1 -0
- package/dist/core/database/index.d.ts +8 -0
- package/dist/core/database/mysqlConnection.d.ts +12 -0
- package/dist/core/database/namedConnections.d.ts +15 -0
- package/dist/core/database/repositoryQuery.d.ts +1 -0
- 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 +49 -32
- 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 +14 -6
- package/dist/entries/database/repositoryQuery.js +96 -81
- 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 +26 -3
- package/dist/entries/runtime/frontendMode.js +39 -10
- package/dist/framework/public-api.d.ts +11 -1
- package/dist/index.js +873 -270
- 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
|
|
|
@@ -1064,6 +1073,19 @@ function relationMatchKey(value) {
|
|
|
1064
1073
|
}
|
|
1065
1074
|
return String(value);
|
|
1066
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
|
+
}
|
|
1067
1089
|
function getByRelationKey(map, key) {
|
|
1068
1090
|
if (map.has(key)) {
|
|
1069
1091
|
return map.get(key);
|
|
@@ -1072,12 +1094,7 @@ function getByRelationKey(map, key) {
|
|
|
1072
1094
|
if (want === "") {
|
|
1073
1095
|
return;
|
|
1074
1096
|
}
|
|
1075
|
-
|
|
1076
|
-
if (relationMatchKey(existing) === want) {
|
|
1077
|
-
return value;
|
|
1078
|
-
}
|
|
1079
|
-
}
|
|
1080
|
-
return;
|
|
1097
|
+
return indexedRelationLookup(map).get(want);
|
|
1081
1098
|
}
|
|
1082
1099
|
function indexHasManyRelation(parents, children, relation) {
|
|
1083
1100
|
const groups = new Map;
|
|
@@ -1292,12 +1309,12 @@ async function eagerLoadOnModels(models, paths) {
|
|
|
1292
1309
|
}
|
|
1293
1310
|
grouped.set(head, existing);
|
|
1294
1311
|
}
|
|
1295
|
-
|
|
1312
|
+
await Promise.all([...grouped.entries()].map(async ([head, nested]) => {
|
|
1296
1313
|
const unloaded = models.filter((model) => model.loaded(head) === undefined);
|
|
1297
1314
|
if (unloaded.length > 0) {
|
|
1298
1315
|
const first = unloaded[0];
|
|
1299
1316
|
if (!first) {
|
|
1300
|
-
|
|
1317
|
+
return;
|
|
1301
1318
|
}
|
|
1302
1319
|
const method = first[head];
|
|
1303
1320
|
if (typeof method !== "function") {
|
|
@@ -1313,14 +1330,14 @@ async function eagerLoadOnModels(models, paths) {
|
|
|
1313
1330
|
}
|
|
1314
1331
|
}
|
|
1315
1332
|
if (nested.length === 0) {
|
|
1316
|
-
|
|
1333
|
+
return;
|
|
1317
1334
|
}
|
|
1318
1335
|
const children = models.flatMap((model) => {
|
|
1319
1336
|
const loaded = model.loaded(head);
|
|
1320
1337
|
return Array.isArray(loaded) ? loaded : loaded ? [loaded] : [];
|
|
1321
1338
|
});
|
|
1322
1339
|
await eagerLoadOnModels(children.filter(isLoadableModel), nested);
|
|
1323
|
-
}
|
|
1340
|
+
}));
|
|
1324
1341
|
}
|
|
1325
1342
|
async function loadNested(model, path) {
|
|
1326
1343
|
await eagerLoadOnModels([model], [path]);
|
|
@@ -1598,12 +1615,12 @@ class ModelQuery {
|
|
|
1598
1615
|
const models = [];
|
|
1599
1616
|
for (const row of rows) {
|
|
1600
1617
|
const model = statics.newFromRecord(row, true);
|
|
1601
|
-
await runObservers(model, "retrieved");
|
|
1602
1618
|
for (const { name, relationQuery } of this.eager) {
|
|
1603
1619
|
model.setLoaded(name, relationQuery.hydrateEager(row, name));
|
|
1604
1620
|
}
|
|
1605
1621
|
models.push(model);
|
|
1606
1622
|
}
|
|
1623
|
+
await Promise.all(models.map((model) => runObservers(model, "retrieved")));
|
|
1607
1624
|
const nested = this.eager.filter((item) => item.path.includes(".")).map((item) => item.path);
|
|
1608
1625
|
await eagerLoadOnModels(models.filter(isLoadableModel), nested);
|
|
1609
1626
|
return models;
|
|
@@ -2060,7 +2077,7 @@ class Model {
|
|
|
2060
2077
|
morphTo(relatedByType, morphName, typeKey, idKey) {
|
|
2061
2078
|
const resolvedName = morphName ?? inferRelationMethodName("morphTo");
|
|
2062
2079
|
if (!resolvedName) {
|
|
2063
|
-
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).`);
|
|
2064
2081
|
}
|
|
2065
2082
|
const resolvedMap = Object.fromEntries(Object.entries(relatedByType).map(([type, related]) => [type, resolveRelated(related)]));
|
|
2066
2083
|
return new MorphToRelationQuery(this, resolvedMap, morphTo({
|
|
@@ -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 {
|
|
@@ -46,6 +46,19 @@ function relationMatchKey(value) {
|
|
|
46
46
|
}
|
|
47
47
|
return String(value);
|
|
48
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
|
+
}
|
|
49
62
|
function getByRelationKey(map, key) {
|
|
50
63
|
if (map.has(key)) {
|
|
51
64
|
return map.get(key);
|
|
@@ -54,12 +67,7 @@ function getByRelationKey(map, key) {
|
|
|
54
67
|
if (want === "") {
|
|
55
68
|
return;
|
|
56
69
|
}
|
|
57
|
-
|
|
58
|
-
if (relationMatchKey(existing) === want) {
|
|
59
|
-
return value;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
return;
|
|
70
|
+
return indexedRelationLookup(map).get(want);
|
|
63
71
|
}
|
|
64
72
|
function indexHasManyRelation(parents, children, relation) {
|
|
65
73
|
const groups = new Map;
|
|
@@ -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
|
|
|
@@ -469,6 +482,19 @@ function relationMatchKey(value) {
|
|
|
469
482
|
}
|
|
470
483
|
return String(value);
|
|
471
484
|
}
|
|
485
|
+
var relationLookupCache = new WeakMap;
|
|
486
|
+
function indexedRelationLookup(map) {
|
|
487
|
+
const cached = relationLookupCache.get(map);
|
|
488
|
+
if (cached) {
|
|
489
|
+
return cached;
|
|
490
|
+
}
|
|
491
|
+
const indexed = new Map;
|
|
492
|
+
for (const [existing, value] of map) {
|
|
493
|
+
indexed.set(relationMatchKey(existing), value);
|
|
494
|
+
}
|
|
495
|
+
relationLookupCache.set(map, indexed);
|
|
496
|
+
return indexed;
|
|
497
|
+
}
|
|
472
498
|
function getByRelationKey(map, key) {
|
|
473
499
|
if (map.has(key)) {
|
|
474
500
|
return map.get(key);
|
|
@@ -477,12 +503,7 @@ function getByRelationKey(map, key) {
|
|
|
477
503
|
if (want === "") {
|
|
478
504
|
return;
|
|
479
505
|
}
|
|
480
|
-
|
|
481
|
-
if (relationMatchKey(existing) === want) {
|
|
482
|
-
return value;
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
return;
|
|
506
|
+
return indexedRelationLookup(map).get(want);
|
|
486
507
|
}
|
|
487
508
|
function indexHasManyRelation(parents, children, relation) {
|
|
488
509
|
const groups = new Map;
|
|
@@ -895,73 +916,67 @@ class RepositoryQuery {
|
|
|
895
916
|
return this;
|
|
896
917
|
}
|
|
897
918
|
async attach(rows) {
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
919
|
+
const result = rows.map((row) => ({ ...row }));
|
|
920
|
+
if (result.length === 0 || this.eagerLoads.length === 0) {
|
|
921
|
+
return result;
|
|
922
|
+
}
|
|
923
|
+
await Promise.all(this.eagerLoads.map((load) => this.hydrateEagerLoad(rows, result, load)));
|
|
924
|
+
return result;
|
|
925
|
+
}
|
|
926
|
+
async hydrateEagerLoad(rows, result, load) {
|
|
927
|
+
if (load.kind === "hasMany") {
|
|
928
|
+
const relation2 = load.relation;
|
|
929
|
+
const grouped2 = await load.repository.withConnection(this.repository.getConnection()).loadHasManyForParents(rows, relation2, load.options);
|
|
930
|
+
for (const row of result) {
|
|
931
|
+
row[load.as] = getByRelationKey(grouped2, row[relation2.localKey]) ?? [];
|
|
911
932
|
}
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
continue;
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
if (load.kind === "morphMany") {
|
|
936
|
+
const relation2 = load.relation;
|
|
937
|
+
const grouped2 = await load.repository.withConnection(this.repository.getConnection()).loadMorphManyForParents(rows, relation2, load.options);
|
|
938
|
+
for (const row of result) {
|
|
939
|
+
row[load.as] = getByRelationKey(grouped2, row[relation2.localKey]) ?? [];
|
|
920
940
|
}
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
continue;
|
|
941
|
+
return;
|
|
942
|
+
}
|
|
943
|
+
if (load.kind === "morphOne") {
|
|
944
|
+
const relation2 = load.relation;
|
|
945
|
+
const grouped2 = await load.repository.withConnection(this.repository.getConnection()).loadMorphOneForParents(rows, relation2, load.options);
|
|
946
|
+
for (const row of result) {
|
|
947
|
+
row[load.as] = getByRelationKey(grouped2, row[relation2.localKey]);
|
|
929
948
|
}
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
continue;
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
if (load.kind === "hasManyThrough") {
|
|
952
|
+
const relation2 = load.relation;
|
|
953
|
+
const grouped2 = await load.repository.withConnection(this.repository.getConnection()).loadHasManyThroughForParents(rows, relation2, load.options);
|
|
954
|
+
for (const row of result) {
|
|
955
|
+
row[load.as] = getByRelationKey(grouped2, row[relation2.localKey]) ?? [];
|
|
938
956
|
}
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
continue;
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
959
|
+
if (load.kind === "belongsToMany") {
|
|
960
|
+
const relation2 = load.relation;
|
|
961
|
+
const grouped2 = await this.repository.loadBelongsToManyForParents(rows, relation2, load.repository, load.options);
|
|
962
|
+
for (const row of result) {
|
|
963
|
+
row[load.as] = getByRelationKey(grouped2, row[relation2.parentKey]) ?? [];
|
|
947
964
|
}
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
continue;
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
if (load.kind === "morphTo") {
|
|
968
|
+
const relation2 = load.relation;
|
|
969
|
+
const grouped2 = await this.repository.loadMorphToForChildren(rows, relation2, load.morphRepositories ?? new Map, load.options);
|
|
970
|
+
for (const row of result) {
|
|
971
|
+
row[load.as] = getByRelationKey(grouped2, row[relation2.morphIdKey]);
|
|
956
972
|
}
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
const relation = load.relation;
|
|
976
|
+
const grouped = await this.repository.loadBelongsToForParents(rows, relation, load.repository, load.options);
|
|
977
|
+
for (const row of result) {
|
|
978
|
+
row[load.as] = getByRelationKey(grouped, row[relation.foreignKey]);
|
|
963
979
|
}
|
|
964
|
-
return result;
|
|
965
980
|
}
|
|
966
981
|
}
|
|
967
982
|
export {
|