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