@getstrata/bootstrap 0.2.7 → 0.2.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bootstrap/http/securedRouteModelBinding.d.ts +11 -0
- package/dist/bootstrap/membershipService.d.ts +3 -0
- package/dist/bootstrap/public-api.d.ts +4 -1
- package/dist/bootstrap/queue/defaultJobs.d.ts +2 -0
- package/dist/core/auth/membershipService.d.ts +1 -2
- package/dist/core/http/securedRouteModelBinding.d.ts +2 -11
- package/dist/core/queue/createAppQueue.d.ts +3 -3
- package/dist/entries/context.js +692 -684
- package/dist/entries/createWebRoutes.js +1 -0
- package/dist/entries/http/securedRouteModelBinding.js +383 -0
- package/dist/entries/membershipService.js +480 -0
- package/dist/entries/providers/view.js +1 -0
- package/dist/entries/providers.js +1943 -1935
- package/dist/entries/queue/defaultJobs.js +549 -0
- package/dist/framework/public-api.d.ts +1 -0
- package/dist/index.js +447 -118
- package/package.json +18 -3
|
@@ -195,6 +195,7 @@ var db = new Proxy(function database() {}, {
|
|
|
195
195
|
return typeof value === "function" ? value.bind(connection) : value;
|
|
196
196
|
}
|
|
197
197
|
});
|
|
198
|
+
var connection_default = db;
|
|
198
199
|
|
|
199
200
|
// ../../src/modules/user/apiTokenTable.ts
|
|
200
201
|
import { defineTable } from "@getstrata/core/database";
|
|
@@ -416,6 +417,11 @@ class UnauthorizedError2 extends HttpError {
|
|
|
416
417
|
super(401, message, details);
|
|
417
418
|
}
|
|
418
419
|
}
|
|
420
|
+
class PreconditionFailedError extends HttpError {
|
|
421
|
+
constructor(message = "Precondition Failed", details) {
|
|
422
|
+
super(412, message, details);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
419
425
|
|
|
420
426
|
// ../../src/core/auth/authContext.ts
|
|
421
427
|
import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
|
|
@@ -1348,2252 +1354,2254 @@ class InvalidateCacheTagsJob extends Job {
|
|
|
1348
1354
|
}
|
|
1349
1355
|
var invalidateCacheTagsJob_default = InvalidateCacheTagsJob;
|
|
1350
1356
|
|
|
1351
|
-
// ../../src/core/
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1357
|
+
// ../../src/core/pagination/index.ts
|
|
1358
|
+
function buildPaginationMeta(input) {
|
|
1359
|
+
const lastPage = Math.max(1, Math.ceil(input.total / input.perPage));
|
|
1360
|
+
return {
|
|
1361
|
+
page: input.page,
|
|
1362
|
+
per_page: input.perPage,
|
|
1363
|
+
total: input.total,
|
|
1364
|
+
last_page: lastPage
|
|
1365
|
+
};
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
// ../../src/core/database/errors.ts
|
|
1369
|
+
function isPostgresError(error) {
|
|
1370
|
+
return typeof error === "object" && error !== null && (("errno" in error) || ("code" in error));
|
|
1371
|
+
}
|
|
1372
|
+
function getPostgresSqlState(error) {
|
|
1373
|
+
if (typeof error.errno === "string" && /^\d{5}$/.test(error.errno)) {
|
|
1374
|
+
return error.errno;
|
|
1356
1375
|
}
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
level,
|
|
1360
|
-
channel: this.channel,
|
|
1361
|
-
message,
|
|
1362
|
-
timestamp: new Date().toISOString(),
|
|
1363
|
-
...context
|
|
1364
|
-
};
|
|
1365
|
-
const line = JSON.stringify(entry);
|
|
1366
|
-
if (level === "error") {
|
|
1367
|
-
console.error(line);
|
|
1368
|
-
return;
|
|
1369
|
-
}
|
|
1370
|
-
console.log(line);
|
|
1376
|
+
if (typeof error.errno === "number") {
|
|
1377
|
+
return String(error.errno).padStart(5, "0");
|
|
1371
1378
|
}
|
|
1372
|
-
|
|
1373
|
-
|
|
1379
|
+
if (typeof error.code === "string" && /^\d{5}$/.test(error.code)) {
|
|
1380
|
+
return error.code;
|
|
1374
1381
|
}
|
|
1375
|
-
|
|
1376
|
-
|
|
1382
|
+
return;
|
|
1383
|
+
}
|
|
1384
|
+
function mapDatabaseError(error) {
|
|
1385
|
+
if (error instanceof HttpError) {
|
|
1386
|
+
return error;
|
|
1377
1387
|
}
|
|
1378
|
-
|
|
1379
|
-
|
|
1388
|
+
if (!isPostgresError(error)) {
|
|
1389
|
+
const message = error instanceof Error ? error.message : "Database operation failed.";
|
|
1390
|
+
return new BadRequestError(message);
|
|
1380
1391
|
}
|
|
1381
|
-
error
|
|
1382
|
-
|
|
1392
|
+
const sqlState = getPostgresSqlState(error);
|
|
1393
|
+
switch (sqlState) {
|
|
1394
|
+
case "23505":
|
|
1395
|
+
return new ConflictError(error.detail ?? "A record with these values already exists.", {
|
|
1396
|
+
constraint: error.constraint
|
|
1397
|
+
});
|
|
1398
|
+
case "23503":
|
|
1399
|
+
return new UnprocessableEntityError(error.detail ?? "Referenced record does not exist.", {
|
|
1400
|
+
constraint: error.constraint
|
|
1401
|
+
});
|
|
1402
|
+
case "23502":
|
|
1403
|
+
return new BadRequestError(error.detail ?? "Required field is missing.", {
|
|
1404
|
+
constraint: error.constraint
|
|
1405
|
+
});
|
|
1406
|
+
case "23514":
|
|
1407
|
+
return new BadRequestError(error.detail ?? "Value violates a database constraint.", {
|
|
1408
|
+
constraint: error.constraint
|
|
1409
|
+
});
|
|
1410
|
+
default:
|
|
1411
|
+
return new BadRequestError(error.message ?? "Database operation failed.", {
|
|
1412
|
+
code: error.code,
|
|
1413
|
+
sqlState
|
|
1414
|
+
});
|
|
1383
1415
|
}
|
|
1384
1416
|
}
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
singletonFactories = new Map;
|
|
1391
|
-
bindings = new Map;
|
|
1392
|
-
set(key, value) {
|
|
1393
|
-
this.singletonFactories.delete(key);
|
|
1394
|
-
this.bindings.delete(key);
|
|
1395
|
-
this.services.set(key, value);
|
|
1396
|
-
return value;
|
|
1397
|
-
}
|
|
1398
|
-
singleton(key, factory) {
|
|
1399
|
-
this.bindings.delete(key);
|
|
1400
|
-
this.services.delete(key);
|
|
1401
|
-
this.singletonFactories.set(key, factory);
|
|
1417
|
+
async function withDatabaseErrorHandling(operation) {
|
|
1418
|
+
try {
|
|
1419
|
+
return await operation();
|
|
1420
|
+
} catch (error) {
|
|
1421
|
+
throw mapDatabaseError(error);
|
|
1402
1422
|
}
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
// ../../src/core/database/query.ts
|
|
1426
|
+
function quoteIdentifier(identifier) {
|
|
1427
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(identifier)) {
|
|
1428
|
+
throw new Error(`Invalid SQL identifier: ${identifier}`);
|
|
1407
1429
|
}
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
const binding = this.bindings.get(key);
|
|
1419
|
-
if (binding) {
|
|
1420
|
-
return binding(this);
|
|
1430
|
+
return `"${identifier}"`;
|
|
1431
|
+
}
|
|
1432
|
+
function qualifyColumn(tableName, column) {
|
|
1433
|
+
return `${quoteIdentifier(tableName)}.${quoteIdentifier(column)}`;
|
|
1434
|
+
}
|
|
1435
|
+
function resolveQualifiedColumn(defaultTable, columnName) {
|
|
1436
|
+
if (columnName.includes(".")) {
|
|
1437
|
+
const [table, column] = columnName.split(".", 2);
|
|
1438
|
+
if (!table || !column) {
|
|
1439
|
+
throw new Error(`Invalid qualified column: ${columnName}`);
|
|
1421
1440
|
}
|
|
1422
|
-
|
|
1441
|
+
return qualifyColumn(table, column);
|
|
1423
1442
|
}
|
|
1424
|
-
|
|
1425
|
-
|
|
1443
|
+
return qualifyColumn(defaultTable, columnName);
|
|
1444
|
+
}
|
|
1445
|
+
function parseQualifiedColumn(reference) {
|
|
1446
|
+
const [table, column] = reference.split(".", 2);
|
|
1447
|
+
if (!table || !column) {
|
|
1448
|
+
throw new Error(`Join columns must be qualified as table.column: ${reference}`);
|
|
1426
1449
|
}
|
|
1427
|
-
|
|
1428
|
-
|
|
1450
|
+
return { table, column };
|
|
1451
|
+
}
|
|
1452
|
+
function normalizeDirection(direction = "ASC") {
|
|
1453
|
+
return direction.toUpperCase() === "DESC" ? "DESC" : "ASC";
|
|
1454
|
+
}
|
|
1455
|
+
function isQueryOperator(value) {
|
|
1456
|
+
return value !== null && !Array.isArray(value) && !(value instanceof Date) && typeof value === "object";
|
|
1457
|
+
}
|
|
1458
|
+
function pushParam(values, value) {
|
|
1459
|
+
values.push(value);
|
|
1460
|
+
return `$${values.length}`;
|
|
1461
|
+
}
|
|
1462
|
+
function buildInClause(column, values, params) {
|
|
1463
|
+
if (values.length === 0) {
|
|
1464
|
+
return "1 = 0";
|
|
1429
1465
|
}
|
|
1466
|
+
const placeholders = values.map((value) => pushParam(params, value)).join(", ");
|
|
1467
|
+
return `${column} IN (${placeholders})`;
|
|
1430
1468
|
}
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
this.values.set(key, value);
|
|
1436
|
-
return value;
|
|
1469
|
+
function buildOperatorClauses(column, operator, params) {
|
|
1470
|
+
const clauses = [];
|
|
1471
|
+
if (operator.isNull === true) {
|
|
1472
|
+
clauses.push(`${column} IS NULL`);
|
|
1437
1473
|
}
|
|
1438
|
-
|
|
1439
|
-
|
|
1474
|
+
if (operator.isNull === false) {
|
|
1475
|
+
clauses.push(`${column} IS NOT NULL`);
|
|
1440
1476
|
}
|
|
1441
|
-
|
|
1442
|
-
if (
|
|
1443
|
-
|
|
1477
|
+
if (operator.eq !== undefined) {
|
|
1478
|
+
if (operator.eq === null) {
|
|
1479
|
+
clauses.push(`${column} IS NULL`);
|
|
1480
|
+
} else {
|
|
1481
|
+
clauses.push(`${column} = ${pushParam(params, operator.eq)}`);
|
|
1444
1482
|
}
|
|
1445
|
-
return this.values.get(key);
|
|
1446
1483
|
}
|
|
1447
|
-
|
|
1448
|
-
|
|
1484
|
+
if (operator.in !== undefined) {
|
|
1485
|
+
clauses.push(buildInClause(column, operator.in, params));
|
|
1486
|
+
}
|
|
1487
|
+
if (operator.gt !== undefined) {
|
|
1488
|
+
clauses.push(`${column} > ${pushParam(params, operator.gt)}`);
|
|
1489
|
+
}
|
|
1490
|
+
if (operator.gte !== undefined) {
|
|
1491
|
+
clauses.push(`${column} >= ${pushParam(params, operator.gte)}`);
|
|
1492
|
+
}
|
|
1493
|
+
if (operator.lt !== undefined) {
|
|
1494
|
+
clauses.push(`${column} < ${pushParam(params, operator.lt)}`);
|
|
1495
|
+
}
|
|
1496
|
+
if (operator.lte !== undefined) {
|
|
1497
|
+
clauses.push(`${column} <= ${pushParam(params, operator.lte)}`);
|
|
1498
|
+
}
|
|
1499
|
+
if (operator.ilike !== undefined) {
|
|
1500
|
+
clauses.push(`${column} ILIKE ${pushParam(params, `%${operator.ilike}%`)}`);
|
|
1449
1501
|
}
|
|
1502
|
+
if (operator.tsMatch !== undefined) {
|
|
1503
|
+
clauses.push(`${column} @@ plainto_tsquery('english', ${pushParam(params, operator.tsMatch)})`);
|
|
1504
|
+
}
|
|
1505
|
+
return clauses;
|
|
1450
1506
|
}
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1507
|
+
function appendWhereParts(tableName, where, params) {
|
|
1508
|
+
const clauses = [];
|
|
1509
|
+
for (const [columnName, filterValue] of Object.entries(where)) {
|
|
1510
|
+
if (filterValue === undefined) {
|
|
1511
|
+
continue;
|
|
1512
|
+
}
|
|
1513
|
+
const column = resolveQualifiedColumn(tableName, columnName);
|
|
1514
|
+
if (Array.isArray(filterValue)) {
|
|
1515
|
+
clauses.push(buildInClause(column, filterValue, params));
|
|
1516
|
+
continue;
|
|
1517
|
+
}
|
|
1518
|
+
if (isQueryOperator(filterValue)) {
|
|
1519
|
+
clauses.push(...buildOperatorClauses(column, filterValue, params));
|
|
1520
|
+
continue;
|
|
1521
|
+
}
|
|
1522
|
+
if (filterValue === null) {
|
|
1523
|
+
clauses.push(`${column} IS NULL`);
|
|
1524
|
+
continue;
|
|
1525
|
+
}
|
|
1526
|
+
clauses.push(`${column} = ${pushParam(params, filterValue)}`);
|
|
1460
1527
|
}
|
|
1461
|
-
return
|
|
1528
|
+
return clauses.join(" AND ");
|
|
1462
1529
|
}
|
|
1463
|
-
function
|
|
1464
|
-
|
|
1465
|
-
|
|
1530
|
+
function buildWhereNodeClause(tableName, node, params) {
|
|
1531
|
+
if ("where" in node) {
|
|
1532
|
+
return appendWhereParts(tableName, node.where, params);
|
|
1533
|
+
}
|
|
1534
|
+
const grouped = buildWhereGroupClause(tableName, node.group, params);
|
|
1535
|
+
if (!grouped) {
|
|
1536
|
+
return "";
|
|
1466
1537
|
}
|
|
1538
|
+
return grouped.includes(" OR ") ? `(${grouped})` : grouped;
|
|
1467
1539
|
}
|
|
1468
|
-
function
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1540
|
+
function buildWhereGroupClause(tableName, nodes, params) {
|
|
1541
|
+
let result = "";
|
|
1542
|
+
for (const node of nodes) {
|
|
1543
|
+
const part = buildWhereNodeClause(tableName, node, params);
|
|
1544
|
+
if (!part) {
|
|
1545
|
+
continue;
|
|
1546
|
+
}
|
|
1547
|
+
if (!result) {
|
|
1548
|
+
result = part;
|
|
1549
|
+
continue;
|
|
1550
|
+
}
|
|
1551
|
+
result = node.kind === "or" ? `${result} OR ${part}` : `${result} AND ${part}`;
|
|
1480
1552
|
}
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
return
|
|
1485
|
-
}
|
|
1486
|
-
function resolveApplicationQueue() {
|
|
1487
|
-
return requireActiveApplicationContext().container.resolve(CORE_QUEUE_TOKEN);
|
|
1488
|
-
}
|
|
1489
|
-
function resolveApplicationAuth() {
|
|
1490
|
-
return requireActiveApplicationContext().container.resolve(CORE_AUTH_TOKEN);
|
|
1491
|
-
}
|
|
1492
|
-
function resolveApplicationPolicyGate() {
|
|
1493
|
-
return requireActiveApplicationContext().container.resolve(CORE_POLICY_GATE_TOKEN);
|
|
1494
|
-
}
|
|
1495
|
-
function resolveApplicationConfig() {
|
|
1496
|
-
return requireActiveApplicationContext().config;
|
|
1497
|
-
}
|
|
1498
|
-
function resolveApplicationLogger() {
|
|
1499
|
-
return appLogger;
|
|
1500
|
-
}
|
|
1501
|
-
function resolveApplicationDependencies() {
|
|
1502
|
-
return requireActiveApplicationContext().dependencies;
|
|
1503
|
-
}
|
|
1504
|
-
|
|
1505
|
-
// ../../src/core/jobs/dispatchWebhookJob.ts
|
|
1506
|
-
import { createHmac as createHmac2 } from "crypto";
|
|
1507
|
-
|
|
1508
|
-
// ../../src/core/database/boundConnection.ts
|
|
1509
|
-
var boundConnectionHolder = {
|
|
1510
|
-
connection: null
|
|
1511
|
-
};
|
|
1512
|
-
function getBoundDatabaseConnection() {
|
|
1513
|
-
return boundConnectionHolder.connection;
|
|
1553
|
+
if (!result) {
|
|
1554
|
+
return "";
|
|
1555
|
+
}
|
|
1556
|
+
return result;
|
|
1514
1557
|
}
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1558
|
+
function buildAdvancedWhereClause(tableName, where = {}, whereNodes = [], params = []) {
|
|
1559
|
+
const nodes = [];
|
|
1560
|
+
if (Object.keys(where).length > 0) {
|
|
1561
|
+
nodes.push({ kind: "and", where });
|
|
1562
|
+
}
|
|
1563
|
+
nodes.push(...whereNodes);
|
|
1564
|
+
const combined = buildWhereGroupClause(tableName, nodes, params);
|
|
1565
|
+
return {
|
|
1566
|
+
clause: combined ? ` WHERE ${combined}` : "",
|
|
1567
|
+
params
|
|
1568
|
+
};
|
|
1519
1569
|
}
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
return
|
|
1523
|
-
},
|
|
1524
|
-
get(_target, property) {
|
|
1525
|
-
const connection = resolveRepositoryConnection();
|
|
1526
|
-
const value = connection[property];
|
|
1527
|
-
return typeof value === "function" ? value.bind(connection) : value;
|
|
1570
|
+
function resolveSoftDeleteColumn(table) {
|
|
1571
|
+
if (!table.softDeletes) {
|
|
1572
|
+
return null;
|
|
1528
1573
|
}
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
// ../../src/core/security/safeUrl.ts
|
|
1532
|
-
import { lookup as dnsLookupImpl } from "dns/promises";
|
|
1533
|
-
var dnsLookup = dnsLookupImpl;
|
|
1534
|
-
var BLOCKED_HOSTNAMES = new Set([
|
|
1535
|
-
"localhost",
|
|
1536
|
-
"127.0.0.1",
|
|
1537
|
-
"0.0.0.0",
|
|
1538
|
-
"::1",
|
|
1539
|
-
"metadata.google.internal"
|
|
1540
|
-
]);
|
|
1541
|
-
function isPrivateIpv4(hostname) {
|
|
1542
|
-
const match = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(hostname);
|
|
1543
|
-
if (!match) {
|
|
1544
|
-
return false;
|
|
1574
|
+
if (table.softDeletes === true) {
|
|
1575
|
+
return "deleted_at";
|
|
1545
1576
|
}
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1577
|
+
return table.softDeletes.column ?? "deleted_at";
|
|
1578
|
+
}
|
|
1579
|
+
function appendSoftDeleteScope(table, options, clauses) {
|
|
1580
|
+
const column = resolveSoftDeleteColumn(table);
|
|
1581
|
+
if (!column) {
|
|
1582
|
+
return;
|
|
1549
1583
|
}
|
|
1550
|
-
const
|
|
1551
|
-
if (
|
|
1552
|
-
|
|
1584
|
+
const qualifiedColumn = qualifyColumn(table.name, column);
|
|
1585
|
+
if (options.onlyTrashed) {
|
|
1586
|
+
clauses.push(`${qualifiedColumn} IS NOT NULL`);
|
|
1587
|
+
return;
|
|
1553
1588
|
}
|
|
1554
|
-
if (
|
|
1555
|
-
|
|
1589
|
+
if (!options.withTrashed) {
|
|
1590
|
+
clauses.push(`${qualifiedColumn} IS NULL`);
|
|
1556
1591
|
}
|
|
1557
|
-
|
|
1558
|
-
|
|
1592
|
+
}
|
|
1593
|
+
function buildQueryWhereClause(table, options = {}, whereNodes = [], params = []) {
|
|
1594
|
+
const { clause, params: whereParams } = buildAdvancedWhereClause(table.name, options.where ?? {}, whereNodes, params);
|
|
1595
|
+
const softDeleteClauses = [];
|
|
1596
|
+
appendSoftDeleteScope(table, options, softDeleteClauses);
|
|
1597
|
+
if (softDeleteClauses.length === 0) {
|
|
1598
|
+
return { clause, params: whereParams };
|
|
1559
1599
|
}
|
|
1560
|
-
|
|
1561
|
-
|
|
1600
|
+
const base = clause.replace(/^ WHERE /, "");
|
|
1601
|
+
const scope = softDeleteClauses.join(" AND ");
|
|
1602
|
+
return {
|
|
1603
|
+
clause: base ? ` WHERE (${base}) AND ${scope}` : ` WHERE ${scope}`,
|
|
1604
|
+
params: whereParams
|
|
1605
|
+
};
|
|
1606
|
+
}
|
|
1607
|
+
function isQueryOrder(value) {
|
|
1608
|
+
return "column" in value;
|
|
1609
|
+
}
|
|
1610
|
+
function normalizeOrderBy(orderBy) {
|
|
1611
|
+
if (!orderBy) {
|
|
1612
|
+
return [];
|
|
1562
1613
|
}
|
|
1563
|
-
if (
|
|
1564
|
-
return
|
|
1614
|
+
if (Array.isArray(orderBy)) {
|
|
1615
|
+
return orderBy;
|
|
1565
1616
|
}
|
|
1566
|
-
if (
|
|
1567
|
-
return
|
|
1617
|
+
if (isQueryOrder(orderBy)) {
|
|
1618
|
+
return [orderBy];
|
|
1568
1619
|
}
|
|
1569
|
-
return
|
|
1620
|
+
return Object.entries(orderBy).map(([column, direction]) => ({
|
|
1621
|
+
column,
|
|
1622
|
+
direction
|
|
1623
|
+
}));
|
|
1570
1624
|
}
|
|
1571
|
-
function
|
|
1572
|
-
const
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
}
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
return true;
|
|
1625
|
+
function buildOrderByClause(tableName, orderBy) {
|
|
1626
|
+
const parts = normalizeOrderBy(orderBy).map(({ column, direction }) => {
|
|
1627
|
+
return `${resolveQualifiedColumn(tableName, column)} ${normalizeDirection(direction)}`;
|
|
1628
|
+
});
|
|
1629
|
+
return parts.length > 0 ? ` ORDER BY ${parts.join(", ")}` : "";
|
|
1630
|
+
}
|
|
1631
|
+
function buildGroupByClause(tableName, groupBy) {
|
|
1632
|
+
if (!groupBy) {
|
|
1633
|
+
return "";
|
|
1581
1634
|
}
|
|
1582
|
-
|
|
1583
|
-
|
|
1635
|
+
const columns = (Array.isArray(groupBy) ? groupBy : [groupBy]).map((column) => String(column));
|
|
1636
|
+
const parts = columns.map((column) => resolveQualifiedColumn(tableName, column));
|
|
1637
|
+
return parts.length > 0 ? ` GROUP BY ${parts.join(", ")}` : "";
|
|
1638
|
+
}
|
|
1639
|
+
function buildHavingClause(tableName, having, params) {
|
|
1640
|
+
if (!having) {
|
|
1641
|
+
return "";
|
|
1584
1642
|
}
|
|
1585
|
-
|
|
1643
|
+
const body = appendWhereParts(tableName, having, params);
|
|
1644
|
+
return body.length > 0 ? ` HAVING ${body}` : "";
|
|
1586
1645
|
}
|
|
1587
|
-
function
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1646
|
+
function buildJoinClause(joins = []) {
|
|
1647
|
+
return joins.map((join3) => {
|
|
1648
|
+
const joinType = join3.type === "left" ? "LEFT JOIN" : "INNER JOIN";
|
|
1649
|
+
const onClause = join3.on.map(({ left, right }) => `${qualifyColumn(left.table, left.column)} = ${qualifyColumn(right.table, right.column)}`).join(" AND ");
|
|
1650
|
+
return ` ${joinType} ${quoteIdentifier(join3.table)} ON ${onClause}`;
|
|
1651
|
+
}).join("");
|
|
1652
|
+
}
|
|
1653
|
+
function buildLimitClause(limit) {
|
|
1654
|
+
if (limit === undefined) {
|
|
1655
|
+
return "";
|
|
1593
1656
|
}
|
|
1594
|
-
if (
|
|
1595
|
-
throw new
|
|
1657
|
+
if (!Number.isInteger(limit) || limit <= 0) {
|
|
1658
|
+
throw new Error("Query limit must be a positive integer.");
|
|
1596
1659
|
}
|
|
1597
|
-
|
|
1598
|
-
|
|
1660
|
+
return ` LIMIT ${limit}`;
|
|
1661
|
+
}
|
|
1662
|
+
function buildOffsetClause(offset) {
|
|
1663
|
+
if (offset === undefined) {
|
|
1664
|
+
return "";
|
|
1599
1665
|
}
|
|
1600
|
-
if (
|
|
1601
|
-
throw new
|
|
1666
|
+
if (!Number.isInteger(offset) || offset < 0) {
|
|
1667
|
+
throw new Error("Query offset must be a non-negative integer.");
|
|
1602
1668
|
}
|
|
1603
|
-
return
|
|
1669
|
+
return ` OFFSET ${offset}`;
|
|
1604
1670
|
}
|
|
1605
|
-
function
|
|
1606
|
-
return
|
|
1671
|
+
function buildReturningColumns(table) {
|
|
1672
|
+
return table.columns.map((column) => qualifyColumn(table.name, column)).join(", ");
|
|
1607
1673
|
}
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
return parsed;
|
|
1612
|
-
}
|
|
1613
|
-
const hostname = parsed.hostname.trim().toLowerCase();
|
|
1614
|
-
const results = await dnsLookup(hostname, { all: true, verbatim: true });
|
|
1615
|
-
if (results.some((result) => isBlockedIpAddress(result.address))) {
|
|
1616
|
-
throw new BadRequestError("Webhook URL targets a blocked host.");
|
|
1674
|
+
function buildSelectList(table, select, params = []) {
|
|
1675
|
+
if (!select || select.length === 0) {
|
|
1676
|
+
return buildReturningColumns(table);
|
|
1617
1677
|
}
|
|
1618
|
-
return
|
|
1678
|
+
return select.map((item) => {
|
|
1679
|
+
if (item.kind === "column") {
|
|
1680
|
+
const column2 = qualifyColumn(item.table, item.column);
|
|
1681
|
+
return item.as ? `${column2} AS ${quoteIdentifier(item.as)}` : column2;
|
|
1682
|
+
}
|
|
1683
|
+
if (item.kind === "literalText") {
|
|
1684
|
+
return `${pushParam(params, item.value)}::text AS ${quoteIdentifier(item.as)}`;
|
|
1685
|
+
}
|
|
1686
|
+
const column = qualifyColumn(item.table, item.column);
|
|
1687
|
+
const placeholder = pushParam(params, item.query);
|
|
1688
|
+
return `ts_rank(${column}, plainto_tsquery('english', ${placeholder})) AS ${quoteIdentifier(item.as)}`;
|
|
1689
|
+
}).join(", ");
|
|
1619
1690
|
}
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
const resolveDns = options.resolveDns ?? appConfig.env === "production";
|
|
1627
|
-
const urlOptions = { allowHttp: options.allowHttp, resolveDns };
|
|
1628
|
-
const controller = new AbortController;
|
|
1629
|
-
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
1630
|
-
try {
|
|
1631
|
-
let currentUrl = (await assertSafeOutboundUrlResolved(input, urlOptions)).toString();
|
|
1632
|
-
let redirectCount = 0;
|
|
1633
|
-
while (true) {
|
|
1634
|
-
const response = await fetch(currentUrl, {
|
|
1635
|
-
...init,
|
|
1636
|
-
signal: controller.signal,
|
|
1637
|
-
redirect: "manual"
|
|
1638
|
-
});
|
|
1639
|
-
if (response.status >= 300 && response.status < 400) {
|
|
1640
|
-
const location = response.headers.get("location");
|
|
1641
|
-
if (!location || redirectCount >= maxRedirects) {
|
|
1642
|
-
return response;
|
|
1643
|
-
}
|
|
1644
|
-
currentUrl = (await assertSafeOutboundUrlResolved(new URL(location, currentUrl).toString(), urlOptions)).toString();
|
|
1645
|
-
redirectCount += 1;
|
|
1646
|
-
continue;
|
|
1647
|
-
}
|
|
1648
|
-
return response;
|
|
1649
|
-
}
|
|
1650
|
-
} finally {
|
|
1651
|
-
clearTimeout(timeout);
|
|
1652
|
-
}
|
|
1653
|
-
}
|
|
1654
|
-
|
|
1655
|
-
// ../../src/core/jobs/dispatchWebhookJob.ts
|
|
1656
|
-
class DispatchWebhookJob extends Job {
|
|
1657
|
-
maxAttempts = 3;
|
|
1658
|
-
backoffMs = 2000;
|
|
1659
|
-
async handle(payload) {
|
|
1660
|
-
const rows = await repositoryConnection`
|
|
1661
|
-
SELECT id, url, secret
|
|
1662
|
-
FROM webhook
|
|
1663
|
-
WHERE id = ${payload.webhookId} AND active = TRUE
|
|
1664
|
-
LIMIT 1
|
|
1665
|
-
`;
|
|
1666
|
-
const webhook = rows[0];
|
|
1667
|
-
if (!webhook) {
|
|
1668
|
-
return;
|
|
1669
|
-
}
|
|
1670
|
-
const body = JSON.stringify({ event: payload.event, payload: payload.payload });
|
|
1671
|
-
const signature = createHmac2("sha256", webhook.secret).update(body).digest("hex");
|
|
1672
|
-
assertSafeOutboundUrl(webhook.url, { allowHttp: appConfig.env !== "production" });
|
|
1673
|
-
let responseStatus = null;
|
|
1674
|
-
let errorMessage = null;
|
|
1675
|
-
try {
|
|
1676
|
-
const response = await safeFetch(webhook.url, {
|
|
1677
|
-
method: "POST",
|
|
1678
|
-
headers: {
|
|
1679
|
-
"content-type": "application/json",
|
|
1680
|
-
"x-workhub-signature": signature
|
|
1681
|
-
},
|
|
1682
|
-
body
|
|
1683
|
-
}, { allowHttp: appConfig.env !== "production" });
|
|
1684
|
-
responseStatus = response.status;
|
|
1685
|
-
if (!response.ok) {
|
|
1686
|
-
throw new Error(`Webhook delivery failed with status ${response.status}.`);
|
|
1687
|
-
}
|
|
1688
|
-
} catch (error) {
|
|
1689
|
-
errorMessage = error instanceof Error ? error.message : String(error);
|
|
1690
|
-
await repositoryConnection`
|
|
1691
|
-
INSERT INTO webhook_delivery (webhook_id, event, payload, response_status, error)
|
|
1692
|
-
VALUES (
|
|
1693
|
-
${webhook.id},
|
|
1694
|
-
${payload.event},
|
|
1695
|
-
${JSON.stringify(payload.payload)}::jsonb,
|
|
1696
|
-
${responseStatus},
|
|
1697
|
-
${errorMessage}
|
|
1698
|
-
)
|
|
1699
|
-
`;
|
|
1700
|
-
throw error instanceof Error ? error : new Error(errorMessage);
|
|
1691
|
+
function getDefinedColumnEntries(table, values, options = {}) {
|
|
1692
|
+
const record = values;
|
|
1693
|
+
const excluded = new Set(options.exclude ?? []);
|
|
1694
|
+
return table.columns.flatMap((column) => {
|
|
1695
|
+
if (excluded.has(column) || !Object.hasOwn(record, column)) {
|
|
1696
|
+
return [];
|
|
1701
1697
|
}
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
${webhook.id},
|
|
1706
|
-
${payload.event},
|
|
1707
|
-
${JSON.stringify(payload.payload)}::jsonb,
|
|
1708
|
-
${responseStatus}
|
|
1709
|
-
)
|
|
1710
|
-
`;
|
|
1711
|
-
}
|
|
1712
|
-
}
|
|
1713
|
-
var dispatchWebhookJob_default = DispatchWebhookJob;
|
|
1714
|
-
|
|
1715
|
-
// ../../src/core/queue/jobRegistry.ts
|
|
1716
|
-
class JobRegistry {
|
|
1717
|
-
constructor() {}
|
|
1718
|
-
factories = new Map;
|
|
1719
|
-
instances = new WeakMap;
|
|
1720
|
-
register(name, factory) {
|
|
1721
|
-
this.factories.set(name, factory);
|
|
1722
|
-
}
|
|
1723
|
-
resolveName(job) {
|
|
1724
|
-
return this.instances.get(job);
|
|
1725
|
-
}
|
|
1726
|
-
track(name, job) {
|
|
1727
|
-
this.instances.set(job, name);
|
|
1728
|
-
return job;
|
|
1729
|
-
}
|
|
1730
|
-
create(name) {
|
|
1731
|
-
const factory = this.factories.get(name);
|
|
1732
|
-
if (!factory) {
|
|
1733
|
-
return;
|
|
1698
|
+
const value = record[column];
|
|
1699
|
+
if (value === undefined) {
|
|
1700
|
+
return [];
|
|
1734
1701
|
}
|
|
1735
|
-
return
|
|
1736
|
-
}
|
|
1737
|
-
names() {
|
|
1738
|
-
return [...this.factories.keys()];
|
|
1739
|
-
}
|
|
1702
|
+
return [[column, value]];
|
|
1703
|
+
});
|
|
1740
1704
|
}
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
const
|
|
1705
|
+
function buildSelectQuery(table, options = {}, whereNodes = []) {
|
|
1706
|
+
const params = [];
|
|
1707
|
+
const columns = buildSelectList(table, options.select, params);
|
|
1708
|
+
const { clause } = buildQueryWhereClause(table, options, whereNodes, params);
|
|
1709
|
+
const joins = buildJoinClause(options.joins);
|
|
1710
|
+
const groupBy = buildGroupByClause(table.name, options.groupBy);
|
|
1711
|
+
const havingClause = buildHavingClause(table.name, options.having, params);
|
|
1712
|
+
const orderBy = buildOrderByClause(table.name, options.orderBy ?? table.defaultOrderBy);
|
|
1713
|
+
const limit = buildLimitClause(options.limit);
|
|
1714
|
+
const offset = buildOffsetClause(options.offset);
|
|
1746
1715
|
return {
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
total: input.total,
|
|
1750
|
-
last_page: lastPage
|
|
1716
|
+
text: `SELECT ${columns} FROM ${quoteIdentifier(table.name)}${joins}${clause}${groupBy}${havingClause}${orderBy}${limit}${offset}`,
|
|
1717
|
+
params
|
|
1751
1718
|
};
|
|
1752
1719
|
}
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
}
|
|
1768
|
-
return;
|
|
1769
|
-
}
|
|
1770
|
-
function mapDatabaseError(error) {
|
|
1771
|
-
if (error instanceof HttpError) {
|
|
1772
|
-
return error;
|
|
1773
|
-
}
|
|
1774
|
-
if (!isPostgresError(error)) {
|
|
1775
|
-
const message = error instanceof Error ? error.message : "Database operation failed.";
|
|
1776
|
-
return new BadRequestError(message);
|
|
1777
|
-
}
|
|
1778
|
-
const sqlState = getPostgresSqlState(error);
|
|
1779
|
-
switch (sqlState) {
|
|
1780
|
-
case "23505":
|
|
1781
|
-
return new ConflictError(error.detail ?? "A record with these values already exists.", {
|
|
1782
|
-
constraint: error.constraint
|
|
1783
|
-
});
|
|
1784
|
-
case "23503":
|
|
1785
|
-
return new UnprocessableEntityError(error.detail ?? "Referenced record does not exist.", {
|
|
1786
|
-
constraint: error.constraint
|
|
1787
|
-
});
|
|
1788
|
-
case "23502":
|
|
1789
|
-
return new BadRequestError(error.detail ?? "Required field is missing.", {
|
|
1790
|
-
constraint: error.constraint
|
|
1791
|
-
});
|
|
1792
|
-
case "23514":
|
|
1793
|
-
return new BadRequestError(error.detail ?? "Value violates a database constraint.", {
|
|
1794
|
-
constraint: error.constraint
|
|
1795
|
-
});
|
|
1796
|
-
default:
|
|
1797
|
-
return new BadRequestError(error.message ?? "Database operation failed.", {
|
|
1798
|
-
code: error.code,
|
|
1799
|
-
sqlState
|
|
1800
|
-
});
|
|
1801
|
-
}
|
|
1720
|
+
function buildCountQuery(table, where = {}, options = {}, whereNodes = []) {
|
|
1721
|
+
const params = [];
|
|
1722
|
+
const { clause, params: whereParams } = buildQueryWhereClause(table, {
|
|
1723
|
+
where,
|
|
1724
|
+
withTrashed: options.withTrashed,
|
|
1725
|
+
onlyTrashed: options.onlyTrashed
|
|
1726
|
+
}, whereNodes);
|
|
1727
|
+
params.push(...whereParams);
|
|
1728
|
+
const joins = buildJoinClause(options.joins);
|
|
1729
|
+
const groupBy = buildGroupByClause(table.name, options.groupBy);
|
|
1730
|
+
return {
|
|
1731
|
+
text: `SELECT COUNT(*) AS count FROM ${quoteIdentifier(table.name)}${joins}${clause}${groupBy}`,
|
|
1732
|
+
params
|
|
1733
|
+
};
|
|
1802
1734
|
}
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
}
|
|
1807
|
-
|
|
1808
|
-
|
|
1735
|
+
function buildProjectionQuery(table, expression, alias, options = {}, whereNodes = []) {
|
|
1736
|
+
assertSafeProjectionExpression(expression);
|
|
1737
|
+
const params = [];
|
|
1738
|
+
const { clause, params: whereParams } = buildQueryWhereClause(table, options, whereNodes);
|
|
1739
|
+
params.push(...whereParams);
|
|
1740
|
+
const joins = buildJoinClause(options.joins);
|
|
1741
|
+
const groupBy = buildGroupByClause(table.name, options.groupBy);
|
|
1742
|
+
const orderBy = buildOrderByClause(table.name, options.orderBy);
|
|
1743
|
+
const limit = buildLimitClause(options.limit);
|
|
1744
|
+
return {
|
|
1745
|
+
text: `SELECT ${expression} AS ${quoteIdentifier(alias)} FROM ${quoteIdentifier(table.name)}${joins}${clause}${groupBy}${orderBy}${limit}`,
|
|
1746
|
+
params
|
|
1747
|
+
};
|
|
1809
1748
|
}
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
throw new Error(`Invalid SQL identifier: ${identifier}`);
|
|
1749
|
+
var SAFE_PROJECTION_EXPRESSION = /^(COUNT\(\*\)|(?:"[\w_]+"(?:\."[\w_]+")?)|(?:AVG|SUM|MIN|MAX)\((?:"[\w_]+"(?:\."[\w_]+")?)\))$/;
|
|
1750
|
+
function assertSafeProjectionExpression(expression) {
|
|
1751
|
+
if (!SAFE_PROJECTION_EXPRESSION.test(expression.trim())) {
|
|
1752
|
+
throw new Error(`Unsafe projection expression: ${expression}`);
|
|
1815
1753
|
}
|
|
1816
|
-
return `"${identifier}"`;
|
|
1817
1754
|
}
|
|
1818
|
-
function
|
|
1819
|
-
|
|
1820
|
-
}
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
}
|
|
1829
|
-
return qualifyColumn(defaultTable, columnName);
|
|
1755
|
+
function buildGroupedCountQuery(table, column, where = {}, options = {}) {
|
|
1756
|
+
const qualifiedColumn = qualifyColumn(table.name, column);
|
|
1757
|
+
const { clause, params } = buildQueryWhereClause(table, {
|
|
1758
|
+
where,
|
|
1759
|
+
...options
|
|
1760
|
+
});
|
|
1761
|
+
return {
|
|
1762
|
+
text: `SELECT ${qualifiedColumn} AS ${quoteIdentifier("value")}, COUNT(*) AS ${quoteIdentifier("count")} FROM ${quoteIdentifier(table.name)}${clause} GROUP BY ${qualifiedColumn} ORDER BY ${qualifiedColumn} ASC`,
|
|
1763
|
+
params
|
|
1764
|
+
};
|
|
1830
1765
|
}
|
|
1831
|
-
function
|
|
1832
|
-
const
|
|
1833
|
-
if (
|
|
1834
|
-
throw new Error(`
|
|
1766
|
+
function buildInsertQuery(table, values) {
|
|
1767
|
+
const entries = getDefinedColumnEntries(table, values);
|
|
1768
|
+
if (entries.length === 0) {
|
|
1769
|
+
throw new Error(`Cannot insert into ${table.name} without any column values.`);
|
|
1835
1770
|
}
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1771
|
+
const params = [];
|
|
1772
|
+
const columns = entries.map(([column]) => quoteIdentifier(column)).join(", ");
|
|
1773
|
+
const placeholders = entries.map(([, value]) => pushParam(params, value)).join(", ");
|
|
1774
|
+
const returningColumns = buildReturningColumns(table);
|
|
1775
|
+
return {
|
|
1776
|
+
text: `INSERT INTO ${quoteIdentifier(table.name)} (${columns}) VALUES (${placeholders}) RETURNING ${returningColumns}`,
|
|
1777
|
+
params
|
|
1778
|
+
};
|
|
1843
1779
|
}
|
|
1844
|
-
function
|
|
1845
|
-
|
|
1846
|
-
|
|
1780
|
+
function buildUpdateQuery(table, id, changes) {
|
|
1781
|
+
const entries = getDefinedColumnEntries(table, changes, {
|
|
1782
|
+
exclude: [table.primaryKey]
|
|
1783
|
+
});
|
|
1784
|
+
if (entries.length === 0) {
|
|
1785
|
+
throw new Error(`Cannot update ${table.name} without any changed column values.`);
|
|
1786
|
+
}
|
|
1787
|
+
const params = [];
|
|
1788
|
+
const setClause = entries.map(([column, value]) => `${quoteIdentifier(column)} = ${pushParam(params, value)}`).join(", ");
|
|
1789
|
+
const primaryKeyPlaceholder = pushParam(params, id);
|
|
1790
|
+
const returningColumns = buildReturningColumns(table);
|
|
1791
|
+
const scopeClauses = [];
|
|
1792
|
+
appendSoftDeleteScope(table, {}, scopeClauses);
|
|
1793
|
+
const scopeSuffix = scopeClauses.length > 0 ? ` AND ${scopeClauses.join(" AND ")}` : "";
|
|
1794
|
+
return {
|
|
1795
|
+
text: `UPDATE ${quoteIdentifier(table.name)} SET ${setClause} WHERE ${quoteIdentifier(table.primaryKey)} = ${primaryKeyPlaceholder}${scopeSuffix} RETURNING ${returningColumns}`,
|
|
1796
|
+
params
|
|
1797
|
+
};
|
|
1847
1798
|
}
|
|
1848
|
-
function
|
|
1849
|
-
|
|
1850
|
-
|
|
1799
|
+
function buildSoftDeleteByIdQuery(table, id, deletedAt) {
|
|
1800
|
+
const deletedAtColumn = resolveSoftDeleteColumn(table);
|
|
1801
|
+
if (!deletedAtColumn) {
|
|
1802
|
+
throw new Error(`Table ${table.name} does not support soft deletes.`);
|
|
1851
1803
|
}
|
|
1852
|
-
const
|
|
1853
|
-
|
|
1804
|
+
const returningColumns = buildReturningColumns(table);
|
|
1805
|
+
const scopeClauses = [];
|
|
1806
|
+
appendSoftDeleteScope(table, {}, scopeClauses);
|
|
1807
|
+
const scopeSuffix = scopeClauses.length > 0 ? ` AND ${scopeClauses.join(" AND ")}` : "";
|
|
1808
|
+
return {
|
|
1809
|
+
text: `UPDATE ${quoteIdentifier(table.name)} SET ${quoteIdentifier(deletedAtColumn)} = $1 WHERE ${quoteIdentifier(table.primaryKey)} = $2${scopeSuffix} RETURNING ${returningColumns}`,
|
|
1810
|
+
params: [deletedAt, id]
|
|
1811
|
+
};
|
|
1854
1812
|
}
|
|
1855
|
-
function
|
|
1856
|
-
const
|
|
1857
|
-
if (
|
|
1858
|
-
|
|
1813
|
+
function buildRestoreByIdQuery(table, id) {
|
|
1814
|
+
const deletedAtColumn = resolveSoftDeleteColumn(table);
|
|
1815
|
+
if (!deletedAtColumn) {
|
|
1816
|
+
throw new Error(`Table ${table.name} does not support soft deletes.`);
|
|
1859
1817
|
}
|
|
1860
|
-
|
|
1861
|
-
|
|
1818
|
+
const returningColumns = buildReturningColumns(table);
|
|
1819
|
+
return {
|
|
1820
|
+
text: `UPDATE ${quoteIdentifier(table.name)} SET ${quoteIdentifier(deletedAtColumn)} = $1 WHERE ${quoteIdentifier(table.primaryKey)} = $2 AND ${qualifyColumn(table.name, deletedAtColumn)} IS NOT NULL RETURNING ${returningColumns}`,
|
|
1821
|
+
params: [null, id]
|
|
1822
|
+
};
|
|
1823
|
+
}
|
|
1824
|
+
function buildDeleteByIdQuery(table, id) {
|
|
1825
|
+
return {
|
|
1826
|
+
text: `DELETE FROM ${quoteIdentifier(table.name)} WHERE ${quoteIdentifier(table.primaryKey)} = $1 RETURNING ${quoteIdentifier(table.primaryKey)} AS ${quoteIdentifier("deleted_id")}`,
|
|
1827
|
+
params: [id]
|
|
1828
|
+
};
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
// ../../src/core/database/relationships.ts
|
|
1832
|
+
function indexHasManyRelation(parents, children, relation) {
|
|
1833
|
+
const groups = new Map;
|
|
1834
|
+
for (const parent of parents) {
|
|
1835
|
+
groups.set(parent[relation.localKey], []);
|
|
1862
1836
|
}
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1837
|
+
for (const child of children) {
|
|
1838
|
+
const key = child[relation.foreignKey];
|
|
1839
|
+
const group = groups.get(key);
|
|
1840
|
+
if (!group) {
|
|
1841
|
+
continue;
|
|
1868
1842
|
}
|
|
1843
|
+
group.push(child);
|
|
1869
1844
|
}
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
if (operator.gte !== undefined) {
|
|
1877
|
-
clauses.push(`${column} >= ${pushParam(params, operator.gte)}`);
|
|
1878
|
-
}
|
|
1879
|
-
if (operator.lt !== undefined) {
|
|
1880
|
-
clauses.push(`${column} < ${pushParam(params, operator.lt)}`);
|
|
1881
|
-
}
|
|
1882
|
-
if (operator.lte !== undefined) {
|
|
1883
|
-
clauses.push(`${column} <= ${pushParam(params, operator.lte)}`);
|
|
1884
|
-
}
|
|
1885
|
-
if (operator.ilike !== undefined) {
|
|
1886
|
-
clauses.push(`${column} ILIKE ${pushParam(params, `%${operator.ilike}%`)}`);
|
|
1845
|
+
return groups;
|
|
1846
|
+
}
|
|
1847
|
+
function indexBelongsToRelation(children, parents, relation) {
|
|
1848
|
+
const parentsById = new Map;
|
|
1849
|
+
for (const parent of parents) {
|
|
1850
|
+
parentsById.set(parent[relation.ownerKey], parent);
|
|
1887
1851
|
}
|
|
1888
|
-
|
|
1889
|
-
|
|
1852
|
+
const result = new Map;
|
|
1853
|
+
for (const child of children) {
|
|
1854
|
+
const foreignKey = child[relation.foreignKey];
|
|
1855
|
+
const parent = parentsById.get(foreignKey);
|
|
1856
|
+
if (parent) {
|
|
1857
|
+
result.set(foreignKey, parent);
|
|
1858
|
+
}
|
|
1890
1859
|
}
|
|
1891
|
-
return
|
|
1860
|
+
return result;
|
|
1892
1861
|
}
|
|
1893
|
-
function
|
|
1894
|
-
const
|
|
1895
|
-
for (const
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
if (Array.isArray(filterValue)) {
|
|
1901
|
-
clauses.push(buildInClause(column, filterValue, params));
|
|
1902
|
-
continue;
|
|
1903
|
-
}
|
|
1904
|
-
if (isQueryOperator(filterValue)) {
|
|
1905
|
-
clauses.push(...buildOperatorClauses(column, filterValue, params));
|
|
1862
|
+
function indexMorphManyRelation(parents, children, relation) {
|
|
1863
|
+
const groups = new Map;
|
|
1864
|
+
for (const parent of parents) {
|
|
1865
|
+
groups.set(parent[relation.localKey], []);
|
|
1866
|
+
}
|
|
1867
|
+
for (const child of children) {
|
|
1868
|
+
if (child[relation.morphTypeKey] !== relation.morphType) {
|
|
1906
1869
|
continue;
|
|
1907
1870
|
}
|
|
1908
|
-
|
|
1909
|
-
|
|
1871
|
+
const key = child[relation.morphIdKey];
|
|
1872
|
+
const group = groups.get(key);
|
|
1873
|
+
if (!group) {
|
|
1910
1874
|
continue;
|
|
1911
1875
|
}
|
|
1912
|
-
|
|
1913
|
-
}
|
|
1914
|
-
return clauses.join(" AND ");
|
|
1915
|
-
}
|
|
1916
|
-
function buildWhereNodeClause(tableName, node, params) {
|
|
1917
|
-
if ("where" in node) {
|
|
1918
|
-
return appendWhereParts(tableName, node.where, params);
|
|
1919
|
-
}
|
|
1920
|
-
const grouped = buildWhereGroupClause(tableName, node.group, params);
|
|
1921
|
-
if (!grouped) {
|
|
1922
|
-
return "";
|
|
1876
|
+
group.push(child);
|
|
1923
1877
|
}
|
|
1924
|
-
return
|
|
1878
|
+
return groups;
|
|
1925
1879
|
}
|
|
1926
|
-
function
|
|
1927
|
-
|
|
1928
|
-
for (const
|
|
1929
|
-
const
|
|
1930
|
-
|
|
1880
|
+
function indexMorphToRelation(children, parentsByType, relation) {
|
|
1881
|
+
const result = new Map;
|
|
1882
|
+
for (const child of children) {
|
|
1883
|
+
const morphType = String(child[relation.morphTypeKey]);
|
|
1884
|
+
const parents = parentsByType.get(morphType);
|
|
1885
|
+
if (!parents) {
|
|
1931
1886
|
continue;
|
|
1932
1887
|
}
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1888
|
+
const parent = parents.get(child[relation.morphIdKey]);
|
|
1889
|
+
if (parent) {
|
|
1890
|
+
result.set(child[relation.morphIdKey], parent);
|
|
1936
1891
|
}
|
|
1937
|
-
result = node.kind === "or" ? `${result} OR ${part}` : `${result} AND ${part}`;
|
|
1938
|
-
}
|
|
1939
|
-
if (!result) {
|
|
1940
|
-
return "";
|
|
1941
1892
|
}
|
|
1942
1893
|
return result;
|
|
1943
1894
|
}
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
return {
|
|
1952
|
-
clause: combined ? ` WHERE ${combined}` : "",
|
|
1953
|
-
params
|
|
1954
|
-
};
|
|
1895
|
+
|
|
1896
|
+
// ../../src/core/database/boundConnection.ts
|
|
1897
|
+
var boundConnectionHolder = {
|
|
1898
|
+
connection: null
|
|
1899
|
+
};
|
|
1900
|
+
function getBoundDatabaseConnection() {
|
|
1901
|
+
return boundConnectionHolder.connection;
|
|
1955
1902
|
}
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1903
|
+
|
|
1904
|
+
// ../../src/core/database/repositoryConnection.ts
|
|
1905
|
+
function resolveRepositoryConnection() {
|
|
1906
|
+
return getBoundDatabaseConnection() ?? getDefaultDatabaseQuery();
|
|
1907
|
+
}
|
|
1908
|
+
var repositoryConnection = new Proxy(function repositoryConnection2() {}, {
|
|
1909
|
+
apply(_target, _thisArg, args) {
|
|
1910
|
+
return resolveRepositoryConnection()(...args);
|
|
1911
|
+
},
|
|
1912
|
+
get(_target, property) {
|
|
1913
|
+
const connection = resolveRepositoryConnection();
|
|
1914
|
+
const value = connection[property];
|
|
1915
|
+
return typeof value === "function" ? value.bind(connection) : value;
|
|
1959
1916
|
}
|
|
1960
|
-
|
|
1961
|
-
|
|
1917
|
+
});
|
|
1918
|
+
|
|
1919
|
+
// ../../src/core/database/whereBuilder.ts
|
|
1920
|
+
class WhereBuilder {
|
|
1921
|
+
nodes = [];
|
|
1922
|
+
where(where) {
|
|
1923
|
+
this.nodes.push({ kind: "and", where });
|
|
1924
|
+
return this;
|
|
1962
1925
|
}
|
|
1963
|
-
|
|
1964
|
-
}
|
|
1965
|
-
|
|
1966
|
-
const column = resolveSoftDeleteColumn(table);
|
|
1967
|
-
if (!column) {
|
|
1968
|
-
return;
|
|
1926
|
+
orWhere(where) {
|
|
1927
|
+
this.nodes.push({ kind: "or", where });
|
|
1928
|
+
return this;
|
|
1969
1929
|
}
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1930
|
+
whereGroup(fn) {
|
|
1931
|
+
const nested = new WhereBuilder;
|
|
1932
|
+
fn(nested);
|
|
1933
|
+
if (nested.nodes.length > 0) {
|
|
1934
|
+
this.nodes.push({ kind: "and", group: nested.nodes });
|
|
1935
|
+
}
|
|
1936
|
+
return this;
|
|
1937
|
+
}
|
|
1938
|
+
orWhereGroup(fn) {
|
|
1939
|
+
const nested = new WhereBuilder;
|
|
1940
|
+
fn(nested);
|
|
1941
|
+
if (nested.nodes.length > 0) {
|
|
1942
|
+
this.nodes.push({ kind: "or", group: nested.nodes });
|
|
1943
|
+
}
|
|
1944
|
+
return this;
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
// ../../src/core/database/repositoryQuery.ts
|
|
1949
|
+
class RepositoryQuery {
|
|
1950
|
+
repository;
|
|
1951
|
+
whereClause;
|
|
1952
|
+
queryOptions;
|
|
1953
|
+
eagerLoads = [];
|
|
1954
|
+
whereNodes = [];
|
|
1955
|
+
constructor(repository, whereClause = {}, queryOptions = {}) {
|
|
1956
|
+
this.repository = repository;
|
|
1957
|
+
this.whereClause = whereClause;
|
|
1958
|
+
this.queryOptions = queryOptions;
|
|
1974
1959
|
}
|
|
1975
|
-
|
|
1976
|
-
|
|
1960
|
+
where(input) {
|
|
1961
|
+
if (typeof input === "function") {
|
|
1962
|
+
const builder = new WhereBuilder;
|
|
1963
|
+
input(builder);
|
|
1964
|
+
this.whereNodes.push(...builder.nodes);
|
|
1965
|
+
return this;
|
|
1966
|
+
}
|
|
1967
|
+
this.whereClause = { ...this.whereClause, ...input };
|
|
1968
|
+
return this;
|
|
1977
1969
|
}
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1970
|
+
orWhere(input) {
|
|
1971
|
+
if (typeof input === "function") {
|
|
1972
|
+
const builder = new WhereBuilder;
|
|
1973
|
+
input(builder);
|
|
1974
|
+
if (builder.nodes.length > 0) {
|
|
1975
|
+
this.whereNodes.push({ kind: "or", group: builder.nodes });
|
|
1976
|
+
}
|
|
1977
|
+
return this;
|
|
1978
|
+
}
|
|
1979
|
+
this.whereNodes.push({ kind: "or", where: input });
|
|
1980
|
+
return this;
|
|
1985
1981
|
}
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
clause: base ? ` WHERE (${base}) AND ${scope}` : ` WHERE ${scope}`,
|
|
1990
|
-
params: whereParams
|
|
1991
|
-
};
|
|
1992
|
-
}
|
|
1993
|
-
function isQueryOrder(value) {
|
|
1994
|
-
return "column" in value;
|
|
1995
|
-
}
|
|
1996
|
-
function normalizeOrderBy(orderBy) {
|
|
1997
|
-
if (!orderBy) {
|
|
1998
|
-
return [];
|
|
1982
|
+
orderBy(orderBy) {
|
|
1983
|
+
this.queryOptions = { ...this.queryOptions, orderBy };
|
|
1984
|
+
return this;
|
|
1999
1985
|
}
|
|
2000
|
-
|
|
2001
|
-
|
|
1986
|
+
limit(limit) {
|
|
1987
|
+
this.queryOptions = { ...this.queryOptions, limit };
|
|
1988
|
+
return this;
|
|
2002
1989
|
}
|
|
2003
|
-
|
|
2004
|
-
|
|
1990
|
+
offset(offset) {
|
|
1991
|
+
this.queryOptions = { ...this.queryOptions, offset };
|
|
1992
|
+
return this;
|
|
2005
1993
|
}
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
direction
|
|
2009
|
-
}));
|
|
2010
|
-
}
|
|
2011
|
-
function buildOrderByClause(tableName, orderBy) {
|
|
2012
|
-
const parts = normalizeOrderBy(orderBy).map(({ column, direction }) => {
|
|
2013
|
-
return `${resolveQualifiedColumn(tableName, column)} ${normalizeDirection(direction)}`;
|
|
2014
|
-
});
|
|
2015
|
-
return parts.length > 0 ? ` ORDER BY ${parts.join(", ")}` : "";
|
|
2016
|
-
}
|
|
2017
|
-
function buildGroupByClause(tableName, groupBy) {
|
|
2018
|
-
if (!groupBy) {
|
|
2019
|
-
return "";
|
|
1994
|
+
join(left, right) {
|
|
1995
|
+
return this.addJoin("inner", left, right);
|
|
2020
1996
|
}
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
return parts.length > 0 ? ` GROUP BY ${parts.join(", ")}` : "";
|
|
2024
|
-
}
|
|
2025
|
-
function buildHavingClause(tableName, having, params) {
|
|
2026
|
-
if (!having) {
|
|
2027
|
-
return "";
|
|
1997
|
+
leftJoin(left, right) {
|
|
1998
|
+
return this.addJoin("left", left, right);
|
|
2028
1999
|
}
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
function buildJoinClause(joins = []) {
|
|
2033
|
-
return joins.map((join3) => {
|
|
2034
|
-
const joinType = join3.type === "left" ? "LEFT JOIN" : "INNER JOIN";
|
|
2035
|
-
const onClause = join3.on.map(({ left, right }) => `${qualifyColumn(left.table, left.column)} = ${qualifyColumn(right.table, right.column)}`).join(" AND ");
|
|
2036
|
-
return ` ${joinType} ${quoteIdentifier(join3.table)} ON ${onClause}`;
|
|
2037
|
-
}).join("");
|
|
2038
|
-
}
|
|
2039
|
-
function buildLimitClause(limit) {
|
|
2040
|
-
if (limit === undefined) {
|
|
2041
|
-
return "";
|
|
2000
|
+
groupBy(groupBy) {
|
|
2001
|
+
this.queryOptions = { ...this.queryOptions, groupBy };
|
|
2002
|
+
return this;
|
|
2042
2003
|
}
|
|
2043
|
-
|
|
2044
|
-
|
|
2004
|
+
having(having) {
|
|
2005
|
+
this.queryOptions = { ...this.queryOptions, having };
|
|
2006
|
+
return this;
|
|
2045
2007
|
}
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2008
|
+
withHasMany(as, relation, childRepository, options = {}) {
|
|
2009
|
+
this.eagerLoads.push({
|
|
2010
|
+
kind: "hasMany",
|
|
2011
|
+
as,
|
|
2012
|
+
relation,
|
|
2013
|
+
repository: childRepository,
|
|
2014
|
+
options
|
|
2015
|
+
});
|
|
2016
|
+
return this;
|
|
2051
2017
|
}
|
|
2052
|
-
|
|
2053
|
-
|
|
2018
|
+
withBelongsTo(as, relation, parentRepository, options = {}) {
|
|
2019
|
+
this.eagerLoads.push({
|
|
2020
|
+
kind: "belongsTo",
|
|
2021
|
+
as,
|
|
2022
|
+
relation,
|
|
2023
|
+
repository: parentRepository,
|
|
2024
|
+
options
|
|
2025
|
+
});
|
|
2026
|
+
return this;
|
|
2054
2027
|
}
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2028
|
+
withMorphMany(as, relation, childRepository, options = {}) {
|
|
2029
|
+
this.eagerLoads.push({
|
|
2030
|
+
kind: "morphMany",
|
|
2031
|
+
as,
|
|
2032
|
+
relation,
|
|
2033
|
+
repository: childRepository,
|
|
2034
|
+
options
|
|
2035
|
+
});
|
|
2036
|
+
return this;
|
|
2063
2037
|
}
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
}
|
|
2072
|
-
|
|
2073
|
-
const placeholder = pushParam(params, item.query);
|
|
2074
|
-
return `ts_rank(${column}, plainto_tsquery('english', ${placeholder})) AS ${quoteIdentifier(item.as)}`;
|
|
2075
|
-
}).join(", ");
|
|
2076
|
-
}
|
|
2077
|
-
function getDefinedColumnEntries(table, values, options = {}) {
|
|
2078
|
-
const record = values;
|
|
2079
|
-
const excluded = new Set(options.exclude ?? []);
|
|
2080
|
-
return table.columns.flatMap((column) => {
|
|
2081
|
-
if (excluded.has(column) || !Object.hasOwn(record, column)) {
|
|
2082
|
-
return [];
|
|
2083
|
-
}
|
|
2084
|
-
const value = record[column];
|
|
2085
|
-
if (value === undefined) {
|
|
2086
|
-
return [];
|
|
2087
|
-
}
|
|
2088
|
-
return [[column, value]];
|
|
2089
|
-
});
|
|
2090
|
-
}
|
|
2091
|
-
function buildSelectQuery(table, options = {}, whereNodes = []) {
|
|
2092
|
-
const params = [];
|
|
2093
|
-
const columns = buildSelectList(table, options.select, params);
|
|
2094
|
-
const { clause } = buildQueryWhereClause(table, options, whereNodes, params);
|
|
2095
|
-
const joins = buildJoinClause(options.joins);
|
|
2096
|
-
const groupBy = buildGroupByClause(table.name, options.groupBy);
|
|
2097
|
-
const havingClause = buildHavingClause(table.name, options.having, params);
|
|
2098
|
-
const orderBy = buildOrderByClause(table.name, options.orderBy ?? table.defaultOrderBy);
|
|
2099
|
-
const limit = buildLimitClause(options.limit);
|
|
2100
|
-
const offset = buildOffsetClause(options.offset);
|
|
2101
|
-
return {
|
|
2102
|
-
text: `SELECT ${columns} FROM ${quoteIdentifier(table.name)}${joins}${clause}${groupBy}${havingClause}${orderBy}${limit}${offset}`,
|
|
2103
|
-
params
|
|
2104
|
-
};
|
|
2105
|
-
}
|
|
2106
|
-
function buildCountQuery(table, where = {}, options = {}, whereNodes = []) {
|
|
2107
|
-
const params = [];
|
|
2108
|
-
const { clause, params: whereParams } = buildQueryWhereClause(table, {
|
|
2109
|
-
where,
|
|
2110
|
-
withTrashed: options.withTrashed,
|
|
2111
|
-
onlyTrashed: options.onlyTrashed
|
|
2112
|
-
}, whereNodes);
|
|
2113
|
-
params.push(...whereParams);
|
|
2114
|
-
const joins = buildJoinClause(options.joins);
|
|
2115
|
-
const groupBy = buildGroupByClause(table.name, options.groupBy);
|
|
2116
|
-
return {
|
|
2117
|
-
text: `SELECT COUNT(*) AS count FROM ${quoteIdentifier(table.name)}${joins}${clause}${groupBy}`,
|
|
2118
|
-
params
|
|
2119
|
-
};
|
|
2120
|
-
}
|
|
2121
|
-
function buildProjectionQuery(table, expression, alias, options = {}, whereNodes = []) {
|
|
2122
|
-
assertSafeProjectionExpression(expression);
|
|
2123
|
-
const params = [];
|
|
2124
|
-
const { clause, params: whereParams } = buildQueryWhereClause(table, options, whereNodes);
|
|
2125
|
-
params.push(...whereParams);
|
|
2126
|
-
const joins = buildJoinClause(options.joins);
|
|
2127
|
-
const groupBy = buildGroupByClause(table.name, options.groupBy);
|
|
2128
|
-
const orderBy = buildOrderByClause(table.name, options.orderBy);
|
|
2129
|
-
const limit = buildLimitClause(options.limit);
|
|
2130
|
-
return {
|
|
2131
|
-
text: `SELECT ${expression} AS ${quoteIdentifier(alias)} FROM ${quoteIdentifier(table.name)}${joins}${clause}${groupBy}${orderBy}${limit}`,
|
|
2132
|
-
params
|
|
2133
|
-
};
|
|
2134
|
-
}
|
|
2135
|
-
var SAFE_PROJECTION_EXPRESSION = /^(COUNT\(\*\)|(?:"[\w_]+"(?:\."[\w_]+")?)|(?:AVG|SUM|MIN|MAX)\((?:"[\w_]+"(?:\."[\w_]+")?)\))$/;
|
|
2136
|
-
function assertSafeProjectionExpression(expression) {
|
|
2137
|
-
if (!SAFE_PROJECTION_EXPRESSION.test(expression.trim())) {
|
|
2138
|
-
throw new Error(`Unsafe projection expression: ${expression}`);
|
|
2038
|
+
withMorphOne(as, relation, childRepository, options = {}) {
|
|
2039
|
+
this.eagerLoads.push({
|
|
2040
|
+
kind: "morphOne",
|
|
2041
|
+
as,
|
|
2042
|
+
relation,
|
|
2043
|
+
repository: childRepository,
|
|
2044
|
+
options
|
|
2045
|
+
});
|
|
2046
|
+
return this;
|
|
2139
2047
|
}
|
|
2140
|
-
}
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
}
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
if (entries.length === 0) {
|
|
2155
|
-
throw new Error(`Cannot insert into ${table.name} without any column values.`);
|
|
2048
|
+
withMorphTo(as, relation, repositoriesByType, options = {}) {
|
|
2049
|
+
this.eagerLoads.push({
|
|
2050
|
+
kind: "morphTo",
|
|
2051
|
+
as,
|
|
2052
|
+
relation,
|
|
2053
|
+
repository: this.repository,
|
|
2054
|
+
morphRepositories: repositoriesByType,
|
|
2055
|
+
options
|
|
2056
|
+
});
|
|
2057
|
+
return this;
|
|
2058
|
+
}
|
|
2059
|
+
async get() {
|
|
2060
|
+
const rows = await this.repository.findAll(this.buildOptions());
|
|
2061
|
+
return await this.attach(rows);
|
|
2156
2062
|
}
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
const returningColumns = buildReturningColumns(table);
|
|
2161
|
-
return {
|
|
2162
|
-
text: `INSERT INTO ${quoteIdentifier(table.name)} (${columns}) VALUES (${placeholders}) RETURNING ${returningColumns}`,
|
|
2163
|
-
params
|
|
2164
|
-
};
|
|
2165
|
-
}
|
|
2166
|
-
function buildUpdateQuery(table, id, changes) {
|
|
2167
|
-
const entries = getDefinedColumnEntries(table, changes, {
|
|
2168
|
-
exclude: [table.primaryKey]
|
|
2169
|
-
});
|
|
2170
|
-
if (entries.length === 0) {
|
|
2171
|
-
throw new Error(`Cannot update ${table.name} without any changed column values.`);
|
|
2063
|
+
async first() {
|
|
2064
|
+
const rows = await this.get();
|
|
2065
|
+
return rows[0] ?? null;
|
|
2172
2066
|
}
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
const scopeSuffix = scopeClauses.length > 0 ? ` AND ${scopeClauses.join(" AND ")}` : "";
|
|
2180
|
-
return {
|
|
2181
|
-
text: `UPDATE ${quoteIdentifier(table.name)} SET ${setClause} WHERE ${quoteIdentifier(table.primaryKey)} = ${primaryKeyPlaceholder}${scopeSuffix} RETURNING ${returningColumns}`,
|
|
2182
|
-
params
|
|
2183
|
-
};
|
|
2184
|
-
}
|
|
2185
|
-
function buildSoftDeleteByIdQuery(table, id, deletedAt) {
|
|
2186
|
-
const deletedAtColumn = resolveSoftDeleteColumn(table);
|
|
2187
|
-
if (!deletedAtColumn) {
|
|
2188
|
-
throw new Error(`Table ${table.name} does not support soft deletes.`);
|
|
2067
|
+
async paginate(options) {
|
|
2068
|
+
return await this.repository.paginate({
|
|
2069
|
+
...this.buildOptions(),
|
|
2070
|
+
page: options.page,
|
|
2071
|
+
perPage: options.perPage
|
|
2072
|
+
});
|
|
2189
2073
|
}
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
params: [deletedAt, id]
|
|
2197
|
-
};
|
|
2198
|
-
}
|
|
2199
|
-
function buildRestoreByIdQuery(table, id) {
|
|
2200
|
-
const deletedAtColumn = resolveSoftDeleteColumn(table);
|
|
2201
|
-
if (!deletedAtColumn) {
|
|
2202
|
-
throw new Error(`Table ${table.name} does not support soft deletes.`);
|
|
2074
|
+
buildOptions() {
|
|
2075
|
+
return {
|
|
2076
|
+
...this.queryOptions,
|
|
2077
|
+
where: this.whereClause,
|
|
2078
|
+
whereNodes: this.whereNodes
|
|
2079
|
+
};
|
|
2203
2080
|
}
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2081
|
+
addJoin(type, left, right) {
|
|
2082
|
+
const leftRef = parseQualifiedColumn(left);
|
|
2083
|
+
const rightRef = parseQualifiedColumn(right);
|
|
2084
|
+
const table = type === "inner" ? rightRef.table : rightRef.table;
|
|
2085
|
+
const joins = this.queryOptions.joins ?? [];
|
|
2086
|
+
const existing = joins.find((join3) => join3.table === table && join3.type === type);
|
|
2087
|
+
if (existing) {
|
|
2088
|
+
existing.on.push({ left: leftRef, right: rightRef });
|
|
2089
|
+
return this;
|
|
2090
|
+
}
|
|
2091
|
+
this.queryOptions = {
|
|
2092
|
+
...this.queryOptions,
|
|
2093
|
+
joins: [
|
|
2094
|
+
...joins,
|
|
2095
|
+
{
|
|
2096
|
+
type,
|
|
2097
|
+
table,
|
|
2098
|
+
on: [{ left: leftRef, right: rightRef }]
|
|
2099
|
+
}
|
|
2100
|
+
]
|
|
2101
|
+
};
|
|
2102
|
+
return this;
|
|
2222
2103
|
}
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
if (!group) {
|
|
2227
|
-
continue;
|
|
2104
|
+
async attach(rows) {
|
|
2105
|
+
if (rows.length === 0 || this.eagerLoads.length === 0) {
|
|
2106
|
+
return rows.map((row) => ({ ...row }));
|
|
2228
2107
|
}
|
|
2229
|
-
|
|
2108
|
+
let result = rows.map((row) => ({ ...row }));
|
|
2109
|
+
for (const load of this.eagerLoads) {
|
|
2110
|
+
if (load.kind === "hasMany") {
|
|
2111
|
+
const relation2 = load.relation;
|
|
2112
|
+
const grouped2 = await load.repository.withConnection(this.repository.getConnection()).loadHasManyForParents(rows, relation2, load.options);
|
|
2113
|
+
result = result.map((row) => ({
|
|
2114
|
+
...row,
|
|
2115
|
+
[load.as]: grouped2.get(row[relation2.localKey]) ?? []
|
|
2116
|
+
}));
|
|
2117
|
+
continue;
|
|
2118
|
+
}
|
|
2119
|
+
if (load.kind === "morphMany") {
|
|
2120
|
+
const relation2 = load.relation;
|
|
2121
|
+
const grouped2 = await load.repository.withConnection(this.repository.getConnection()).loadMorphManyForParents(rows, relation2, load.options);
|
|
2122
|
+
result = result.map((row) => ({
|
|
2123
|
+
...row,
|
|
2124
|
+
[load.as]: grouped2.get(row[relation2.localKey]) ?? []
|
|
2125
|
+
}));
|
|
2126
|
+
continue;
|
|
2127
|
+
}
|
|
2128
|
+
if (load.kind === "morphOne") {
|
|
2129
|
+
const relation2 = load.relation;
|
|
2130
|
+
const grouped2 = await load.repository.withConnection(this.repository.getConnection()).loadMorphOneForParents(rows, relation2, load.options);
|
|
2131
|
+
result = result.map((row) => ({
|
|
2132
|
+
...row,
|
|
2133
|
+
[load.as]: grouped2.get(row[relation2.localKey])
|
|
2134
|
+
}));
|
|
2135
|
+
continue;
|
|
2136
|
+
}
|
|
2137
|
+
if (load.kind === "morphTo") {
|
|
2138
|
+
const relation2 = load.relation;
|
|
2139
|
+
const grouped2 = await this.repository.loadMorphToForChildren(rows, relation2, load.morphRepositories ?? new Map, load.options);
|
|
2140
|
+
result = result.map((row) => ({
|
|
2141
|
+
...row,
|
|
2142
|
+
[load.as]: grouped2.get(row[relation2.morphIdKey])
|
|
2143
|
+
}));
|
|
2144
|
+
continue;
|
|
2145
|
+
}
|
|
2146
|
+
const relation = load.relation;
|
|
2147
|
+
const grouped = await this.repository.loadBelongsToForParents(rows, relation, load.repository, load.options);
|
|
2148
|
+
result = result.map((row) => ({
|
|
2149
|
+
...row,
|
|
2150
|
+
[load.as]: grouped.get(row[relation.foreignKey])
|
|
2151
|
+
}));
|
|
2152
|
+
}
|
|
2153
|
+
return result;
|
|
2230
2154
|
}
|
|
2231
|
-
return groups;
|
|
2232
2155
|
}
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2156
|
+
|
|
2157
|
+
// ../../src/core/database/baseRepository.ts
|
|
2158
|
+
class BaseRepository5 {
|
|
2159
|
+
table;
|
|
2160
|
+
connection;
|
|
2161
|
+
constructor(table, connection = repositoryConnection) {
|
|
2162
|
+
this.table = table;
|
|
2163
|
+
this.connection = connection;
|
|
2237
2164
|
}
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
}
|
|
2165
|
+
async findAll(options = {}) {
|
|
2166
|
+
return await withDatabaseErrorHandling(async () => {
|
|
2167
|
+
const { whereNodes, ...queryOptions } = options;
|
|
2168
|
+
const { text, params } = buildSelectQuery(this.table, queryOptions, whereNodes ?? []);
|
|
2169
|
+
return await this.connection.unsafe(text, params);
|
|
2170
|
+
});
|
|
2245
2171
|
}
|
|
2246
|
-
|
|
2247
|
-
}
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2172
|
+
async paginate(options) {
|
|
2173
|
+
const { whereNodes, where = {}, page, perPage, ...queryOptions } = options;
|
|
2174
|
+
const total = await this.countWhere(where, {
|
|
2175
|
+
withTrashed: options.withTrashed,
|
|
2176
|
+
onlyTrashed: options.onlyTrashed,
|
|
2177
|
+
joins: options.joins,
|
|
2178
|
+
groupBy: options.groupBy
|
|
2179
|
+
}, whereNodes);
|
|
2180
|
+
const offset = (page - 1) * perPage;
|
|
2181
|
+
const data = await this.findAll({
|
|
2182
|
+
...queryOptions,
|
|
2183
|
+
where,
|
|
2184
|
+
whereNodes,
|
|
2185
|
+
limit: perPage,
|
|
2186
|
+
offset
|
|
2187
|
+
});
|
|
2188
|
+
return {
|
|
2189
|
+
data,
|
|
2190
|
+
meta: buildPaginationMeta({ page, perPage, total })
|
|
2191
|
+
};
|
|
2252
2192
|
}
|
|
2253
|
-
|
|
2254
|
-
if (
|
|
2255
|
-
|
|
2193
|
+
async chunk(count, callback, options = {}) {
|
|
2194
|
+
if (!Number.isInteger(count) || count <= 0) {
|
|
2195
|
+
throw new Error("Chunk size must be a positive integer.");
|
|
2256
2196
|
}
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2197
|
+
let offset = 0;
|
|
2198
|
+
while (true) {
|
|
2199
|
+
const rows = await this.findAll({
|
|
2200
|
+
...options,
|
|
2201
|
+
limit: count,
|
|
2202
|
+
offset
|
|
2203
|
+
});
|
|
2204
|
+
if (rows.length === 0) {
|
|
2205
|
+
return;
|
|
2206
|
+
}
|
|
2207
|
+
const shouldContinue = await callback(rows);
|
|
2208
|
+
if (shouldContinue === false || rows.length < count) {
|
|
2209
|
+
return;
|
|
2210
|
+
}
|
|
2211
|
+
offset += count;
|
|
2261
2212
|
}
|
|
2262
|
-
group.push(child);
|
|
2263
2213
|
}
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2214
|
+
async cursorPaginate(options) {
|
|
2215
|
+
const {
|
|
2216
|
+
perPage,
|
|
2217
|
+
cursor,
|
|
2218
|
+
cursorColumn = this.table.primaryKey,
|
|
2219
|
+
direction = "asc",
|
|
2220
|
+
where = {},
|
|
2221
|
+
whereNodes,
|
|
2222
|
+
...queryOptions
|
|
2223
|
+
} = options;
|
|
2224
|
+
if (!Number.isInteger(perPage) || perPage <= 0) {
|
|
2225
|
+
throw new Error("Cursor page size must be a positive integer.");
|
|
2273
2226
|
}
|
|
2274
|
-
const
|
|
2275
|
-
if (
|
|
2276
|
-
|
|
2227
|
+
const cursorWhere = { ...where };
|
|
2228
|
+
if (cursor !== undefined) {
|
|
2229
|
+
cursorWhere[cursorColumn] = direction === "asc" ? { gt: cursor } : { lt: cursor };
|
|
2277
2230
|
}
|
|
2231
|
+
const rows = await this.findAll({
|
|
2232
|
+
...queryOptions,
|
|
2233
|
+
where: cursorWhere,
|
|
2234
|
+
whereNodes,
|
|
2235
|
+
orderBy: { [cursorColumn]: direction },
|
|
2236
|
+
limit: perPage + 1
|
|
2237
|
+
});
|
|
2238
|
+
const hasMore = rows.length > perPage;
|
|
2239
|
+
const data = hasMore ? rows.slice(0, perPage) : rows;
|
|
2240
|
+
const nextCursor = hasMore ? data[data.length - 1]?.[cursorColumn] ?? null : null;
|
|
2241
|
+
const prevCursor = cursor ?? null;
|
|
2242
|
+
return {
|
|
2243
|
+
data,
|
|
2244
|
+
meta: {
|
|
2245
|
+
per_page: perPage,
|
|
2246
|
+
next_cursor: nextCursor,
|
|
2247
|
+
prev_cursor: prevCursor,
|
|
2248
|
+
has_more: hasMore
|
|
2249
|
+
}
|
|
2250
|
+
};
|
|
2278
2251
|
}
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
class WhereBuilder {
|
|
2284
|
-
nodes = [];
|
|
2285
|
-
where(where) {
|
|
2286
|
-
this.nodes.push({ kind: "and", where });
|
|
2287
|
-
return this;
|
|
2288
|
-
}
|
|
2289
|
-
orWhere(where) {
|
|
2290
|
-
this.nodes.push({ kind: "or", where });
|
|
2291
|
-
return this;
|
|
2252
|
+
async findById(id) {
|
|
2253
|
+
return await this.firstOrNull({
|
|
2254
|
+
[this.table.primaryKey]: id
|
|
2255
|
+
});
|
|
2292
2256
|
}
|
|
2293
|
-
|
|
2294
|
-
const
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
this.nodes.push({ kind: "and", group: nested.nodes });
|
|
2257
|
+
async findByIdOrThrow(id, errorFactory) {
|
|
2258
|
+
const record = await this.findById(id);
|
|
2259
|
+
if (record) {
|
|
2260
|
+
return record;
|
|
2298
2261
|
}
|
|
2299
|
-
|
|
2262
|
+
throw errorFactory?.(id) ?? new Error(`${this.table.name} ${String(id)} was not found.`);
|
|
2300
2263
|
}
|
|
2301
|
-
|
|
2302
|
-
const
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
this.nodes.push({ kind: "or", group: nested.nodes });
|
|
2264
|
+
async findByIds(ids) {
|
|
2265
|
+
const uniqueIds = [...new Set(ids)];
|
|
2266
|
+
if (uniqueIds.length === 0) {
|
|
2267
|
+
return [];
|
|
2306
2268
|
}
|
|
2307
|
-
return this
|
|
2269
|
+
return await this.findWhere({
|
|
2270
|
+
[this.table.primaryKey]: uniqueIds
|
|
2271
|
+
});
|
|
2308
2272
|
}
|
|
2309
|
-
}
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
class RepositoryQuery {
|
|
2313
|
-
repository;
|
|
2314
|
-
whereClause;
|
|
2315
|
-
queryOptions;
|
|
2316
|
-
eagerLoads = [];
|
|
2317
|
-
whereNodes = [];
|
|
2318
|
-
constructor(repository, whereClause = {}, queryOptions = {}) {
|
|
2319
|
-
this.repository = repository;
|
|
2320
|
-
this.whereClause = whereClause;
|
|
2321
|
-
this.queryOptions = queryOptions;
|
|
2273
|
+
async firstOrNull(where, options = {}) {
|
|
2274
|
+
const [record] = await this.findAll({ ...options, where, limit: 1 });
|
|
2275
|
+
return record ?? null;
|
|
2322
2276
|
}
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
const
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2277
|
+
async create(values) {
|
|
2278
|
+
return await withDatabaseErrorHandling(async () => {
|
|
2279
|
+
const { text, params } = buildInsertQuery(this.table, values);
|
|
2280
|
+
const [record] = await this.connection.unsafe(text, params);
|
|
2281
|
+
if (!record) {
|
|
2282
|
+
throw new Error(`Insert into ${this.table.name} did not return a record.`);
|
|
2283
|
+
}
|
|
2284
|
+
const entity = record;
|
|
2285
|
+
await eventBus.dispatch(modelEventName(this.table.name, "created"), entity);
|
|
2286
|
+
return entity;
|
|
2287
|
+
});
|
|
2332
2288
|
}
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
const
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2289
|
+
async updateById(id, changes) {
|
|
2290
|
+
return await withDatabaseErrorHandling(async () => {
|
|
2291
|
+
const { text, params } = buildUpdateQuery(this.table, id, changes);
|
|
2292
|
+
const [record] = await this.connection.unsafe(text, params);
|
|
2293
|
+
const entity = record ?? null;
|
|
2294
|
+
if (entity) {
|
|
2295
|
+
await eventBus.dispatch(modelEventName(this.table.name, "updated"), entity);
|
|
2339
2296
|
}
|
|
2340
|
-
return
|
|
2297
|
+
return entity;
|
|
2298
|
+
});
|
|
2299
|
+
}
|
|
2300
|
+
async updateByIdOrThrow(id, changes, errorFactory) {
|
|
2301
|
+
const record = await this.updateById(id, changes);
|
|
2302
|
+
if (record) {
|
|
2303
|
+
return record;
|
|
2341
2304
|
}
|
|
2342
|
-
this.
|
|
2343
|
-
return this;
|
|
2305
|
+
throw errorFactory?.(id) ?? new Error(`${this.table.name} ${String(id)} was not found.`);
|
|
2344
2306
|
}
|
|
2345
|
-
|
|
2346
|
-
this.
|
|
2347
|
-
|
|
2307
|
+
async deleteById(id) {
|
|
2308
|
+
if (resolveSoftDeleteColumn(this.table)) {
|
|
2309
|
+
return await this.softDeleteById(id);
|
|
2310
|
+
}
|
|
2311
|
+
return await this.forceDeleteById(id);
|
|
2348
2312
|
}
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2313
|
+
async softDeleteById(id) {
|
|
2314
|
+
return await withDatabaseErrorHandling(async () => {
|
|
2315
|
+
const { text, params } = buildSoftDeleteByIdQuery(this.table, id, new Date);
|
|
2316
|
+
const [record] = await this.connection.unsafe(text, params);
|
|
2317
|
+
if (!record) {
|
|
2318
|
+
return false;
|
|
2319
|
+
}
|
|
2320
|
+
await eventBus.dispatch(modelEventName(this.table.name, "deleted"), record);
|
|
2321
|
+
return true;
|
|
2322
|
+
});
|
|
2352
2323
|
}
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2324
|
+
async forceDeleteById(id) {
|
|
2325
|
+
return await withDatabaseErrorHandling(async () => {
|
|
2326
|
+
const { text, params } = buildDeleteByIdQuery(this.table, id);
|
|
2327
|
+
const [row] = await this.connection.unsafe(text, params);
|
|
2328
|
+
if (!row) {
|
|
2329
|
+
return false;
|
|
2330
|
+
}
|
|
2331
|
+
await eventBus.dispatch(modelEventName(this.table.name, "force-deleted"), {
|
|
2332
|
+
id
|
|
2333
|
+
});
|
|
2334
|
+
return true;
|
|
2335
|
+
});
|
|
2356
2336
|
}
|
|
2357
|
-
|
|
2358
|
-
return
|
|
2337
|
+
async restoreById(id) {
|
|
2338
|
+
return await withDatabaseErrorHandling(async () => {
|
|
2339
|
+
const { text, params } = buildRestoreByIdQuery(this.table, id);
|
|
2340
|
+
const [record] = await this.connection.unsafe(text, params);
|
|
2341
|
+
if (!record) {
|
|
2342
|
+
return null;
|
|
2343
|
+
}
|
|
2344
|
+
const entity = record;
|
|
2345
|
+
await eventBus.dispatch(modelEventName(this.table.name, "restored"), entity);
|
|
2346
|
+
return entity;
|
|
2347
|
+
});
|
|
2359
2348
|
}
|
|
2360
|
-
|
|
2361
|
-
|
|
2349
|
+
withConnection(connection) {
|
|
2350
|
+
const clone = Object.create(Object.getPrototypeOf(this));
|
|
2351
|
+
Object.assign(clone, this);
|
|
2352
|
+
clone.connection = connection;
|
|
2353
|
+
return clone;
|
|
2362
2354
|
}
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
return this;
|
|
2355
|
+
getConnection() {
|
|
2356
|
+
return this.connection;
|
|
2366
2357
|
}
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
return this;
|
|
2358
|
+
getTable() {
|
|
2359
|
+
return this.table;
|
|
2370
2360
|
}
|
|
2371
|
-
|
|
2372
|
-
this
|
|
2373
|
-
kind: "hasMany",
|
|
2374
|
-
as,
|
|
2375
|
-
relation,
|
|
2376
|
-
repository: childRepository,
|
|
2377
|
-
options
|
|
2378
|
-
});
|
|
2379
|
-
return this;
|
|
2361
|
+
query(where = {}) {
|
|
2362
|
+
return new RepositoryQuery(this, where);
|
|
2380
2363
|
}
|
|
2381
|
-
|
|
2382
|
-
this.
|
|
2383
|
-
kind: "belongsTo",
|
|
2384
|
-
as,
|
|
2385
|
-
relation,
|
|
2386
|
-
repository: parentRepository,
|
|
2387
|
-
options
|
|
2388
|
-
});
|
|
2389
|
-
return this;
|
|
2364
|
+
async findWhere(where, options = {}) {
|
|
2365
|
+
return await this.findAll({ ...options, where });
|
|
2390
2366
|
}
|
|
2391
|
-
|
|
2392
|
-
this.
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
relation,
|
|
2396
|
-
repository: childRepository,
|
|
2397
|
-
options
|
|
2398
|
-
});
|
|
2399
|
-
return this;
|
|
2367
|
+
async countWhere(where = {}, options = {}, whereNodes = []) {
|
|
2368
|
+
const { text, params } = buildCountQuery(this.table, where, options, whereNodes);
|
|
2369
|
+
const [row] = await this.connection.unsafe(text, params);
|
|
2370
|
+
return Number(row?.count ?? 0);
|
|
2400
2371
|
}
|
|
2401
|
-
|
|
2402
|
-
this.
|
|
2403
|
-
|
|
2404
|
-
as,
|
|
2405
|
-
relation,
|
|
2406
|
-
repository: childRepository,
|
|
2407
|
-
options
|
|
2408
|
-
});
|
|
2409
|
-
return this;
|
|
2372
|
+
async averageColumn(column, where = {}) {
|
|
2373
|
+
const qualifiedColumn = qualifyColumn(this.table.name, column);
|
|
2374
|
+
return await this.averageExpression(`AVG(${qualifiedColumn})`, "value", where);
|
|
2410
2375
|
}
|
|
2411
|
-
|
|
2412
|
-
this.
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2376
|
+
async averageExpression(expression, alias, where = {}) {
|
|
2377
|
+
const { text, params } = buildProjectionQuery(this.table, expression, alias, { where });
|
|
2378
|
+
const [row] = await this.connection.unsafe(text, params);
|
|
2379
|
+
return Math.round(Number(row?.[alias] ?? 0));
|
|
2380
|
+
}
|
|
2381
|
+
async pluckNumberValues(expression, alias, options = {}) {
|
|
2382
|
+
const { text, params } = buildProjectionQuery(this.table, expression, alias, options);
|
|
2383
|
+
const rows = await this.connection.unsafe(text, params);
|
|
2384
|
+
return rows.flatMap((row) => {
|
|
2385
|
+
const value = row[alias];
|
|
2386
|
+
return value === null || value === undefined ? [] : [Number(value)];
|
|
2419
2387
|
});
|
|
2420
|
-
return this;
|
|
2421
2388
|
}
|
|
2422
|
-
async
|
|
2423
|
-
const
|
|
2424
|
-
|
|
2389
|
+
async countGroupedBy(column, where = {}) {
|
|
2390
|
+
const { text, params } = buildGroupedCountQuery(this.table, column, where);
|
|
2391
|
+
const rows = await this.connection.unsafe(text, params);
|
|
2392
|
+
return rows.map(({ value, count }) => ({
|
|
2393
|
+
value,
|
|
2394
|
+
count: Number(count)
|
|
2395
|
+
}));
|
|
2425
2396
|
}
|
|
2426
|
-
async
|
|
2427
|
-
|
|
2428
|
-
|
|
2397
|
+
async findByHasManyRelation(relation, parentId, options = {}) {
|
|
2398
|
+
return await this.findWhere({
|
|
2399
|
+
[relation.foreignKey]: parentId
|
|
2400
|
+
}, options);
|
|
2429
2401
|
}
|
|
2430
|
-
async
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2402
|
+
async loadHasManyForParents(parents, relation, options = {}) {
|
|
2403
|
+
if (parents.length === 0) {
|
|
2404
|
+
return indexHasManyRelation(parents, [], relation);
|
|
2405
|
+
}
|
|
2406
|
+
const parentIds = [...new Set(parents.map((parent) => parent[relation.localKey]))];
|
|
2407
|
+
const children = await this.findWhere({
|
|
2408
|
+
[relation.foreignKey]: parentIds
|
|
2409
|
+
}, options);
|
|
2410
|
+
return indexHasManyRelation(parents, children, relation);
|
|
2436
2411
|
}
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2412
|
+
async loadBelongsToForParents(children, relation, parentRepository, options = {}) {
|
|
2413
|
+
if (children.length === 0) {
|
|
2414
|
+
return new Map;
|
|
2415
|
+
}
|
|
2416
|
+
const ownerIds = [...new Set(children.map((child) => child[relation.foreignKey]))];
|
|
2417
|
+
const parents = await parentRepository.withConnection(this.connection).findWhere({
|
|
2418
|
+
[relation.ownerKey]: ownerIds
|
|
2419
|
+
}, options);
|
|
2420
|
+
return indexBelongsToRelation(children, parents, relation);
|
|
2443
2421
|
}
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
const table = type === "inner" ? rightRef.table : rightRef.table;
|
|
2448
|
-
const joins = this.queryOptions.joins ?? [];
|
|
2449
|
-
const existing = joins.find((join3) => join3.table === table && join3.type === type);
|
|
2450
|
-
if (existing) {
|
|
2451
|
-
existing.on.push({ left: leftRef, right: rightRef });
|
|
2452
|
-
return this;
|
|
2422
|
+
async loadMorphManyForParents(parents, relation, options = {}) {
|
|
2423
|
+
if (parents.length === 0) {
|
|
2424
|
+
return indexMorphManyRelation(parents, [], relation);
|
|
2453
2425
|
}
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
table,
|
|
2461
|
-
on: [{ left: leftRef, right: rightRef }]
|
|
2462
|
-
}
|
|
2463
|
-
]
|
|
2464
|
-
};
|
|
2465
|
-
return this;
|
|
2426
|
+
const parentIds = [...new Set(parents.map((parent) => parent[relation.localKey]))];
|
|
2427
|
+
const children = await this.findWhere({
|
|
2428
|
+
[relation.morphTypeKey]: relation.morphType,
|
|
2429
|
+
[relation.morphIdKey]: parentIds
|
|
2430
|
+
}, options);
|
|
2431
|
+
return indexMorphManyRelation(parents, children, relation);
|
|
2466
2432
|
}
|
|
2467
|
-
async
|
|
2468
|
-
|
|
2469
|
-
|
|
2433
|
+
async loadMorphOneForParents(parents, relation, options = {}) {
|
|
2434
|
+
const grouped = await this.loadMorphManyForParents(parents, relation, options);
|
|
2435
|
+
const result = new Map;
|
|
2436
|
+
for (const parent of parents) {
|
|
2437
|
+
const matches = grouped.get(parent[relation.localKey]) ?? [];
|
|
2438
|
+
result.set(parent[relation.localKey], matches[0]);
|
|
2470
2439
|
}
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
continue;
|
|
2490
|
-
}
|
|
2491
|
-
if (load.kind === "morphOne") {
|
|
2492
|
-
const relation2 = load.relation;
|
|
2493
|
-
const grouped2 = await load.repository.withConnection(this.repository.getConnection()).loadMorphOneForParents(rows, relation2, load.options);
|
|
2494
|
-
result = result.map((row) => ({
|
|
2495
|
-
...row,
|
|
2496
|
-
[load.as]: grouped2.get(row[relation2.localKey])
|
|
2497
|
-
}));
|
|
2440
|
+
return result;
|
|
2441
|
+
}
|
|
2442
|
+
async loadMorphToForChildren(children, relation, repositoriesByType, options = {}) {
|
|
2443
|
+
if (children.length === 0) {
|
|
2444
|
+
return new Map;
|
|
2445
|
+
}
|
|
2446
|
+
const idsByType = new Map;
|
|
2447
|
+
for (const child of children) {
|
|
2448
|
+
const morphType = String(child[relation.morphTypeKey]);
|
|
2449
|
+
const morphId = child[relation.morphIdKey];
|
|
2450
|
+
const ids = idsByType.get(morphType) ?? new Set;
|
|
2451
|
+
ids.add(morphId);
|
|
2452
|
+
idsByType.set(morphType, ids);
|
|
2453
|
+
}
|
|
2454
|
+
const parentsByType = new Map;
|
|
2455
|
+
for (const [morphType, ids] of idsByType) {
|
|
2456
|
+
const repository = repositoriesByType.get(morphType);
|
|
2457
|
+
if (!repository) {
|
|
2498
2458
|
continue;
|
|
2499
2459
|
}
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
continue;
|
|
2460
|
+
const ownerKey = repository.getTable().primaryKey;
|
|
2461
|
+
const parents = await repository.withConnection(this.connection).findWhere({
|
|
2462
|
+
[ownerKey]: [...ids]
|
|
2463
|
+
}, options);
|
|
2464
|
+
const indexed = new Map;
|
|
2465
|
+
for (const parent of parents) {
|
|
2466
|
+
indexed.set(parent[ownerKey], parent);
|
|
2508
2467
|
}
|
|
2509
|
-
|
|
2510
|
-
const grouped = await this.repository.loadBelongsToForParents(rows, relation, load.repository, load.options);
|
|
2511
|
-
result = result.map((row) => ({
|
|
2512
|
-
...row,
|
|
2513
|
-
[load.as]: grouped.get(row[relation.foreignKey])
|
|
2514
|
-
}));
|
|
2468
|
+
parentsByType.set(morphType, indexed);
|
|
2515
2469
|
}
|
|
2516
|
-
return
|
|
2470
|
+
return indexMorphToRelation(children, parentsByType, relation);
|
|
2517
2471
|
}
|
|
2518
2472
|
}
|
|
2519
|
-
|
|
2520
|
-
// ../../src/core/database/
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2473
|
+
var baseRepository_default = BaseRepository5;
|
|
2474
|
+
// ../../src/core/database/model.ts
|
|
2475
|
+
var modelRepositories = new WeakMap;
|
|
2476
|
+
var modelGlobalScopes = new WeakMap;
|
|
2477
|
+
var modelBooted = new WeakSet;
|
|
2478
|
+
// ../../src/core/database/schema/columnDefinition.ts
|
|
2479
|
+
class ColumnDefinition {
|
|
2480
|
+
name;
|
|
2481
|
+
kind;
|
|
2482
|
+
length;
|
|
2483
|
+
isNullable = false;
|
|
2484
|
+
isPrimary = false;
|
|
2485
|
+
isUnique = false;
|
|
2486
|
+
autoIncrement = false;
|
|
2487
|
+
defaultValue;
|
|
2488
|
+
checkExpression;
|
|
2489
|
+
foreignKey;
|
|
2490
|
+
constructor(name, kind) {
|
|
2491
|
+
this.name = name;
|
|
2492
|
+
this.kind = kind;
|
|
2534
2493
|
}
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
withTrashed: options.withTrashed,
|
|
2539
|
-
onlyTrashed: options.onlyTrashed,
|
|
2540
|
-
joins: options.joins,
|
|
2541
|
-
groupBy: options.groupBy
|
|
2542
|
-
}, whereNodes);
|
|
2543
|
-
const offset = (page - 1) * perPage;
|
|
2544
|
-
const data = await this.findAll({
|
|
2545
|
-
...queryOptions,
|
|
2546
|
-
where,
|
|
2547
|
-
whereNodes,
|
|
2548
|
-
limit: perPage,
|
|
2549
|
-
offset
|
|
2550
|
-
});
|
|
2551
|
-
return {
|
|
2552
|
-
data,
|
|
2553
|
-
meta: buildPaginationMeta({ page, perPage, total })
|
|
2554
|
-
};
|
|
2494
|
+
nullable() {
|
|
2495
|
+
this.isNullable = true;
|
|
2496
|
+
return this;
|
|
2555
2497
|
}
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
}
|
|
2560
|
-
let offset = 0;
|
|
2561
|
-
while (true) {
|
|
2562
|
-
const rows = await this.findAll({
|
|
2563
|
-
...options,
|
|
2564
|
-
limit: count,
|
|
2565
|
-
offset
|
|
2566
|
-
});
|
|
2567
|
-
if (rows.length === 0) {
|
|
2568
|
-
return;
|
|
2569
|
-
}
|
|
2570
|
-
const shouldContinue = await callback(rows);
|
|
2571
|
-
if (shouldContinue === false || rows.length < count) {
|
|
2572
|
-
return;
|
|
2573
|
-
}
|
|
2574
|
-
offset += count;
|
|
2575
|
-
}
|
|
2498
|
+
notNullable() {
|
|
2499
|
+
this.isNullable = false;
|
|
2500
|
+
return this;
|
|
2576
2501
|
}
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
cursorColumn = this.table.primaryKey,
|
|
2582
|
-
direction = "asc",
|
|
2583
|
-
where = {},
|
|
2584
|
-
whereNodes,
|
|
2585
|
-
...queryOptions
|
|
2586
|
-
} = options;
|
|
2587
|
-
if (!Number.isInteger(perPage) || perPage <= 0) {
|
|
2588
|
-
throw new Error("Cursor page size must be a positive integer.");
|
|
2502
|
+
default(value) {
|
|
2503
|
+
if (typeof value === "boolean") {
|
|
2504
|
+
this.defaultValue = value ? "TRUE" : "FALSE";
|
|
2505
|
+
return this;
|
|
2589
2506
|
}
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2507
|
+
if (typeof value === "number") {
|
|
2508
|
+
this.defaultValue = String(value);
|
|
2509
|
+
return this;
|
|
2593
2510
|
}
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2511
|
+
this.defaultValue = `'${value.replace(/'/g, "''")}'`;
|
|
2512
|
+
return this;
|
|
2513
|
+
}
|
|
2514
|
+
defaultRaw(expression) {
|
|
2515
|
+
this.defaultValue = expression;
|
|
2516
|
+
return this;
|
|
2517
|
+
}
|
|
2518
|
+
unique() {
|
|
2519
|
+
this.isUnique = true;
|
|
2520
|
+
return this;
|
|
2521
|
+
}
|
|
2522
|
+
primary() {
|
|
2523
|
+
this.isPrimary = true;
|
|
2524
|
+
return this;
|
|
2525
|
+
}
|
|
2526
|
+
check(expression) {
|
|
2527
|
+
this.checkExpression = expression;
|
|
2528
|
+
return this;
|
|
2529
|
+
}
|
|
2530
|
+
}
|
|
2531
|
+
|
|
2532
|
+
class ForeignIdColumnDefinition extends ColumnDefinition {
|
|
2533
|
+
constructor(name) {
|
|
2534
|
+
super(name, "foreignId");
|
|
2535
|
+
this.notNullable();
|
|
2536
|
+
}
|
|
2537
|
+
references(table, column = "id") {
|
|
2538
|
+
this.foreignKey = {
|
|
2539
|
+
referencesTable: table,
|
|
2540
|
+
referencesColumn: column
|
|
2613
2541
|
};
|
|
2542
|
+
return this;
|
|
2614
2543
|
}
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
});
|
|
2544
|
+
constrained(table) {
|
|
2545
|
+
const referencesTable = table ?? inferReferencedTable(this.name);
|
|
2546
|
+
return this.references(referencesTable);
|
|
2619
2547
|
}
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
return record;
|
|
2548
|
+
cascadeOnDelete() {
|
|
2549
|
+
if (!this.foreignKey) {
|
|
2550
|
+
throw new Error(`Foreign key is not defined for column ${this.name}`);
|
|
2624
2551
|
}
|
|
2625
|
-
|
|
2552
|
+
this.foreignKey.onDelete = "cascade";
|
|
2553
|
+
return this;
|
|
2626
2554
|
}
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
return [];
|
|
2555
|
+
nullOnDelete() {
|
|
2556
|
+
if (!this.foreignKey) {
|
|
2557
|
+
throw new Error(`Foreign key is not defined for column ${this.name}`);
|
|
2631
2558
|
}
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
});
|
|
2559
|
+
this.foreignKey.onDelete = "set null";
|
|
2560
|
+
return this;
|
|
2635
2561
|
}
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2562
|
+
}
|
|
2563
|
+
function inferReferencedTable(columnName) {
|
|
2564
|
+
if (!columnName.endsWith("_id")) {
|
|
2565
|
+
throw new Error(`Cannot infer referenced table from column ${columnName}`);
|
|
2639
2566
|
}
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2567
|
+
return columnName.slice(0, -3);
|
|
2568
|
+
}
|
|
2569
|
+
|
|
2570
|
+
// ../../src/core/database/schema/blueprint.ts
|
|
2571
|
+
class Blueprint {
|
|
2572
|
+
table;
|
|
2573
|
+
action;
|
|
2574
|
+
columns = [];
|
|
2575
|
+
indexes = [];
|
|
2576
|
+
droppedColumns = [];
|
|
2577
|
+
droppedIndexes = [];
|
|
2578
|
+
constructor(table, action) {
|
|
2579
|
+
this.table = table;
|
|
2580
|
+
this.action = action;
|
|
2651
2581
|
}
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
await eventBus.dispatch(modelEventName(this.table.name, "updated"), entity);
|
|
2659
|
-
}
|
|
2660
|
-
return entity;
|
|
2661
|
-
});
|
|
2582
|
+
id(name = "id") {
|
|
2583
|
+
const column = new ColumnDefinition(name, "id");
|
|
2584
|
+
column.primary();
|
|
2585
|
+
column.autoIncrement = true;
|
|
2586
|
+
this.columns.push(column);
|
|
2587
|
+
return column;
|
|
2662
2588
|
}
|
|
2663
|
-
|
|
2664
|
-
const
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2589
|
+
string(name, length) {
|
|
2590
|
+
const column = new ColumnDefinition(name, "string");
|
|
2591
|
+
column.length = length;
|
|
2592
|
+
column.notNullable();
|
|
2593
|
+
this.columns.push(column);
|
|
2594
|
+
return column;
|
|
2669
2595
|
}
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
return
|
|
2596
|
+
text(name) {
|
|
2597
|
+
const column = new ColumnDefinition(name, "text");
|
|
2598
|
+
column.notNullable();
|
|
2599
|
+
this.columns.push(column);
|
|
2600
|
+
return column;
|
|
2675
2601
|
}
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
return false;
|
|
2682
|
-
}
|
|
2683
|
-
await eventBus.dispatch(modelEventName(this.table.name, "deleted"), record);
|
|
2684
|
-
return true;
|
|
2685
|
-
});
|
|
2602
|
+
boolean(name) {
|
|
2603
|
+
const column = new ColumnDefinition(name, "boolean");
|
|
2604
|
+
column.notNullable();
|
|
2605
|
+
this.columns.push(column);
|
|
2606
|
+
return column;
|
|
2686
2607
|
}
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
return false;
|
|
2693
|
-
}
|
|
2694
|
-
await eventBus.dispatch(modelEventName(this.table.name, "force-deleted"), {
|
|
2695
|
-
id
|
|
2696
|
-
});
|
|
2697
|
-
return true;
|
|
2698
|
-
});
|
|
2608
|
+
integer(name) {
|
|
2609
|
+
const column = new ColumnDefinition(name, "integer");
|
|
2610
|
+
column.notNullable();
|
|
2611
|
+
this.columns.push(column);
|
|
2612
|
+
return column;
|
|
2699
2613
|
}
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
return null;
|
|
2706
|
-
}
|
|
2707
|
-
const entity = record;
|
|
2708
|
-
await eventBus.dispatch(modelEventName(this.table.name, "restored"), entity);
|
|
2709
|
-
return entity;
|
|
2710
|
-
});
|
|
2614
|
+
bigInteger(name) {
|
|
2615
|
+
const column = new ColumnDefinition(name, "bigInteger");
|
|
2616
|
+
column.notNullable();
|
|
2617
|
+
this.columns.push(column);
|
|
2618
|
+
return column;
|
|
2711
2619
|
}
|
|
2712
|
-
|
|
2713
|
-
const
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
return
|
|
2620
|
+
timestamp(name) {
|
|
2621
|
+
const column = new ColumnDefinition(name, "timestamp");
|
|
2622
|
+
column.notNullable();
|
|
2623
|
+
this.columns.push(column);
|
|
2624
|
+
return column;
|
|
2717
2625
|
}
|
|
2718
|
-
|
|
2719
|
-
|
|
2626
|
+
json(name) {
|
|
2627
|
+
const column = new ColumnDefinition(name, "json");
|
|
2628
|
+
column.notNullable();
|
|
2629
|
+
this.columns.push(column);
|
|
2630
|
+
return column;
|
|
2720
2631
|
}
|
|
2721
|
-
|
|
2722
|
-
|
|
2632
|
+
jsonb(name) {
|
|
2633
|
+
const column = new ColumnDefinition(name, "jsonb");
|
|
2634
|
+
column.notNullable();
|
|
2635
|
+
this.columns.push(column);
|
|
2636
|
+
return column;
|
|
2723
2637
|
}
|
|
2724
|
-
|
|
2725
|
-
|
|
2638
|
+
foreignId(name) {
|
|
2639
|
+
const column = new ForeignIdColumnDefinition(name);
|
|
2640
|
+
this.columns.push(column);
|
|
2641
|
+
return column;
|
|
2726
2642
|
}
|
|
2727
|
-
|
|
2728
|
-
|
|
2643
|
+
timestamps() {
|
|
2644
|
+
this.timestamp("created_at").defaultRaw("NOW()");
|
|
2645
|
+
this.timestamp("updated_at").defaultRaw("NOW()");
|
|
2646
|
+
}
|
|
2647
|
+
softDeletes() {
|
|
2648
|
+
this.timestamp("deleted_at").nullable();
|
|
2649
|
+
}
|
|
2650
|
+
dropColumn(name) {
|
|
2651
|
+
this.droppedColumns.push(name);
|
|
2652
|
+
}
|
|
2653
|
+
dropSoftDeletes() {
|
|
2654
|
+
this.dropColumn("deleted_at");
|
|
2655
|
+
this.dropIndex(`idx_${this.table}_deleted_at`);
|
|
2656
|
+
}
|
|
2657
|
+
dropIndex(name) {
|
|
2658
|
+
this.droppedIndexes.push(name);
|
|
2659
|
+
}
|
|
2660
|
+
unique(columns, name) {
|
|
2661
|
+
this.indexes.push({
|
|
2662
|
+
name,
|
|
2663
|
+
columns: Array.isArray(columns) ? columns : [columns],
|
|
2664
|
+
kind: "unique"
|
|
2665
|
+
});
|
|
2729
2666
|
}
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2667
|
+
index(columns, options = {}) {
|
|
2668
|
+
this.indexes.push({
|
|
2669
|
+
name: options.name,
|
|
2670
|
+
columns: Array.isArray(columns) ? columns : [columns],
|
|
2671
|
+
kind: "index",
|
|
2672
|
+
order: options.order
|
|
2673
|
+
});
|
|
2734
2674
|
}
|
|
2735
|
-
|
|
2736
|
-
const
|
|
2737
|
-
|
|
2675
|
+
partialIndex(columns, where, nameOrOptions) {
|
|
2676
|
+
const options = typeof nameOrOptions === "string" ? { name: nameOrOptions } : nameOrOptions ?? {};
|
|
2677
|
+
this.indexes.push({
|
|
2678
|
+
name: options.name,
|
|
2679
|
+
columns: Array.isArray(columns) ? columns : [columns],
|
|
2680
|
+
kind: options.unique ? "uniquePartial" : "partial",
|
|
2681
|
+
where
|
|
2682
|
+
});
|
|
2738
2683
|
}
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2684
|
+
fullText(columns, name) {
|
|
2685
|
+
this.indexes.push({
|
|
2686
|
+
name,
|
|
2687
|
+
columns: Array.isArray(columns) ? columns : [columns],
|
|
2688
|
+
kind: "fullText"
|
|
2689
|
+
});
|
|
2743
2690
|
}
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
return value === null || value === undefined ? [] : [Number(value)];
|
|
2691
|
+
ginIndex(column, name) {
|
|
2692
|
+
this.indexes.push({
|
|
2693
|
+
name,
|
|
2694
|
+
columns: [column],
|
|
2695
|
+
kind: "gin"
|
|
2750
2696
|
});
|
|
2751
2697
|
}
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
}));
|
|
2698
|
+
}
|
|
2699
|
+
// ../../src/core/database/schema/errors.ts
|
|
2700
|
+
class UnsupportedSchemaFeatureError extends Error {
|
|
2701
|
+
constructor(feature, driver) {
|
|
2702
|
+
super(`${feature} is not supported for the ${driver} driver`);
|
|
2703
|
+
this.name = "UnsupportedSchemaFeatureError";
|
|
2759
2704
|
}
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2705
|
+
}
|
|
2706
|
+
// ../../src/core/database/schema/grammars/grammar.ts
|
|
2707
|
+
function compileColumnType(driver, column) {
|
|
2708
|
+
switch (column.kind) {
|
|
2709
|
+
case "id":
|
|
2710
|
+
return compileIdType(driver);
|
|
2711
|
+
case "string":
|
|
2712
|
+
return compileStringType(driver, column.length);
|
|
2713
|
+
case "text":
|
|
2714
|
+
return compileTextType(driver);
|
|
2715
|
+
case "boolean":
|
|
2716
|
+
return compileBooleanType(driver);
|
|
2717
|
+
case "integer":
|
|
2718
|
+
case "foreignId":
|
|
2719
|
+
return compileIntegerType(driver);
|
|
2720
|
+
case "bigInteger":
|
|
2721
|
+
return compileBigIntegerType(driver);
|
|
2722
|
+
case "timestamp":
|
|
2723
|
+
return compileTimestampType(driver);
|
|
2724
|
+
case "json":
|
|
2725
|
+
return compileJsonType(driver);
|
|
2726
|
+
case "jsonb":
|
|
2727
|
+
return compileJsonbType(driver);
|
|
2728
|
+
default:
|
|
2729
|
+
throw new Error(`Unsupported column kind: ${column.kind}`);
|
|
2764
2730
|
}
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2731
|
+
}
|
|
2732
|
+
function compileIdType(driver) {
|
|
2733
|
+
switch (driver) {
|
|
2734
|
+
case "pgsql":
|
|
2735
|
+
return "SERIAL";
|
|
2736
|
+
case "mysql":
|
|
2737
|
+
return "BIGINT UNSIGNED";
|
|
2738
|
+
case "sqlite":
|
|
2739
|
+
return "INTEGER";
|
|
2774
2740
|
}
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2741
|
+
}
|
|
2742
|
+
function compileStringType(driver, length) {
|
|
2743
|
+
switch (driver) {
|
|
2744
|
+
case "pgsql":
|
|
2745
|
+
return "TEXT";
|
|
2746
|
+
case "mysql":
|
|
2747
|
+
return length ? `VARCHAR(${length})` : "VARCHAR(255)";
|
|
2748
|
+
case "sqlite":
|
|
2749
|
+
return "TEXT";
|
|
2784
2750
|
}
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
}, options);
|
|
2794
|
-
return indexMorphManyRelation(parents, children, relation);
|
|
2751
|
+
}
|
|
2752
|
+
function compileTextType(driver) {
|
|
2753
|
+
switch (driver) {
|
|
2754
|
+
case "pgsql":
|
|
2755
|
+
case "sqlite":
|
|
2756
|
+
return "TEXT";
|
|
2757
|
+
case "mysql":
|
|
2758
|
+
return "TEXT";
|
|
2795
2759
|
}
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2760
|
+
}
|
|
2761
|
+
function compileBooleanType(driver) {
|
|
2762
|
+
switch (driver) {
|
|
2763
|
+
case "pgsql":
|
|
2764
|
+
return "BOOLEAN";
|
|
2765
|
+
case "mysql":
|
|
2766
|
+
return "BOOLEAN";
|
|
2767
|
+
case "sqlite":
|
|
2768
|
+
return "INTEGER";
|
|
2804
2769
|
}
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
ids.add(morphId);
|
|
2815
|
-
idsByType.set(morphType, ids);
|
|
2816
|
-
}
|
|
2817
|
-
const parentsByType = new Map;
|
|
2818
|
-
for (const [morphType, ids] of idsByType) {
|
|
2819
|
-
const repository = repositoriesByType.get(morphType);
|
|
2820
|
-
if (!repository) {
|
|
2821
|
-
continue;
|
|
2822
|
-
}
|
|
2823
|
-
const ownerKey = repository.getTable().primaryKey;
|
|
2824
|
-
const parents = await repository.withConnection(this.connection).findWhere({
|
|
2825
|
-
[ownerKey]: [...ids]
|
|
2826
|
-
}, options);
|
|
2827
|
-
const indexed = new Map;
|
|
2828
|
-
for (const parent of parents) {
|
|
2829
|
-
indexed.set(parent[ownerKey], parent);
|
|
2830
|
-
}
|
|
2831
|
-
parentsByType.set(morphType, indexed);
|
|
2832
|
-
}
|
|
2833
|
-
return indexMorphToRelation(children, parentsByType, relation);
|
|
2770
|
+
}
|
|
2771
|
+
function compileIntegerType(driver) {
|
|
2772
|
+
switch (driver) {
|
|
2773
|
+
case "pgsql":
|
|
2774
|
+
return "INTEGER";
|
|
2775
|
+
case "mysql":
|
|
2776
|
+
return "INT";
|
|
2777
|
+
case "sqlite":
|
|
2778
|
+
return "INTEGER";
|
|
2834
2779
|
}
|
|
2835
2780
|
}
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
kind;
|
|
2845
|
-
length;
|
|
2846
|
-
isNullable = false;
|
|
2847
|
-
isPrimary = false;
|
|
2848
|
-
isUnique = false;
|
|
2849
|
-
autoIncrement = false;
|
|
2850
|
-
defaultValue;
|
|
2851
|
-
checkExpression;
|
|
2852
|
-
foreignKey;
|
|
2853
|
-
constructor(name, kind) {
|
|
2854
|
-
this.name = name;
|
|
2855
|
-
this.kind = kind;
|
|
2781
|
+
function compileBigIntegerType(driver) {
|
|
2782
|
+
switch (driver) {
|
|
2783
|
+
case "pgsql":
|
|
2784
|
+
return "BIGINT";
|
|
2785
|
+
case "mysql":
|
|
2786
|
+
return "BIGINT";
|
|
2787
|
+
case "sqlite":
|
|
2788
|
+
return "INTEGER";
|
|
2856
2789
|
}
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2790
|
+
}
|
|
2791
|
+
function compileTimestampType(driver) {
|
|
2792
|
+
switch (driver) {
|
|
2793
|
+
case "pgsql":
|
|
2794
|
+
return "TIMESTAMPTZ";
|
|
2795
|
+
case "mysql":
|
|
2796
|
+
return "TIMESTAMP";
|
|
2797
|
+
case "sqlite":
|
|
2798
|
+
return "TEXT";
|
|
2799
|
+
}
|
|
2800
|
+
}
|
|
2801
|
+
function compileJsonType(driver) {
|
|
2802
|
+
switch (driver) {
|
|
2803
|
+
case "pgsql":
|
|
2804
|
+
return "JSONB";
|
|
2805
|
+
case "mysql":
|
|
2806
|
+
return "JSON";
|
|
2807
|
+
case "sqlite":
|
|
2808
|
+
return "TEXT";
|
|
2809
|
+
}
|
|
2810
|
+
}
|
|
2811
|
+
function compileJsonbType(driver) {
|
|
2812
|
+
switch (driver) {
|
|
2813
|
+
case "pgsql":
|
|
2814
|
+
return "JSONB";
|
|
2815
|
+
case "mysql":
|
|
2816
|
+
return "JSON";
|
|
2817
|
+
case "sqlite":
|
|
2818
|
+
return "TEXT";
|
|
2860
2819
|
}
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2820
|
+
}
|
|
2821
|
+
|
|
2822
|
+
// ../../src/core/database/schema/grammars/compileStatements.ts
|
|
2823
|
+
function compileCreateTable(driver, blueprint) {
|
|
2824
|
+
const table = quoteIdentifier(blueprint.table);
|
|
2825
|
+
const parts = blueprint.columns.map((column) => compileColumn(driver, column, "create"));
|
|
2826
|
+
for (const index of blueprint.indexes) {
|
|
2827
|
+
if (index.kind === "unique" && index.columns.length > 1) {
|
|
2828
|
+
const columns = index.columns.map((column) => quoteIdentifier(column)).join(", ");
|
|
2829
|
+
parts.push(`UNIQUE (${columns})`);
|
|
2830
|
+
}
|
|
2864
2831
|
}
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2832
|
+
const statements = [`CREATE TABLE IF NOT EXISTS ${table} (
|
|
2833
|
+
${parts.join(`,
|
|
2834
|
+
`)}
|
|
2835
|
+
)`];
|
|
2836
|
+
for (const index of blueprint.indexes) {
|
|
2837
|
+
if (index.kind === "unique" && index.columns.length === 1) {
|
|
2838
|
+
continue;
|
|
2869
2839
|
}
|
|
2870
|
-
if (
|
|
2871
|
-
|
|
2872
|
-
|
|
2840
|
+
if (index.kind === "index") {
|
|
2841
|
+
statements.push(compileIndex(driver, blueprint.table, index));
|
|
2842
|
+
} else if (index.kind === "partial" || index.kind === "uniquePartial" || index.kind === "gin" || index.kind === "fullText") {
|
|
2843
|
+
statements.push(...compileSpecialIndex(driver, blueprint.table, index));
|
|
2873
2844
|
}
|
|
2874
|
-
this.defaultValue = `'${value.replace(/'/g, "''")}'`;
|
|
2875
|
-
return this;
|
|
2876
2845
|
}
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2846
|
+
return statements;
|
|
2847
|
+
}
|
|
2848
|
+
function compileAlterTable(driver, blueprint) {
|
|
2849
|
+
const statements = [];
|
|
2850
|
+
const table = quoteIdentifier(blueprint.table);
|
|
2851
|
+
for (const column of blueprint.columns) {
|
|
2852
|
+
const addPrefix = driver === "pgsql" ? "ADD COLUMN IF NOT EXISTS" : "ADD COLUMN";
|
|
2853
|
+
statements.push(`ALTER TABLE ${table}
|
|
2854
|
+
${addPrefix} ${compileColumn(driver, column, "alter")}`);
|
|
2880
2855
|
}
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2856
|
+
for (const columnName of blueprint.droppedColumns) {
|
|
2857
|
+
const dropPrefix = driver === "pgsql" ? "DROP COLUMN IF EXISTS" : "DROP COLUMN";
|
|
2858
|
+
statements.push(`ALTER TABLE ${table} ${dropPrefix} ${quoteIdentifier(columnName)}`);
|
|
2884
2859
|
}
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
return this;
|
|
2860
|
+
for (const indexName of blueprint.droppedIndexes) {
|
|
2861
|
+
statements.push(`DROP INDEX IF EXISTS ${quoteIdentifier(indexName)}`);
|
|
2888
2862
|
}
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2863
|
+
for (const index of blueprint.indexes) {
|
|
2864
|
+
if (index.kind === "index" || index.kind === "unique") {
|
|
2865
|
+
statements.push(compileIndex(driver, blueprint.table, index));
|
|
2866
|
+
} else {
|
|
2867
|
+
statements.push(...compileSpecialIndex(driver, blueprint.table, index));
|
|
2868
|
+
}
|
|
2892
2869
|
}
|
|
2870
|
+
return statements;
|
|
2893
2871
|
}
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2872
|
+
function compileDropTable(driver, tableName) {
|
|
2873
|
+
const cascade = driver === "pgsql" ? " CASCADE" : "";
|
|
2874
|
+
return [`DROP TABLE IF EXISTS ${quoteIdentifier(tableName)}${cascade}`];
|
|
2875
|
+
}
|
|
2876
|
+
function compileColumn(driver, column, mode) {
|
|
2877
|
+
const parts = [quoteIdentifier(column.name), compileColumnType(driver, column)];
|
|
2878
|
+
if (column.autoIncrement && driver === "mysql") {
|
|
2879
|
+
parts[1] = `${parts[1]} AUTO_INCREMENT`;
|
|
2899
2880
|
}
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2881
|
+
if (column.isPrimary && mode === "create") {
|
|
2882
|
+
if (driver === "sqlite") {
|
|
2883
|
+
parts.push("PRIMARY KEY AUTOINCREMENT");
|
|
2884
|
+
} else {
|
|
2885
|
+
parts.push("PRIMARY KEY");
|
|
2886
|
+
}
|
|
2887
|
+
} else if (!column.isNullable) {
|
|
2888
|
+
parts.push("NOT NULL");
|
|
2889
|
+
} else if (column.isNullable) {
|
|
2890
|
+
parts.push("NULL");
|
|
2906
2891
|
}
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
return this.references(referencesTable);
|
|
2892
|
+
if (column.defaultValue !== undefined) {
|
|
2893
|
+
parts.push(`DEFAULT ${column.defaultValue}`);
|
|
2910
2894
|
}
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2895
|
+
if (column.isUnique) {
|
|
2896
|
+
parts.push("UNIQUE");
|
|
2897
|
+
}
|
|
2898
|
+
if (column.checkExpression) {
|
|
2899
|
+
parts.push(`CHECK (${column.checkExpression})`);
|
|
2900
|
+
}
|
|
2901
|
+
if (column.foreignKey) {
|
|
2902
|
+
const { referencesTable, referencesColumn, onDelete } = column.foreignKey;
|
|
2903
|
+
const reference = `${quoteIdentifier(referencesTable)}(${quoteIdentifier(referencesColumn)})`;
|
|
2904
|
+
let clause = `REFERENCES ${reference}`;
|
|
2905
|
+
if (onDelete === "cascade") {
|
|
2906
|
+
clause += " ON DELETE CASCADE";
|
|
2907
|
+
} else if (onDelete === "set null") {
|
|
2908
|
+
clause += " ON DELETE SET NULL";
|
|
2914
2909
|
}
|
|
2915
|
-
|
|
2916
|
-
return this;
|
|
2910
|
+
parts.push(clause);
|
|
2917
2911
|
}
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2912
|
+
return parts.join(" ");
|
|
2913
|
+
}
|
|
2914
|
+
function compileIndex(_driver, tableName, index) {
|
|
2915
|
+
const indexName = index.name ?? defaultIndexName(tableName, index.columns, index.kind === "unique" ? "unique" : "index");
|
|
2916
|
+
const columns = index.columns.map((column) => {
|
|
2917
|
+
const quoted = quoteIdentifier(column);
|
|
2918
|
+
if (index.order === "desc") {
|
|
2919
|
+
return `${quoted} DESC`;
|
|
2921
2920
|
}
|
|
2922
|
-
|
|
2923
|
-
|
|
2921
|
+
return quoted;
|
|
2922
|
+
}).join(", ");
|
|
2923
|
+
const unique = index.kind === "unique" ? "UNIQUE " : "";
|
|
2924
|
+
return `CREATE ${unique}INDEX IF NOT EXISTS ${quoteIdentifier(indexName)} ON ${quoteIdentifier(tableName)}(${columns})`;
|
|
2925
|
+
}
|
|
2926
|
+
function compileSpecialIndex(driver, tableName, index) {
|
|
2927
|
+
const indexName = index.name ?? defaultIndexName(tableName, index.columns, index.kind);
|
|
2928
|
+
const columns = index.columns.map((column) => quoteIdentifier(column)).join(", ");
|
|
2929
|
+
switch (index.kind) {
|
|
2930
|
+
case "partial":
|
|
2931
|
+
case "uniquePartial": {
|
|
2932
|
+
if (driver !== "pgsql") {
|
|
2933
|
+
throw new UnsupportedSchemaFeatureError("partialIndex()", driver);
|
|
2934
|
+
}
|
|
2935
|
+
const unique = index.kind === "uniquePartial" ? "UNIQUE " : "";
|
|
2936
|
+
return [
|
|
2937
|
+
`CREATE ${unique}INDEX IF NOT EXISTS ${quoteIdentifier(indexName)} ON ${quoteIdentifier(tableName)}(${columns}) WHERE ${index.where}`
|
|
2938
|
+
];
|
|
2939
|
+
}
|
|
2940
|
+
case "gin": {
|
|
2941
|
+
if (driver !== "pgsql") {
|
|
2942
|
+
throw new UnsupportedSchemaFeatureError("ginIndex()", driver);
|
|
2943
|
+
}
|
|
2944
|
+
return [
|
|
2945
|
+
`CREATE INDEX IF NOT EXISTS ${quoteIdentifier(indexName)} ON ${quoteIdentifier(tableName)} USING GIN(${columns})`
|
|
2946
|
+
];
|
|
2947
|
+
}
|
|
2948
|
+
case "fullText": {
|
|
2949
|
+
if (driver === "mysql") {
|
|
2950
|
+
return [
|
|
2951
|
+
`CREATE FULLTEXT INDEX ${quoteIdentifier(indexName)} ON ${quoteIdentifier(tableName)}(${columns})`
|
|
2952
|
+
];
|
|
2953
|
+
}
|
|
2954
|
+
if (driver === "pgsql") {
|
|
2955
|
+
throw new UnsupportedSchemaFeatureError("fullText(); use ginIndex() with a tsvector column on PostgreSQL", driver);
|
|
2956
|
+
}
|
|
2957
|
+
throw new UnsupportedSchemaFeatureError("fullText()", driver);
|
|
2958
|
+
}
|
|
2959
|
+
default:
|
|
2960
|
+
return [];
|
|
2924
2961
|
}
|
|
2925
2962
|
}
|
|
2926
|
-
function
|
|
2927
|
-
|
|
2928
|
-
|
|
2963
|
+
function defaultIndexName(tableName, columns, kind) {
|
|
2964
|
+
return `idx_${tableName}_${columns.join("_")}_${kind}`;
|
|
2965
|
+
}
|
|
2966
|
+
function compileBlueprint(driver, blueprint) {
|
|
2967
|
+
switch (blueprint.action) {
|
|
2968
|
+
case "create":
|
|
2969
|
+
return compileCreateTable(driver, blueprint);
|
|
2970
|
+
case "alter":
|
|
2971
|
+
return compileAlterTable(driver, blueprint);
|
|
2972
|
+
case "drop":
|
|
2973
|
+
return compileDropTable(driver, blueprint.table);
|
|
2974
|
+
default:
|
|
2975
|
+
throw new Error(`Unsupported blueprint action: ${blueprint.action}`);
|
|
2929
2976
|
}
|
|
2930
|
-
|
|
2977
|
+
}
|
|
2978
|
+
// ../../src/core/database/schema/grammars/createGrammar.ts
|
|
2979
|
+
function createGrammar(driver) {
|
|
2980
|
+
return {
|
|
2981
|
+
driver,
|
|
2982
|
+
compile(blueprint) {
|
|
2983
|
+
return compileBlueprint(driver, blueprint);
|
|
2984
|
+
}
|
|
2985
|
+
};
|
|
2931
2986
|
}
|
|
2932
2987
|
|
|
2933
|
-
// ../../src/core/database/schema/
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
const column = new ColumnDefinition(name, "string");
|
|
2954
|
-
column.length = length;
|
|
2955
|
-
column.notNullable();
|
|
2956
|
-
this.columns.push(column);
|
|
2957
|
-
return column;
|
|
2958
|
-
}
|
|
2959
|
-
text(name) {
|
|
2960
|
-
const column = new ColumnDefinition(name, "text");
|
|
2961
|
-
column.notNullable();
|
|
2962
|
-
this.columns.push(column);
|
|
2963
|
-
return column;
|
|
2964
|
-
}
|
|
2965
|
-
boolean(name) {
|
|
2966
|
-
const column = new ColumnDefinition(name, "boolean");
|
|
2967
|
-
column.notNullable();
|
|
2968
|
-
this.columns.push(column);
|
|
2969
|
-
return column;
|
|
2970
|
-
}
|
|
2971
|
-
integer(name) {
|
|
2972
|
-
const column = new ColumnDefinition(name, "integer");
|
|
2973
|
-
column.notNullable();
|
|
2974
|
-
this.columns.push(column);
|
|
2975
|
-
return column;
|
|
2976
|
-
}
|
|
2977
|
-
bigInteger(name) {
|
|
2978
|
-
const column = new ColumnDefinition(name, "bigInteger");
|
|
2979
|
-
column.notNullable();
|
|
2980
|
-
this.columns.push(column);
|
|
2981
|
-
return column;
|
|
2982
|
-
}
|
|
2983
|
-
timestamp(name) {
|
|
2984
|
-
const column = new ColumnDefinition(name, "timestamp");
|
|
2985
|
-
column.notNullable();
|
|
2986
|
-
this.columns.push(column);
|
|
2987
|
-
return column;
|
|
2988
|
+
// ../../src/core/database/schema/grammars/mysqlGrammar.ts
|
|
2989
|
+
var MySqlGrammar = createGrammar("mysql");
|
|
2990
|
+
|
|
2991
|
+
// ../../src/core/database/schema/grammars/postgresGrammar.ts
|
|
2992
|
+
var PostgresGrammar = createGrammar("pgsql");
|
|
2993
|
+
|
|
2994
|
+
// ../../src/core/database/schema/grammars/sqliteGrammar.ts
|
|
2995
|
+
var SqliteGrammar = createGrammar("sqlite");
|
|
2996
|
+
|
|
2997
|
+
// ../../src/core/database/schema/grammars/index.ts
|
|
2998
|
+
function grammarForDriver(driver) {
|
|
2999
|
+
switch (driver) {
|
|
3000
|
+
case "pgsql":
|
|
3001
|
+
return PostgresGrammar;
|
|
3002
|
+
case "mysql":
|
|
3003
|
+
return MySqlGrammar;
|
|
3004
|
+
case "sqlite":
|
|
3005
|
+
return SqliteGrammar;
|
|
3006
|
+
default:
|
|
3007
|
+
throw new Error(`Unsupported database driver: ${driver}`);
|
|
2988
3008
|
}
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
3009
|
+
}
|
|
3010
|
+
// ../../src/core/database/schema/schema.ts
|
|
3011
|
+
class SchemaBuilder {
|
|
3012
|
+
#driver;
|
|
3013
|
+
#statements = [];
|
|
3014
|
+
constructor(driver) {
|
|
3015
|
+
this.#driver = driver;
|
|
2994
3016
|
}
|
|
2995
|
-
|
|
2996
|
-
const
|
|
2997
|
-
|
|
2998
|
-
this.
|
|
2999
|
-
return
|
|
3017
|
+
create(table, callback) {
|
|
3018
|
+
const blueprint = new Blueprint(table, "create");
|
|
3019
|
+
callback(blueprint);
|
|
3020
|
+
this.#statements.push(...grammarForDriver(this.#driver).compile(blueprint));
|
|
3021
|
+
return this;
|
|
3000
3022
|
}
|
|
3001
|
-
|
|
3002
|
-
const
|
|
3003
|
-
|
|
3004
|
-
|
|
3023
|
+
table(table, callback) {
|
|
3024
|
+
const blueprint = new Blueprint(table, "alter");
|
|
3025
|
+
callback(blueprint);
|
|
3026
|
+
this.#statements.push(...grammarForDriver(this.#driver).compile(blueprint));
|
|
3027
|
+
return this;
|
|
3005
3028
|
}
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
this.
|
|
3029
|
+
drop(table) {
|
|
3030
|
+
const blueprint = new Blueprint(table, "drop");
|
|
3031
|
+
this.#statements.push(...grammarForDriver(this.#driver).compile(blueprint));
|
|
3032
|
+
return this;
|
|
3009
3033
|
}
|
|
3010
|
-
|
|
3011
|
-
this
|
|
3034
|
+
toSql() {
|
|
3035
|
+
return [...this.#statements];
|
|
3012
3036
|
}
|
|
3013
|
-
|
|
3014
|
-
this
|
|
3037
|
+
async execute(db2) {
|
|
3038
|
+
for (const statement of this.#statements) {
|
|
3039
|
+
await db2.unsafe(statement);
|
|
3040
|
+
}
|
|
3015
3041
|
}
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3042
|
+
}
|
|
3043
|
+
// ../../src/core/database/table.ts
|
|
3044
|
+
function defineTable5(definition) {
|
|
3045
|
+
return definition;
|
|
3046
|
+
}
|
|
3047
|
+
// ../../src/core/queue/failedJobTable.ts
|
|
3048
|
+
var failedJobTable = defineTable5({
|
|
3049
|
+
name: "failed_job",
|
|
3050
|
+
primaryKey: "id",
|
|
3051
|
+
columns: ["id", "job_name", "payload", "exception", "failed_at"],
|
|
3052
|
+
defaultOrderBy: { column: "failed_at", direction: "DESC" }
|
|
3053
|
+
});
|
|
3054
|
+
|
|
3055
|
+
// ../../src/core/queue/failedJobRepository.ts
|
|
3056
|
+
class FailedJobRepository extends baseRepository_default {
|
|
3057
|
+
constructor() {
|
|
3058
|
+
super(failedJobTable);
|
|
3019
3059
|
}
|
|
3020
|
-
|
|
3021
|
-
|
|
3060
|
+
}
|
|
3061
|
+
var failedJobRepository_default = FailedJobRepository;
|
|
3062
|
+
|
|
3063
|
+
// ../../src/core/queue/failedJobService.ts
|
|
3064
|
+
class FailedJobService {
|
|
3065
|
+
repository;
|
|
3066
|
+
constructor(repository) {
|
|
3067
|
+
this.repository = repository;
|
|
3022
3068
|
}
|
|
3023
|
-
|
|
3024
|
-
this.
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3069
|
+
async recordFailure(input) {
|
|
3070
|
+
return await this.repository.create({
|
|
3071
|
+
job_name: input.jobName,
|
|
3072
|
+
payload: input.payload,
|
|
3073
|
+
exception: input.exception,
|
|
3074
|
+
failed_at: new Date
|
|
3028
3075
|
});
|
|
3029
3076
|
}
|
|
3030
|
-
|
|
3031
|
-
this.
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
kind: "index",
|
|
3035
|
-
order: options.order
|
|
3077
|
+
listRecent(limit = 50) {
|
|
3078
|
+
return this.repository.findAll({
|
|
3079
|
+
limit,
|
|
3080
|
+
orderBy: { column: "failed_at", direction: "DESC" }
|
|
3036
3081
|
});
|
|
3037
3082
|
}
|
|
3038
|
-
|
|
3039
|
-
const
|
|
3040
|
-
this.
|
|
3041
|
-
|
|
3042
|
-
columns: Array.isArray(columns) ? columns : [columns],
|
|
3043
|
-
kind: options.unique ? "uniquePartial" : "partial",
|
|
3044
|
-
where
|
|
3045
|
-
});
|
|
3083
|
+
async retry(id) {
|
|
3084
|
+
const failedJob = await this.repository.findByIdOrThrow(id, (jobId) => new Error(`Failed job ${jobId} not found.`));
|
|
3085
|
+
await this.repository.deleteById(id);
|
|
3086
|
+
return failedJob;
|
|
3046
3087
|
}
|
|
3047
|
-
|
|
3048
|
-
this.
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
});
|
|
3088
|
+
async delete(id) {
|
|
3089
|
+
const deleted = await this.repository.deleteById(id);
|
|
3090
|
+
if (!deleted) {
|
|
3091
|
+
throw new Error(`Failed job ${id} not found.`);
|
|
3092
|
+
}
|
|
3053
3093
|
}
|
|
3054
|
-
|
|
3055
|
-
this.
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3094
|
+
async flush() {
|
|
3095
|
+
const jobs = await this.repository.findAll();
|
|
3096
|
+
let deleted = 0;
|
|
3097
|
+
for (const job of jobs) {
|
|
3098
|
+
if (await this.repository.deleteById(job.id)) {
|
|
3099
|
+
deleted += 1;
|
|
3100
|
+
}
|
|
3101
|
+
}
|
|
3102
|
+
return deleted;
|
|
3060
3103
|
}
|
|
3061
3104
|
}
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3105
|
+
var failedJobService_default = FailedJobService;
|
|
3106
|
+
|
|
3107
|
+
// ../../src/core/queue/jobRegistry.ts
|
|
3108
|
+
class JobRegistry {
|
|
3109
|
+
constructor() {}
|
|
3110
|
+
factories = new Map;
|
|
3111
|
+
instances = new WeakMap;
|
|
3112
|
+
register(name, factory) {
|
|
3113
|
+
this.factories.set(name, factory);
|
|
3067
3114
|
}
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
function compileColumnType(driver, column) {
|
|
3071
|
-
switch (column.kind) {
|
|
3072
|
-
case "id":
|
|
3073
|
-
return compileIdType(driver);
|
|
3074
|
-
case "string":
|
|
3075
|
-
return compileStringType(driver, column.length);
|
|
3076
|
-
case "text":
|
|
3077
|
-
return compileTextType(driver);
|
|
3078
|
-
case "boolean":
|
|
3079
|
-
return compileBooleanType(driver);
|
|
3080
|
-
case "integer":
|
|
3081
|
-
case "foreignId":
|
|
3082
|
-
return compileIntegerType(driver);
|
|
3083
|
-
case "bigInteger":
|
|
3084
|
-
return compileBigIntegerType(driver);
|
|
3085
|
-
case "timestamp":
|
|
3086
|
-
return compileTimestampType(driver);
|
|
3087
|
-
case "json":
|
|
3088
|
-
return compileJsonType(driver);
|
|
3089
|
-
case "jsonb":
|
|
3090
|
-
return compileJsonbType(driver);
|
|
3091
|
-
default:
|
|
3092
|
-
throw new Error(`Unsupported column kind: ${column.kind}`);
|
|
3115
|
+
resolveName(job) {
|
|
3116
|
+
return this.instances.get(job);
|
|
3093
3117
|
}
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3118
|
+
track(name, job) {
|
|
3119
|
+
this.instances.set(job, name);
|
|
3120
|
+
return job;
|
|
3121
|
+
}
|
|
3122
|
+
create(name) {
|
|
3123
|
+
const factory = this.factories.get(name);
|
|
3124
|
+
if (!factory) {
|
|
3125
|
+
return;
|
|
3126
|
+
}
|
|
3127
|
+
return factory();
|
|
3128
|
+
}
|
|
3129
|
+
names() {
|
|
3130
|
+
return [...this.factories.keys()];
|
|
3103
3131
|
}
|
|
3104
3132
|
}
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3133
|
+
var jobRegistry = new JobRegistry;
|
|
3134
|
+
|
|
3135
|
+
// ../../src/core/queue/jobRunner.ts
|
|
3136
|
+
async function runQueueJob(envelope, failedJobs) {
|
|
3137
|
+
const job = jobRegistry.create(envelope.name);
|
|
3138
|
+
if (!job) {
|
|
3139
|
+
throw new Error(`Unknown job "${envelope.name}".`);
|
|
3140
|
+
}
|
|
3141
|
+
const attempts = envelope.attempts ?? 0;
|
|
3142
|
+
try {
|
|
3143
|
+
await job.handle(envelope.payload);
|
|
3144
|
+
} catch (error) {
|
|
3145
|
+
const nextAttempt = attempts + 1;
|
|
3146
|
+
const maxAttempts = job.maxAttempts ?? queueConfig.maxAttempts;
|
|
3147
|
+
if (nextAttempt < maxAttempts) {
|
|
3148
|
+
const backoffMs = job.backoffMs ?? queueConfig.backoffMs;
|
|
3149
|
+
await new Promise((resolve) => setTimeout(resolve, backoffMs * nextAttempt));
|
|
3150
|
+
await runQueueJob({
|
|
3151
|
+
...envelope,
|
|
3152
|
+
attempts: nextAttempt
|
|
3153
|
+
}, failedJobs);
|
|
3154
|
+
return;
|
|
3155
|
+
}
|
|
3156
|
+
await failedJobs.recordFailure({
|
|
3157
|
+
jobName: envelope.name,
|
|
3158
|
+
payload: envelope.payload,
|
|
3159
|
+
exception: error instanceof Error ? error.stack ?? error.message : String(error)
|
|
3160
|
+
});
|
|
3161
|
+
throw error;
|
|
3113
3162
|
}
|
|
3114
3163
|
}
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3164
|
+
|
|
3165
|
+
// ../../src/core/queue/redisQueue.ts
|
|
3166
|
+
var {RedisClient: RedisClient2 } = globalThis.Bun;
|
|
3167
|
+
var QUEUE_LIST_KEY = "workhub:queue:default";
|
|
3168
|
+
var QUEUE_HIGH_KEY = "workhub:queue:high";
|
|
3169
|
+
var QUEUE_LOW_KEY = "workhub:queue:low";
|
|
3170
|
+
function queueKeyForPriority(priority = "default") {
|
|
3171
|
+
switch (priority) {
|
|
3172
|
+
case "high":
|
|
3173
|
+
return QUEUE_HIGH_KEY;
|
|
3174
|
+
case "low":
|
|
3175
|
+
return QUEUE_LOW_KEY;
|
|
3176
|
+
default:
|
|
3177
|
+
return QUEUE_LIST_KEY;
|
|
3122
3178
|
}
|
|
3123
3179
|
}
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
case "mysql":
|
|
3129
|
-
return "BOOLEAN";
|
|
3130
|
-
case "sqlite":
|
|
3131
|
-
return "INTEGER";
|
|
3180
|
+
class RedisQueue {
|
|
3181
|
+
client;
|
|
3182
|
+
constructor(redisUrl) {
|
|
3183
|
+
this.client = new RedisClient2(redisUrl);
|
|
3132
3184
|
}
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3185
|
+
async dispatch(job, payload) {
|
|
3186
|
+
const name = jobRegistry.resolveName(job);
|
|
3187
|
+
if (!name) {
|
|
3188
|
+
throw new Error("Job is not registered with the queue worker registry.");
|
|
3189
|
+
}
|
|
3190
|
+
const envelope = {
|
|
3191
|
+
name,
|
|
3192
|
+
payload,
|
|
3193
|
+
attempts: 0
|
|
3194
|
+
};
|
|
3195
|
+
const queueKey = queueKeyForPriority(job.priority);
|
|
3196
|
+
await this.client.lpush(queueKey, JSON.stringify(envelope));
|
|
3142
3197
|
}
|
|
3143
3198
|
}
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3199
|
+
|
|
3200
|
+
// ../../src/core/queue/resilientQueue.ts
|
|
3201
|
+
class ResilientQueue {
|
|
3202
|
+
failedJobs;
|
|
3203
|
+
asyncDispatch;
|
|
3204
|
+
constructor(failedJobs, asyncDispatch = false) {
|
|
3205
|
+
this.failedJobs = failedJobs;
|
|
3206
|
+
this.asyncDispatch = asyncDispatch;
|
|
3152
3207
|
}
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3208
|
+
async dispatch(job, payload) {
|
|
3209
|
+
const name = jobRegistry.resolveName(job);
|
|
3210
|
+
if (!name) {
|
|
3211
|
+
throw new Error("Job is not registered with the queue worker registry.");
|
|
3212
|
+
}
|
|
3213
|
+
const envelope = {
|
|
3214
|
+
name,
|
|
3215
|
+
payload,
|
|
3216
|
+
attempts: 0
|
|
3217
|
+
};
|
|
3218
|
+
if (this.asyncDispatch) {
|
|
3219
|
+
setTimeout(() => {
|
|
3220
|
+
runQueueJob(envelope, this.failedJobs).catch((error) => {
|
|
3221
|
+
console.error("[ResilientQueue] Job failed:", error);
|
|
3222
|
+
});
|
|
3223
|
+
}, 0);
|
|
3224
|
+
return;
|
|
3225
|
+
}
|
|
3226
|
+
await runQueueJob(envelope, this.failedJobs);
|
|
3162
3227
|
}
|
|
3163
3228
|
}
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
case "mysql":
|
|
3169
|
-
return "JSON";
|
|
3170
|
-
case "sqlite":
|
|
3171
|
-
return "TEXT";
|
|
3172
|
-
}
|
|
3229
|
+
|
|
3230
|
+
// ../../src/core/queue/publicQueue.ts
|
|
3231
|
+
function createFailedJobService() {
|
|
3232
|
+
return new failedJobService_default(new failedJobRepository_default);
|
|
3173
3233
|
}
|
|
3174
|
-
function
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3234
|
+
function createTrackedJob(name, job) {
|
|
3235
|
+
return jobRegistry.track(name, job);
|
|
3236
|
+
}
|
|
3237
|
+
function createProductionQueue(driver, options = {}) {
|
|
3238
|
+
options.registerJobs?.();
|
|
3239
|
+
const failedJobs = options.failedJobs ?? createFailedJobService();
|
|
3240
|
+
if (driver === "redis") {
|
|
3241
|
+
if (!options.redisUrl) {
|
|
3242
|
+
throw new Error('QUEUE_DRIVER="redis" requires REDIS_URL to be set.');
|
|
3243
|
+
}
|
|
3244
|
+
return new RedisQueue(options.redisUrl);
|
|
3182
3245
|
}
|
|
3246
|
+
return new ResilientQueue(failedJobs, driver === "async");
|
|
3183
3247
|
}
|
|
3184
3248
|
|
|
3185
|
-
// ../../src/core/
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3249
|
+
// ../../src/core/jobs/dispatchWebhookJob.ts
|
|
3250
|
+
import { createHmac as createHmac2 } from "crypto";
|
|
3251
|
+
|
|
3252
|
+
// ../../src/core/security/safeUrl.ts
|
|
3253
|
+
import { lookup as dnsLookupImpl } from "dns/promises";
|
|
3254
|
+
var dnsLookup = dnsLookupImpl;
|
|
3255
|
+
var BLOCKED_HOSTNAMES = new Set([
|
|
3256
|
+
"localhost",
|
|
3257
|
+
"127.0.0.1",
|
|
3258
|
+
"0.0.0.0",
|
|
3259
|
+
"::1",
|
|
3260
|
+
"metadata.google.internal"
|
|
3261
|
+
]);
|
|
3262
|
+
function isPrivateIpv4(hostname) {
|
|
3263
|
+
const match = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(hostname);
|
|
3264
|
+
if (!match) {
|
|
3265
|
+
return false;
|
|
3194
3266
|
}
|
|
3195
|
-
const
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
)`];
|
|
3199
|
-
for (const index of blueprint.indexes) {
|
|
3200
|
-
if (index.kind === "unique" && index.columns.length === 1) {
|
|
3201
|
-
continue;
|
|
3202
|
-
}
|
|
3203
|
-
if (index.kind === "index") {
|
|
3204
|
-
statements.push(compileIndex(driver, blueprint.table, index));
|
|
3205
|
-
} else if (index.kind === "partial" || index.kind === "uniquePartial" || index.kind === "gin" || index.kind === "fullText") {
|
|
3206
|
-
statements.push(...compileSpecialIndex(driver, blueprint.table, index));
|
|
3207
|
-
}
|
|
3267
|
+
const octets = match.slice(1, 5).map((part) => Number.parseInt(part, 10));
|
|
3268
|
+
if (octets.some((octet) => octet < 0 || octet > 255)) {
|
|
3269
|
+
return true;
|
|
3208
3270
|
}
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
const statements = [];
|
|
3213
|
-
const table = quoteIdentifier(blueprint.table);
|
|
3214
|
-
for (const column of blueprint.columns) {
|
|
3215
|
-
const addPrefix = driver === "pgsql" ? "ADD COLUMN IF NOT EXISTS" : "ADD COLUMN";
|
|
3216
|
-
statements.push(`ALTER TABLE ${table}
|
|
3217
|
-
${addPrefix} ${compileColumn(driver, column, "alter")}`);
|
|
3271
|
+
const [a = 0, b = 0] = octets;
|
|
3272
|
+
if (a === 10) {
|
|
3273
|
+
return true;
|
|
3218
3274
|
}
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
statements.push(`ALTER TABLE ${table} ${dropPrefix} ${quoteIdentifier(columnName)}`);
|
|
3275
|
+
if (a === 127) {
|
|
3276
|
+
return true;
|
|
3222
3277
|
}
|
|
3223
|
-
|
|
3224
|
-
|
|
3278
|
+
if (a === 0) {
|
|
3279
|
+
return true;
|
|
3225
3280
|
}
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
statements.push(compileIndex(driver, blueprint.table, index));
|
|
3229
|
-
} else {
|
|
3230
|
-
statements.push(...compileSpecialIndex(driver, blueprint.table, index));
|
|
3231
|
-
}
|
|
3281
|
+
if (a === 169 && b === 254) {
|
|
3282
|
+
return true;
|
|
3232
3283
|
}
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3284
|
+
if (a === 172 && b >= 16 && b <= 31) {
|
|
3285
|
+
return true;
|
|
3286
|
+
}
|
|
3287
|
+
if (a === 192 && b === 168) {
|
|
3288
|
+
return true;
|
|
3289
|
+
}
|
|
3290
|
+
return false;
|
|
3238
3291
|
}
|
|
3239
|
-
function
|
|
3240
|
-
const
|
|
3241
|
-
if (
|
|
3242
|
-
|
|
3292
|
+
function isBlockedHostname(hostname) {
|
|
3293
|
+
const normalized = hostname.trim().toLowerCase();
|
|
3294
|
+
if (normalized.length === 0) {
|
|
3295
|
+
return true;
|
|
3243
3296
|
}
|
|
3244
|
-
if (
|
|
3245
|
-
|
|
3246
|
-
parts.push("PRIMARY KEY AUTOINCREMENT");
|
|
3247
|
-
} else {
|
|
3248
|
-
parts.push("PRIMARY KEY");
|
|
3249
|
-
}
|
|
3250
|
-
} else if (!column.isNullable) {
|
|
3251
|
-
parts.push("NOT NULL");
|
|
3252
|
-
} else if (column.isNullable) {
|
|
3253
|
-
parts.push("NULL");
|
|
3297
|
+
if (BLOCKED_HOSTNAMES.has(normalized)) {
|
|
3298
|
+
return true;
|
|
3254
3299
|
}
|
|
3255
|
-
if (
|
|
3256
|
-
|
|
3300
|
+
if (normalized.endsWith(".local") || normalized.endsWith(".internal")) {
|
|
3301
|
+
return true;
|
|
3257
3302
|
}
|
|
3258
|
-
if (
|
|
3259
|
-
|
|
3303
|
+
if (normalized.includes(":")) {
|
|
3304
|
+
return true;
|
|
3260
3305
|
}
|
|
3261
|
-
|
|
3262
|
-
|
|
3306
|
+
return isPrivateIpv4(normalized);
|
|
3307
|
+
}
|
|
3308
|
+
function assertSafeOutboundUrl(rawUrl, options = {}) {
|
|
3309
|
+
let parsed;
|
|
3310
|
+
try {
|
|
3311
|
+
parsed = new URL(rawUrl);
|
|
3312
|
+
} catch {
|
|
3313
|
+
throw new BadRequestError("Webhook URL is invalid.");
|
|
3314
|
+
}
|
|
3315
|
+
if (parsed.protocol !== "https:" && !(options.allowHttp && parsed.protocol === "http:")) {
|
|
3316
|
+
throw new BadRequestError("Webhook URL must use HTTPS.");
|
|
3317
|
+
}
|
|
3318
|
+
if (parsed.username || parsed.password) {
|
|
3319
|
+
throw new BadRequestError("Webhook URL must not include credentials.");
|
|
3263
3320
|
}
|
|
3264
|
-
if (
|
|
3265
|
-
|
|
3266
|
-
const reference = `${quoteIdentifier(referencesTable)}(${quoteIdentifier(referencesColumn)})`;
|
|
3267
|
-
let clause = `REFERENCES ${reference}`;
|
|
3268
|
-
if (onDelete === "cascade") {
|
|
3269
|
-
clause += " ON DELETE CASCADE";
|
|
3270
|
-
} else if (onDelete === "set null") {
|
|
3271
|
-
clause += " ON DELETE SET NULL";
|
|
3272
|
-
}
|
|
3273
|
-
parts.push(clause);
|
|
3321
|
+
if (isBlockedHostname(parsed.hostname)) {
|
|
3322
|
+
throw new BadRequestError("Webhook URL targets a blocked host.");
|
|
3274
3323
|
}
|
|
3275
|
-
return
|
|
3324
|
+
return parsed;
|
|
3276
3325
|
}
|
|
3277
|
-
function
|
|
3278
|
-
|
|
3279
|
-
const columns = index.columns.map((column) => {
|
|
3280
|
-
const quoted = quoteIdentifier(column);
|
|
3281
|
-
if (index.order === "desc") {
|
|
3282
|
-
return `${quoted} DESC`;
|
|
3283
|
-
}
|
|
3284
|
-
return quoted;
|
|
3285
|
-
}).join(", ");
|
|
3286
|
-
const unique = index.kind === "unique" ? "UNIQUE " : "";
|
|
3287
|
-
return `CREATE ${unique}INDEX IF NOT EXISTS ${quoteIdentifier(indexName)} ON ${quoteIdentifier(tableName)}(${columns})`;
|
|
3326
|
+
function isBlockedIpAddress(address) {
|
|
3327
|
+
return isBlockedHostname(address.trim().toLowerCase());
|
|
3288
3328
|
}
|
|
3289
|
-
function
|
|
3290
|
-
const
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
case "partial":
|
|
3294
|
-
case "uniquePartial": {
|
|
3295
|
-
if (driver !== "pgsql") {
|
|
3296
|
-
throw new UnsupportedSchemaFeatureError("partialIndex()", driver);
|
|
3297
|
-
}
|
|
3298
|
-
const unique = index.kind === "uniquePartial" ? "UNIQUE " : "";
|
|
3299
|
-
return [
|
|
3300
|
-
`CREATE ${unique}INDEX IF NOT EXISTS ${quoteIdentifier(indexName)} ON ${quoteIdentifier(tableName)}(${columns}) WHERE ${index.where}`
|
|
3301
|
-
];
|
|
3302
|
-
}
|
|
3303
|
-
case "gin": {
|
|
3304
|
-
if (driver !== "pgsql") {
|
|
3305
|
-
throw new UnsupportedSchemaFeatureError("ginIndex()", driver);
|
|
3306
|
-
}
|
|
3307
|
-
return [
|
|
3308
|
-
`CREATE INDEX IF NOT EXISTS ${quoteIdentifier(indexName)} ON ${quoteIdentifier(tableName)} USING GIN(${columns})`
|
|
3309
|
-
];
|
|
3310
|
-
}
|
|
3311
|
-
case "fullText": {
|
|
3312
|
-
if (driver === "mysql") {
|
|
3313
|
-
return [
|
|
3314
|
-
`CREATE FULLTEXT INDEX ${quoteIdentifier(indexName)} ON ${quoteIdentifier(tableName)}(${columns})`
|
|
3315
|
-
];
|
|
3316
|
-
}
|
|
3317
|
-
if (driver === "pgsql") {
|
|
3318
|
-
throw new UnsupportedSchemaFeatureError("fullText(); use ginIndex() with a tsvector column on PostgreSQL", driver);
|
|
3319
|
-
}
|
|
3320
|
-
throw new UnsupportedSchemaFeatureError("fullText()", driver);
|
|
3321
|
-
}
|
|
3322
|
-
default:
|
|
3323
|
-
return [];
|
|
3329
|
+
async function assertSafeOutboundUrlResolved(rawUrl, options = {}) {
|
|
3330
|
+
const parsed = assertSafeOutboundUrl(rawUrl, options);
|
|
3331
|
+
if (options.resolveDns === false) {
|
|
3332
|
+
return parsed;
|
|
3324
3333
|
}
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
function compileBlueprint(driver, blueprint) {
|
|
3330
|
-
switch (blueprint.action) {
|
|
3331
|
-
case "create":
|
|
3332
|
-
return compileCreateTable(driver, blueprint);
|
|
3333
|
-
case "alter":
|
|
3334
|
-
return compileAlterTable(driver, blueprint);
|
|
3335
|
-
case "drop":
|
|
3336
|
-
return compileDropTable(driver, blueprint.table);
|
|
3337
|
-
default:
|
|
3338
|
-
throw new Error(`Unsupported blueprint action: ${blueprint.action}`);
|
|
3334
|
+
const hostname = parsed.hostname.trim().toLowerCase();
|
|
3335
|
+
const results = await dnsLookup(hostname, { all: true, verbatim: true });
|
|
3336
|
+
if (results.some((result) => isBlockedIpAddress(result.address))) {
|
|
3337
|
+
throw new BadRequestError("Webhook URL targets a blocked host.");
|
|
3339
3338
|
}
|
|
3339
|
+
return parsed;
|
|
3340
3340
|
}
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3341
|
+
|
|
3342
|
+
// ../../src/core/security/safeFetch.ts
|
|
3343
|
+
var DEFAULT_FETCH_TIMEOUT_MS = 1e4;
|
|
3344
|
+
async function safeFetch(input, init = {}, options = {}) {
|
|
3345
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;
|
|
3346
|
+
const maxRedirects = options.maxRedirects ?? 0;
|
|
3347
|
+
const resolveDns = options.resolveDns ?? appConfig.env === "production";
|
|
3348
|
+
const urlOptions = { allowHttp: options.allowHttp, resolveDns };
|
|
3349
|
+
const controller = new AbortController;
|
|
3350
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
3351
|
+
try {
|
|
3352
|
+
let currentUrl = (await assertSafeOutboundUrlResolved(input, urlOptions)).toString();
|
|
3353
|
+
let redirectCount = 0;
|
|
3354
|
+
while (true) {
|
|
3355
|
+
const response = await fetch(currentUrl, {
|
|
3356
|
+
...init,
|
|
3357
|
+
signal: controller.signal,
|
|
3358
|
+
redirect: "manual"
|
|
3359
|
+
});
|
|
3360
|
+
if (response.status >= 300 && response.status < 400) {
|
|
3361
|
+
const location = response.headers.get("location");
|
|
3362
|
+
if (!location || redirectCount >= maxRedirects) {
|
|
3363
|
+
return response;
|
|
3364
|
+
}
|
|
3365
|
+
currentUrl = (await assertSafeOutboundUrlResolved(new URL(location, currentUrl).toString(), urlOptions)).toString();
|
|
3366
|
+
redirectCount += 1;
|
|
3367
|
+
continue;
|
|
3368
|
+
}
|
|
3369
|
+
return response;
|
|
3347
3370
|
}
|
|
3348
|
-
}
|
|
3371
|
+
} finally {
|
|
3372
|
+
clearTimeout(timeout);
|
|
3373
|
+
}
|
|
3349
3374
|
}
|
|
3350
3375
|
|
|
3351
|
-
// ../../src/core/
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
return
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3376
|
+
// ../../src/core/jobs/dispatchWebhookJob.ts
|
|
3377
|
+
class DispatchWebhookJob extends Job {
|
|
3378
|
+
maxAttempts = 3;
|
|
3379
|
+
backoffMs = 2000;
|
|
3380
|
+
async handle(payload) {
|
|
3381
|
+
const rows = await repositoryConnection`
|
|
3382
|
+
SELECT id, url, secret
|
|
3383
|
+
FROM webhook
|
|
3384
|
+
WHERE id = ${payload.webhookId} AND active = TRUE
|
|
3385
|
+
LIMIT 1
|
|
3386
|
+
`;
|
|
3387
|
+
const webhook = rows[0];
|
|
3388
|
+
if (!webhook) {
|
|
3389
|
+
return;
|
|
3390
|
+
}
|
|
3391
|
+
const body = JSON.stringify({ event: payload.event, payload: payload.payload });
|
|
3392
|
+
const signature = createHmac2("sha256", webhook.secret).update(body).digest("hex");
|
|
3393
|
+
assertSafeOutboundUrl(webhook.url, { allowHttp: appConfig.env !== "production" });
|
|
3394
|
+
let responseStatus = null;
|
|
3395
|
+
let errorMessage = null;
|
|
3396
|
+
try {
|
|
3397
|
+
const response = await safeFetch(webhook.url, {
|
|
3398
|
+
method: "POST",
|
|
3399
|
+
headers: {
|
|
3400
|
+
"content-type": "application/json",
|
|
3401
|
+
"x-workhub-signature": signature
|
|
3402
|
+
},
|
|
3403
|
+
body
|
|
3404
|
+
}, { allowHttp: appConfig.env !== "production" });
|
|
3405
|
+
responseStatus = response.status;
|
|
3406
|
+
if (!response.ok) {
|
|
3407
|
+
throw new Error(`Webhook delivery failed with status ${response.status}.`);
|
|
3408
|
+
}
|
|
3409
|
+
} catch (error) {
|
|
3410
|
+
errorMessage = error instanceof Error ? error.message : String(error);
|
|
3411
|
+
await repositoryConnection`
|
|
3412
|
+
INSERT INTO webhook_delivery (webhook_id, event, payload, response_status, error)
|
|
3413
|
+
VALUES (
|
|
3414
|
+
${webhook.id},
|
|
3415
|
+
${payload.event},
|
|
3416
|
+
${JSON.stringify(payload.payload)}::jsonb,
|
|
3417
|
+
${responseStatus},
|
|
3418
|
+
${errorMessage}
|
|
3419
|
+
)
|
|
3420
|
+
`;
|
|
3421
|
+
throw error instanceof Error ? error : new Error(errorMessage);
|
|
3422
|
+
}
|
|
3423
|
+
await repositoryConnection`
|
|
3424
|
+
INSERT INTO webhook_delivery (webhook_id, event, payload, response_status)
|
|
3425
|
+
VALUES (
|
|
3426
|
+
${webhook.id},
|
|
3427
|
+
${payload.event},
|
|
3428
|
+
${JSON.stringify(payload.payload)}::jsonb,
|
|
3429
|
+
${responseStatus}
|
|
3430
|
+
)
|
|
3431
|
+
`;
|
|
3371
3432
|
}
|
|
3372
3433
|
}
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
create(table, callback) {
|
|
3381
|
-
const blueprint = new Blueprint(table, "create");
|
|
3382
|
-
callback(blueprint);
|
|
3383
|
-
this.#statements.push(...grammarForDriver(this.#driver).compile(blueprint));
|
|
3384
|
-
return this;
|
|
3434
|
+
var dispatchWebhookJob_default = DispatchWebhookJob;
|
|
3435
|
+
|
|
3436
|
+
// ../../src/core/logging/logger.ts
|
|
3437
|
+
class Logger {
|
|
3438
|
+
channel;
|
|
3439
|
+
constructor(channel = "app") {
|
|
3440
|
+
this.channel = channel;
|
|
3385
3441
|
}
|
|
3386
|
-
|
|
3387
|
-
const
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3442
|
+
write(level, message, context = {}) {
|
|
3443
|
+
const entry = {
|
|
3444
|
+
level,
|
|
3445
|
+
channel: this.channel,
|
|
3446
|
+
message,
|
|
3447
|
+
timestamp: new Date().toISOString(),
|
|
3448
|
+
...context
|
|
3449
|
+
};
|
|
3450
|
+
const line = JSON.stringify(entry);
|
|
3451
|
+
if (level === "error") {
|
|
3452
|
+
console.error(line);
|
|
3453
|
+
return;
|
|
3454
|
+
}
|
|
3455
|
+
console.log(line);
|
|
3391
3456
|
}
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
this.#statements.push(...grammarForDriver(this.#driver).compile(blueprint));
|
|
3395
|
-
return this;
|
|
3457
|
+
debug(message, context) {
|
|
3458
|
+
this.write("debug", message, context);
|
|
3396
3459
|
}
|
|
3397
|
-
|
|
3398
|
-
|
|
3460
|
+
info(message, context) {
|
|
3461
|
+
this.write("info", message, context);
|
|
3399
3462
|
}
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
await db2.unsafe(statement);
|
|
3403
|
-
}
|
|
3463
|
+
warn(message, context) {
|
|
3464
|
+
this.write("warn", message, context);
|
|
3404
3465
|
}
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
function defineTable5(definition) {
|
|
3408
|
-
return definition;
|
|
3409
|
-
}
|
|
3410
|
-
// ../../src/core/queue/failedJobTable.ts
|
|
3411
|
-
var failedJobTable = defineTable5({
|
|
3412
|
-
name: "failed_job",
|
|
3413
|
-
primaryKey: "id",
|
|
3414
|
-
columns: ["id", "job_name", "payload", "exception", "failed_at"],
|
|
3415
|
-
defaultOrderBy: { column: "failed_at", direction: "DESC" }
|
|
3416
|
-
});
|
|
3417
|
-
|
|
3418
|
-
// ../../src/core/queue/failedJobRepository.ts
|
|
3419
|
-
class FailedJobRepository extends baseRepository_default {
|
|
3420
|
-
constructor() {
|
|
3421
|
-
super(failedJobTable);
|
|
3466
|
+
error(message, context) {
|
|
3467
|
+
this.write("error", message, context);
|
|
3422
3468
|
}
|
|
3423
3469
|
}
|
|
3424
|
-
var
|
|
3470
|
+
var appLogger = new Logger("app");
|
|
3425
3471
|
|
|
3426
|
-
// ../../src/
|
|
3427
|
-
class
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
exception: input.exception,
|
|
3437
|
-
failed_at: new Date
|
|
3438
|
-
});
|
|
3472
|
+
// ../../src/bootstrap/contracts.ts
|
|
3473
|
+
class ServiceContainer {
|
|
3474
|
+
services = new Map;
|
|
3475
|
+
singletonFactories = new Map;
|
|
3476
|
+
bindings = new Map;
|
|
3477
|
+
set(key, value) {
|
|
3478
|
+
this.singletonFactories.delete(key);
|
|
3479
|
+
this.bindings.delete(key);
|
|
3480
|
+
this.services.set(key, value);
|
|
3481
|
+
return value;
|
|
3439
3482
|
}
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
});
|
|
3483
|
+
singleton(key, factory) {
|
|
3484
|
+
this.bindings.delete(key);
|
|
3485
|
+
this.services.delete(key);
|
|
3486
|
+
this.singletonFactories.set(key, factory);
|
|
3445
3487
|
}
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3488
|
+
bind(key, factory) {
|
|
3489
|
+
this.singletonFactories.delete(key);
|
|
3490
|
+
this.services.delete(key);
|
|
3491
|
+
this.bindings.set(key, factory);
|
|
3450
3492
|
}
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
throw new Error(`Failed job ${id} not found.`);
|
|
3493
|
+
get(key) {
|
|
3494
|
+
if (this.services.has(key)) {
|
|
3495
|
+
return this.services.get(key);
|
|
3455
3496
|
}
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
if (await this.repository.deleteById(job.id)) {
|
|
3462
|
-
deleted += 1;
|
|
3463
|
-
}
|
|
3497
|
+
const singletonFactory = this.singletonFactories.get(key);
|
|
3498
|
+
if (singletonFactory) {
|
|
3499
|
+
const value = singletonFactory(this);
|
|
3500
|
+
this.services.set(key, value);
|
|
3501
|
+
return value;
|
|
3464
3502
|
}
|
|
3465
|
-
|
|
3503
|
+
const binding = this.bindings.get(key);
|
|
3504
|
+
if (binding) {
|
|
3505
|
+
return binding(this);
|
|
3506
|
+
}
|
|
3507
|
+
throw new Error(`Service "${key}" is not registered.`);
|
|
3466
3508
|
}
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
// ../../src/core/queue/jobRunner.ts
|
|
3471
|
-
async function runQueueJob(envelope, failedJobs) {
|
|
3472
|
-
const job = jobRegistry.create(envelope.name);
|
|
3473
|
-
if (!job) {
|
|
3474
|
-
throw new Error(`Unknown job "${envelope.name}".`);
|
|
3509
|
+
resolve(key) {
|
|
3510
|
+
return this.get(key);
|
|
3475
3511
|
}
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
await job.handle(envelope.payload);
|
|
3479
|
-
} catch (error) {
|
|
3480
|
-
const nextAttempt = attempts + 1;
|
|
3481
|
-
const maxAttempts = job.maxAttempts ?? queueConfig.maxAttempts;
|
|
3482
|
-
if (nextAttempt < maxAttempts) {
|
|
3483
|
-
const backoffMs = job.backoffMs ?? queueConfig.backoffMs;
|
|
3484
|
-
await new Promise((resolve) => setTimeout(resolve, backoffMs * nextAttempt));
|
|
3485
|
-
await runQueueJob({
|
|
3486
|
-
...envelope,
|
|
3487
|
-
attempts: nextAttempt
|
|
3488
|
-
}, failedJobs);
|
|
3489
|
-
return;
|
|
3490
|
-
}
|
|
3491
|
-
await failedJobs.recordFailure({
|
|
3492
|
-
jobName: envelope.name,
|
|
3493
|
-
payload: envelope.payload,
|
|
3494
|
-
exception: error instanceof Error ? error.stack ?? error.message : String(error)
|
|
3495
|
-
});
|
|
3496
|
-
throw error;
|
|
3512
|
+
has(key) {
|
|
3513
|
+
return this.services.has(key) || this.singletonFactories.has(key) || this.bindings.has(key);
|
|
3497
3514
|
}
|
|
3498
3515
|
}
|
|
3499
3516
|
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
function queueKeyForPriority(priority = "default") {
|
|
3506
|
-
switch (priority) {
|
|
3507
|
-
case "high":
|
|
3508
|
-
return QUEUE_HIGH_KEY;
|
|
3509
|
-
case "low":
|
|
3510
|
-
return QUEUE_LOW_KEY;
|
|
3511
|
-
default:
|
|
3512
|
-
return QUEUE_LIST_KEY;
|
|
3517
|
+
class ConfigStore {
|
|
3518
|
+
values = new Map;
|
|
3519
|
+
set(key, value) {
|
|
3520
|
+
this.values.set(key, value);
|
|
3521
|
+
return value;
|
|
3513
3522
|
}
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
client;
|
|
3517
|
-
constructor(redisUrl) {
|
|
3518
|
-
this.client = new RedisClient2(redisUrl);
|
|
3523
|
+
get(key) {
|
|
3524
|
+
return this.values.get(key);
|
|
3519
3525
|
}
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
throw new Error("Job is not registered with the queue worker registry.");
|
|
3526
|
+
require(key) {
|
|
3527
|
+
if (!this.values.has(key)) {
|
|
3528
|
+
throw new Error(`Config key "${key}" is not defined.`);
|
|
3524
3529
|
}
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
};
|
|
3530
|
-
const queueKey = queueKeyForPriority(job.priority);
|
|
3531
|
-
await this.client.lpush(queueKey, JSON.stringify(envelope));
|
|
3530
|
+
return this.values.get(key);
|
|
3531
|
+
}
|
|
3532
|
+
has(key) {
|
|
3533
|
+
return this.values.has(key);
|
|
3532
3534
|
}
|
|
3533
3535
|
}
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3536
|
+
var requiredDependencyKeys = [
|
|
3537
|
+
"container",
|
|
3538
|
+
"cache",
|
|
3539
|
+
"storage"
|
|
3540
|
+
];
|
|
3541
|
+
function getRequiredDependency(dependencies, key) {
|
|
3542
|
+
const dependency = dependencies[key];
|
|
3543
|
+
if (dependency === undefined) {
|
|
3544
|
+
throw new Error(`Required dependency "${key}" is not registered.`);
|
|
3542
3545
|
}
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
const envelope = {
|
|
3549
|
-
name,
|
|
3550
|
-
payload,
|
|
3551
|
-
attempts: 0
|
|
3552
|
-
};
|
|
3553
|
-
if (this.asyncDispatch) {
|
|
3554
|
-
setTimeout(() => {
|
|
3555
|
-
runQueueJob(envelope, this.failedJobs).catch((error) => {
|
|
3556
|
-
console.error("[ResilientQueue] Job failed:", error);
|
|
3557
|
-
});
|
|
3558
|
-
}, 0);
|
|
3559
|
-
return;
|
|
3560
|
-
}
|
|
3561
|
-
await runQueueJob(envelope, this.failedJobs);
|
|
3546
|
+
return dependency;
|
|
3547
|
+
}
|
|
3548
|
+
function assertAppDependenciesComplete(dependencies) {
|
|
3549
|
+
for (const key of requiredDependencyKeys) {
|
|
3550
|
+
getRequiredDependency(dependencies, key);
|
|
3562
3551
|
}
|
|
3563
3552
|
}
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
function createFailedJobService() {
|
|
3567
|
-
return new failedJobService_default(new failedJobRepository_default);
|
|
3553
|
+
function resolveService(dependencies, token) {
|
|
3554
|
+
return dependencies.container.resolve(token);
|
|
3568
3555
|
}
|
|
3569
|
-
|
|
3570
|
-
|
|
3556
|
+
|
|
3557
|
+
// ../../src/bootstrap/applicationRegistry.ts
|
|
3558
|
+
var activeContext;
|
|
3559
|
+
function setActiveApplicationContext(context) {
|
|
3560
|
+
activeContext = context;
|
|
3571
3561
|
}
|
|
3572
|
-
function
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
if (driver === "redis") {
|
|
3576
|
-
if (!options.redisUrl) {
|
|
3577
|
-
throw new Error('QUEUE_DRIVER="redis" requires REDIS_URL to be set.');
|
|
3578
|
-
}
|
|
3579
|
-
return new RedisQueue(options.redisUrl);
|
|
3562
|
+
function requireActiveApplicationContext() {
|
|
3563
|
+
if (!activeContext) {
|
|
3564
|
+
throw new Error("The application context has not been bootstrapped.");
|
|
3580
3565
|
}
|
|
3581
|
-
return
|
|
3566
|
+
return activeContext;
|
|
3567
|
+
}
|
|
3568
|
+
function resolveApplicationCache() {
|
|
3569
|
+
return getRequiredDependency(requireActiveApplicationContext().dependencies, "cache");
|
|
3570
|
+
}
|
|
3571
|
+
function resolveApplicationQueue() {
|
|
3572
|
+
return requireActiveApplicationContext().container.resolve(CORE_QUEUE_TOKEN);
|
|
3573
|
+
}
|
|
3574
|
+
function resolveApplicationAuth() {
|
|
3575
|
+
return requireActiveApplicationContext().container.resolve(CORE_AUTH_TOKEN);
|
|
3576
|
+
}
|
|
3577
|
+
function resolveApplicationPolicyGate() {
|
|
3578
|
+
return requireActiveApplicationContext().container.resolve(CORE_POLICY_GATE_TOKEN);
|
|
3579
|
+
}
|
|
3580
|
+
function resolveApplicationConfig() {
|
|
3581
|
+
return requireActiveApplicationContext().config;
|
|
3582
|
+
}
|
|
3583
|
+
function resolveApplicationLogger() {
|
|
3584
|
+
return appLogger;
|
|
3585
|
+
}
|
|
3586
|
+
function resolveApplicationDependencies() {
|
|
3587
|
+
return requireActiveApplicationContext().dependencies;
|
|
3582
3588
|
}
|
|
3583
3589
|
|
|
3584
|
-
// ../../src/
|
|
3585
|
-
var FAILED_JOB_SERVICE_TOKEN = "core.failedJobs";
|
|
3590
|
+
// ../../src/bootstrap/queue/defaultJobs.ts
|
|
3586
3591
|
function registerDefaultJobs() {
|
|
3587
3592
|
jobRegistry.register("cache.invalidate-tags", () => {
|
|
3588
3593
|
return new invalidateCacheTagsJob_default(resolveApplicationCache());
|
|
3589
3594
|
});
|
|
3590
3595
|
jobRegistry.register("webhook.dispatch", () => new dispatchWebhookJob_default);
|
|
3591
3596
|
}
|
|
3592
|
-
|
|
3597
|
+
|
|
3598
|
+
// ../../src/core/queue/createAppQueue.ts
|
|
3599
|
+
var FAILED_JOB_SERVICE_TOKEN = "core.failedJobs";
|
|
3600
|
+
function createAppQueue(driver, redisUrl, failedJobs = createFailedJobService(), registerJobs) {
|
|
3593
3601
|
return createProductionQueue(driver, {
|
|
3594
3602
|
redisUrl,
|
|
3595
3603
|
failedJobs,
|
|
3596
|
-
registerJobs
|
|
3604
|
+
registerJobs
|
|
3597
3605
|
});
|
|
3598
3606
|
}
|
|
3599
3607
|
|
|
@@ -3701,7 +3709,7 @@ var queueProvider = {
|
|
|
3701
3709
|
config.set("queue.driver", driver);
|
|
3702
3710
|
const failedJobs = createFailedJobService();
|
|
3703
3711
|
container.set(FAILED_JOB_SERVICE_TOKEN, failedJobs);
|
|
3704
|
-
container.set(CORE_QUEUE_TOKEN, createAppQueue(driver, config.get(REDIS_URL_CONFIG_KEY) ?? process.env.REDIS_URL, failedJobs));
|
|
3712
|
+
container.set(CORE_QUEUE_TOKEN, createAppQueue(driver, config.get(REDIS_URL_CONFIG_KEY) ?? process.env.REDIS_URL, failedJobs, registerDefaultJobs));
|
|
3705
3713
|
}
|
|
3706
3714
|
};
|
|
3707
3715
|
var queue_default = queueProvider;
|