@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
package/dist/entries/context.js
CHANGED
|
@@ -306,6 +306,7 @@ var db = new Proxy(function database() {}, {
|
|
|
306
306
|
return typeof value === "function" ? value.bind(connection) : value;
|
|
307
307
|
}
|
|
308
308
|
});
|
|
309
|
+
var connection_default = db;
|
|
309
310
|
|
|
310
311
|
// ../../src/modules/user/apiTokenTable.ts
|
|
311
312
|
import { defineTable } from "@getstrata/core/database";
|
|
@@ -527,6 +528,11 @@ class UnauthorizedError2 extends HttpError {
|
|
|
527
528
|
super(401, message, details);
|
|
528
529
|
}
|
|
529
530
|
}
|
|
531
|
+
class PreconditionFailedError extends HttpError {
|
|
532
|
+
constructor(message = "Precondition Failed", details) {
|
|
533
|
+
super(412, message, details);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
530
536
|
|
|
531
537
|
// ../../src/core/auth/authContext.ts
|
|
532
538
|
import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
|
|
@@ -1436,590 +1442,283 @@ class InvalidateCacheTagsJob extends Job {
|
|
|
1436
1442
|
}
|
|
1437
1443
|
var invalidateCacheTagsJob_default = InvalidateCacheTagsJob;
|
|
1438
1444
|
|
|
1439
|
-
// ../../src/core/
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1445
|
+
// ../../src/core/pagination/index.ts
|
|
1446
|
+
function buildPaginationMeta(input) {
|
|
1447
|
+
const lastPage = Math.max(1, Math.ceil(input.total / input.perPage));
|
|
1448
|
+
return {
|
|
1449
|
+
page: input.page,
|
|
1450
|
+
per_page: input.perPage,
|
|
1451
|
+
total: input.total,
|
|
1452
|
+
last_page: lastPage
|
|
1453
|
+
};
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
// ../../src/core/database/errors.ts
|
|
1457
|
+
function isPostgresError(error) {
|
|
1458
|
+
return typeof error === "object" && error !== null && (("errno" in error) || ("code" in error));
|
|
1459
|
+
}
|
|
1460
|
+
function getPostgresSqlState(error) {
|
|
1461
|
+
if (typeof error.errno === "string" && /^\d{5}$/.test(error.errno)) {
|
|
1462
|
+
return error.errno;
|
|
1444
1463
|
}
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
level,
|
|
1448
|
-
channel: this.channel,
|
|
1449
|
-
message,
|
|
1450
|
-
timestamp: new Date().toISOString(),
|
|
1451
|
-
...context
|
|
1452
|
-
};
|
|
1453
|
-
const line = JSON.stringify(entry);
|
|
1454
|
-
if (level === "error") {
|
|
1455
|
-
console.error(line);
|
|
1456
|
-
return;
|
|
1457
|
-
}
|
|
1458
|
-
console.log(line);
|
|
1464
|
+
if (typeof error.errno === "number") {
|
|
1465
|
+
return String(error.errno).padStart(5, "0");
|
|
1459
1466
|
}
|
|
1460
|
-
|
|
1461
|
-
|
|
1467
|
+
if (typeof error.code === "string" && /^\d{5}$/.test(error.code)) {
|
|
1468
|
+
return error.code;
|
|
1462
1469
|
}
|
|
1463
|
-
|
|
1464
|
-
|
|
1470
|
+
return;
|
|
1471
|
+
}
|
|
1472
|
+
function mapDatabaseError(error) {
|
|
1473
|
+
if (error instanceof HttpError) {
|
|
1474
|
+
return error;
|
|
1465
1475
|
}
|
|
1466
|
-
|
|
1467
|
-
|
|
1476
|
+
if (!isPostgresError(error)) {
|
|
1477
|
+
const message = error instanceof Error ? error.message : "Database operation failed.";
|
|
1478
|
+
return new BadRequestError(message);
|
|
1468
1479
|
}
|
|
1469
|
-
error
|
|
1470
|
-
|
|
1480
|
+
const sqlState = getPostgresSqlState(error);
|
|
1481
|
+
switch (sqlState) {
|
|
1482
|
+
case "23505":
|
|
1483
|
+
return new ConflictError(error.detail ?? "A record with these values already exists.", {
|
|
1484
|
+
constraint: error.constraint
|
|
1485
|
+
});
|
|
1486
|
+
case "23503":
|
|
1487
|
+
return new UnprocessableEntityError(error.detail ?? "Referenced record does not exist.", {
|
|
1488
|
+
constraint: error.constraint
|
|
1489
|
+
});
|
|
1490
|
+
case "23502":
|
|
1491
|
+
return new BadRequestError(error.detail ?? "Required field is missing.", {
|
|
1492
|
+
constraint: error.constraint
|
|
1493
|
+
});
|
|
1494
|
+
case "23514":
|
|
1495
|
+
return new BadRequestError(error.detail ?? "Value violates a database constraint.", {
|
|
1496
|
+
constraint: error.constraint
|
|
1497
|
+
});
|
|
1498
|
+
default:
|
|
1499
|
+
return new BadRequestError(error.message ?? "Database operation failed.", {
|
|
1500
|
+
code: error.code,
|
|
1501
|
+
sqlState
|
|
1502
|
+
});
|
|
1471
1503
|
}
|
|
1472
1504
|
}
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
activeContext = context;
|
|
1479
|
-
}
|
|
1480
|
-
function requireActiveApplicationContext() {
|
|
1481
|
-
if (!activeContext) {
|
|
1482
|
-
throw new Error("The application context has not been bootstrapped.");
|
|
1505
|
+
async function withDatabaseErrorHandling(operation) {
|
|
1506
|
+
try {
|
|
1507
|
+
return await operation();
|
|
1508
|
+
} catch (error) {
|
|
1509
|
+
throw mapDatabaseError(error);
|
|
1483
1510
|
}
|
|
1484
|
-
return activeContext;
|
|
1485
|
-
}
|
|
1486
|
-
function resolveApplicationCache() {
|
|
1487
|
-
return getRequiredDependency(requireActiveApplicationContext().dependencies, "cache");
|
|
1488
|
-
}
|
|
1489
|
-
function resolveApplicationQueue() {
|
|
1490
|
-
return requireActiveApplicationContext().container.resolve(CORE_QUEUE_TOKEN);
|
|
1491
1511
|
}
|
|
1492
|
-
|
|
1493
|
-
|
|
1512
|
+
|
|
1513
|
+
// ../../src/core/database/query.ts
|
|
1514
|
+
function quoteIdentifier(identifier) {
|
|
1515
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(identifier)) {
|
|
1516
|
+
throw new Error(`Invalid SQL identifier: ${identifier}`);
|
|
1517
|
+
}
|
|
1518
|
+
return `"${identifier}"`;
|
|
1494
1519
|
}
|
|
1495
|
-
function
|
|
1496
|
-
return
|
|
1520
|
+
function qualifyColumn(tableName, column) {
|
|
1521
|
+
return `${quoteIdentifier(tableName)}.${quoteIdentifier(column)}`;
|
|
1497
1522
|
}
|
|
1498
|
-
function
|
|
1499
|
-
|
|
1523
|
+
function resolveQualifiedColumn(defaultTable, columnName) {
|
|
1524
|
+
if (columnName.includes(".")) {
|
|
1525
|
+
const [table, column] = columnName.split(".", 2);
|
|
1526
|
+
if (!table || !column) {
|
|
1527
|
+
throw new Error(`Invalid qualified column: ${columnName}`);
|
|
1528
|
+
}
|
|
1529
|
+
return qualifyColumn(table, column);
|
|
1530
|
+
}
|
|
1531
|
+
return qualifyColumn(defaultTable, columnName);
|
|
1500
1532
|
}
|
|
1501
|
-
function
|
|
1502
|
-
|
|
1533
|
+
function parseQualifiedColumn(reference) {
|
|
1534
|
+
const [table, column] = reference.split(".", 2);
|
|
1535
|
+
if (!table || !column) {
|
|
1536
|
+
throw new Error(`Join columns must be qualified as table.column: ${reference}`);
|
|
1537
|
+
}
|
|
1538
|
+
return { table, column };
|
|
1503
1539
|
}
|
|
1504
|
-
function
|
|
1505
|
-
return
|
|
1540
|
+
function normalizeDirection(direction = "ASC") {
|
|
1541
|
+
return direction.toUpperCase() === "DESC" ? "DESC" : "ASC";
|
|
1506
1542
|
}
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
import { createHmac as createHmac2 } from "crypto";
|
|
1510
|
-
|
|
1511
|
-
// ../../src/core/database/boundConnection.ts
|
|
1512
|
-
var boundConnectionHolder = {
|
|
1513
|
-
connection: null
|
|
1514
|
-
};
|
|
1515
|
-
function getBoundDatabaseConnection() {
|
|
1516
|
-
return boundConnectionHolder.connection;
|
|
1543
|
+
function isQueryOperator(value) {
|
|
1544
|
+
return value !== null && !Array.isArray(value) && !(value instanceof Date) && typeof value === "object";
|
|
1517
1545
|
}
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
return getBoundDatabaseConnection() ?? getDefaultDatabaseQuery();
|
|
1546
|
+
function pushParam(values, value) {
|
|
1547
|
+
values.push(value);
|
|
1548
|
+
return `$${values.length}`;
|
|
1522
1549
|
}
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
return
|
|
1526
|
-
},
|
|
1527
|
-
get(_target, property) {
|
|
1528
|
-
const connection = resolveRepositoryConnection();
|
|
1529
|
-
const value = connection[property];
|
|
1530
|
-
return typeof value === "function" ? value.bind(connection) : value;
|
|
1531
|
-
}
|
|
1532
|
-
});
|
|
1533
|
-
|
|
1534
|
-
// ../../src/core/security/safeUrl.ts
|
|
1535
|
-
import { lookup as dnsLookupImpl } from "dns/promises";
|
|
1536
|
-
var dnsLookup = dnsLookupImpl;
|
|
1537
|
-
var BLOCKED_HOSTNAMES = new Set([
|
|
1538
|
-
"localhost",
|
|
1539
|
-
"127.0.0.1",
|
|
1540
|
-
"0.0.0.0",
|
|
1541
|
-
"::1",
|
|
1542
|
-
"metadata.google.internal"
|
|
1543
|
-
]);
|
|
1544
|
-
function isPrivateIpv4(hostname) {
|
|
1545
|
-
const match = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(hostname);
|
|
1546
|
-
if (!match) {
|
|
1547
|
-
return false;
|
|
1548
|
-
}
|
|
1549
|
-
const octets = match.slice(1, 5).map((part) => Number.parseInt(part, 10));
|
|
1550
|
-
if (octets.some((octet) => octet < 0 || octet > 255)) {
|
|
1551
|
-
return true;
|
|
1550
|
+
function buildInClause(column, values, params) {
|
|
1551
|
+
if (values.length === 0) {
|
|
1552
|
+
return "1 = 0";
|
|
1552
1553
|
}
|
|
1553
|
-
const
|
|
1554
|
-
|
|
1555
|
-
|
|
1554
|
+
const placeholders = values.map((value) => pushParam(params, value)).join(", ");
|
|
1555
|
+
return `${column} IN (${placeholders})`;
|
|
1556
|
+
}
|
|
1557
|
+
function buildOperatorClauses(column, operator, params) {
|
|
1558
|
+
const clauses = [];
|
|
1559
|
+
if (operator.isNull === true) {
|
|
1560
|
+
clauses.push(`${column} IS NULL`);
|
|
1556
1561
|
}
|
|
1557
|
-
if (
|
|
1558
|
-
|
|
1562
|
+
if (operator.isNull === false) {
|
|
1563
|
+
clauses.push(`${column} IS NOT NULL`);
|
|
1559
1564
|
}
|
|
1560
|
-
if (
|
|
1561
|
-
|
|
1565
|
+
if (operator.eq !== undefined) {
|
|
1566
|
+
if (operator.eq === null) {
|
|
1567
|
+
clauses.push(`${column} IS NULL`);
|
|
1568
|
+
} else {
|
|
1569
|
+
clauses.push(`${column} = ${pushParam(params, operator.eq)}`);
|
|
1570
|
+
}
|
|
1562
1571
|
}
|
|
1563
|
-
if (
|
|
1564
|
-
|
|
1572
|
+
if (operator.in !== undefined) {
|
|
1573
|
+
clauses.push(buildInClause(column, operator.in, params));
|
|
1565
1574
|
}
|
|
1566
|
-
if (
|
|
1567
|
-
|
|
1575
|
+
if (operator.gt !== undefined) {
|
|
1576
|
+
clauses.push(`${column} > ${pushParam(params, operator.gt)}`);
|
|
1568
1577
|
}
|
|
1569
|
-
if (
|
|
1570
|
-
|
|
1578
|
+
if (operator.gte !== undefined) {
|
|
1579
|
+
clauses.push(`${column} >= ${pushParam(params, operator.gte)}`);
|
|
1571
1580
|
}
|
|
1572
|
-
|
|
1573
|
-
}
|
|
1574
|
-
function isBlockedHostname(hostname) {
|
|
1575
|
-
const normalized = hostname.trim().toLowerCase();
|
|
1576
|
-
if (normalized.length === 0) {
|
|
1577
|
-
return true;
|
|
1581
|
+
if (operator.lt !== undefined) {
|
|
1582
|
+
clauses.push(`${column} < ${pushParam(params, operator.lt)}`);
|
|
1578
1583
|
}
|
|
1579
|
-
if (
|
|
1580
|
-
|
|
1584
|
+
if (operator.lte !== undefined) {
|
|
1585
|
+
clauses.push(`${column} <= ${pushParam(params, operator.lte)}`);
|
|
1581
1586
|
}
|
|
1582
|
-
if (
|
|
1583
|
-
|
|
1587
|
+
if (operator.ilike !== undefined) {
|
|
1588
|
+
clauses.push(`${column} ILIKE ${pushParam(params, `%${operator.ilike}%`)}`);
|
|
1584
1589
|
}
|
|
1585
|
-
if (
|
|
1586
|
-
|
|
1590
|
+
if (operator.tsMatch !== undefined) {
|
|
1591
|
+
clauses.push(`${column} @@ plainto_tsquery('english', ${pushParam(params, operator.tsMatch)})`);
|
|
1587
1592
|
}
|
|
1588
|
-
return
|
|
1593
|
+
return clauses;
|
|
1589
1594
|
}
|
|
1590
|
-
function
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1595
|
+
function appendWhereParts(tableName, where, params) {
|
|
1596
|
+
const clauses = [];
|
|
1597
|
+
for (const [columnName, filterValue] of Object.entries(where)) {
|
|
1598
|
+
if (filterValue === undefined) {
|
|
1599
|
+
continue;
|
|
1600
|
+
}
|
|
1601
|
+
const column = resolveQualifiedColumn(tableName, columnName);
|
|
1602
|
+
if (Array.isArray(filterValue)) {
|
|
1603
|
+
clauses.push(buildInClause(column, filterValue, params));
|
|
1604
|
+
continue;
|
|
1605
|
+
}
|
|
1606
|
+
if (isQueryOperator(filterValue)) {
|
|
1607
|
+
clauses.push(...buildOperatorClauses(column, filterValue, params));
|
|
1608
|
+
continue;
|
|
1609
|
+
}
|
|
1610
|
+
if (filterValue === null) {
|
|
1611
|
+
clauses.push(`${column} IS NULL`);
|
|
1612
|
+
continue;
|
|
1613
|
+
}
|
|
1614
|
+
clauses.push(`${column} = ${pushParam(params, filterValue)}`);
|
|
1605
1615
|
}
|
|
1606
|
-
return
|
|
1607
|
-
}
|
|
1608
|
-
function isBlockedIpAddress(address) {
|
|
1609
|
-
return isBlockedHostname(address.trim().toLowerCase());
|
|
1616
|
+
return clauses.join(" AND ");
|
|
1610
1617
|
}
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
return parsed;
|
|
1618
|
+
function buildWhereNodeClause(tableName, node, params) {
|
|
1619
|
+
if ("where" in node) {
|
|
1620
|
+
return appendWhereParts(tableName, node.where, params);
|
|
1615
1621
|
}
|
|
1616
|
-
const
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
throw new BadRequestError("Webhook URL targets a blocked host.");
|
|
1622
|
+
const grouped = buildWhereGroupClause(tableName, node.group, params);
|
|
1623
|
+
if (!grouped) {
|
|
1624
|
+
return "";
|
|
1620
1625
|
}
|
|
1621
|
-
return
|
|
1626
|
+
return grouped.includes(" OR ") ? `(${grouped})` : grouped;
|
|
1622
1627
|
}
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
const resolveDns = options.resolveDns ?? appConfig.env === "production";
|
|
1630
|
-
const urlOptions = { allowHttp: options.allowHttp, resolveDns };
|
|
1631
|
-
const controller = new AbortController;
|
|
1632
|
-
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
1633
|
-
try {
|
|
1634
|
-
let currentUrl = (await assertSafeOutboundUrlResolved(input, urlOptions)).toString();
|
|
1635
|
-
let redirectCount = 0;
|
|
1636
|
-
while (true) {
|
|
1637
|
-
const response = await fetch(currentUrl, {
|
|
1638
|
-
...init,
|
|
1639
|
-
signal: controller.signal,
|
|
1640
|
-
redirect: "manual"
|
|
1641
|
-
});
|
|
1642
|
-
if (response.status >= 300 && response.status < 400) {
|
|
1643
|
-
const location = response.headers.get("location");
|
|
1644
|
-
if (!location || redirectCount >= maxRedirects) {
|
|
1645
|
-
return response;
|
|
1646
|
-
}
|
|
1647
|
-
currentUrl = (await assertSafeOutboundUrlResolved(new URL(location, currentUrl).toString(), urlOptions)).toString();
|
|
1648
|
-
redirectCount += 1;
|
|
1649
|
-
continue;
|
|
1650
|
-
}
|
|
1651
|
-
return response;
|
|
1628
|
+
function buildWhereGroupClause(tableName, nodes, params) {
|
|
1629
|
+
let result = "";
|
|
1630
|
+
for (const node of nodes) {
|
|
1631
|
+
const part = buildWhereNodeClause(tableName, node, params);
|
|
1632
|
+
if (!part) {
|
|
1633
|
+
continue;
|
|
1652
1634
|
}
|
|
1653
|
-
|
|
1654
|
-
|
|
1635
|
+
if (!result) {
|
|
1636
|
+
result = part;
|
|
1637
|
+
continue;
|
|
1638
|
+
}
|
|
1639
|
+
result = node.kind === "or" ? `${result} OR ${part}` : `${result} AND ${part}`;
|
|
1640
|
+
}
|
|
1641
|
+
if (!result) {
|
|
1642
|
+
return "";
|
|
1655
1643
|
}
|
|
1644
|
+
return result;
|
|
1656
1645
|
}
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
backoffMs = 2000;
|
|
1662
|
-
async handle(payload) {
|
|
1663
|
-
const rows = await repositoryConnection`
|
|
1664
|
-
SELECT id, url, secret
|
|
1665
|
-
FROM webhook
|
|
1666
|
-
WHERE id = ${payload.webhookId} AND active = TRUE
|
|
1667
|
-
LIMIT 1
|
|
1668
|
-
`;
|
|
1669
|
-
const webhook = rows[0];
|
|
1670
|
-
if (!webhook) {
|
|
1671
|
-
return;
|
|
1672
|
-
}
|
|
1673
|
-
const body = JSON.stringify({ event: payload.event, payload: payload.payload });
|
|
1674
|
-
const signature = createHmac2("sha256", webhook.secret).update(body).digest("hex");
|
|
1675
|
-
assertSafeOutboundUrl(webhook.url, { allowHttp: appConfig.env !== "production" });
|
|
1676
|
-
let responseStatus = null;
|
|
1677
|
-
let errorMessage = null;
|
|
1678
|
-
try {
|
|
1679
|
-
const response = await safeFetch(webhook.url, {
|
|
1680
|
-
method: "POST",
|
|
1681
|
-
headers: {
|
|
1682
|
-
"content-type": "application/json",
|
|
1683
|
-
"x-workhub-signature": signature
|
|
1684
|
-
},
|
|
1685
|
-
body
|
|
1686
|
-
}, { allowHttp: appConfig.env !== "production" });
|
|
1687
|
-
responseStatus = response.status;
|
|
1688
|
-
if (!response.ok) {
|
|
1689
|
-
throw new Error(`Webhook delivery failed with status ${response.status}.`);
|
|
1690
|
-
}
|
|
1691
|
-
} catch (error) {
|
|
1692
|
-
errorMessage = error instanceof Error ? error.message : String(error);
|
|
1693
|
-
await repositoryConnection`
|
|
1694
|
-
INSERT INTO webhook_delivery (webhook_id, event, payload, response_status, error)
|
|
1695
|
-
VALUES (
|
|
1696
|
-
${webhook.id},
|
|
1697
|
-
${payload.event},
|
|
1698
|
-
${JSON.stringify(payload.payload)}::jsonb,
|
|
1699
|
-
${responseStatus},
|
|
1700
|
-
${errorMessage}
|
|
1701
|
-
)
|
|
1702
|
-
`;
|
|
1703
|
-
throw error instanceof Error ? error : new Error(errorMessage);
|
|
1704
|
-
}
|
|
1705
|
-
await repositoryConnection`
|
|
1706
|
-
INSERT INTO webhook_delivery (webhook_id, event, payload, response_status)
|
|
1707
|
-
VALUES (
|
|
1708
|
-
${webhook.id},
|
|
1709
|
-
${payload.event},
|
|
1710
|
-
${JSON.stringify(payload.payload)}::jsonb,
|
|
1711
|
-
${responseStatus}
|
|
1712
|
-
)
|
|
1713
|
-
`;
|
|
1646
|
+
function buildAdvancedWhereClause(tableName, where = {}, whereNodes = [], params = []) {
|
|
1647
|
+
const nodes = [];
|
|
1648
|
+
if (Object.keys(where).length > 0) {
|
|
1649
|
+
nodes.push({ kind: "and", where });
|
|
1714
1650
|
}
|
|
1651
|
+
nodes.push(...whereNodes);
|
|
1652
|
+
const combined = buildWhereGroupClause(tableName, nodes, params);
|
|
1653
|
+
return {
|
|
1654
|
+
clause: combined ? ` WHERE ${combined}` : "",
|
|
1655
|
+
params
|
|
1656
|
+
};
|
|
1715
1657
|
}
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
class JobRegistry {
|
|
1720
|
-
constructor() {}
|
|
1721
|
-
factories = new Map;
|
|
1722
|
-
instances = new WeakMap;
|
|
1723
|
-
register(name, factory) {
|
|
1724
|
-
this.factories.set(name, factory);
|
|
1658
|
+
function resolveSoftDeleteColumn(table) {
|
|
1659
|
+
if (!table.softDeletes) {
|
|
1660
|
+
return null;
|
|
1725
1661
|
}
|
|
1726
|
-
|
|
1727
|
-
return
|
|
1662
|
+
if (table.softDeletes === true) {
|
|
1663
|
+
return "deleted_at";
|
|
1728
1664
|
}
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1665
|
+
return table.softDeletes.column ?? "deleted_at";
|
|
1666
|
+
}
|
|
1667
|
+
function appendSoftDeleteScope(table, options, clauses) {
|
|
1668
|
+
const column = resolveSoftDeleteColumn(table);
|
|
1669
|
+
if (!column) {
|
|
1670
|
+
return;
|
|
1732
1671
|
}
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
}
|
|
1738
|
-
return factory();
|
|
1672
|
+
const qualifiedColumn = qualifyColumn(table.name, column);
|
|
1673
|
+
if (options.onlyTrashed) {
|
|
1674
|
+
clauses.push(`${qualifiedColumn} IS NOT NULL`);
|
|
1675
|
+
return;
|
|
1739
1676
|
}
|
|
1740
|
-
|
|
1741
|
-
|
|
1677
|
+
if (!options.withTrashed) {
|
|
1678
|
+
clauses.push(`${qualifiedColumn} IS NULL`);
|
|
1742
1679
|
}
|
|
1743
1680
|
}
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1681
|
+
function buildQueryWhereClause(table, options = {}, whereNodes = [], params = []) {
|
|
1682
|
+
const { clause, params: whereParams } = buildAdvancedWhereClause(table.name, options.where ?? {}, whereNodes, params);
|
|
1683
|
+
const softDeleteClauses = [];
|
|
1684
|
+
appendSoftDeleteScope(table, options, softDeleteClauses);
|
|
1685
|
+
if (softDeleteClauses.length === 0) {
|
|
1686
|
+
return { clause, params: whereParams };
|
|
1687
|
+
}
|
|
1688
|
+
const base = clause.replace(/^ WHERE /, "");
|
|
1689
|
+
const scope = softDeleteClauses.join(" AND ");
|
|
1749
1690
|
return {
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
total: input.total,
|
|
1753
|
-
last_page: lastPage
|
|
1691
|
+
clause: base ? ` WHERE (${base}) AND ${scope}` : ` WHERE ${scope}`,
|
|
1692
|
+
params: whereParams
|
|
1754
1693
|
};
|
|
1755
1694
|
}
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
function isPostgresError(error) {
|
|
1759
|
-
return typeof error === "object" && error !== null && (("errno" in error) || ("code" in error));
|
|
1695
|
+
function isQueryOrder(value) {
|
|
1696
|
+
return "column" in value;
|
|
1760
1697
|
}
|
|
1761
|
-
function
|
|
1762
|
-
if (
|
|
1763
|
-
return
|
|
1698
|
+
function normalizeOrderBy(orderBy) {
|
|
1699
|
+
if (!orderBy) {
|
|
1700
|
+
return [];
|
|
1764
1701
|
}
|
|
1765
|
-
if (
|
|
1766
|
-
return
|
|
1702
|
+
if (Array.isArray(orderBy)) {
|
|
1703
|
+
return orderBy;
|
|
1767
1704
|
}
|
|
1768
|
-
if (
|
|
1769
|
-
return
|
|
1705
|
+
if (isQueryOrder(orderBy)) {
|
|
1706
|
+
return [orderBy];
|
|
1770
1707
|
}
|
|
1771
|
-
return
|
|
1708
|
+
return Object.entries(orderBy).map(([column, direction]) => ({
|
|
1709
|
+
column,
|
|
1710
|
+
direction
|
|
1711
|
+
}));
|
|
1772
1712
|
}
|
|
1773
|
-
function
|
|
1774
|
-
|
|
1775
|
-
return
|
|
1776
|
-
}
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
switch (sqlState) {
|
|
1783
|
-
case "23505":
|
|
1784
|
-
return new ConflictError(error.detail ?? "A record with these values already exists.", {
|
|
1785
|
-
constraint: error.constraint
|
|
1786
|
-
});
|
|
1787
|
-
case "23503":
|
|
1788
|
-
return new UnprocessableEntityError(error.detail ?? "Referenced record does not exist.", {
|
|
1789
|
-
constraint: error.constraint
|
|
1790
|
-
});
|
|
1791
|
-
case "23502":
|
|
1792
|
-
return new BadRequestError(error.detail ?? "Required field is missing.", {
|
|
1793
|
-
constraint: error.constraint
|
|
1794
|
-
});
|
|
1795
|
-
case "23514":
|
|
1796
|
-
return new BadRequestError(error.detail ?? "Value violates a database constraint.", {
|
|
1797
|
-
constraint: error.constraint
|
|
1798
|
-
});
|
|
1799
|
-
default:
|
|
1800
|
-
return new BadRequestError(error.message ?? "Database operation failed.", {
|
|
1801
|
-
code: error.code,
|
|
1802
|
-
sqlState
|
|
1803
|
-
});
|
|
1804
|
-
}
|
|
1805
|
-
}
|
|
1806
|
-
async function withDatabaseErrorHandling(operation) {
|
|
1807
|
-
try {
|
|
1808
|
-
return await operation();
|
|
1809
|
-
} catch (error) {
|
|
1810
|
-
throw mapDatabaseError(error);
|
|
1811
|
-
}
|
|
1812
|
-
}
|
|
1813
|
-
|
|
1814
|
-
// ../../src/core/database/query.ts
|
|
1815
|
-
function quoteIdentifier(identifier) {
|
|
1816
|
-
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(identifier)) {
|
|
1817
|
-
throw new Error(`Invalid SQL identifier: ${identifier}`);
|
|
1818
|
-
}
|
|
1819
|
-
return `"${identifier}"`;
|
|
1820
|
-
}
|
|
1821
|
-
function qualifyColumn(tableName, column) {
|
|
1822
|
-
return `${quoteIdentifier(tableName)}.${quoteIdentifier(column)}`;
|
|
1823
|
-
}
|
|
1824
|
-
function resolveQualifiedColumn(defaultTable, columnName) {
|
|
1825
|
-
if (columnName.includes(".")) {
|
|
1826
|
-
const [table, column] = columnName.split(".", 2);
|
|
1827
|
-
if (!table || !column) {
|
|
1828
|
-
throw new Error(`Invalid qualified column: ${columnName}`);
|
|
1829
|
-
}
|
|
1830
|
-
return qualifyColumn(table, column);
|
|
1831
|
-
}
|
|
1832
|
-
return qualifyColumn(defaultTable, columnName);
|
|
1833
|
-
}
|
|
1834
|
-
function parseQualifiedColumn(reference) {
|
|
1835
|
-
const [table, column] = reference.split(".", 2);
|
|
1836
|
-
if (!table || !column) {
|
|
1837
|
-
throw new Error(`Join columns must be qualified as table.column: ${reference}`);
|
|
1838
|
-
}
|
|
1839
|
-
return { table, column };
|
|
1840
|
-
}
|
|
1841
|
-
function normalizeDirection(direction = "ASC") {
|
|
1842
|
-
return direction.toUpperCase() === "DESC" ? "DESC" : "ASC";
|
|
1843
|
-
}
|
|
1844
|
-
function isQueryOperator(value) {
|
|
1845
|
-
return value !== null && !Array.isArray(value) && !(value instanceof Date) && typeof value === "object";
|
|
1846
|
-
}
|
|
1847
|
-
function pushParam(values, value) {
|
|
1848
|
-
values.push(value);
|
|
1849
|
-
return `$${values.length}`;
|
|
1850
|
-
}
|
|
1851
|
-
function buildInClause(column, values, params) {
|
|
1852
|
-
if (values.length === 0) {
|
|
1853
|
-
return "1 = 0";
|
|
1854
|
-
}
|
|
1855
|
-
const placeholders = values.map((value) => pushParam(params, value)).join(", ");
|
|
1856
|
-
return `${column} IN (${placeholders})`;
|
|
1857
|
-
}
|
|
1858
|
-
function buildOperatorClauses(column, operator, params) {
|
|
1859
|
-
const clauses = [];
|
|
1860
|
-
if (operator.isNull === true) {
|
|
1861
|
-
clauses.push(`${column} IS NULL`);
|
|
1862
|
-
}
|
|
1863
|
-
if (operator.isNull === false) {
|
|
1864
|
-
clauses.push(`${column} IS NOT NULL`);
|
|
1865
|
-
}
|
|
1866
|
-
if (operator.eq !== undefined) {
|
|
1867
|
-
if (operator.eq === null) {
|
|
1868
|
-
clauses.push(`${column} IS NULL`);
|
|
1869
|
-
} else {
|
|
1870
|
-
clauses.push(`${column} = ${pushParam(params, operator.eq)}`);
|
|
1871
|
-
}
|
|
1872
|
-
}
|
|
1873
|
-
if (operator.in !== undefined) {
|
|
1874
|
-
clauses.push(buildInClause(column, operator.in, params));
|
|
1875
|
-
}
|
|
1876
|
-
if (operator.gt !== undefined) {
|
|
1877
|
-
clauses.push(`${column} > ${pushParam(params, operator.gt)}`);
|
|
1878
|
-
}
|
|
1879
|
-
if (operator.gte !== undefined) {
|
|
1880
|
-
clauses.push(`${column} >= ${pushParam(params, operator.gte)}`);
|
|
1881
|
-
}
|
|
1882
|
-
if (operator.lt !== undefined) {
|
|
1883
|
-
clauses.push(`${column} < ${pushParam(params, operator.lt)}`);
|
|
1884
|
-
}
|
|
1885
|
-
if (operator.lte !== undefined) {
|
|
1886
|
-
clauses.push(`${column} <= ${pushParam(params, operator.lte)}`);
|
|
1887
|
-
}
|
|
1888
|
-
if (operator.ilike !== undefined) {
|
|
1889
|
-
clauses.push(`${column} ILIKE ${pushParam(params, `%${operator.ilike}%`)}`);
|
|
1890
|
-
}
|
|
1891
|
-
if (operator.tsMatch !== undefined) {
|
|
1892
|
-
clauses.push(`${column} @@ plainto_tsquery('english', ${pushParam(params, operator.tsMatch)})`);
|
|
1893
|
-
}
|
|
1894
|
-
return clauses;
|
|
1895
|
-
}
|
|
1896
|
-
function appendWhereParts(tableName, where, params) {
|
|
1897
|
-
const clauses = [];
|
|
1898
|
-
for (const [columnName, filterValue] of Object.entries(where)) {
|
|
1899
|
-
if (filterValue === undefined) {
|
|
1900
|
-
continue;
|
|
1901
|
-
}
|
|
1902
|
-
const column = resolveQualifiedColumn(tableName, columnName);
|
|
1903
|
-
if (Array.isArray(filterValue)) {
|
|
1904
|
-
clauses.push(buildInClause(column, filterValue, params));
|
|
1905
|
-
continue;
|
|
1906
|
-
}
|
|
1907
|
-
if (isQueryOperator(filterValue)) {
|
|
1908
|
-
clauses.push(...buildOperatorClauses(column, filterValue, params));
|
|
1909
|
-
continue;
|
|
1910
|
-
}
|
|
1911
|
-
if (filterValue === null) {
|
|
1912
|
-
clauses.push(`${column} IS NULL`);
|
|
1913
|
-
continue;
|
|
1914
|
-
}
|
|
1915
|
-
clauses.push(`${column} = ${pushParam(params, filterValue)}`);
|
|
1916
|
-
}
|
|
1917
|
-
return clauses.join(" AND ");
|
|
1918
|
-
}
|
|
1919
|
-
function buildWhereNodeClause(tableName, node, params) {
|
|
1920
|
-
if ("where" in node) {
|
|
1921
|
-
return appendWhereParts(tableName, node.where, params);
|
|
1922
|
-
}
|
|
1923
|
-
const grouped = buildWhereGroupClause(tableName, node.group, params);
|
|
1924
|
-
if (!grouped) {
|
|
1925
|
-
return "";
|
|
1926
|
-
}
|
|
1927
|
-
return grouped.includes(" OR ") ? `(${grouped})` : grouped;
|
|
1928
|
-
}
|
|
1929
|
-
function buildWhereGroupClause(tableName, nodes, params) {
|
|
1930
|
-
let result = "";
|
|
1931
|
-
for (const node of nodes) {
|
|
1932
|
-
const part = buildWhereNodeClause(tableName, node, params);
|
|
1933
|
-
if (!part) {
|
|
1934
|
-
continue;
|
|
1935
|
-
}
|
|
1936
|
-
if (!result) {
|
|
1937
|
-
result = part;
|
|
1938
|
-
continue;
|
|
1939
|
-
}
|
|
1940
|
-
result = node.kind === "or" ? `${result} OR ${part}` : `${result} AND ${part}`;
|
|
1941
|
-
}
|
|
1942
|
-
if (!result) {
|
|
1943
|
-
return "";
|
|
1944
|
-
}
|
|
1945
|
-
return result;
|
|
1946
|
-
}
|
|
1947
|
-
function buildAdvancedWhereClause(tableName, where = {}, whereNodes = [], params = []) {
|
|
1948
|
-
const nodes = [];
|
|
1949
|
-
if (Object.keys(where).length > 0) {
|
|
1950
|
-
nodes.push({ kind: "and", where });
|
|
1951
|
-
}
|
|
1952
|
-
nodes.push(...whereNodes);
|
|
1953
|
-
const combined = buildWhereGroupClause(tableName, nodes, params);
|
|
1954
|
-
return {
|
|
1955
|
-
clause: combined ? ` WHERE ${combined}` : "",
|
|
1956
|
-
params
|
|
1957
|
-
};
|
|
1958
|
-
}
|
|
1959
|
-
function resolveSoftDeleteColumn(table) {
|
|
1960
|
-
if (!table.softDeletes) {
|
|
1961
|
-
return null;
|
|
1962
|
-
}
|
|
1963
|
-
if (table.softDeletes === true) {
|
|
1964
|
-
return "deleted_at";
|
|
1965
|
-
}
|
|
1966
|
-
return table.softDeletes.column ?? "deleted_at";
|
|
1967
|
-
}
|
|
1968
|
-
function appendSoftDeleteScope(table, options, clauses) {
|
|
1969
|
-
const column = resolveSoftDeleteColumn(table);
|
|
1970
|
-
if (!column) {
|
|
1971
|
-
return;
|
|
1972
|
-
}
|
|
1973
|
-
const qualifiedColumn = qualifyColumn(table.name, column);
|
|
1974
|
-
if (options.onlyTrashed) {
|
|
1975
|
-
clauses.push(`${qualifiedColumn} IS NOT NULL`);
|
|
1976
|
-
return;
|
|
1977
|
-
}
|
|
1978
|
-
if (!options.withTrashed) {
|
|
1979
|
-
clauses.push(`${qualifiedColumn} IS NULL`);
|
|
1980
|
-
}
|
|
1981
|
-
}
|
|
1982
|
-
function buildQueryWhereClause(table, options = {}, whereNodes = [], params = []) {
|
|
1983
|
-
const { clause, params: whereParams } = buildAdvancedWhereClause(table.name, options.where ?? {}, whereNodes, params);
|
|
1984
|
-
const softDeleteClauses = [];
|
|
1985
|
-
appendSoftDeleteScope(table, options, softDeleteClauses);
|
|
1986
|
-
if (softDeleteClauses.length === 0) {
|
|
1987
|
-
return { clause, params: whereParams };
|
|
1988
|
-
}
|
|
1989
|
-
const base = clause.replace(/^ WHERE /, "");
|
|
1990
|
-
const scope = softDeleteClauses.join(" AND ");
|
|
1991
|
-
return {
|
|
1992
|
-
clause: base ? ` WHERE (${base}) AND ${scope}` : ` WHERE ${scope}`,
|
|
1993
|
-
params: whereParams
|
|
1994
|
-
};
|
|
1995
|
-
}
|
|
1996
|
-
function isQueryOrder(value) {
|
|
1997
|
-
return "column" in value;
|
|
1998
|
-
}
|
|
1999
|
-
function normalizeOrderBy(orderBy) {
|
|
2000
|
-
if (!orderBy) {
|
|
2001
|
-
return [];
|
|
2002
|
-
}
|
|
2003
|
-
if (Array.isArray(orderBy)) {
|
|
2004
|
-
return orderBy;
|
|
2005
|
-
}
|
|
2006
|
-
if (isQueryOrder(orderBy)) {
|
|
2007
|
-
return [orderBy];
|
|
2008
|
-
}
|
|
2009
|
-
return Object.entries(orderBy).map(([column, direction]) => ({
|
|
2010
|
-
column,
|
|
2011
|
-
direction
|
|
2012
|
-
}));
|
|
2013
|
-
}
|
|
2014
|
-
function buildOrderByClause(tableName, orderBy) {
|
|
2015
|
-
const parts = normalizeOrderBy(orderBy).map(({ column, direction }) => {
|
|
2016
|
-
return `${resolveQualifiedColumn(tableName, column)} ${normalizeDirection(direction)}`;
|
|
2017
|
-
});
|
|
2018
|
-
return parts.length > 0 ? ` ORDER BY ${parts.join(", ")}` : "";
|
|
2019
|
-
}
|
|
2020
|
-
function buildGroupByClause(tableName, groupBy) {
|
|
2021
|
-
if (!groupBy) {
|
|
2022
|
-
return "";
|
|
1713
|
+
function buildOrderByClause(tableName, orderBy) {
|
|
1714
|
+
const parts = normalizeOrderBy(orderBy).map(({ column, direction }) => {
|
|
1715
|
+
return `${resolveQualifiedColumn(tableName, column)} ${normalizeDirection(direction)}`;
|
|
1716
|
+
});
|
|
1717
|
+
return parts.length > 0 ? ` ORDER BY ${parts.join(", ")}` : "";
|
|
1718
|
+
}
|
|
1719
|
+
function buildGroupByClause(tableName, groupBy) {
|
|
1720
|
+
if (!groupBy) {
|
|
1721
|
+
return "";
|
|
2023
1722
|
}
|
|
2024
1723
|
const columns = (Array.isArray(groupBy) ? groupBy : [groupBy]).map((column) => String(column));
|
|
2025
1724
|
const parts = columns.map((column) => resolveQualifiedColumn(tableName, column));
|
|
@@ -2282,15 +1981,38 @@ function indexMorphToRelation(children, parentsByType, relation) {
|
|
|
2282
1981
|
return result;
|
|
2283
1982
|
}
|
|
2284
1983
|
|
|
2285
|
-
// ../../src/core/database/
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
1984
|
+
// ../../src/core/database/boundConnection.ts
|
|
1985
|
+
var boundConnectionHolder = {
|
|
1986
|
+
connection: null
|
|
1987
|
+
};
|
|
1988
|
+
function getBoundDatabaseConnection() {
|
|
1989
|
+
return boundConnectionHolder.connection;
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
// ../../src/core/database/repositoryConnection.ts
|
|
1993
|
+
function resolveRepositoryConnection() {
|
|
1994
|
+
return getBoundDatabaseConnection() ?? getDefaultDatabaseQuery();
|
|
1995
|
+
}
|
|
1996
|
+
var repositoryConnection = new Proxy(function repositoryConnection2() {}, {
|
|
1997
|
+
apply(_target, _thisArg, args) {
|
|
1998
|
+
return resolveRepositoryConnection()(...args);
|
|
1999
|
+
},
|
|
2000
|
+
get(_target, property) {
|
|
2001
|
+
const connection = resolveRepositoryConnection();
|
|
2002
|
+
const value = connection[property];
|
|
2003
|
+
return typeof value === "function" ? value.bind(connection) : value;
|
|
2004
|
+
}
|
|
2005
|
+
});
|
|
2006
|
+
|
|
2007
|
+
// ../../src/core/database/whereBuilder.ts
|
|
2008
|
+
class WhereBuilder {
|
|
2009
|
+
nodes = [];
|
|
2010
|
+
where(where) {
|
|
2011
|
+
this.nodes.push({ kind: "and", where });
|
|
2012
|
+
return this;
|
|
2013
|
+
}
|
|
2014
|
+
orWhere(where) {
|
|
2015
|
+
this.nodes.push({ kind: "or", where });
|
|
2294
2016
|
return this;
|
|
2295
2017
|
}
|
|
2296
2018
|
whereGroup(fn) {
|
|
@@ -3424,179 +3146,465 @@ class FailedJobRepository extends baseRepository_default {
|
|
|
3424
3146
|
super(failedJobTable);
|
|
3425
3147
|
}
|
|
3426
3148
|
}
|
|
3427
|
-
var failedJobRepository_default = FailedJobRepository;
|
|
3149
|
+
var failedJobRepository_default = FailedJobRepository;
|
|
3150
|
+
|
|
3151
|
+
// ../../src/core/queue/failedJobService.ts
|
|
3152
|
+
class FailedJobService {
|
|
3153
|
+
repository;
|
|
3154
|
+
constructor(repository) {
|
|
3155
|
+
this.repository = repository;
|
|
3156
|
+
}
|
|
3157
|
+
async recordFailure(input) {
|
|
3158
|
+
return await this.repository.create({
|
|
3159
|
+
job_name: input.jobName,
|
|
3160
|
+
payload: input.payload,
|
|
3161
|
+
exception: input.exception,
|
|
3162
|
+
failed_at: new Date
|
|
3163
|
+
});
|
|
3164
|
+
}
|
|
3165
|
+
listRecent(limit = 50) {
|
|
3166
|
+
return this.repository.findAll({
|
|
3167
|
+
limit,
|
|
3168
|
+
orderBy: { column: "failed_at", direction: "DESC" }
|
|
3169
|
+
});
|
|
3170
|
+
}
|
|
3171
|
+
async retry(id) {
|
|
3172
|
+
const failedJob = await this.repository.findByIdOrThrow(id, (jobId) => new Error(`Failed job ${jobId} not found.`));
|
|
3173
|
+
await this.repository.deleteById(id);
|
|
3174
|
+
return failedJob;
|
|
3175
|
+
}
|
|
3176
|
+
async delete(id) {
|
|
3177
|
+
const deleted = await this.repository.deleteById(id);
|
|
3178
|
+
if (!deleted) {
|
|
3179
|
+
throw new Error(`Failed job ${id} not found.`);
|
|
3180
|
+
}
|
|
3181
|
+
}
|
|
3182
|
+
async flush() {
|
|
3183
|
+
const jobs = await this.repository.findAll();
|
|
3184
|
+
let deleted = 0;
|
|
3185
|
+
for (const job of jobs) {
|
|
3186
|
+
if (await this.repository.deleteById(job.id)) {
|
|
3187
|
+
deleted += 1;
|
|
3188
|
+
}
|
|
3189
|
+
}
|
|
3190
|
+
return deleted;
|
|
3191
|
+
}
|
|
3192
|
+
}
|
|
3193
|
+
var failedJobService_default = FailedJobService;
|
|
3194
|
+
|
|
3195
|
+
// ../../src/core/queue/jobRegistry.ts
|
|
3196
|
+
class JobRegistry {
|
|
3197
|
+
constructor() {}
|
|
3198
|
+
factories = new Map;
|
|
3199
|
+
instances = new WeakMap;
|
|
3200
|
+
register(name, factory) {
|
|
3201
|
+
this.factories.set(name, factory);
|
|
3202
|
+
}
|
|
3203
|
+
resolveName(job) {
|
|
3204
|
+
return this.instances.get(job);
|
|
3205
|
+
}
|
|
3206
|
+
track(name, job) {
|
|
3207
|
+
this.instances.set(job, name);
|
|
3208
|
+
return job;
|
|
3209
|
+
}
|
|
3210
|
+
create(name) {
|
|
3211
|
+
const factory = this.factories.get(name);
|
|
3212
|
+
if (!factory) {
|
|
3213
|
+
return;
|
|
3214
|
+
}
|
|
3215
|
+
return factory();
|
|
3216
|
+
}
|
|
3217
|
+
names() {
|
|
3218
|
+
return [...this.factories.keys()];
|
|
3219
|
+
}
|
|
3220
|
+
}
|
|
3221
|
+
var jobRegistry = new JobRegistry;
|
|
3222
|
+
|
|
3223
|
+
// ../../src/core/queue/jobRunner.ts
|
|
3224
|
+
async function runQueueJob(envelope, failedJobs) {
|
|
3225
|
+
const job = jobRegistry.create(envelope.name);
|
|
3226
|
+
if (!job) {
|
|
3227
|
+
throw new Error(`Unknown job "${envelope.name}".`);
|
|
3228
|
+
}
|
|
3229
|
+
const attempts = envelope.attempts ?? 0;
|
|
3230
|
+
try {
|
|
3231
|
+
await job.handle(envelope.payload);
|
|
3232
|
+
} catch (error) {
|
|
3233
|
+
const nextAttempt = attempts + 1;
|
|
3234
|
+
const maxAttempts = job.maxAttempts ?? queueConfig.maxAttempts;
|
|
3235
|
+
if (nextAttempt < maxAttempts) {
|
|
3236
|
+
const backoffMs = job.backoffMs ?? queueConfig.backoffMs;
|
|
3237
|
+
await new Promise((resolve) => setTimeout(resolve, backoffMs * nextAttempt));
|
|
3238
|
+
await runQueueJob({
|
|
3239
|
+
...envelope,
|
|
3240
|
+
attempts: nextAttempt
|
|
3241
|
+
}, failedJobs);
|
|
3242
|
+
return;
|
|
3243
|
+
}
|
|
3244
|
+
await failedJobs.recordFailure({
|
|
3245
|
+
jobName: envelope.name,
|
|
3246
|
+
payload: envelope.payload,
|
|
3247
|
+
exception: error instanceof Error ? error.stack ?? error.message : String(error)
|
|
3248
|
+
});
|
|
3249
|
+
throw error;
|
|
3250
|
+
}
|
|
3251
|
+
}
|
|
3252
|
+
|
|
3253
|
+
// ../../src/core/queue/redisQueue.ts
|
|
3254
|
+
var {RedisClient: RedisClient2 } = globalThis.Bun;
|
|
3255
|
+
var QUEUE_LIST_KEY = "workhub:queue:default";
|
|
3256
|
+
var QUEUE_HIGH_KEY = "workhub:queue:high";
|
|
3257
|
+
var QUEUE_LOW_KEY = "workhub:queue:low";
|
|
3258
|
+
function queueKeyForPriority(priority = "default") {
|
|
3259
|
+
switch (priority) {
|
|
3260
|
+
case "high":
|
|
3261
|
+
return QUEUE_HIGH_KEY;
|
|
3262
|
+
case "low":
|
|
3263
|
+
return QUEUE_LOW_KEY;
|
|
3264
|
+
default:
|
|
3265
|
+
return QUEUE_LIST_KEY;
|
|
3266
|
+
}
|
|
3267
|
+
}
|
|
3268
|
+
class RedisQueue {
|
|
3269
|
+
client;
|
|
3270
|
+
constructor(redisUrl) {
|
|
3271
|
+
this.client = new RedisClient2(redisUrl);
|
|
3272
|
+
}
|
|
3273
|
+
async dispatch(job, payload) {
|
|
3274
|
+
const name = jobRegistry.resolveName(job);
|
|
3275
|
+
if (!name) {
|
|
3276
|
+
throw new Error("Job is not registered with the queue worker registry.");
|
|
3277
|
+
}
|
|
3278
|
+
const envelope = {
|
|
3279
|
+
name,
|
|
3280
|
+
payload,
|
|
3281
|
+
attempts: 0
|
|
3282
|
+
};
|
|
3283
|
+
const queueKey = queueKeyForPriority(job.priority);
|
|
3284
|
+
await this.client.lpush(queueKey, JSON.stringify(envelope));
|
|
3285
|
+
}
|
|
3286
|
+
}
|
|
3287
|
+
|
|
3288
|
+
// ../../src/core/queue/resilientQueue.ts
|
|
3289
|
+
class ResilientQueue {
|
|
3290
|
+
failedJobs;
|
|
3291
|
+
asyncDispatch;
|
|
3292
|
+
constructor(failedJobs, asyncDispatch = false) {
|
|
3293
|
+
this.failedJobs = failedJobs;
|
|
3294
|
+
this.asyncDispatch = asyncDispatch;
|
|
3295
|
+
}
|
|
3296
|
+
async dispatch(job, payload) {
|
|
3297
|
+
const name = jobRegistry.resolveName(job);
|
|
3298
|
+
if (!name) {
|
|
3299
|
+
throw new Error("Job is not registered with the queue worker registry.");
|
|
3300
|
+
}
|
|
3301
|
+
const envelope = {
|
|
3302
|
+
name,
|
|
3303
|
+
payload,
|
|
3304
|
+
attempts: 0
|
|
3305
|
+
};
|
|
3306
|
+
if (this.asyncDispatch) {
|
|
3307
|
+
setTimeout(() => {
|
|
3308
|
+
runQueueJob(envelope, this.failedJobs).catch((error) => {
|
|
3309
|
+
console.error("[ResilientQueue] Job failed:", error);
|
|
3310
|
+
});
|
|
3311
|
+
}, 0);
|
|
3312
|
+
return;
|
|
3313
|
+
}
|
|
3314
|
+
await runQueueJob(envelope, this.failedJobs);
|
|
3315
|
+
}
|
|
3316
|
+
}
|
|
3317
|
+
|
|
3318
|
+
// ../../src/core/queue/publicQueue.ts
|
|
3319
|
+
function createFailedJobService() {
|
|
3320
|
+
return new failedJobService_default(new failedJobRepository_default);
|
|
3321
|
+
}
|
|
3322
|
+
function createTrackedJob(name, job) {
|
|
3323
|
+
return jobRegistry.track(name, job);
|
|
3324
|
+
}
|
|
3325
|
+
function createProductionQueue(driver, options = {}) {
|
|
3326
|
+
options.registerJobs?.();
|
|
3327
|
+
const failedJobs = options.failedJobs ?? createFailedJobService();
|
|
3328
|
+
if (driver === "redis") {
|
|
3329
|
+
if (!options.redisUrl) {
|
|
3330
|
+
throw new Error('QUEUE_DRIVER="redis" requires REDIS_URL to be set.');
|
|
3331
|
+
}
|
|
3332
|
+
return new RedisQueue(options.redisUrl);
|
|
3333
|
+
}
|
|
3334
|
+
return new ResilientQueue(failedJobs, driver === "async");
|
|
3335
|
+
}
|
|
3336
|
+
|
|
3337
|
+
// ../../src/core/jobs/dispatchWebhookJob.ts
|
|
3338
|
+
import { createHmac as createHmac2 } from "crypto";
|
|
3339
|
+
|
|
3340
|
+
// ../../src/core/security/safeUrl.ts
|
|
3341
|
+
import { lookup as dnsLookupImpl } from "dns/promises";
|
|
3342
|
+
var dnsLookup = dnsLookupImpl;
|
|
3343
|
+
var BLOCKED_HOSTNAMES = new Set([
|
|
3344
|
+
"localhost",
|
|
3345
|
+
"127.0.0.1",
|
|
3346
|
+
"0.0.0.0",
|
|
3347
|
+
"::1",
|
|
3348
|
+
"metadata.google.internal"
|
|
3349
|
+
]);
|
|
3350
|
+
function isPrivateIpv4(hostname) {
|
|
3351
|
+
const match = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(hostname);
|
|
3352
|
+
if (!match) {
|
|
3353
|
+
return false;
|
|
3354
|
+
}
|
|
3355
|
+
const octets = match.slice(1, 5).map((part) => Number.parseInt(part, 10));
|
|
3356
|
+
if (octets.some((octet) => octet < 0 || octet > 255)) {
|
|
3357
|
+
return true;
|
|
3358
|
+
}
|
|
3359
|
+
const [a = 0, b = 0] = octets;
|
|
3360
|
+
if (a === 10) {
|
|
3361
|
+
return true;
|
|
3362
|
+
}
|
|
3363
|
+
if (a === 127) {
|
|
3364
|
+
return true;
|
|
3365
|
+
}
|
|
3366
|
+
if (a === 0) {
|
|
3367
|
+
return true;
|
|
3368
|
+
}
|
|
3369
|
+
if (a === 169 && b === 254) {
|
|
3370
|
+
return true;
|
|
3371
|
+
}
|
|
3372
|
+
if (a === 172 && b >= 16 && b <= 31) {
|
|
3373
|
+
return true;
|
|
3374
|
+
}
|
|
3375
|
+
if (a === 192 && b === 168) {
|
|
3376
|
+
return true;
|
|
3377
|
+
}
|
|
3378
|
+
return false;
|
|
3379
|
+
}
|
|
3380
|
+
function isBlockedHostname(hostname) {
|
|
3381
|
+
const normalized = hostname.trim().toLowerCase();
|
|
3382
|
+
if (normalized.length === 0) {
|
|
3383
|
+
return true;
|
|
3384
|
+
}
|
|
3385
|
+
if (BLOCKED_HOSTNAMES.has(normalized)) {
|
|
3386
|
+
return true;
|
|
3387
|
+
}
|
|
3388
|
+
if (normalized.endsWith(".local") || normalized.endsWith(".internal")) {
|
|
3389
|
+
return true;
|
|
3390
|
+
}
|
|
3391
|
+
if (normalized.includes(":")) {
|
|
3392
|
+
return true;
|
|
3393
|
+
}
|
|
3394
|
+
return isPrivateIpv4(normalized);
|
|
3395
|
+
}
|
|
3396
|
+
function assertSafeOutboundUrl(rawUrl, options = {}) {
|
|
3397
|
+
let parsed;
|
|
3398
|
+
try {
|
|
3399
|
+
parsed = new URL(rawUrl);
|
|
3400
|
+
} catch {
|
|
3401
|
+
throw new BadRequestError("Webhook URL is invalid.");
|
|
3402
|
+
}
|
|
3403
|
+
if (parsed.protocol !== "https:" && !(options.allowHttp && parsed.protocol === "http:")) {
|
|
3404
|
+
throw new BadRequestError("Webhook URL must use HTTPS.");
|
|
3405
|
+
}
|
|
3406
|
+
if (parsed.username || parsed.password) {
|
|
3407
|
+
throw new BadRequestError("Webhook URL must not include credentials.");
|
|
3408
|
+
}
|
|
3409
|
+
if (isBlockedHostname(parsed.hostname)) {
|
|
3410
|
+
throw new BadRequestError("Webhook URL targets a blocked host.");
|
|
3411
|
+
}
|
|
3412
|
+
return parsed;
|
|
3413
|
+
}
|
|
3414
|
+
function isBlockedIpAddress(address) {
|
|
3415
|
+
return isBlockedHostname(address.trim().toLowerCase());
|
|
3416
|
+
}
|
|
3417
|
+
async function assertSafeOutboundUrlResolved(rawUrl, options = {}) {
|
|
3418
|
+
const parsed = assertSafeOutboundUrl(rawUrl, options);
|
|
3419
|
+
if (options.resolveDns === false) {
|
|
3420
|
+
return parsed;
|
|
3421
|
+
}
|
|
3422
|
+
const hostname = parsed.hostname.trim().toLowerCase();
|
|
3423
|
+
const results = await dnsLookup(hostname, { all: true, verbatim: true });
|
|
3424
|
+
if (results.some((result) => isBlockedIpAddress(result.address))) {
|
|
3425
|
+
throw new BadRequestError("Webhook URL targets a blocked host.");
|
|
3426
|
+
}
|
|
3427
|
+
return parsed;
|
|
3428
|
+
}
|
|
3429
|
+
|
|
3430
|
+
// ../../src/core/security/safeFetch.ts
|
|
3431
|
+
var DEFAULT_FETCH_TIMEOUT_MS = 1e4;
|
|
3432
|
+
async function safeFetch(input, init = {}, options = {}) {
|
|
3433
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;
|
|
3434
|
+
const maxRedirects = options.maxRedirects ?? 0;
|
|
3435
|
+
const resolveDns = options.resolveDns ?? appConfig.env === "production";
|
|
3436
|
+
const urlOptions = { allowHttp: options.allowHttp, resolveDns };
|
|
3437
|
+
const controller = new AbortController;
|
|
3438
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
3439
|
+
try {
|
|
3440
|
+
let currentUrl = (await assertSafeOutboundUrlResolved(input, urlOptions)).toString();
|
|
3441
|
+
let redirectCount = 0;
|
|
3442
|
+
while (true) {
|
|
3443
|
+
const response = await fetch(currentUrl, {
|
|
3444
|
+
...init,
|
|
3445
|
+
signal: controller.signal,
|
|
3446
|
+
redirect: "manual"
|
|
3447
|
+
});
|
|
3448
|
+
if (response.status >= 300 && response.status < 400) {
|
|
3449
|
+
const location = response.headers.get("location");
|
|
3450
|
+
if (!location || redirectCount >= maxRedirects) {
|
|
3451
|
+
return response;
|
|
3452
|
+
}
|
|
3453
|
+
currentUrl = (await assertSafeOutboundUrlResolved(new URL(location, currentUrl).toString(), urlOptions)).toString();
|
|
3454
|
+
redirectCount += 1;
|
|
3455
|
+
continue;
|
|
3456
|
+
}
|
|
3457
|
+
return response;
|
|
3458
|
+
}
|
|
3459
|
+
} finally {
|
|
3460
|
+
clearTimeout(timeout);
|
|
3461
|
+
}
|
|
3462
|
+
}
|
|
3428
3463
|
|
|
3429
|
-
// ../../src/core/
|
|
3430
|
-
class
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
listRecent(limit = 50) {
|
|
3444
|
-
return this.repository.findAll({
|
|
3445
|
-
limit,
|
|
3446
|
-
orderBy: { column: "failed_at", direction: "DESC" }
|
|
3447
|
-
});
|
|
3448
|
-
}
|
|
3449
|
-
async retry(id) {
|
|
3450
|
-
const failedJob = await this.repository.findByIdOrThrow(id, (jobId) => new Error(`Failed job ${jobId} not found.`));
|
|
3451
|
-
await this.repository.deleteById(id);
|
|
3452
|
-
return failedJob;
|
|
3453
|
-
}
|
|
3454
|
-
async delete(id) {
|
|
3455
|
-
const deleted = await this.repository.deleteById(id);
|
|
3456
|
-
if (!deleted) {
|
|
3457
|
-
throw new Error(`Failed job ${id} not found.`);
|
|
3464
|
+
// ../../src/core/jobs/dispatchWebhookJob.ts
|
|
3465
|
+
class DispatchWebhookJob extends Job {
|
|
3466
|
+
maxAttempts = 3;
|
|
3467
|
+
backoffMs = 2000;
|
|
3468
|
+
async handle(payload) {
|
|
3469
|
+
const rows = await repositoryConnection`
|
|
3470
|
+
SELECT id, url, secret
|
|
3471
|
+
FROM webhook
|
|
3472
|
+
WHERE id = ${payload.webhookId} AND active = TRUE
|
|
3473
|
+
LIMIT 1
|
|
3474
|
+
`;
|
|
3475
|
+
const webhook = rows[0];
|
|
3476
|
+
if (!webhook) {
|
|
3477
|
+
return;
|
|
3458
3478
|
}
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
let
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3479
|
+
const body = JSON.stringify({ event: payload.event, payload: payload.payload });
|
|
3480
|
+
const signature = createHmac2("sha256", webhook.secret).update(body).digest("hex");
|
|
3481
|
+
assertSafeOutboundUrl(webhook.url, { allowHttp: appConfig.env !== "production" });
|
|
3482
|
+
let responseStatus = null;
|
|
3483
|
+
let errorMessage = null;
|
|
3484
|
+
try {
|
|
3485
|
+
const response = await safeFetch(webhook.url, {
|
|
3486
|
+
method: "POST",
|
|
3487
|
+
headers: {
|
|
3488
|
+
"content-type": "application/json",
|
|
3489
|
+
"x-workhub-signature": signature
|
|
3490
|
+
},
|
|
3491
|
+
body
|
|
3492
|
+
}, { allowHttp: appConfig.env !== "production" });
|
|
3493
|
+
responseStatus = response.status;
|
|
3494
|
+
if (!response.ok) {
|
|
3495
|
+
throw new Error(`Webhook delivery failed with status ${response.status}.`);
|
|
3466
3496
|
}
|
|
3497
|
+
} catch (error) {
|
|
3498
|
+
errorMessage = error instanceof Error ? error.message : String(error);
|
|
3499
|
+
await repositoryConnection`
|
|
3500
|
+
INSERT INTO webhook_delivery (webhook_id, event, payload, response_status, error)
|
|
3501
|
+
VALUES (
|
|
3502
|
+
${webhook.id},
|
|
3503
|
+
${payload.event},
|
|
3504
|
+
${JSON.stringify(payload.payload)}::jsonb,
|
|
3505
|
+
${responseStatus},
|
|
3506
|
+
${errorMessage}
|
|
3507
|
+
)
|
|
3508
|
+
`;
|
|
3509
|
+
throw error instanceof Error ? error : new Error(errorMessage);
|
|
3467
3510
|
}
|
|
3468
|
-
|
|
3511
|
+
await repositoryConnection`
|
|
3512
|
+
INSERT INTO webhook_delivery (webhook_id, event, payload, response_status)
|
|
3513
|
+
VALUES (
|
|
3514
|
+
${webhook.id},
|
|
3515
|
+
${payload.event},
|
|
3516
|
+
${JSON.stringify(payload.payload)}::jsonb,
|
|
3517
|
+
${responseStatus}
|
|
3518
|
+
)
|
|
3519
|
+
`;
|
|
3469
3520
|
}
|
|
3470
3521
|
}
|
|
3471
|
-
var
|
|
3522
|
+
var dispatchWebhookJob_default = DispatchWebhookJob;
|
|
3472
3523
|
|
|
3473
|
-
// ../../src/core/
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3524
|
+
// ../../src/core/logging/logger.ts
|
|
3525
|
+
class Logger {
|
|
3526
|
+
channel;
|
|
3527
|
+
constructor(channel = "app") {
|
|
3528
|
+
this.channel = channel;
|
|
3478
3529
|
}
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
attempts: nextAttempt
|
|
3491
|
-
}, failedJobs);
|
|
3530
|
+
write(level, message, context = {}) {
|
|
3531
|
+
const entry = {
|
|
3532
|
+
level,
|
|
3533
|
+
channel: this.channel,
|
|
3534
|
+
message,
|
|
3535
|
+
timestamp: new Date().toISOString(),
|
|
3536
|
+
...context
|
|
3537
|
+
};
|
|
3538
|
+
const line = JSON.stringify(entry);
|
|
3539
|
+
if (level === "error") {
|
|
3540
|
+
console.error(line);
|
|
3492
3541
|
return;
|
|
3493
3542
|
}
|
|
3494
|
-
|
|
3495
|
-
jobName: envelope.name,
|
|
3496
|
-
payload: envelope.payload,
|
|
3497
|
-
exception: error instanceof Error ? error.stack ?? error.message : String(error)
|
|
3498
|
-
});
|
|
3499
|
-
throw error;
|
|
3543
|
+
console.log(line);
|
|
3500
3544
|
}
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
// ../../src/core/queue/redisQueue.ts
|
|
3504
|
-
var {RedisClient: RedisClient2 } = globalThis.Bun;
|
|
3505
|
-
var QUEUE_LIST_KEY = "workhub:queue:default";
|
|
3506
|
-
var QUEUE_HIGH_KEY = "workhub:queue:high";
|
|
3507
|
-
var QUEUE_LOW_KEY = "workhub:queue:low";
|
|
3508
|
-
function queueKeyForPriority(priority = "default") {
|
|
3509
|
-
switch (priority) {
|
|
3510
|
-
case "high":
|
|
3511
|
-
return QUEUE_HIGH_KEY;
|
|
3512
|
-
case "low":
|
|
3513
|
-
return QUEUE_LOW_KEY;
|
|
3514
|
-
default:
|
|
3515
|
-
return QUEUE_LIST_KEY;
|
|
3545
|
+
debug(message, context) {
|
|
3546
|
+
this.write("debug", message, context);
|
|
3516
3547
|
}
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
client;
|
|
3520
|
-
constructor(redisUrl) {
|
|
3521
|
-
this.client = new RedisClient2(redisUrl);
|
|
3548
|
+
info(message, context) {
|
|
3549
|
+
this.write("info", message, context);
|
|
3522
3550
|
}
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
const envelope = {
|
|
3529
|
-
name,
|
|
3530
|
-
payload,
|
|
3531
|
-
attempts: 0
|
|
3532
|
-
};
|
|
3533
|
-
const queueKey = queueKeyForPriority(job.priority);
|
|
3534
|
-
await this.client.lpush(queueKey, JSON.stringify(envelope));
|
|
3551
|
+
warn(message, context) {
|
|
3552
|
+
this.write("warn", message, context);
|
|
3553
|
+
}
|
|
3554
|
+
error(message, context) {
|
|
3555
|
+
this.write("error", message, context);
|
|
3535
3556
|
}
|
|
3536
3557
|
}
|
|
3558
|
+
var appLogger = new Logger("app");
|
|
3537
3559
|
|
|
3538
|
-
// ../../src/
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
async dispatch(job, payload) {
|
|
3547
|
-
const name = jobRegistry.resolveName(job);
|
|
3548
|
-
if (!name) {
|
|
3549
|
-
throw new Error("Job is not registered with the queue worker registry.");
|
|
3550
|
-
}
|
|
3551
|
-
const envelope = {
|
|
3552
|
-
name,
|
|
3553
|
-
payload,
|
|
3554
|
-
attempts: 0
|
|
3555
|
-
};
|
|
3556
|
-
if (this.asyncDispatch) {
|
|
3557
|
-
setTimeout(() => {
|
|
3558
|
-
runQueueJob(envelope, this.failedJobs).catch((error) => {
|
|
3559
|
-
console.error("[ResilientQueue] Job failed:", error);
|
|
3560
|
-
});
|
|
3561
|
-
}, 0);
|
|
3562
|
-
return;
|
|
3563
|
-
}
|
|
3564
|
-
await runQueueJob(envelope, this.failedJobs);
|
|
3560
|
+
// ../../src/bootstrap/applicationRegistry.ts
|
|
3561
|
+
var activeContext;
|
|
3562
|
+
function setActiveApplicationContext(context) {
|
|
3563
|
+
activeContext = context;
|
|
3564
|
+
}
|
|
3565
|
+
function requireActiveApplicationContext() {
|
|
3566
|
+
if (!activeContext) {
|
|
3567
|
+
throw new Error("The application context has not been bootstrapped.");
|
|
3565
3568
|
}
|
|
3569
|
+
return activeContext;
|
|
3566
3570
|
}
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
function createFailedJobService() {
|
|
3570
|
-
return new failedJobService_default(new failedJobRepository_default);
|
|
3571
|
+
function resolveApplicationCache() {
|
|
3572
|
+
return getRequiredDependency(requireActiveApplicationContext().dependencies, "cache");
|
|
3571
3573
|
}
|
|
3572
|
-
function
|
|
3573
|
-
return
|
|
3574
|
+
function resolveApplicationQueue() {
|
|
3575
|
+
return requireActiveApplicationContext().container.resolve(CORE_QUEUE_TOKEN);
|
|
3574
3576
|
}
|
|
3575
|
-
function
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
3577
|
+
function resolveApplicationAuth() {
|
|
3578
|
+
return requireActiveApplicationContext().container.resolve(CORE_AUTH_TOKEN);
|
|
3579
|
+
}
|
|
3580
|
+
function resolveApplicationPolicyGate() {
|
|
3581
|
+
return requireActiveApplicationContext().container.resolve(CORE_POLICY_GATE_TOKEN);
|
|
3582
|
+
}
|
|
3583
|
+
function resolveApplicationConfig() {
|
|
3584
|
+
return requireActiveApplicationContext().config;
|
|
3585
|
+
}
|
|
3586
|
+
function resolveApplicationLogger() {
|
|
3587
|
+
return appLogger;
|
|
3588
|
+
}
|
|
3589
|
+
function resolveApplicationDependencies() {
|
|
3590
|
+
return requireActiveApplicationContext().dependencies;
|
|
3585
3591
|
}
|
|
3586
3592
|
|
|
3587
|
-
// ../../src/
|
|
3588
|
-
var FAILED_JOB_SERVICE_TOKEN = "core.failedJobs";
|
|
3593
|
+
// ../../src/bootstrap/queue/defaultJobs.ts
|
|
3589
3594
|
function registerDefaultJobs() {
|
|
3590
3595
|
jobRegistry.register("cache.invalidate-tags", () => {
|
|
3591
3596
|
return new invalidateCacheTagsJob_default(resolveApplicationCache());
|
|
3592
3597
|
});
|
|
3593
3598
|
jobRegistry.register("webhook.dispatch", () => new dispatchWebhookJob_default);
|
|
3594
3599
|
}
|
|
3595
|
-
|
|
3600
|
+
|
|
3601
|
+
// ../../src/core/queue/createAppQueue.ts
|
|
3602
|
+
var FAILED_JOB_SERVICE_TOKEN = "core.failedJobs";
|
|
3603
|
+
function createAppQueue(driver, redisUrl, failedJobs = createFailedJobService(), registerJobs) {
|
|
3596
3604
|
return createProductionQueue(driver, {
|
|
3597
3605
|
redisUrl,
|
|
3598
3606
|
failedJobs,
|
|
3599
|
-
registerJobs
|
|
3607
|
+
registerJobs
|
|
3600
3608
|
});
|
|
3601
3609
|
}
|
|
3602
3610
|
|
|
@@ -3704,7 +3712,7 @@ var queueProvider = {
|
|
|
3704
3712
|
config.set("queue.driver", driver);
|
|
3705
3713
|
const failedJobs = createFailedJobService();
|
|
3706
3714
|
container.set(FAILED_JOB_SERVICE_TOKEN, failedJobs);
|
|
3707
|
-
container.set(CORE_QUEUE_TOKEN, createAppQueue(driver, config.get(REDIS_URL_CONFIG_KEY) ?? process.env.REDIS_URL, failedJobs));
|
|
3715
|
+
container.set(CORE_QUEUE_TOKEN, createAppQueue(driver, config.get(REDIS_URL_CONFIG_KEY) ?? process.env.REDIS_URL, failedJobs, registerDefaultJobs));
|
|
3708
3716
|
}
|
|
3709
3717
|
};
|
|
3710
3718
|
var queue_default = queueProvider;
|