@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.
@@ -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/logging/logger.ts
1357
- class Logger {
1358
- channel;
1359
- constructor(channel = "app") {
1360
- this.channel = channel;
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
- write(level, message, context = {}) {
1363
- const entry = {
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
- debug(message, context) {
1378
- this.write("debug", message, context);
1379
+ if (typeof error.code === "string" && /^\d{5}$/.test(error.code)) {
1380
+ return error.code;
1379
1381
  }
1380
- info(message, context) {
1381
- this.write("info", message, context);
1382
+ return;
1383
+ }
1384
+ function mapDatabaseError(error) {
1385
+ if (error instanceof HttpError) {
1386
+ return error;
1382
1387
  }
1383
- warn(message, context) {
1384
- this.write("warn", message, context);
1388
+ if (!isPostgresError(error)) {
1389
+ const message = error instanceof Error ? error.message : "Database operation failed.";
1390
+ return new BadRequestError(message);
1385
1391
  }
1386
- error(message, context) {
1387
- this.write("error", message, context);
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
- var appLogger = new Logger("app");
1391
-
1392
- // ../../src/bootstrap/contracts.ts
1393
- class ServiceContainer {
1394
- services = new Map;
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
- bind(key, factory) {
1409
- this.singletonFactories.delete(key);
1410
- this.services.delete(key);
1411
- this.bindings.set(key, factory);
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
- get(key) {
1414
- if (this.services.has(key)) {
1415
- return this.services.get(key);
1416
- }
1417
- const singletonFactory = this.singletonFactories.get(key);
1418
- if (singletonFactory) {
1419
- const value = singletonFactory(this);
1420
- this.services.set(key, value);
1421
- return value;
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
- throw new Error(`Service "${key}" is not registered.`);
1441
+ return qualifyColumn(table, column);
1428
1442
  }
1429
- resolve(key) {
1430
- return this.get(key);
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
- has(key) {
1433
- return this.services.has(key) || this.singletonFactories.has(key) || this.bindings.has(key);
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
- class ConfigStore {
1438
- values = new Map;
1439
- set(key, value) {
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
- get(key) {
1444
- return this.values.get(key);
1474
+ if (operator.isNull === false) {
1475
+ clauses.push(`${column} IS NOT NULL`);
1445
1476
  }
1446
- require(key) {
1447
- if (!this.values.has(key)) {
1448
- throw new Error(`Config key "${key}" is not defined.`);
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
- has(key) {
1453
- return this.values.has(key);
1484
+ if (operator.in !== undefined) {
1485
+ clauses.push(buildInClause(column, operator.in, params));
1454
1486
  }
1455
- }
1456
- var requiredDependencyKeys = [
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
- return dependency;
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 assertAppDependenciesComplete(dependencies) {
1469
- for (const key of requiredDependencyKeys) {
1470
- getRequiredDependency(dependencies, key);
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 resolveService(dependencies, token) {
1474
- return dependencies.container.resolve(token);
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
- // ../../src/bootstrap/applicationRegistry.ts
1478
- var activeContext;
1479
- function setActiveApplicationContext(context) {
1480
- activeContext = context;
1481
- }
1482
- function requireActiveApplicationContext() {
1483
- if (!activeContext) {
1484
- throw new Error("The application context has not been bootstrapped.");
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
- return activeContext;
1487
- }
1488
- function resolveApplicationCache() {
1489
- return getRequiredDependency(requireActiveApplicationContext().dependencies, "cache");
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
- // ../../src/core/database/repositoryConnection.ts
1522
- function resolveRepositoryConnection() {
1523
- return getBoundDatabaseConnection() ?? getDefaultDatabaseQuery();
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
- var repositoryConnection = new Proxy(function repositoryConnection2() {}, {
1526
- apply(_target, _thisArg, args) {
1527
- return resolveRepositoryConnection()(...args);
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
- const octets = match.slice(1, 5).map((part) => Number.parseInt(part, 10));
1552
- if (octets.some((octet) => octet < 0 || octet > 255)) {
1553
- return true;
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 [a = 0, b = 0] = octets;
1556
- if (a === 10) {
1557
- return true;
1584
+ const qualifiedColumn = qualifyColumn(table.name, column);
1585
+ if (options.onlyTrashed) {
1586
+ clauses.push(`${qualifiedColumn} IS NOT NULL`);
1587
+ return;
1558
1588
  }
1559
- if (a === 127) {
1560
- return true;
1589
+ if (!options.withTrashed) {
1590
+ clauses.push(`${qualifiedColumn} IS NULL`);
1561
1591
  }
1562
- if (a === 0) {
1563
- return true;
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
- if (a === 169 && b === 254) {
1566
- return true;
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 (a === 172 && b >= 16 && b <= 31) {
1569
- return true;
1614
+ if (Array.isArray(orderBy)) {
1615
+ return orderBy;
1570
1616
  }
1571
- if (a === 192 && b === 168) {
1572
- return true;
1617
+ if (isQueryOrder(orderBy)) {
1618
+ return [orderBy];
1573
1619
  }
1574
- return false;
1620
+ return Object.entries(orderBy).map(([column, direction]) => ({
1621
+ column,
1622
+ direction
1623
+ }));
1575
1624
  }
1576
- function isBlockedHostname(hostname) {
1577
- const normalized = hostname.trim().toLowerCase();
1578
- if (normalized.length === 0) {
1579
- return true;
1580
- }
1581
- if (BLOCKED_HOSTNAMES.has(normalized)) {
1582
- return true;
1583
- }
1584
- if (normalized.endsWith(".local") || normalized.endsWith(".internal")) {
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
- if (normalized.includes(":")) {
1588
- return true;
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
- return isPrivateIpv4(normalized);
1643
+ const body = appendWhereParts(tableName, having, params);
1644
+ return body.length > 0 ? ` HAVING ${body}` : "";
1591
1645
  }
1592
- function assertSafeOutboundUrl(rawUrl, options = {}) {
1593
- let parsed;
1594
- try {
1595
- parsed = new URL(rawUrl);
1596
- } catch {
1597
- throw new BadRequestError("Webhook URL is invalid.");
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 (parsed.protocol !== "https:" && !(options.allowHttp && parsed.protocol === "http:")) {
1600
- throw new BadRequestError("Webhook URL must use HTTPS.");
1657
+ if (!Number.isInteger(limit) || limit <= 0) {
1658
+ throw new Error("Query limit must be a positive integer.");
1601
1659
  }
1602
- if (parsed.username || parsed.password) {
1603
- throw new BadRequestError("Webhook URL must not include credentials.");
1660
+ return ` LIMIT ${limit}`;
1661
+ }
1662
+ function buildOffsetClause(offset) {
1663
+ if (offset === undefined) {
1664
+ return "";
1604
1665
  }
1605
- if (isBlockedHostname(parsed.hostname)) {
1606
- throw new BadRequestError("Webhook URL targets a blocked host.");
1666
+ if (!Number.isInteger(offset) || offset < 0) {
1667
+ throw new Error("Query offset must be a non-negative integer.");
1607
1668
  }
1608
- return parsed;
1669
+ return ` OFFSET ${offset}`;
1609
1670
  }
1610
- function isBlockedIpAddress(address) {
1611
- return isBlockedHostname(address.trim().toLowerCase());
1671
+ function buildReturningColumns(table) {
1672
+ return table.columns.map((column) => qualifyColumn(table.name, column)).join(", ");
1612
1673
  }
1613
- async function assertSafeOutboundUrlResolved(rawUrl, options = {}) {
1614
- const parsed = assertSafeOutboundUrl(rawUrl, options);
1615
- if (options.resolveDns === false) {
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 parsed;
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
- // ../../src/core/security/safeFetch.ts
1627
- var DEFAULT_FETCH_TIMEOUT_MS = 1e4;
1628
- async function safeFetch(input, init = {}, options = {}) {
1629
- const timeoutMs = options.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;
1630
- const maxRedirects = options.maxRedirects ?? 0;
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
- await repositoryConnection`
1708
- INSERT INTO webhook_delivery (webhook_id, event, payload, response_status)
1709
- VALUES (
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 factory();
1741
- }
1742
- names() {
1743
- return [...this.factories.keys()];
1744
- }
1702
+ return [[column, value]];
1703
+ });
1745
1704
  }
1746
- var jobRegistry = new JobRegistry;
1747
-
1748
- // ../../src/core/pagination/index.ts
1749
- function buildPaginationMeta(input) {
1750
- const lastPage = Math.max(1, Math.ceil(input.total / input.perPage));
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
- page: input.page,
1753
- per_page: input.perPage,
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
- // ../../src/core/database/errors.ts
1760
- function isPostgresError(error) {
1761
- return typeof error === "object" && error !== null && (("errno" in error) || ("code" in error));
1762
- }
1763
- function getPostgresSqlState(error) {
1764
- if (typeof error.errno === "string" && /^\d{5}$/.test(error.errno)) {
1765
- return error.errno;
1766
- }
1767
- if (typeof error.errno === "number") {
1768
- return String(error.errno).padStart(5, "0");
1769
- }
1770
- if (typeof error.code === "string" && /^\d{5}$/.test(error.code)) {
1771
- return error.code;
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
- async function withDatabaseErrorHandling(operation) {
1809
- try {
1810
- return await operation();
1811
- } catch (error) {
1812
- throw mapDatabaseError(error);
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
- // ../../src/core/database/query.ts
1817
- function quoteIdentifier(identifier) {
1818
- if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(identifier)) {
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 qualifyColumn(tableName, column) {
1824
- return `${quoteIdentifier(tableName)}.${quoteIdentifier(column)}`;
1825
- }
1826
- function resolveQualifiedColumn(defaultTable, columnName) {
1827
- if (columnName.includes(".")) {
1828
- const [table, column] = columnName.split(".", 2);
1829
- if (!table || !column) {
1830
- throw new Error(`Invalid qualified column: ${columnName}`);
1831
- }
1832
- return qualifyColumn(table, column);
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 parseQualifiedColumn(reference) {
1837
- const [table, column] = reference.split(".", 2);
1838
- if (!table || !column) {
1839
- throw new Error(`Join columns must be qualified as table.column: ${reference}`);
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
- return { table, column };
1842
- }
1843
- function normalizeDirection(direction = "ASC") {
1844
- return direction.toUpperCase() === "DESC" ? "DESC" : "ASC";
1845
- }
1846
- function isQueryOperator(value) {
1847
- return value !== null && !Array.isArray(value) && !(value instanceof Date) && typeof value === "object";
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 pushParam(values, value) {
1850
- values.push(value);
1851
- return `$${values.length}`;
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 buildInClause(column, values, params) {
1854
- if (values.length === 0) {
1855
- return "1 = 0";
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 placeholders = values.map((value) => pushParam(params, value)).join(", ");
1858
- return `${column} IN (${placeholders})`;
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 buildOperatorClauses(column, operator, params) {
1861
- const clauses = [];
1862
- if (operator.isNull === true) {
1863
- clauses.push(`${column} IS NULL`);
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
- if (operator.isNull === false) {
1866
- clauses.push(`${column} IS NOT NULL`);
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
- if (operator.eq !== undefined) {
1869
- if (operator.eq === null) {
1870
- clauses.push(`${column} IS NULL`);
1871
- } else {
1872
- clauses.push(`${column} = ${pushParam(params, operator.eq)}`);
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
- if (operator.in !== undefined) {
1876
- clauses.push(buildInClause(column, operator.in, params));
1877
- }
1878
- if (operator.gt !== undefined) {
1879
- clauses.push(`${column} > ${pushParam(params, operator.gt)}`);
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
- if (operator.tsMatch !== undefined) {
1894
- clauses.push(`${column} @@ plainto_tsquery('english', ${pushParam(params, operator.tsMatch)})`);
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 clauses;
1860
+ return result;
1897
1861
  }
1898
- function appendWhereParts(tableName, where, params) {
1899
- const clauses = [];
1900
- for (const [columnName, filterValue] of Object.entries(where)) {
1901
- if (filterValue === undefined) {
1902
- continue;
1903
- }
1904
- const column = resolveQualifiedColumn(tableName, columnName);
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
- if (filterValue === null) {
1914
- clauses.push(`${column} IS NULL`);
1871
+ const key = child[relation.morphIdKey];
1872
+ const group = groups.get(key);
1873
+ if (!group) {
1915
1874
  continue;
1916
1875
  }
1917
- clauses.push(`${column} = ${pushParam(params, filterValue)}`);
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 grouped.includes(" OR ") ? `(${grouped})` : grouped;
1878
+ return groups;
1930
1879
  }
1931
- function buildWhereGroupClause(tableName, nodes, params) {
1932
- let result = "";
1933
- for (const node of nodes) {
1934
- const part = buildWhereNodeClause(tableName, node, params);
1935
- if (!part) {
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
- if (!result) {
1939
- result = part;
1940
- continue;
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
- function buildAdvancedWhereClause(tableName, where = {}, whereNodes = [], params = []) {
1950
- const nodes = [];
1951
- if (Object.keys(where).length > 0) {
1952
- nodes.push({ kind: "and", where });
1953
- }
1954
- nodes.push(...whereNodes);
1955
- const combined = buildWhereGroupClause(tableName, nodes, params);
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
- function resolveSoftDeleteColumn(table) {
1962
- if (!table.softDeletes) {
1963
- return null;
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
- if (table.softDeletes === true) {
1966
- return "deleted_at";
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
- return table.softDeletes.column ?? "deleted_at";
1969
- }
1970
- function appendSoftDeleteScope(table, options, clauses) {
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
- const qualifiedColumn = qualifyColumn(table.name, column);
1976
- if (options.onlyTrashed) {
1977
- clauses.push(`${qualifiedColumn} IS NOT NULL`);
1978
- return;
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
- if (!options.withTrashed) {
1981
- clauses.push(`${qualifiedColumn} IS NULL`);
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
- function buildQueryWhereClause(table, options = {}, whereNodes = [], params = []) {
1985
- const { clause, params: whereParams } = buildAdvancedWhereClause(table.name, options.where ?? {}, whereNodes, params);
1986
- const softDeleteClauses = [];
1987
- appendSoftDeleteScope(table, options, softDeleteClauses);
1988
- if (softDeleteClauses.length === 0) {
1989
- return { clause, params: whereParams };
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
- const base = clause.replace(/^ WHERE /, "");
1992
- const scope = softDeleteClauses.join(" AND ");
1993
- return {
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
- if (Array.isArray(orderBy)) {
2006
- return orderBy;
1986
+ limit(limit) {
1987
+ this.queryOptions = { ...this.queryOptions, limit };
1988
+ return this;
2007
1989
  }
2008
- if (isQueryOrder(orderBy)) {
2009
- return [orderBy];
1990
+ offset(offset) {
1991
+ this.queryOptions = { ...this.queryOptions, offset };
1992
+ return this;
2010
1993
  }
2011
- return Object.entries(orderBy).map(([column, direction]) => ({
2012
- column,
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
- const columns = (Array.isArray(groupBy) ? groupBy : [groupBy]).map((column) => String(column));
2027
- const parts = columns.map((column) => resolveQualifiedColumn(tableName, column));
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
- const body = appendWhereParts(tableName, having, params);
2035
- return body.length > 0 ? ` HAVING ${body}` : "";
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
- if (!Number.isInteger(limit) || limit <= 0) {
2049
- throw new Error("Query limit must be a positive integer.");
2004
+ having(having) {
2005
+ this.queryOptions = { ...this.queryOptions, having };
2006
+ return this;
2050
2007
  }
2051
- return ` LIMIT ${limit}`;
2052
- }
2053
- function buildOffsetClause(offset) {
2054
- if (offset === undefined) {
2055
- return "";
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
- if (!Number.isInteger(offset) || offset < 0) {
2058
- throw new Error("Query offset must be a non-negative integer.");
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
- return ` OFFSET ${offset}`;
2061
- }
2062
- function buildReturningColumns(table) {
2063
- return table.columns.map((column) => qualifyColumn(table.name, column)).join(", ");
2064
- }
2065
- function buildSelectList(table, select, params = []) {
2066
- if (!select || select.length === 0) {
2067
- return buildReturningColumns(table);
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
- return select.map((item) => {
2070
- if (item.kind === "column") {
2071
- const column2 = qualifyColumn(item.table, item.column);
2072
- return item.as ? `${column2} AS ${quoteIdentifier(item.as)}` : column2;
2073
- }
2074
- if (item.kind === "literalText") {
2075
- return `${pushParam(params, item.value)}::text AS ${quoteIdentifier(item.as)}`;
2076
- }
2077
- const column = qualifyColumn(item.table, item.column);
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
- function buildGroupedCountQuery(table, column, where = {}, options = {}) {
2147
- const qualifiedColumn = qualifyColumn(table.name, column);
2148
- const { clause, params } = buildQueryWhereClause(table, {
2149
- where,
2150
- ...options
2151
- });
2152
- return {
2153
- text: `SELECT ${qualifiedColumn} AS ${quoteIdentifier("value")}, COUNT(*) AS ${quoteIdentifier("count")} FROM ${quoteIdentifier(table.name)}${clause} GROUP BY ${qualifiedColumn} ORDER BY ${qualifiedColumn} ASC`,
2154
- params
2155
- };
2156
- }
2157
- function buildInsertQuery(table, values) {
2158
- const entries = getDefinedColumnEntries(table, values);
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
- const params = [];
2163
- const columns = entries.map(([column]) => quoteIdentifier(column)).join(", ");
2164
- const placeholders = entries.map(([, value]) => pushParam(params, value)).join(", ");
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
- const params = [];
2179
- const setClause = entries.map(([column, value]) => `${quoteIdentifier(column)} = ${pushParam(params, value)}`).join(", ");
2180
- const primaryKeyPlaceholder = pushParam(params, id);
2181
- const returningColumns = buildReturningColumns(table);
2182
- const scopeClauses = [];
2183
- appendSoftDeleteScope(table, {}, scopeClauses);
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
- const returningColumns = buildReturningColumns(table);
2196
- const scopeClauses = [];
2197
- appendSoftDeleteScope(table, {}, scopeClauses);
2198
- const scopeSuffix = scopeClauses.length > 0 ? ` AND ${scopeClauses.join(" AND ")}` : "";
2199
- return {
2200
- text: `UPDATE ${quoteIdentifier(table.name)} SET ${quoteIdentifier(deletedAtColumn)} = $1 WHERE ${quoteIdentifier(table.primaryKey)} = $2${scopeSuffix} RETURNING ${returningColumns}`,
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
- const returningColumns = buildReturningColumns(table);
2210
- return {
2211
- text: `UPDATE ${quoteIdentifier(table.name)} SET ${quoteIdentifier(deletedAtColumn)} = $1 WHERE ${quoteIdentifier(table.primaryKey)} = $2 AND ${qualifyColumn(table.name, deletedAtColumn)} IS NOT NULL RETURNING ${returningColumns}`,
2212
- params: [null, id]
2213
- };
2214
- }
2215
- function buildDeleteByIdQuery(table, id) {
2216
- return {
2217
- text: `DELETE FROM ${quoteIdentifier(table.name)} WHERE ${quoteIdentifier(table.primaryKey)} = $1 RETURNING ${quoteIdentifier(table.primaryKey)} AS ${quoteIdentifier("deleted_id")}`,
2218
- params: [id]
2219
- };
2220
- }
2221
-
2222
- // ../../src/core/database/relationships.ts
2223
- function indexHasManyRelation(parents, children, relation) {
2224
- const groups = new Map;
2225
- for (const parent of parents) {
2226
- groups.set(parent[relation.localKey], []);
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
- for (const child of children) {
2229
- const key = child[relation.foreignKey];
2230
- const group = groups.get(key);
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
- group.push(child);
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
- function indexBelongsToRelation(children, parents, relation) {
2239
- const parentsById = new Map;
2240
- for (const parent of parents) {
2241
- parentsById.set(parent[relation.ownerKey], parent);
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
- const result = new Map;
2244
- for (const child of children) {
2245
- const foreignKey = child[relation.foreignKey];
2246
- const parent = parentsById.get(foreignKey);
2247
- if (parent) {
2248
- result.set(foreignKey, parent);
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
- return result;
2252
- }
2253
- function indexMorphManyRelation(parents, children, relation) {
2254
- const groups = new Map;
2255
- for (const parent of parents) {
2256
- groups.set(parent[relation.localKey], []);
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
- for (const child of children) {
2259
- if (child[relation.morphTypeKey] !== relation.morphType) {
2260
- continue;
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
- const key = child[relation.morphIdKey];
2263
- const group = groups.get(key);
2264
- if (!group) {
2265
- continue;
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
- return groups;
2270
- }
2271
- function indexMorphToRelation(children, parentsByType, relation) {
2272
- const result = new Map;
2273
- for (const child of children) {
2274
- const morphType = String(child[relation.morphTypeKey]);
2275
- const parents = parentsByType.get(morphType);
2276
- if (!parents) {
2277
- continue;
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 parent = parents.get(child[relation.morphIdKey]);
2280
- if (parent) {
2281
- result.set(child[relation.morphIdKey], parent);
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
- return result;
2285
- }
2286
-
2287
- // ../../src/core/database/whereBuilder.ts
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
- whereGroup(fn) {
2299
- const nested = new WhereBuilder;
2300
- fn(nested);
2301
- if (nested.nodes.length > 0) {
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
- return this;
2262
+ throw errorFactory?.(id) ?? new Error(`${this.table.name} ${String(id)} was not found.`);
2305
2263
  }
2306
- orWhereGroup(fn) {
2307
- const nested = new WhereBuilder;
2308
- fn(nested);
2309
- if (nested.nodes.length > 0) {
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
- // ../../src/core/database/repositoryQuery.ts
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
- where(input) {
2329
- if (typeof input === "function") {
2330
- const builder = new WhereBuilder;
2331
- input(builder);
2332
- this.whereNodes.push(...builder.nodes);
2333
- return this;
2334
- }
2335
- this.whereClause = { ...this.whereClause, ...input };
2336
- return this;
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
- orWhere(input) {
2339
- if (typeof input === "function") {
2340
- const builder = new WhereBuilder;
2341
- input(builder);
2342
- if (builder.nodes.length > 0) {
2343
- this.whereNodes.push({ kind: "or", group: builder.nodes });
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 this;
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.whereNodes.push({ kind: "or", where: input });
2348
- return this;
2305
+ throw errorFactory?.(id) ?? new Error(`${this.table.name} ${String(id)} was not found.`);
2349
2306
  }
2350
- orderBy(orderBy) {
2351
- this.queryOptions = { ...this.queryOptions, orderBy };
2352
- return this;
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
- limit(limit) {
2355
- this.queryOptions = { ...this.queryOptions, limit };
2356
- return this;
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
- offset(offset) {
2359
- this.queryOptions = { ...this.queryOptions, offset };
2360
- return this;
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
- join(left, right) {
2363
- return this.addJoin("inner", left, right);
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
- leftJoin(left, right) {
2366
- return this.addJoin("left", left, right);
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
- groupBy(groupBy) {
2369
- this.queryOptions = { ...this.queryOptions, groupBy };
2370
- return this;
2355
+ getConnection() {
2356
+ return this.connection;
2371
2357
  }
2372
- having(having) {
2373
- this.queryOptions = { ...this.queryOptions, having };
2374
- return this;
2358
+ getTable() {
2359
+ return this.table;
2375
2360
  }
2376
- withHasMany(as, relation, childRepository, options = {}) {
2377
- this.eagerLoads.push({
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
- withBelongsTo(as, relation, parentRepository, options = {}) {
2387
- this.eagerLoads.push({
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
- withMorphMany(as, relation, childRepository, options = {}) {
2397
- this.eagerLoads.push({
2398
- kind: "morphMany",
2399
- as,
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
- withMorphOne(as, relation, childRepository, options = {}) {
2407
- this.eagerLoads.push({
2408
- kind: "morphOne",
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
- withMorphTo(as, relation, repositoriesByType, options = {}) {
2417
- this.eagerLoads.push({
2418
- kind: "morphTo",
2419
- as,
2420
- relation,
2421
- repository: this.repository,
2422
- morphRepositories: repositoriesByType,
2423
- options
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 get() {
2428
- const rows = await this.repository.findAll(this.buildOptions());
2429
- return await this.attach(rows);
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 first() {
2432
- const rows = await this.get();
2433
- return rows[0] ?? null;
2397
+ async findByHasManyRelation(relation, parentId, options = {}) {
2398
+ return await this.findWhere({
2399
+ [relation.foreignKey]: parentId
2400
+ }, options);
2434
2401
  }
2435
- async paginate(options) {
2436
- return await this.repository.paginate({
2437
- ...this.buildOptions(),
2438
- page: options.page,
2439
- perPage: options.perPage
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
- buildOptions() {
2443
- return {
2444
- ...this.queryOptions,
2445
- where: this.whereClause,
2446
- whereNodes: this.whereNodes
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
- addJoin(type, left, right) {
2450
- const leftRef = parseQualifiedColumn(left);
2451
- const rightRef = parseQualifiedColumn(right);
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
- this.queryOptions = {
2460
- ...this.queryOptions,
2461
- joins: [
2462
- ...joins,
2463
- {
2464
- type,
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 attach(rows) {
2473
- if (rows.length === 0 || this.eagerLoads.length === 0) {
2474
- return rows.map((row) => ({ ...row }));
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
- let result = rows.map((row) => ({ ...row }));
2477
- for (const load of this.eagerLoads) {
2478
- if (load.kind === "hasMany") {
2479
- const relation2 = load.relation;
2480
- const grouped2 = await load.repository.withConnection(this.repository.getConnection()).loadHasManyForParents(rows, relation2, load.options);
2481
- result = result.map((row) => ({
2482
- ...row,
2483
- [load.as]: grouped2.get(row[relation2.localKey]) ?? []
2484
- }));
2485
- continue;
2486
- }
2487
- if (load.kind === "morphMany") {
2488
- const relation2 = load.relation;
2489
- const grouped2 = await load.repository.withConnection(this.repository.getConnection()).loadMorphManyForParents(rows, relation2, load.options);
2490
- result = result.map((row) => ({
2491
- ...row,
2492
- [load.as]: grouped2.get(row[relation2.localKey]) ?? []
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
- if (load.kind === "morphTo") {
2506
- const relation2 = load.relation;
2507
- const grouped2 = await this.repository.loadMorphToForChildren(rows, relation2, load.morphRepositories ?? new Map, load.options);
2508
- result = result.map((row) => ({
2509
- ...row,
2510
- [load.as]: grouped2.get(row[relation2.morphIdKey])
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
- const relation = load.relation;
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 result;
2470
+ return indexMorphToRelation(children, parentsByType, relation);
2522
2471
  }
2523
2472
  }
2524
-
2525
- // ../../src/core/database/baseRepository.ts
2526
- class BaseRepository5 {
2527
- table;
2528
- connection;
2529
- constructor(table, connection = repositoryConnection) {
2530
- this.table = table;
2531
- this.connection = connection;
2532
- }
2533
- async findAll(options = {}) {
2534
- return await withDatabaseErrorHandling(async () => {
2535
- const { whereNodes, ...queryOptions } = options;
2536
- const { text, params } = buildSelectQuery(this.table, queryOptions, whereNodes ?? []);
2537
- return await this.connection.unsafe(text, params);
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
- async paginate(options) {
2541
- const { whereNodes, where = {}, page, perPage, ...queryOptions } = options;
2542
- const total = await this.countWhere(where, {
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
- async chunk(count, callback, options = {}) {
2562
- if (!Number.isInteger(count) || count <= 0) {
2563
- throw new Error("Chunk size must be a positive integer.");
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
- async cursorPaginate(options) {
2583
- const {
2584
- perPage,
2585
- cursor,
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
- const cursorWhere = { ...where };
2596
- if (cursor !== undefined) {
2597
- cursorWhere[cursorColumn] = direction === "asc" ? { gt: cursor } : { lt: cursor };
2507
+ if (typeof value === "number") {
2508
+ this.defaultValue = String(value);
2509
+ return this;
2598
2510
  }
2599
- const rows = await this.findAll({
2600
- ...queryOptions,
2601
- where: cursorWhere,
2602
- whereNodes,
2603
- orderBy: { [cursorColumn]: direction },
2604
- limit: perPage + 1
2605
- });
2606
- const hasMore = rows.length > perPage;
2607
- const data = hasMore ? rows.slice(0, perPage) : rows;
2608
- const nextCursor = hasMore ? data[data.length - 1]?.[cursorColumn] ?? null : null;
2609
- const prevCursor = cursor ?? null;
2610
- return {
2611
- data,
2612
- meta: {
2613
- per_page: perPage,
2614
- next_cursor: nextCursor,
2615
- prev_cursor: prevCursor,
2616
- has_more: hasMore
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
- async findById(id) {
2621
- return await this.firstOrNull({
2622
- [this.table.primaryKey]: id
2623
- });
2544
+ constrained(table) {
2545
+ const referencesTable = table ?? inferReferencedTable(this.name);
2546
+ return this.references(referencesTable);
2624
2547
  }
2625
- async findByIdOrThrow(id, errorFactory) {
2626
- const record = await this.findById(id);
2627
- if (record) {
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
- throw errorFactory?.(id) ?? new Error(`${this.table.name} ${String(id)} was not found.`);
2552
+ this.foreignKey.onDelete = "cascade";
2553
+ return this;
2631
2554
  }
2632
- async findByIds(ids) {
2633
- const uniqueIds = [...new Set(ids)];
2634
- if (uniqueIds.length === 0) {
2635
- return [];
2555
+ nullOnDelete() {
2556
+ if (!this.foreignKey) {
2557
+ throw new Error(`Foreign key is not defined for column ${this.name}`);
2636
2558
  }
2637
- return await this.findWhere({
2638
- [this.table.primaryKey]: uniqueIds
2639
- });
2559
+ this.foreignKey.onDelete = "set null";
2560
+ return this;
2640
2561
  }
2641
- async firstOrNull(where, options = {}) {
2642
- const [record] = await this.findAll({ ...options, where, limit: 1 });
2643
- return record ?? null;
2562
+ }
2563
+ function inferReferencedTable(columnName) {
2564
+ if (!columnName.endsWith("_id")) {
2565
+ throw new Error(`Cannot infer referenced table from column ${columnName}`);
2644
2566
  }
2645
- async create(values) {
2646
- return await withDatabaseErrorHandling(async () => {
2647
- const { text, params } = buildInsertQuery(this.table, values);
2648
- const [record] = await this.connection.unsafe(text, params);
2649
- if (!record) {
2650
- throw new Error(`Insert into ${this.table.name} did not return a record.`);
2651
- }
2652
- const entity = record;
2653
- await eventBus.dispatch(modelEventName(this.table.name, "created"), entity);
2654
- return entity;
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
- async updateById(id, changes) {
2658
- return await withDatabaseErrorHandling(async () => {
2659
- const { text, params } = buildUpdateQuery(this.table, id, changes);
2660
- const [record] = await this.connection.unsafe(text, params);
2661
- const entity = record ?? null;
2662
- if (entity) {
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
- async updateByIdOrThrow(id, changes, errorFactory) {
2669
- const record = await this.updateById(id, changes);
2670
- if (record) {
2671
- return record;
2672
- }
2673
- throw errorFactory?.(id) ?? new Error(`${this.table.name} ${String(id)} was not found.`);
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
- async deleteById(id) {
2676
- if (resolveSoftDeleteColumn(this.table)) {
2677
- return await this.softDeleteById(id);
2678
- }
2679
- return await this.forceDeleteById(id);
2596
+ text(name) {
2597
+ const column = new ColumnDefinition(name, "text");
2598
+ column.notNullable();
2599
+ this.columns.push(column);
2600
+ return column;
2680
2601
  }
2681
- async softDeleteById(id) {
2682
- return await withDatabaseErrorHandling(async () => {
2683
- const { text, params } = buildSoftDeleteByIdQuery(this.table, id, new Date);
2684
- const [record] = await this.connection.unsafe(text, params);
2685
- if (!record) {
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
- async forceDeleteById(id) {
2693
- return await withDatabaseErrorHandling(async () => {
2694
- const { text, params } = buildDeleteByIdQuery(this.table, id);
2695
- const [row] = await this.connection.unsafe(text, params);
2696
- if (!row) {
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
- async restoreById(id) {
2706
- return await withDatabaseErrorHandling(async () => {
2707
- const { text, params } = buildRestoreByIdQuery(this.table, id);
2708
- const [record] = await this.connection.unsafe(text, params);
2709
- if (!record) {
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
- withConnection(connection) {
2718
- const clone = Object.create(Object.getPrototypeOf(this));
2719
- Object.assign(clone, this);
2720
- clone.connection = connection;
2721
- return clone;
2620
+ timestamp(name) {
2621
+ const column = new ColumnDefinition(name, "timestamp");
2622
+ column.notNullable();
2623
+ this.columns.push(column);
2624
+ return column;
2722
2625
  }
2723
- getConnection() {
2724
- return this.connection;
2626
+ json(name) {
2627
+ const column = new ColumnDefinition(name, "json");
2628
+ column.notNullable();
2629
+ this.columns.push(column);
2630
+ return column;
2725
2631
  }
2726
- getTable() {
2727
- return this.table;
2632
+ jsonb(name) {
2633
+ const column = new ColumnDefinition(name, "jsonb");
2634
+ column.notNullable();
2635
+ this.columns.push(column);
2636
+ return column;
2728
2637
  }
2729
- query(where = {}) {
2730
- return new RepositoryQuery(this, where);
2638
+ foreignId(name) {
2639
+ const column = new ForeignIdColumnDefinition(name);
2640
+ this.columns.push(column);
2641
+ return column;
2731
2642
  }
2732
- async findWhere(where, options = {}) {
2733
- return await this.findAll({ ...options, where });
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
- async countWhere(where = {}, options = {}, whereNodes = []) {
2736
- const { text, params } = buildCountQuery(this.table, where, options, whereNodes);
2737
- const [row] = await this.connection.unsafe(text, params);
2738
- return Number(row?.count ?? 0);
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
- async averageColumn(column, where = {}) {
2741
- const qualifiedColumn = qualifyColumn(this.table.name, column);
2742
- return await this.averageExpression(`AVG(${qualifiedColumn})`, "value", where);
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
- async averageExpression(expression, alias, where = {}) {
2745
- const { text, params } = buildProjectionQuery(this.table, expression, alias, { where });
2746
- const [row] = await this.connection.unsafe(text, params);
2747
- return Math.round(Number(row?.[alias] ?? 0));
2684
+ fullText(columns, name) {
2685
+ this.indexes.push({
2686
+ name,
2687
+ columns: Array.isArray(columns) ? columns : [columns],
2688
+ kind: "fullText"
2689
+ });
2748
2690
  }
2749
- async pluckNumberValues(expression, alias, options = {}) {
2750
- const { text, params } = buildProjectionQuery(this.table, expression, alias, options);
2751
- const rows = await this.connection.unsafe(text, params);
2752
- return rows.flatMap((row) => {
2753
- const value = row[alias];
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
- async countGroupedBy(column, where = {}) {
2758
- const { text, params } = buildGroupedCountQuery(this.table, column, where);
2759
- const rows = await this.connection.unsafe(text, params);
2760
- return rows.map(({ value, count }) => ({
2761
- value,
2762
- count: Number(count)
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
- async findByHasManyRelation(relation, parentId, options = {}) {
2766
- return await this.findWhere({
2767
- [relation.foreignKey]: parentId
2768
- }, options);
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
- async loadHasManyForParents(parents, relation, options = {}) {
2771
- if (parents.length === 0) {
2772
- return indexHasManyRelation(parents, [], relation);
2773
- }
2774
- const parentIds = [...new Set(parents.map((parent) => parent[relation.localKey]))];
2775
- const children = await this.findWhere({
2776
- [relation.foreignKey]: parentIds
2777
- }, options);
2778
- return indexHasManyRelation(parents, children, relation);
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
- async loadBelongsToForParents(children, relation, parentRepository, options = {}) {
2781
- if (children.length === 0) {
2782
- return new Map;
2783
- }
2784
- const ownerIds = [...new Set(children.map((child) => child[relation.foreignKey]))];
2785
- const parents = await parentRepository.withConnection(this.connection).findWhere({
2786
- [relation.ownerKey]: ownerIds
2787
- }, options);
2788
- return indexBelongsToRelation(children, parents, relation);
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
- async loadMorphManyForParents(parents, relation, options = {}) {
2791
- if (parents.length === 0) {
2792
- return indexMorphManyRelation(parents, [], relation);
2793
- }
2794
- const parentIds = [...new Set(parents.map((parent) => parent[relation.localKey]))];
2795
- const children = await this.findWhere({
2796
- [relation.morphTypeKey]: relation.morphType,
2797
- [relation.morphIdKey]: parentIds
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
- async loadMorphOneForParents(parents, relation, options = {}) {
2802
- const grouped = await this.loadMorphManyForParents(parents, relation, options);
2803
- const result = new Map;
2804
- for (const parent of parents) {
2805
- const matches = grouped.get(parent[relation.localKey]) ?? [];
2806
- result.set(parent[relation.localKey], matches[0]);
2807
- }
2808
- return result;
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
- async loadMorphToForChildren(children, relation, repositoriesByType, options = {}) {
2811
- if (children.length === 0) {
2812
- return new Map;
2813
- }
2814
- const idsByType = new Map;
2815
- for (const child of children) {
2816
- const morphType = String(child[relation.morphTypeKey]);
2817
- const morphId = child[relation.morphIdKey];
2818
- const ids = idsByType.get(morphType) ?? new Set;
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
- var baseRepository_default = BaseRepository5;
2842
- // ../../src/core/database/model.ts
2843
- var modelRepositories = new WeakMap;
2844
- var modelGlobalScopes = new WeakMap;
2845
- var modelBooted = new WeakSet;
2846
- // ../../src/core/database/schema/columnDefinition.ts
2847
- class ColumnDefinition {
2848
- name;
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
- nullable() {
2863
- this.isNullable = true;
2864
- return this;
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
- notNullable() {
2867
- this.isNullable = false;
2868
- return this;
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
- default(value) {
2871
- if (typeof value === "boolean") {
2872
- this.defaultValue = value ? "TRUE" : "FALSE";
2873
- return this;
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 (typeof value === "number") {
2876
- this.defaultValue = String(value);
2877
- return this;
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
- defaultRaw(expression) {
2883
- this.defaultValue = expression;
2884
- return this;
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
- unique() {
2887
- this.isUnique = true;
2888
- return this;
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
- primary() {
2891
- this.isPrimary = true;
2892
- return this;
2860
+ for (const indexName of blueprint.droppedIndexes) {
2861
+ statements.push(`DROP INDEX IF EXISTS ${quoteIdentifier(indexName)}`);
2893
2862
  }
2894
- check(expression) {
2895
- this.checkExpression = expression;
2896
- return this;
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
- class ForeignIdColumnDefinition extends ColumnDefinition {
2901
- constructor(name) {
2902
- super(name, "foreignId");
2903
- this.notNullable();
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
- references(table, column = "id") {
2906
- this.foreignKey = {
2907
- referencesTable: table,
2908
- referencesColumn: column
2909
- };
2910
- return this;
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
- constrained(table) {
2913
- const referencesTable = table ?? inferReferencedTable(this.name);
2914
- return this.references(referencesTable);
2892
+ if (column.defaultValue !== undefined) {
2893
+ parts.push(`DEFAULT ${column.defaultValue}`);
2915
2894
  }
2916
- cascadeOnDelete() {
2917
- if (!this.foreignKey) {
2918
- throw new Error(`Foreign key is not defined for column ${this.name}`);
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
- this.foreignKey.onDelete = "cascade";
2921
- return this;
2910
+ parts.push(clause);
2922
2911
  }
2923
- nullOnDelete() {
2924
- if (!this.foreignKey) {
2925
- throw new Error(`Foreign key is not defined for column ${this.name}`);
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
- this.foreignKey.onDelete = "set null";
2928
- return this;
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 inferReferencedTable(columnName) {
2932
- if (!columnName.endsWith("_id")) {
2933
- throw new Error(`Cannot infer referenced table from column ${columnName}`);
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
- return columnName.slice(0, -3);
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/blueprint.ts
2939
- class Blueprint {
2940
- table;
2941
- action;
2942
- columns = [];
2943
- indexes = [];
2944
- droppedColumns = [];
2945
- droppedIndexes = [];
2946
- constructor(table, action) {
2947
- this.table = table;
2948
- this.action = action;
2949
- }
2950
- id(name = "id") {
2951
- const column = new ColumnDefinition(name, "id");
2952
- column.primary();
2953
- column.autoIncrement = true;
2954
- this.columns.push(column);
2955
- return column;
2956
- }
2957
- string(name, length) {
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
- json(name) {
2995
- const column = new ColumnDefinition(name, "json");
2996
- column.notNullable();
2997
- this.columns.push(column);
2998
- return column;
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
- jsonb(name) {
3001
- const column = new ColumnDefinition(name, "jsonb");
3002
- column.notNullable();
3003
- this.columns.push(column);
3004
- return column;
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
- foreignId(name) {
3007
- const column = new ForeignIdColumnDefinition(name);
3008
- this.columns.push(column);
3009
- return column;
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
- timestamps() {
3012
- this.timestamp("created_at").defaultRaw("NOW()");
3013
- this.timestamp("updated_at").defaultRaw("NOW()");
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
- softDeletes() {
3016
- this.timestamp("deleted_at").nullable();
3034
+ toSql() {
3035
+ return [...this.#statements];
3017
3036
  }
3018
- dropColumn(name) {
3019
- this.droppedColumns.push(name);
3037
+ async execute(db2) {
3038
+ for (const statement of this.#statements) {
3039
+ await db2.unsafe(statement);
3040
+ }
3020
3041
  }
3021
- dropSoftDeletes() {
3022
- this.dropColumn("deleted_at");
3023
- this.dropIndex(`idx_${this.table}_deleted_at`);
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
- dropIndex(name) {
3026
- this.droppedIndexes.push(name);
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
- unique(columns, name) {
3029
- this.indexes.push({
3030
- name,
3031
- columns: Array.isArray(columns) ? columns : [columns],
3032
- kind: "unique"
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
- index(columns, options = {}) {
3036
- this.indexes.push({
3037
- name: options.name,
3038
- columns: Array.isArray(columns) ? columns : [columns],
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
- partialIndex(columns, where, nameOrOptions) {
3044
- const options = typeof nameOrOptions === "string" ? { name: nameOrOptions } : nameOrOptions ?? {};
3045
- this.indexes.push({
3046
- name: options.name,
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
- fullText(columns, name) {
3053
- this.indexes.push({
3054
- name,
3055
- columns: Array.isArray(columns) ? columns : [columns],
3056
- kind: "fullText"
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
- ginIndex(column, name) {
3060
- this.indexes.push({
3061
- name,
3062
- columns: [column],
3063
- kind: "gin"
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
- // ../../src/core/database/schema/errors.ts
3068
- class UnsupportedSchemaFeatureError extends Error {
3069
- constructor(feature, driver) {
3070
- super(`${feature} is not supported for the ${driver} driver`);
3071
- this.name = "UnsupportedSchemaFeatureError";
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
- // ../../src/core/database/schema/grammars/grammar.ts
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
- function compileIdType(driver) {
3101
- switch (driver) {
3102
- case "pgsql":
3103
- return "SERIAL";
3104
- case "mysql":
3105
- return "BIGINT UNSIGNED";
3106
- case "sqlite":
3107
- return "INTEGER";
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
- function compileStringType(driver, length) {
3111
- switch (driver) {
3112
- case "pgsql":
3113
- return "TEXT";
3114
- case "mysql":
3115
- return length ? `VARCHAR(${length})` : "VARCHAR(255)";
3116
- case "sqlite":
3117
- return "TEXT";
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
- function compileTextType(driver) {
3121
- switch (driver) {
3122
- case "pgsql":
3123
- case "sqlite":
3124
- return "TEXT";
3125
- case "mysql":
3126
- return "TEXT";
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
- function compileBooleanType(driver) {
3130
- switch (driver) {
3131
- case "pgsql":
3132
- return "BOOLEAN";
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
- function compileIntegerType(driver) {
3140
- switch (driver) {
3141
- case "pgsql":
3142
- return "INTEGER";
3143
- case "mysql":
3144
- return "INT";
3145
- case "sqlite":
3146
- return "INTEGER";
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
- function compileBigIntegerType(driver) {
3150
- switch (driver) {
3151
- case "pgsql":
3152
- return "BIGINT";
3153
- case "mysql":
3154
- return "BIGINT";
3155
- case "sqlite":
3156
- return "INTEGER";
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
- function compileTimestampType(driver) {
3160
- switch (driver) {
3161
- case "pgsql":
3162
- return "TIMESTAMPTZ";
3163
- case "mysql":
3164
- return "TIMESTAMP";
3165
- case "sqlite":
3166
- return "TEXT";
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
- function compileJsonType(driver) {
3170
- switch (driver) {
3171
- case "pgsql":
3172
- return "JSONB";
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 compileJsonbType(driver) {
3180
- switch (driver) {
3181
- case "pgsql":
3182
- return "JSONB";
3183
- case "mysql":
3184
- return "JSON";
3185
- case "sqlite":
3186
- return "TEXT";
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/database/schema/grammars/compileStatements.ts
3191
- function compileCreateTable(driver, blueprint) {
3192
- const table = quoteIdentifier(blueprint.table);
3193
- const parts = blueprint.columns.map((column) => compileColumn(driver, column, "create"));
3194
- for (const index of blueprint.indexes) {
3195
- if (index.kind === "unique" && index.columns.length > 1) {
3196
- const columns = index.columns.map((column) => quoteIdentifier(column)).join(", ");
3197
- parts.push(`UNIQUE (${columns})`);
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 statements = [`CREATE TABLE IF NOT EXISTS ${table} (
3201
- ${parts.join(`,
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
- return statements;
3215
- }
3216
- function compileAlterTable(driver, blueprint) {
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
- for (const columnName of blueprint.droppedColumns) {
3225
- const dropPrefix = driver === "pgsql" ? "DROP COLUMN IF EXISTS" : "DROP COLUMN";
3226
- statements.push(`ALTER TABLE ${table} ${dropPrefix} ${quoteIdentifier(columnName)}`);
3275
+ if (a === 127) {
3276
+ return true;
3227
3277
  }
3228
- for (const indexName of blueprint.droppedIndexes) {
3229
- statements.push(`DROP INDEX IF EXISTS ${quoteIdentifier(indexName)}`);
3278
+ if (a === 0) {
3279
+ return true;
3230
3280
  }
3231
- for (const index of blueprint.indexes) {
3232
- if (index.kind === "index" || index.kind === "unique") {
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
- return statements;
3239
- }
3240
- function compileDropTable(driver, tableName) {
3241
- const cascade = driver === "pgsql" ? " CASCADE" : "";
3242
- return [`DROP TABLE IF EXISTS ${quoteIdentifier(tableName)}${cascade}`];
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 compileColumn(driver, column, mode) {
3245
- const parts = [quoteIdentifier(column.name), compileColumnType(driver, column)];
3246
- if (column.autoIncrement && driver === "mysql") {
3247
- parts[1] = `${parts[1]} AUTO_INCREMENT`;
3292
+ function isBlockedHostname(hostname) {
3293
+ const normalized = hostname.trim().toLowerCase();
3294
+ if (normalized.length === 0) {
3295
+ return true;
3248
3296
  }
3249
- if (column.isPrimary && mode === "create") {
3250
- if (driver === "sqlite") {
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 (column.defaultValue !== undefined) {
3261
- parts.push(`DEFAULT ${column.defaultValue}`);
3300
+ if (normalized.endsWith(".local") || normalized.endsWith(".internal")) {
3301
+ return true;
3262
3302
  }
3263
- if (column.isUnique) {
3264
- parts.push("UNIQUE");
3303
+ if (normalized.includes(":")) {
3304
+ return true;
3265
3305
  }
3266
- if (column.checkExpression) {
3267
- parts.push(`CHECK (${column.checkExpression})`);
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 (column.foreignKey) {
3270
- const { referencesTable, referencesColumn, onDelete } = column.foreignKey;
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 parts.join(" ");
3324
+ return parsed;
3281
3325
  }
3282
- function compileIndex(_driver, tableName, index) {
3283
- const indexName = index.name ?? defaultIndexName(tableName, index.columns, index.kind === "unique" ? "unique" : "index");
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 compileSpecialIndex(driver, tableName, index) {
3295
- const indexName = index.name ?? defaultIndexName(tableName, index.columns, index.kind);
3296
- const columns = index.columns.map((column) => quoteIdentifier(column)).join(", ");
3297
- switch (index.kind) {
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
- function defaultIndexName(tableName, columns, kind) {
3332
- return `idx_${tableName}_${columns.join("_")}_${kind}`;
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
- // ../../src/core/database/schema/grammars/createGrammar.ts
3347
- function createGrammar(driver) {
3348
- return {
3349
- driver,
3350
- compile(blueprint) {
3351
- return compileBlueprint(driver, blueprint);
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/database/schema/grammars/mysqlGrammar.ts
3357
- var MySqlGrammar = createGrammar("mysql");
3358
-
3359
- // ../../src/core/database/schema/grammars/postgresGrammar.ts
3360
- var PostgresGrammar = createGrammar("pgsql");
3361
-
3362
- // ../../src/core/database/schema/grammars/sqliteGrammar.ts
3363
- var SqliteGrammar = createGrammar("sqlite");
3364
-
3365
- // ../../src/core/database/schema/grammars/index.ts
3366
- function grammarForDriver(driver) {
3367
- switch (driver) {
3368
- case "pgsql":
3369
- return PostgresGrammar;
3370
- case "mysql":
3371
- return MySqlGrammar;
3372
- case "sqlite":
3373
- return SqliteGrammar;
3374
- default:
3375
- throw new Error(`Unsupported database driver: ${driver}`);
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
- // ../../src/core/database/schema/schema.ts
3379
- class SchemaBuilder {
3380
- #driver;
3381
- #statements = [];
3382
- constructor(driver) {
3383
- this.#driver = driver;
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
- table(table, callback) {
3392
- const blueprint = new Blueprint(table, "alter");
3393
- callback(blueprint);
3394
- this.#statements.push(...grammarForDriver(this.#driver).compile(blueprint));
3395
- return this;
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
- drop(table) {
3398
- const blueprint = new Blueprint(table, "drop");
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
- toSql() {
3403
- return [...this.#statements];
3460
+ info(message, context) {
3461
+ this.write("info", message, context);
3404
3462
  }
3405
- async execute(db2) {
3406
- for (const statement of this.#statements) {
3407
- await db2.unsafe(statement);
3408
- }
3463
+ warn(message, context) {
3464
+ this.write("warn", message, context);
3409
3465
  }
3410
- }
3411
- // ../../src/core/database/table.ts
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 failedJobRepository_default = FailedJobRepository;
3470
+ var appLogger = new Logger("app");
3430
3471
 
3431
- // ../../src/core/queue/failedJobService.ts
3432
- class FailedJobService {
3433
- repository;
3434
- constructor(repository) {
3435
- this.repository = repository;
3436
- }
3437
- async recordFailure(input) {
3438
- return await this.repository.create({
3439
- job_name: input.jobName,
3440
- payload: input.payload,
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
- listRecent(limit = 50) {
3446
- return this.repository.findAll({
3447
- limit,
3448
- orderBy: { column: "failed_at", direction: "DESC" }
3449
- });
3483
+ singleton(key, factory) {
3484
+ this.bindings.delete(key);
3485
+ this.services.delete(key);
3486
+ this.singletonFactories.set(key, factory);
3450
3487
  }
3451
- async retry(id) {
3452
- const failedJob = await this.repository.findByIdOrThrow(id, (jobId) => new Error(`Failed job ${jobId} not found.`));
3453
- await this.repository.deleteById(id);
3454
- return failedJob;
3488
+ bind(key, factory) {
3489
+ this.singletonFactories.delete(key);
3490
+ this.services.delete(key);
3491
+ this.bindings.set(key, factory);
3455
3492
  }
3456
- async delete(id) {
3457
- const deleted = await this.repository.deleteById(id);
3458
- if (!deleted) {
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
- async flush() {
3463
- const jobs = await this.repository.findAll();
3464
- let deleted = 0;
3465
- for (const job of jobs) {
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
- return deleted;
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
- var failedJobService_default = FailedJobService;
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
- const attempts = envelope.attempts ?? 0;
3482
- try {
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
- // ../../src/core/queue/redisQueue.ts
3506
- var {RedisClient: RedisClient2 } = globalThis.Bun;
3507
- var QUEUE_LIST_KEY = "workhub:queue:default";
3508
- var QUEUE_HIGH_KEY = "workhub:queue:high";
3509
- var QUEUE_LOW_KEY = "workhub:queue:low";
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
- class RedisQueue {
3521
- client;
3522
- constructor(redisUrl) {
3523
- this.client = new RedisClient2(redisUrl);
3523
+ get(key) {
3524
+ return this.values.get(key);
3524
3525
  }
3525
- async dispatch(job, payload) {
3526
- const name = jobRegistry.resolveName(job);
3527
- if (!name) {
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
- const envelope = {
3531
- name,
3532
- payload,
3533
- attempts: 0
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
- // ../../src/core/queue/resilientQueue.ts
3541
- class ResilientQueue {
3542
- failedJobs;
3543
- asyncDispatch;
3544
- constructor(failedJobs, asyncDispatch = false) {
3545
- this.failedJobs = failedJobs;
3546
- this.asyncDispatch = asyncDispatch;
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
- async dispatch(job, payload) {
3549
- const name = jobRegistry.resolveName(job);
3550
- if (!name) {
3551
- throw new Error("Job is not registered with the queue worker registry.");
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
- // ../../src/core/queue/publicQueue.ts
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
- function createTrackedJob(name, job) {
3575
- return jobRegistry.track(name, job);
3556
+
3557
+ // ../../src/bootstrap/applicationRegistry.ts
3558
+ var activeContext;
3559
+ function setActiveApplicationContext(context) {
3560
+ activeContext = context;
3576
3561
  }
3577
- function createProductionQueue(driver, options = {}) {
3578
- options.registerJobs?.();
3579
- const failedJobs = options.failedJobs ?? createFailedJobService();
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 new ResilientQueue(failedJobs, driver === "async");
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/core/queue/createAppQueue.ts
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
- function createAppQueue(driver, redisUrl, failedJobs = createFailedJobService()) {
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: registerDefaultJobs
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;