@getstrata/bootstrap 0.2.7 → 0.2.9

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