@getstrata/core 0.5.52 → 0.5.54
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -3
- package/dist/core/queue/failedJobRepository.d.ts +1 -1
- package/dist/core/queue/failedJobTable.d.ts +1 -1
- package/dist/entries/auth/accessControl.js +60 -1
- package/dist/entries/auth/membershipMiddleware.js +21 -0
- package/dist/entries/auth/membershipScope.js +336 -1
- package/dist/entries/auth/membershipService.js +304 -1
- package/dist/entries/auth/policy.js +81 -1
- package/dist/entries/auth/sessionGuard.js +8 -7
- package/dist/entries/database/repositoryQuery.js +655 -0
- package/dist/entries/database/whereBuilder.js +32 -0
- package/dist/entries/http/formRequest.js +102 -0
- package/dist/entries/http/pagination.js +102 -0
- package/dist/entries/http/routeModelBinding.js +102 -0
- package/dist/entries/http/securedRouteModelBinding.js +102 -0
- package/dist/entries/http/validation.js +145 -0
- package/dist/entries/http/webErrorResponse.js +8 -7
- package/dist/entries/http/webFormRequest.js +102 -0
- package/dist/entries/queue/createAppQueue.js +2 -2
- package/dist/entries/queue/failedJobRepository.js +2 -2
- package/dist/entries/queue/publicQueue.js +2 -2
- package/dist/entries/queue/queueMetrics.js +2 -2
- package/dist/entries/view.js +8 -7
- package/dist/index.js +2215 -2213
- package/dist/modules/user/apiTokenRepository.d.ts +1 -1
- package/dist/modules/user/apiTokenTable.d.ts +1 -1
- package/dist/modules/user/notificationRepository.d.ts +1 -1
- package/dist/modules/user/notificationTable.d.ts +1 -1
- package/dist/modules/user/oauthIdentityRepository.d.ts +2 -2
- package/dist/modules/user/repository.d.ts +1 -1
- package/dist/modules/user/table.d.ts +1 -1
- package/package.json +17 -2
package/dist/index.js
CHANGED
|
@@ -1530,2580 +1530,2569 @@ class BaseRepository {
|
|
|
1530
1530
|
}
|
|
1531
1531
|
}
|
|
1532
1532
|
var baseRepository_default = BaseRepository;
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
}
|
|
1539
|
-
};
|
|
1533
|
+
|
|
1534
|
+
// ../../src/config/database.ts
|
|
1535
|
+
function readInteger(name, fallback) {
|
|
1536
|
+
const parsed = Number.parseInt(process.env[name] ?? String(fallback), 10);
|
|
1537
|
+
return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback;
|
|
1540
1538
|
}
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1539
|
+
var databaseConfig = {
|
|
1540
|
+
url: process.env.DATABASE_URL ?? "",
|
|
1541
|
+
poolMax: readInteger("DB_POOL_MAX", 10),
|
|
1542
|
+
idleTimeoutSeconds: readInteger("DB_POOL_IDLE_TIMEOUT", 30),
|
|
1543
|
+
maxLifetimeSeconds: readInteger("DB_POOL_MAX_LIFETIME", 3600),
|
|
1544
|
+
connectionTimeoutSeconds: readInteger("DB_CONNECTION_TIMEOUT", 10)
|
|
1545
|
+
};
|
|
1546
|
+
|
|
1547
|
+
// ../../src/db/connection/createConnection.ts
|
|
1548
|
+
var {SQL } = globalThis.Bun;
|
|
1549
|
+
function createDatabaseConnection(config) {
|
|
1550
|
+
if (!config.url) {
|
|
1551
|
+
throw new Error("DATABASE_URL is not configured. Set DATABASE_URL before starting the app or running integration tests.");
|
|
1549
1552
|
}
|
|
1550
|
-
return
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1553
|
+
return new SQL({
|
|
1554
|
+
url: config.url,
|
|
1555
|
+
max: config.poolMax,
|
|
1556
|
+
idleTimeout: config.idleTimeoutSeconds,
|
|
1557
|
+
maxLifetime: config.maxLifetimeSeconds,
|
|
1558
|
+
connectionTimeout: config.connectionTimeoutSeconds
|
|
1559
|
+
});
|
|
1554
1560
|
}
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
if (
|
|
1562
|
-
|
|
1561
|
+
|
|
1562
|
+
// ../../src/db/connection/index.ts
|
|
1563
|
+
var connectionHolder = {
|
|
1564
|
+
connection: null
|
|
1565
|
+
};
|
|
1566
|
+
function getDatabase() {
|
|
1567
|
+
if (!connectionHolder.connection) {
|
|
1568
|
+
connectionHolder.connection = createDatabaseConnection(databaseConfig);
|
|
1569
|
+
registerDefaultDatabasePool(connectionHolder.connection);
|
|
1563
1570
|
}
|
|
1571
|
+
return connectionHolder.connection;
|
|
1564
1572
|
}
|
|
1565
|
-
function
|
|
1566
|
-
|
|
1573
|
+
function getDb() {
|
|
1574
|
+
getDatabase();
|
|
1575
|
+
return getDefaultDatabaseQuery();
|
|
1567
1576
|
}
|
|
1568
|
-
function
|
|
1569
|
-
|
|
1570
|
-
return
|
|
1571
|
-
}
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
case "json":
|
|
1577
|
-
return typeof value === "string" ? JSON.parse(value) : value;
|
|
1578
|
-
case "bool":
|
|
1579
|
-
case "boolean":
|
|
1580
|
-
return value === true || value === 1 || value === "1" || value === "true";
|
|
1581
|
-
default:
|
|
1582
|
-
return value;
|
|
1577
|
+
var db = new Proxy(function database() {}, {
|
|
1578
|
+
apply(_target, _thisArg, args) {
|
|
1579
|
+
return getDb()(...args);
|
|
1580
|
+
},
|
|
1581
|
+
get(_target, property) {
|
|
1582
|
+
const connection = getDb();
|
|
1583
|
+
const value = connection[property];
|
|
1584
|
+
return typeof value === "function" ? value.bind(connection) : value;
|
|
1583
1585
|
}
|
|
1586
|
+
});
|
|
1587
|
+
var connection_default = db;
|
|
1588
|
+
|
|
1589
|
+
// ../../src/core/database/table.ts
|
|
1590
|
+
function defineTable(definition) {
|
|
1591
|
+
return definition;
|
|
1584
1592
|
}
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
}
|
|
1593
|
+
|
|
1594
|
+
// ../../src/modules/user/apiTokenTable.ts
|
|
1595
|
+
var apiTokenTable = defineTable({
|
|
1596
|
+
name: "api_token",
|
|
1597
|
+
primaryKey: "id",
|
|
1598
|
+
columns: [
|
|
1599
|
+
"id",
|
|
1600
|
+
"user_id",
|
|
1601
|
+
"name",
|
|
1602
|
+
"token_hash",
|
|
1603
|
+
"abilities",
|
|
1604
|
+
"last_used_at",
|
|
1605
|
+
"expires_at",
|
|
1606
|
+
"created_at"
|
|
1607
|
+
],
|
|
1608
|
+
defaultOrderBy: { column: "id", direction: "ASC" }
|
|
1609
|
+
});
|
|
1610
|
+
|
|
1611
|
+
// ../../src/core/auth/password.ts
|
|
1612
|
+
async function verifyPassword(password, passwordHash) {
|
|
1613
|
+
return await Bun.password.verify(password, passwordHash);
|
|
1601
1614
|
}
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1615
|
+
|
|
1616
|
+
// ../../src/core/crypto/fieldEncryption.ts
|
|
1617
|
+
import { createCipheriv, createDecipheriv, createHmac, randomBytes } from "crypto";
|
|
1618
|
+
var ENCRYPTION_PREFIX = "enc:v1:";
|
|
1619
|
+
var IV_LENGTH = 12;
|
|
1620
|
+
var TAG_LENGTH = 16;
|
|
1621
|
+
function resolveEncryptionKey() {
|
|
1622
|
+
const raw = process.env.KMS_ENCRYPTION_KEY?.trim();
|
|
1623
|
+
if (!raw) {
|
|
1624
|
+
return null;
|
|
1607
1625
|
}
|
|
1608
|
-
if (
|
|
1609
|
-
return
|
|
1626
|
+
if (/^[0-9a-f]{64}$/i.test(raw)) {
|
|
1627
|
+
return Buffer.from(raw, "hex");
|
|
1610
1628
|
}
|
|
1611
|
-
const
|
|
1612
|
-
|
|
1629
|
+
const decoded = Buffer.from(raw, "base64");
|
|
1630
|
+
if (decoded.length === 32) {
|
|
1631
|
+
return decoded;
|
|
1632
|
+
}
|
|
1633
|
+
throw new Error("KMS_ENCRYPTION_KEY must be 32 bytes (hex or base64).");
|
|
1613
1634
|
}
|
|
1614
|
-
function
|
|
1615
|
-
|
|
1616
|
-
|
|
1635
|
+
function isFieldEncryptionEnabled() {
|
|
1636
|
+
const featureFlag = process.env.FEATURE_FIELD_ENCRYPTION;
|
|
1637
|
+
if (featureFlag === "false") {
|
|
1638
|
+
return false;
|
|
1617
1639
|
}
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
for (const [key, cast] of Object.entries(casts)) {
|
|
1621
|
-
if (key in result && cast) {
|
|
1622
|
-
result[key] = castFn(result[key], cast);
|
|
1623
|
-
}
|
|
1640
|
+
if (featureFlag === "true") {
|
|
1641
|
+
return true;
|
|
1624
1642
|
}
|
|
1625
|
-
return
|
|
1643
|
+
return (process.env.APP_ENV ?? "local") === "production";
|
|
1626
1644
|
}
|
|
1627
|
-
function
|
|
1628
|
-
if (!
|
|
1629
|
-
return
|
|
1645
|
+
function decryptField(value, key) {
|
|
1646
|
+
if (!value.startsWith(ENCRYPTION_PREFIX)) {
|
|
1647
|
+
return value;
|
|
1630
1648
|
}
|
|
1631
|
-
const
|
|
1632
|
-
const
|
|
1633
|
-
|
|
1634
|
-
|
|
1649
|
+
const payload = Buffer.from(value.slice(ENCRYPTION_PREFIX.length), "base64");
|
|
1650
|
+
const iv = payload.subarray(0, IV_LENGTH);
|
|
1651
|
+
const tag = payload.subarray(payload.length - TAG_LENGTH);
|
|
1652
|
+
const ciphertext = payload.subarray(IV_LENGTH, payload.length - TAG_LENGTH);
|
|
1653
|
+
const decipher = createDecipheriv("aes-256-gcm", key, iv);
|
|
1654
|
+
decipher.setAuthTag(tag);
|
|
1655
|
+
return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf8");
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
// ../../src/core/crypto/mfaSecret.ts
|
|
1659
|
+
function revealMfaSecret(stored) {
|
|
1660
|
+
if (!stored) {
|
|
1661
|
+
return null;
|
|
1635
1662
|
}
|
|
1636
|
-
|
|
1637
|
-
|
|
1663
|
+
const key = resolveEncryptionKey();
|
|
1664
|
+
if (!isFieldEncryptionEnabled() || !key || !stored.startsWith("enc:v1:")) {
|
|
1665
|
+
return stored;
|
|
1638
1666
|
}
|
|
1639
|
-
return
|
|
1667
|
+
return decryptField(stored, key);
|
|
1640
1668
|
}
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1669
|
+
|
|
1670
|
+
// ../../src/core/http/requestMetaContext.ts
|
|
1671
|
+
var requestMetaContext = createAsyncContextStore("@getstrata/requestMetaContext");
|
|
1672
|
+
function runWithRequestMeta(meta, callback) {
|
|
1673
|
+
return requestMetaContext.run(meta, callback);
|
|
1674
|
+
}
|
|
1675
|
+
function currentRequestMeta() {
|
|
1676
|
+
return requestMetaContext.getStore() ?? {
|
|
1677
|
+
ipAddress: null,
|
|
1678
|
+
userAgent: null
|
|
1679
|
+
};
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
// ../../src/core/security/securityEvents.ts
|
|
1683
|
+
function logSecurityEvent(event, details = {}) {
|
|
1684
|
+
const meta = currentRequestMeta();
|
|
1685
|
+
const user = currentAuthUser();
|
|
1686
|
+
console.log(JSON.stringify({
|
|
1687
|
+
level: "security",
|
|
1688
|
+
event,
|
|
1689
|
+
timestamp: new Date().toISOString(),
|
|
1690
|
+
ip_address: meta.ipAddress ?? null,
|
|
1691
|
+
user_agent: meta.userAgent ?? null,
|
|
1692
|
+
user_id: user?.id ?? null,
|
|
1693
|
+
...details
|
|
1694
|
+
}));
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
// ../../src/core/security/tokenExpiry.ts
|
|
1698
|
+
function resolveDefaultTokenExpiryDays() {
|
|
1699
|
+
const raw = process.env.API_TOKEN_DEFAULT_EXPIRY_DAYS?.trim();
|
|
1700
|
+
if (!raw) {
|
|
1701
|
+
return null;
|
|
1644
1702
|
}
|
|
1645
|
-
const
|
|
1646
|
-
if (
|
|
1647
|
-
|
|
1703
|
+
const parsed = Number.parseInt(raw, 10);
|
|
1704
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
1705
|
+
return null;
|
|
1648
1706
|
}
|
|
1649
|
-
return
|
|
1707
|
+
return parsed;
|
|
1650
1708
|
}
|
|
1651
1709
|
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
}
|
|
1665
|
-
get $exists() {
|
|
1666
|
-
return this._exists;
|
|
1667
|
-
}
|
|
1668
|
-
get(key) {
|
|
1669
|
-
return this.attributes[key];
|
|
1670
|
-
}
|
|
1671
|
-
get id() {
|
|
1672
|
-
return this.attributes[this.primaryKey()];
|
|
1673
|
-
}
|
|
1674
|
-
toObject() {
|
|
1675
|
-
return { ...this.attributes };
|
|
1676
|
-
}
|
|
1677
|
-
primaryKey() {
|
|
1678
|
-
throw new Error(`${this.constructor.name}.primaryKey() is not implemented.`);
|
|
1679
|
-
}
|
|
1680
|
-
static primaryKeyField() {
|
|
1681
|
-
return resolveModelRepository(this).getTable().primaryKey;
|
|
1682
|
-
}
|
|
1683
|
-
static hydrateAttributes(attributes) {
|
|
1684
|
-
const casts = modelStatics(this).$casts ?? {};
|
|
1685
|
-
return applyCasts(attributes, casts, "hydrate");
|
|
1710
|
+
// ../../src/core/security/totp.ts
|
|
1711
|
+
import { createHmac as createHmac2 } from "crypto";
|
|
1712
|
+
function decodeBase32(input) {
|
|
1713
|
+
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
|
1714
|
+
const normalized = input.replace(/=+$/u, "").toUpperCase();
|
|
1715
|
+
let bits = "";
|
|
1716
|
+
for (const char of normalized) {
|
|
1717
|
+
const value = alphabet.indexOf(char);
|
|
1718
|
+
if (value === -1) {
|
|
1719
|
+
throw new Error("Invalid base32 character in MFA secret.");
|
|
1720
|
+
}
|
|
1721
|
+
bits += value.toString(2).padStart(5, "0");
|
|
1686
1722
|
}
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1723
|
+
const bytes = [];
|
|
1724
|
+
for (let index = 0;index + 8 <= bits.length; index += 8) {
|
|
1725
|
+
bytes.push(Number.parseInt(bits.slice(index, index + 8), 2));
|
|
1690
1726
|
}
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1727
|
+
return Buffer.from(bytes);
|
|
1728
|
+
}
|
|
1729
|
+
function generateTotp(secret, counter, digits = 6) {
|
|
1730
|
+
const key = decodeBase32(secret);
|
|
1731
|
+
const buffer = Buffer.alloc(8);
|
|
1732
|
+
buffer.writeBigUInt64BE(BigInt(counter));
|
|
1733
|
+
const digest = createHmac2("sha1", key).update(buffer).digest();
|
|
1734
|
+
const lastByte = digest[digest.length - 1] ?? 0;
|
|
1735
|
+
const offset = lastByte & 15;
|
|
1736
|
+
const b0 = digest[offset] ?? 0;
|
|
1737
|
+
const b1 = digest[offset + 1] ?? 0;
|
|
1738
|
+
const b2 = digest[offset + 2] ?? 0;
|
|
1739
|
+
const b3 = digest[offset + 3] ?? 0;
|
|
1740
|
+
const code = (b0 & 127) << 24 | (b1 & 255) << 16 | (b2 & 255) << 8 | b3 & 255;
|
|
1741
|
+
return String(code % 10 ** digits).padStart(digits, "0");
|
|
1742
|
+
}
|
|
1743
|
+
function verifyTotp(secret, token, window = 1) {
|
|
1744
|
+
const normalized = token.trim();
|
|
1745
|
+
if (!/^\d{6}$/u.test(normalized)) {
|
|
1746
|
+
return false;
|
|
1695
1747
|
}
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
...existing,
|
|
1702
|
-
scope
|
|
1703
|
-
]);
|
|
1748
|
+
const timestep = Math.floor(Date.now() / 30000);
|
|
1749
|
+
for (let offset = -window;offset <= window; offset += 1) {
|
|
1750
|
+
if (generateTotp(secret, timestep + offset) === normalized) {
|
|
1751
|
+
return true;
|
|
1752
|
+
}
|
|
1704
1753
|
}
|
|
1705
|
-
|
|
1706
|
-
|
|
1754
|
+
return false;
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
// ../../src/core/tenant/tenantContext.ts
|
|
1758
|
+
var tenantContext = createAsyncContextStore("@getstrata/tenantContext");
|
|
1759
|
+
function runWithTenant(tenant, callback) {
|
|
1760
|
+
return tenantContext.run(tenant, callback);
|
|
1761
|
+
}
|
|
1762
|
+
function currentTenant() {
|
|
1763
|
+
return tenantContext.getStore() ?? null;
|
|
1764
|
+
}
|
|
1765
|
+
function currentTenantId() {
|
|
1766
|
+
return currentTenant()?.id ?? 1;
|
|
1767
|
+
}
|
|
1768
|
+
function rateLimitMultiplierForPlan(plan) {
|
|
1769
|
+
switch (plan) {
|
|
1770
|
+
case "enterprise":
|
|
1771
|
+
return 4;
|
|
1772
|
+
case "pro":
|
|
1773
|
+
return 2;
|
|
1774
|
+
default:
|
|
1775
|
+
return 1;
|
|
1707
1776
|
}
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
// ../../src/modules/user/authService.ts
|
|
1780
|
+
class AuthService {
|
|
1781
|
+
users;
|
|
1782
|
+
tokens;
|
|
1783
|
+
oauthIdentities;
|
|
1784
|
+
oauthProviders = new Map;
|
|
1785
|
+
constructor(users, tokens, oauthIdentities) {
|
|
1786
|
+
this.users = users;
|
|
1787
|
+
this.tokens = tokens;
|
|
1788
|
+
this.oauthIdentities = oauthIdentities;
|
|
1716
1789
|
}
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
ensureBooted(this);
|
|
1720
|
-
const repository = resolveModelRepository(this);
|
|
1721
|
-
const table = repository.getTable();
|
|
1722
|
-
const timestamps = statics.$timestamps ?? true;
|
|
1723
|
-
const assignable = filterMassAssignable(statics.$fillable, statics.$guarded, attributes);
|
|
1724
|
-
const withTimestamps = applyTimestampsOnCreate(table.columns, assignable, timestamps);
|
|
1725
|
-
const payload = statics.dehydrateAttributes(withTimestamps);
|
|
1726
|
-
const record = await repository.create(payload);
|
|
1727
|
-
return statics.fromRecord(record, repository, true);
|
|
1790
|
+
registerOAuthProvider(provider) {
|
|
1791
|
+
this.oauthProviders.set(provider.name, provider);
|
|
1728
1792
|
}
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
const repository = resolveModelRepository(this);
|
|
1732
|
-
const primaryKey = repository.getTable().primaryKey;
|
|
1733
|
-
const record = await Model.query.call(this).where({ [primaryKey]: id }).first();
|
|
1734
|
-
return record ? statics.fromRecord(record, repository, true) : null;
|
|
1793
|
+
getOAuthProvider(name) {
|
|
1794
|
+
return this.oauthProviders.get(name);
|
|
1735
1795
|
}
|
|
1736
|
-
|
|
1737
|
-
const
|
|
1738
|
-
if (
|
|
1739
|
-
|
|
1796
|
+
async loginWithPassword(email, password, options = {}) {
|
|
1797
|
+
const user = await this.users.findByEmail(email);
|
|
1798
|
+
if (!user?.password_hash) {
|
|
1799
|
+
logSecurityEvent("auth_login_failed", { reason: "unknown_user", email });
|
|
1800
|
+
throw new UnauthorizedError("Invalid credentials.");
|
|
1740
1801
|
}
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
const repository = resolveModelRepository(this);
|
|
1746
|
-
let query = Model.query.call(this);
|
|
1747
|
-
if (options.orderBy) {
|
|
1748
|
-
query = query.orderBy(options.orderBy);
|
|
1802
|
+
const valid = await verifyPassword(password, user.password_hash);
|
|
1803
|
+
if (!valid) {
|
|
1804
|
+
logSecurityEvent("auth_login_failed", { reason: "invalid_password", user_id: user.id });
|
|
1805
|
+
throw new UnauthorizedError("Invalid credentials.");
|
|
1749
1806
|
}
|
|
1750
|
-
if (
|
|
1751
|
-
|
|
1807
|
+
if (isFeatureEnabled("emailVerification") && !user.email_verified_at) {
|
|
1808
|
+
logSecurityEvent("auth_login_blocked", { reason: "email_unverified", user_id: user.id });
|
|
1809
|
+
throw new UnauthorizedError("Email address is not verified.");
|
|
1752
1810
|
}
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
let query = Model.query.call(this).where(where);
|
|
1760
|
-
if (options.orderBy) {
|
|
1761
|
-
query = query.orderBy(options.orderBy);
|
|
1811
|
+
if (isFeatureEnabled("mfa") && user.mfa_enabled) {
|
|
1812
|
+
const mfaSecret = revealMfaSecret(user.mfa_secret);
|
|
1813
|
+
if (!mfaSecret || !options.mfaCode || !verifyTotp(mfaSecret, options.mfaCode)) {
|
|
1814
|
+
logSecurityEvent("auth_login_failed", { reason: "invalid_mfa", user_id: user.id });
|
|
1815
|
+
throw new UnauthorizedError("Invalid MFA code.");
|
|
1816
|
+
}
|
|
1762
1817
|
}
|
|
1763
|
-
|
|
1764
|
-
return
|
|
1818
|
+
logSecurityEvent("auth_login_success", { user_id: user.id, method: "password" });
|
|
1819
|
+
return await this.tokens.createToken(user.id, {
|
|
1820
|
+
name: "password-login",
|
|
1821
|
+
abilities: resolveAbilitiesForRole(user.role),
|
|
1822
|
+
expiresInDays: resolveDefaultTokenExpiryDays() ?? undefined
|
|
1823
|
+
});
|
|
1765
1824
|
}
|
|
1766
|
-
async
|
|
1767
|
-
const
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
const table = this.repository.getTable();
|
|
1771
|
-
if (this.$exists) {
|
|
1772
|
-
const changes = applyTimestampsOnUpdate(table.columns, applyCasts(this.attributes, casts, "dehydrate"), timestamps);
|
|
1773
|
-
const record2 = await this.repository.updateByIdOrThrow(this.id, changes);
|
|
1774
|
-
this.attributes = ModelClass.hydrateAttributes(record2);
|
|
1775
|
-
return this;
|
|
1825
|
+
async loginWithOAuth(providerName, code) {
|
|
1826
|
+
const provider = this.oauthProviders.get(providerName);
|
|
1827
|
+
if (!provider) {
|
|
1828
|
+
throw new UnauthorizedError("Unsupported OAuth provider.");
|
|
1776
1829
|
}
|
|
1777
|
-
const
|
|
1778
|
-
const
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
async update(changes) {
|
|
1786
|
-
const ModelClass = modelStatics(this.constructor);
|
|
1787
|
-
const assignable = filterMassAssignable(ModelClass.$fillable, ModelClass.$guarded, changes);
|
|
1788
|
-
Object.assign(this.attributes, assignable);
|
|
1789
|
-
return await this.save();
|
|
1830
|
+
const profile = await provider.exchangeCode(code);
|
|
1831
|
+
const user = await this.findOrCreateOAuthUser(providerName, profile);
|
|
1832
|
+
logSecurityEvent("auth_login_success", { user_id: user.id, method: `oauth:${providerName}` });
|
|
1833
|
+
return await this.tokens.createToken(user.id, {
|
|
1834
|
+
name: `${providerName}-oauth`,
|
|
1835
|
+
abilities: resolveAbilitiesForRole(user.role),
|
|
1836
|
+
expiresInDays: resolveDefaultTokenExpiryDays() ?? undefined
|
|
1837
|
+
});
|
|
1790
1838
|
}
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1839
|
+
buildOAuthAuthorizationUrl(providerName, state) {
|
|
1840
|
+
const provider = this.oauthProviders.get(providerName);
|
|
1841
|
+
if (!provider) {
|
|
1842
|
+
throw new UnauthorizedError("Unsupported OAuth provider.");
|
|
1794
1843
|
}
|
|
1795
|
-
return
|
|
1796
|
-
}
|
|
1797
|
-
async forceDelete() {
|
|
1798
|
-
return await this.repository.forceDeleteById(this.id);
|
|
1844
|
+
return provider.getAuthorizationUrl(state);
|
|
1799
1845
|
}
|
|
1800
|
-
async
|
|
1801
|
-
const
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
return null;
|
|
1846
|
+
async findOrCreateOAuthUser(providerName, profile) {
|
|
1847
|
+
const existingIdentity = await this.oauthIdentities.findByProviderUser(providerName, profile.providerUserId);
|
|
1848
|
+
if (existingIdentity) {
|
|
1849
|
+
return await this.users.findByIdOrThrow(existingIdentity.user_id);
|
|
1805
1850
|
}
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
const loaded = await this.loadHasMany(as, relation, childRepository, { ...options, limit: 1 });
|
|
1816
|
-
const value = loaded[as]?.[0];
|
|
1817
|
-
return Object.assign(this, { [as]: value });
|
|
1818
|
-
}
|
|
1819
|
-
async loadBelongsTo(as, relation, parentRepository, options = {}) {
|
|
1820
|
-
const grouped = await this.repository.loadBelongsToForParents([this.attributes], relation, parentRepository, options);
|
|
1821
|
-
const loaded = grouped.get(this.attributes[relation.foreignKey]);
|
|
1822
|
-
return Object.assign(this, { [as]: loaded });
|
|
1823
|
-
}
|
|
1824
|
-
async loadBelongsToMany(as, relation, relatedRepository, options = {}) {
|
|
1825
|
-
const connection = this.repository.getConnection();
|
|
1826
|
-
const parentId = this.attributes[relation.parentKey];
|
|
1827
|
-
const pivotRows = await connection.unsafe(`SELECT * FROM ${relation.pivotTable} WHERE ${String(relation.foreignPivotKey)} = $1`, [parentId]);
|
|
1828
|
-
if (pivotRows.length === 0) {
|
|
1829
|
-
return Object.assign(this, { [as]: [] });
|
|
1830
|
-
}
|
|
1831
|
-
const relatedIds = [
|
|
1832
|
-
...new Set(pivotRows.map((row) => row[relation.relatedPivotKey]))
|
|
1833
|
-
];
|
|
1834
|
-
const relatedRows = await relatedRepository.withConnection(connection).findAll({
|
|
1835
|
-
...options,
|
|
1836
|
-
where: {
|
|
1837
|
-
[relation.relatedKey]: relatedIds
|
|
1838
|
-
}
|
|
1851
|
+
const existingUser = await this.users.findByEmail(profile.email);
|
|
1852
|
+
const user = existingUser ?? await this.users.create({
|
|
1853
|
+
name: profile.name,
|
|
1854
|
+
email: profile.email,
|
|
1855
|
+
role: "member",
|
|
1856
|
+
tenant_id: currentTenantId(),
|
|
1857
|
+
email_verified_at: new Date,
|
|
1858
|
+
created_at: new Date,
|
|
1859
|
+
updated_at: new Date
|
|
1839
1860
|
});
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1861
|
+
await this.oauthIdentities.create({
|
|
1862
|
+
user_id: user.id,
|
|
1863
|
+
provider: providerName,
|
|
1864
|
+
provider_user_id: profile.providerUserId,
|
|
1865
|
+
email: profile.email,
|
|
1866
|
+
created_at: new Date
|
|
1867
|
+
});
|
|
1868
|
+
return user;
|
|
1847
1869
|
}
|
|
1848
1870
|
}
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1871
|
+
|
|
1872
|
+
// ../../src/modules/user/notificationTable.ts
|
|
1873
|
+
var notificationTable = defineTable({
|
|
1874
|
+
name: "notification",
|
|
1875
|
+
primaryKey: "id",
|
|
1876
|
+
columns: ["id", "user_id", "tenant_id", "type", "title", "body", "data", "read_at", "created_at"],
|
|
1877
|
+
defaultOrderBy: { column: "created_at", direction: "DESC" }
|
|
1878
|
+
});
|
|
1879
|
+
|
|
1880
|
+
// ../../src/modules/user/oauthIdentityRepository.ts
|
|
1881
|
+
var oauthIdentityTable = defineTable({
|
|
1882
|
+
name: "oauth_identity",
|
|
1883
|
+
primaryKey: "id",
|
|
1884
|
+
columns: ["id", "user_id", "provider", "provider_user_id", "email", "created_at"]
|
|
1885
|
+
});
|
|
1886
|
+
|
|
1887
|
+
// ../../src/modules/user/table.ts
|
|
1888
|
+
var userTable = defineTable({
|
|
1889
|
+
name: "users",
|
|
1890
|
+
primaryKey: "id",
|
|
1891
|
+
columns: [
|
|
1892
|
+
"id",
|
|
1893
|
+
"name",
|
|
1894
|
+
"email",
|
|
1895
|
+
"email_lookup",
|
|
1896
|
+
"role",
|
|
1897
|
+
"tenant_id",
|
|
1898
|
+
"password_hash",
|
|
1899
|
+
"email_verified_at",
|
|
1900
|
+
"mfa_secret",
|
|
1901
|
+
"mfa_enabled",
|
|
1902
|
+
"created_at",
|
|
1903
|
+
"updated_at"
|
|
1904
|
+
],
|
|
1905
|
+
defaultOrderBy: { column: "id", direction: "ASC" }
|
|
1906
|
+
});
|
|
1907
|
+
|
|
1908
|
+
// ../../src/modules/user/provider.ts
|
|
1909
|
+
var tokenServiceToken = CORE_TOKEN_SERVICE_TOKEN;
|
|
1910
|
+
|
|
1911
|
+
// ../../src/core/auth/guard.ts
|
|
1912
|
+
function devHeaderAbilities(role) {
|
|
1913
|
+
if (role === "admin") {
|
|
1914
|
+
return [...ADMIN_ABILITIES];
|
|
1869
1915
|
}
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1916
|
+
return [...MEMBER_ABILITIES];
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
class GuestGuard {
|
|
1920
|
+
resolve(request) {
|
|
1921
|
+
const userId = request.headers.get("x-authenticated-user-id");
|
|
1922
|
+
if (!userId) {
|
|
1923
|
+
return null;
|
|
1924
|
+
}
|
|
1925
|
+
const role = request.headers.get("x-authenticated-user-role");
|
|
1926
|
+
return {
|
|
1927
|
+
id: userId,
|
|
1928
|
+
abilities: devHeaderAbilities(role),
|
|
1929
|
+
...role ? { role } : {}
|
|
1930
|
+
};
|
|
1873
1931
|
}
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
class ApiTokenGuard {
|
|
1935
|
+
options;
|
|
1936
|
+
constructor(options) {
|
|
1937
|
+
this.options = options;
|
|
1877
1938
|
}
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
return
|
|
1939
|
+
resolve(request) {
|
|
1940
|
+
const authorization = request.headers.get("authorization");
|
|
1941
|
+
if (!authorization?.startsWith("Bearer ")) {
|
|
1942
|
+
return null;
|
|
1882
1943
|
}
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
return
|
|
1944
|
+
const token = authorization.slice("Bearer ".length).trim();
|
|
1945
|
+
if (token !== this.options.token) {
|
|
1946
|
+
return null;
|
|
1886
1947
|
}
|
|
1887
|
-
this.
|
|
1888
|
-
return this;
|
|
1948
|
+
return this.options.user;
|
|
1889
1949
|
}
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1952
|
+
class DatabaseTokenGuard {
|
|
1953
|
+
container;
|
|
1954
|
+
constructor(container) {
|
|
1955
|
+
this.container = container;
|
|
1893
1956
|
}
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1957
|
+
async resolve(request) {
|
|
1958
|
+
const authorization = request.headers.get("authorization");
|
|
1959
|
+
if (!authorization?.startsWith("Bearer ")) {
|
|
1960
|
+
return null;
|
|
1961
|
+
}
|
|
1962
|
+
const token = authorization.slice("Bearer ".length).trim();
|
|
1963
|
+
if (!token) {
|
|
1964
|
+
return null;
|
|
1965
|
+
}
|
|
1966
|
+
if (!this.container.has(tokenServiceToken)) {
|
|
1967
|
+
return null;
|
|
1968
|
+
}
|
|
1969
|
+
const tokenService = this.container.resolve(tokenServiceToken);
|
|
1970
|
+
return await tokenService.resolveUserFromToken(token);
|
|
1897
1971
|
}
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1974
|
+
class CompositeGuard {
|
|
1975
|
+
guards;
|
|
1976
|
+
constructor(guards) {
|
|
1977
|
+
this.guards = guards;
|
|
1901
1978
|
}
|
|
1902
|
-
|
|
1903
|
-
this.
|
|
1904
|
-
|
|
1979
|
+
async resolve(request) {
|
|
1980
|
+
for (const guard of this.guards) {
|
|
1981
|
+
const user = await Promise.resolve(guard.resolve(request));
|
|
1982
|
+
if (user) {
|
|
1983
|
+
return user;
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
return null;
|
|
1905
1987
|
}
|
|
1906
1988
|
}
|
|
1907
1989
|
|
|
1908
|
-
class
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
this.
|
|
1990
|
+
class AuthManager {
|
|
1991
|
+
guard;
|
|
1992
|
+
constructor(guard) {
|
|
1993
|
+
this.guard = guard;
|
|
1912
1994
|
}
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
return this;
|
|
1995
|
+
async resolve(request) {
|
|
1996
|
+
if (request) {
|
|
1997
|
+
return await Promise.resolve(this.guard.resolve(request));
|
|
1998
|
+
}
|
|
1999
|
+
return currentAuthUser();
|
|
1919
2000
|
}
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
return this.references(referencesTable);
|
|
2001
|
+
user(request) {
|
|
2002
|
+
return this.resolve(request);
|
|
1923
2003
|
}
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
throw new Error(`Foreign key is not defined for column ${this.name}`);
|
|
1927
|
-
}
|
|
1928
|
-
this.foreignKey.onDelete = "cascade";
|
|
1929
|
-
return this;
|
|
2004
|
+
async check(request) {
|
|
2005
|
+
return await this.user(request) !== null;
|
|
1930
2006
|
}
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
2007
|
+
async requireUser(request) {
|
|
2008
|
+
const user = await this.user(request);
|
|
2009
|
+
if (!user) {
|
|
2010
|
+
throw new UnauthorizedError;
|
|
1934
2011
|
}
|
|
1935
|
-
|
|
1936
|
-
return this;
|
|
1937
|
-
}
|
|
1938
|
-
}
|
|
1939
|
-
function inferReferencedTable(columnName) {
|
|
1940
|
-
if (!columnName.endsWith("_id")) {
|
|
1941
|
-
throw new Error(`Cannot infer referenced table from column ${columnName}`);
|
|
2012
|
+
return user;
|
|
1942
2013
|
}
|
|
1943
|
-
return columnName.slice(0, -3);
|
|
1944
2014
|
}
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
this.action = action;
|
|
2015
|
+
// ../../src/modules/organization/memberRepository.ts
|
|
2016
|
+
class OrganizationMemberRepository {
|
|
2017
|
+
constructor() {}
|
|
2018
|
+
async findMembership(userId, organizationId) {
|
|
2019
|
+
const rows = await connection_default`
|
|
2020
|
+
SELECT id, organization_id, user_id, role, created_at
|
|
2021
|
+
FROM organization_member
|
|
2022
|
+
WHERE user_id = ${userId} AND organization_id = ${organizationId}
|
|
2023
|
+
LIMIT 1
|
|
2024
|
+
`;
|
|
2025
|
+
return rows[0] ?? null;
|
|
1957
2026
|
}
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
2027
|
+
async listForUser(userId) {
|
|
2028
|
+
return await connection_default`
|
|
2029
|
+
SELECT id, organization_id, user_id, role, created_at
|
|
2030
|
+
FROM organization_member
|
|
2031
|
+
WHERE user_id = ${userId}
|
|
2032
|
+
ORDER BY organization_id
|
|
2033
|
+
`;
|
|
1964
2034
|
}
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
2035
|
+
async listForOrganization(organizationId) {
|
|
2036
|
+
return await connection_default`
|
|
2037
|
+
SELECT id, organization_id, user_id, role, created_at
|
|
2038
|
+
FROM organization_member
|
|
2039
|
+
WHERE organization_id = ${organizationId}
|
|
2040
|
+
ORDER BY id
|
|
2041
|
+
`;
|
|
1971
2042
|
}
|
|
1972
|
-
|
|
1973
|
-
const
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
2043
|
+
async addMember(input) {
|
|
2044
|
+
const rows = await connection_default`
|
|
2045
|
+
INSERT INTO organization_member (organization_id, user_id, role)
|
|
2046
|
+
VALUES (${input.organizationId}, ${input.userId}, ${input.role ?? "member"})
|
|
2047
|
+
RETURNING id, organization_id, user_id, role, created_at
|
|
2048
|
+
`;
|
|
2049
|
+
const row = rows[0];
|
|
2050
|
+
if (!row) {
|
|
2051
|
+
throw new Error("Organization member insert did not return a row.");
|
|
2052
|
+
}
|
|
2053
|
+
return row;
|
|
1977
2054
|
}
|
|
1978
|
-
|
|
1979
|
-
const
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
2055
|
+
async removeMember(organizationId, userId) {
|
|
2056
|
+
const rows = await connection_default`
|
|
2057
|
+
DELETE FROM organization_member
|
|
2058
|
+
WHERE organization_id = ${organizationId} AND user_id = ${userId}
|
|
2059
|
+
RETURNING id
|
|
2060
|
+
`;
|
|
2061
|
+
return rows.length > 0;
|
|
1983
2062
|
}
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
2063
|
+
}
|
|
2064
|
+
var memberRepository_default = OrganizationMemberRepository;
|
|
2065
|
+
|
|
2066
|
+
// ../../src/core/auth/membershipContext.ts
|
|
2067
|
+
var membershipContext = createAsyncContextStore("@getstrata/membershipContext");
|
|
2068
|
+
var membershipRepository = new memberRepository_default;
|
|
2069
|
+
async function runWithMembershipContext(callback) {
|
|
2070
|
+
const user = currentAuthUser();
|
|
2071
|
+
if (!user || isGlobalAdmin(user)) {
|
|
2072
|
+
return await callback();
|
|
1989
2073
|
}
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
2074
|
+
const memberships = await membershipRepository.listForUser(resolveUserId(user));
|
|
2075
|
+
const context = {
|
|
2076
|
+
organizationIds: memberships.map((membership) => membership.organization_id),
|
|
2077
|
+
rolesByOrganizationId: new Map(memberships.map((membership) => [membership.organization_id, membership.role]))
|
|
2078
|
+
};
|
|
2079
|
+
return await membershipContext.run(context, callback);
|
|
2080
|
+
}
|
|
2081
|
+
function currentOrgRole(organizationId) {
|
|
2082
|
+
return membershipContext.getStore()?.rolesByOrganizationId.get(organizationId) ?? null;
|
|
2083
|
+
}
|
|
2084
|
+
function hasOrgMembership(organizationId) {
|
|
2085
|
+
return currentOrgRole(organizationId) !== null;
|
|
2086
|
+
}
|
|
2087
|
+
function currentOrganizationIds() {
|
|
2088
|
+
return membershipContext.getStore()?.organizationIds ?? [];
|
|
2089
|
+
}
|
|
2090
|
+
function hasMinimumOrgRole2(organizationId, minimum) {
|
|
2091
|
+
const role = currentOrgRole(organizationId);
|
|
2092
|
+
if (!role) {
|
|
2093
|
+
return false;
|
|
1995
2094
|
}
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2095
|
+
const ranks = {
|
|
2096
|
+
member: 1,
|
|
2097
|
+
admin: 2,
|
|
2098
|
+
owner: 3
|
|
2099
|
+
};
|
|
2100
|
+
return ranks[role] >= ranks[minimum];
|
|
2101
|
+
}
|
|
2102
|
+
// ../../src/core/auth/membershipContextMiddleware.ts
|
|
2103
|
+
function createMembershipContextMiddleware() {
|
|
2104
|
+
return async (_request, next) => {
|
|
2105
|
+
return await runWithMembershipContext(async () => await next());
|
|
2106
|
+
};
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
// ../../src/core/auth/membershipMiddleware.ts
|
|
2110
|
+
function createMembershipMiddleware() {
|
|
2111
|
+
return createMembershipContextMiddleware();
|
|
2112
|
+
}
|
|
2113
|
+
// ../../src/core/auth/membershipScope.ts
|
|
2114
|
+
function resolveOrganizationScope() {
|
|
2115
|
+
const user = currentAuthUser();
|
|
2116
|
+
if (!user) {
|
|
2117
|
+
return null;
|
|
2001
2118
|
}
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
column.notNullable();
|
|
2005
|
-
this.columns.push(column);
|
|
2006
|
-
return column;
|
|
2119
|
+
if (isGlobalAdmin(user)) {
|
|
2120
|
+
return null;
|
|
2007
2121
|
}
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2122
|
+
return currentOrganizationIds();
|
|
2123
|
+
}
|
|
2124
|
+
function scopedOrganizationIds(requestedOrganizationId) {
|
|
2125
|
+
const scope = resolveOrganizationScope();
|
|
2126
|
+
if (scope === null) {
|
|
2127
|
+
return requestedOrganizationId === undefined ? null : [requestedOrganizationId];
|
|
2013
2128
|
}
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
this.columns.push(column);
|
|
2017
|
-
return column;
|
|
2129
|
+
if (requestedOrganizationId !== undefined) {
|
|
2130
|
+
return scope.includes(requestedOrganizationId) ? [requestedOrganizationId] : [];
|
|
2018
2131
|
}
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2132
|
+
return scope;
|
|
2133
|
+
}
|
|
2134
|
+
function appendOrganizationScope(where, requestedOrganizationId) {
|
|
2135
|
+
const organizationIds = scopedOrganizationIds(requestedOrganizationId);
|
|
2136
|
+
if (organizationIds === null) {
|
|
2137
|
+
return where;
|
|
2022
2138
|
}
|
|
2023
|
-
|
|
2024
|
-
|
|
2139
|
+
if (organizationIds.length === 0) {
|
|
2140
|
+
return {
|
|
2141
|
+
...where,
|
|
2142
|
+
organization_id: [-1]
|
|
2143
|
+
};
|
|
2025
2144
|
}
|
|
2026
|
-
|
|
2027
|
-
|
|
2145
|
+
return {
|
|
2146
|
+
...where,
|
|
2147
|
+
organization_id: organizationIds.length === 1 ? organizationIds[0] : organizationIds
|
|
2148
|
+
};
|
|
2149
|
+
}
|
|
2150
|
+
function appendProjectScope(where, accessibleProjectIds, requestedProjectId) {
|
|
2151
|
+
if (accessibleProjectIds === null) {
|
|
2152
|
+
if (requestedProjectId === undefined) {
|
|
2153
|
+
return where;
|
|
2154
|
+
}
|
|
2155
|
+
return {
|
|
2156
|
+
...where,
|
|
2157
|
+
project_id: requestedProjectId
|
|
2158
|
+
};
|
|
2028
2159
|
}
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2160
|
+
if (accessibleProjectIds.length === 0) {
|
|
2161
|
+
return {
|
|
2162
|
+
...where,
|
|
2163
|
+
project_id: [-1]
|
|
2164
|
+
};
|
|
2032
2165
|
}
|
|
2033
|
-
|
|
2034
|
-
|
|
2166
|
+
if (requestedProjectId !== undefined) {
|
|
2167
|
+
return {
|
|
2168
|
+
...where,
|
|
2169
|
+
project_id: accessibleProjectIds.includes(requestedProjectId) ? requestedProjectId : -1
|
|
2170
|
+
};
|
|
2035
2171
|
}
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2172
|
+
return {
|
|
2173
|
+
...where,
|
|
2174
|
+
project_id: accessibleProjectIds
|
|
2175
|
+
};
|
|
2176
|
+
}
|
|
2177
|
+
function emptyPaginateResult(page, perPage) {
|
|
2178
|
+
return {
|
|
2179
|
+
data: [],
|
|
2180
|
+
meta: {
|
|
2181
|
+
page,
|
|
2182
|
+
per_page: perPage,
|
|
2183
|
+
total: 0,
|
|
2184
|
+
last_page: 1
|
|
2185
|
+
}
|
|
2186
|
+
};
|
|
2187
|
+
}
|
|
2188
|
+
function assertResourceInCurrentTenant(resourceTenantId, resourceLabel, resourceId) {
|
|
2189
|
+
if (resourceTenantId !== currentTenantId()) {
|
|
2190
|
+
throw new NotFoundError(`${resourceLabel} ${resourceId} not found.`);
|
|
2042
2191
|
}
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
order: options.order
|
|
2049
|
-
});
|
|
2192
|
+
}
|
|
2193
|
+
function assertOrganizationReadable(organizationId) {
|
|
2194
|
+
const user = currentAuthUser();
|
|
2195
|
+
if (!user || isGlobalAdmin(user)) {
|
|
2196
|
+
return;
|
|
2050
2197
|
}
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
name: options.name,
|
|
2055
|
-
columns: Array.isArray(columns) ? columns : [columns],
|
|
2056
|
-
kind: options.unique ? "uniquePartial" : "partial",
|
|
2057
|
-
where
|
|
2058
|
-
});
|
|
2059
|
-
}
|
|
2060
|
-
fullText(columns, name) {
|
|
2061
|
-
this.indexes.push({
|
|
2062
|
-
name,
|
|
2063
|
-
columns: Array.isArray(columns) ? columns : [columns],
|
|
2064
|
-
kind: "fullText"
|
|
2065
|
-
});
|
|
2198
|
+
const organizationIds = scopedOrganizationIds();
|
|
2199
|
+
if (organizationIds !== null && !organizationIds.includes(organizationId)) {
|
|
2200
|
+
throw new NotFoundError(`Organization ${organizationId} not found.`);
|
|
2066
2201
|
}
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
});
|
|
2202
|
+
}
|
|
2203
|
+
// ../../src/core/contracts/di.ts
|
|
2204
|
+
function getRequiredDependency(dependencies, key) {
|
|
2205
|
+
const dependency = dependencies[key];
|
|
2206
|
+
if (dependency === undefined) {
|
|
2207
|
+
throw new Error(`Required dependency "${key}" is not registered.`);
|
|
2073
2208
|
}
|
|
2209
|
+
return dependency;
|
|
2074
2210
|
}
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2211
|
+
function resolveService(dependencies, token) {
|
|
2212
|
+
return dependencies.container.resolve(token);
|
|
2213
|
+
}
|
|
2214
|
+
|
|
2215
|
+
// ../../src/core/logging/logger.ts
|
|
2216
|
+
class Logger {
|
|
2217
|
+
channel;
|
|
2218
|
+
constructor(channel = "app") {
|
|
2219
|
+
this.channel = channel;
|
|
2080
2220
|
}
|
|
2081
|
-
|
|
2082
|
-
|
|
2221
|
+
write(level, message, context = {}) {
|
|
2222
|
+
const entry = {
|
|
2223
|
+
level,
|
|
2224
|
+
channel: this.channel,
|
|
2225
|
+
message,
|
|
2226
|
+
timestamp: new Date().toISOString(),
|
|
2227
|
+
...context
|
|
2228
|
+
};
|
|
2229
|
+
const line = JSON.stringify(entry);
|
|
2230
|
+
if (level === "error") {
|
|
2231
|
+
console.error(line);
|
|
2232
|
+
return;
|
|
2233
|
+
}
|
|
2234
|
+
console.log(line);
|
|
2083
2235
|
}
|
|
2084
|
-
|
|
2085
|
-
|
|
2236
|
+
debug(message, context) {
|
|
2237
|
+
this.write("debug", message, context);
|
|
2086
2238
|
}
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
function resolveDriverFromUrl(url) {
|
|
2090
|
-
const normalized = url.trim().toLowerCase();
|
|
2091
|
-
if (normalized.startsWith("postgres://") || normalized.startsWith("postgresql://") || normalized.startsWith("postgres:")) {
|
|
2092
|
-
return "pgsql";
|
|
2239
|
+
info(message, context) {
|
|
2240
|
+
this.write("info", message, context);
|
|
2093
2241
|
}
|
|
2094
|
-
|
|
2095
|
-
|
|
2242
|
+
warn(message, context) {
|
|
2243
|
+
this.write("warn", message, context);
|
|
2096
2244
|
}
|
|
2097
|
-
|
|
2098
|
-
|
|
2245
|
+
error(message, context) {
|
|
2246
|
+
this.write("error", message, context);
|
|
2099
2247
|
}
|
|
2100
|
-
return null;
|
|
2101
2248
|
}
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2249
|
+
var appLogger = new Logger("app");
|
|
2250
|
+
|
|
2251
|
+
// ../../src/core/runtime/applicationRegistry.ts
|
|
2252
|
+
var APPLICATION_CONTEXT_KEY = Symbol.for("@getstrata/applicationContext");
|
|
2253
|
+
var activeContext;
|
|
2254
|
+
function readStoredApplicationContext() {
|
|
2255
|
+
if (activeContext) {
|
|
2256
|
+
return activeContext;
|
|
2106
2257
|
}
|
|
2107
|
-
const
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
return fromUrl;
|
|
2258
|
+
const globalContext = globalThis[APPLICATION_CONTEXT_KEY];
|
|
2259
|
+
if (globalContext) {
|
|
2260
|
+
activeContext = globalContext;
|
|
2111
2261
|
}
|
|
2112
|
-
return
|
|
2262
|
+
return activeContext;
|
|
2113
2263
|
}
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
super(`${feature} is not supported for the ${driver} driver`);
|
|
2118
|
-
this.name = "UnsupportedSchemaFeatureError";
|
|
2119
|
-
}
|
|
2264
|
+
function setActiveApplicationContext(context) {
|
|
2265
|
+
activeContext = context;
|
|
2266
|
+
globalThis[APPLICATION_CONTEXT_KEY] = context;
|
|
2120
2267
|
}
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
return compileIdType(driver);
|
|
2126
|
-
case "string":
|
|
2127
|
-
return compileStringType(driver, column.length);
|
|
2128
|
-
case "text":
|
|
2129
|
-
return compileTextType(driver);
|
|
2130
|
-
case "boolean":
|
|
2131
|
-
return compileBooleanType(driver);
|
|
2132
|
-
case "integer":
|
|
2133
|
-
case "foreignId":
|
|
2134
|
-
return compileIntegerType(driver);
|
|
2135
|
-
case "bigInteger":
|
|
2136
|
-
return compileBigIntegerType(driver);
|
|
2137
|
-
case "timestamp":
|
|
2138
|
-
return compileTimestampType(driver);
|
|
2139
|
-
case "json":
|
|
2140
|
-
return compileJsonType(driver);
|
|
2141
|
-
case "jsonb":
|
|
2142
|
-
return compileJsonbType(driver);
|
|
2143
|
-
default:
|
|
2144
|
-
throw new Error(`Unsupported column kind: ${column.kind}`);
|
|
2268
|
+
function requireActiveApplicationContext() {
|
|
2269
|
+
const context = readStoredApplicationContext();
|
|
2270
|
+
if (!context) {
|
|
2271
|
+
throw new Error("The application context has not been bootstrapped.");
|
|
2145
2272
|
}
|
|
2273
|
+
return context;
|
|
2146
2274
|
}
|
|
2147
|
-
function
|
|
2148
|
-
|
|
2149
|
-
case "pgsql":
|
|
2150
|
-
return "SERIAL";
|
|
2151
|
-
case "mysql":
|
|
2152
|
-
return "BIGINT UNSIGNED";
|
|
2153
|
-
case "sqlite":
|
|
2154
|
-
return "INTEGER";
|
|
2155
|
-
}
|
|
2275
|
+
function resolveApplicationCache() {
|
|
2276
|
+
return getRequiredDependency(requireActiveApplicationContext().dependencies, "cache");
|
|
2156
2277
|
}
|
|
2157
|
-
function
|
|
2158
|
-
|
|
2159
|
-
case "pgsql":
|
|
2160
|
-
return "TEXT";
|
|
2161
|
-
case "mysql":
|
|
2162
|
-
return length ? `VARCHAR(${length})` : "VARCHAR(255)";
|
|
2163
|
-
case "sqlite":
|
|
2164
|
-
return "TEXT";
|
|
2165
|
-
}
|
|
2278
|
+
function resolveApplicationQueue() {
|
|
2279
|
+
return requireActiveApplicationContext().container.resolve(CORE_QUEUE_TOKEN);
|
|
2166
2280
|
}
|
|
2167
|
-
function
|
|
2168
|
-
|
|
2169
|
-
case "pgsql":
|
|
2170
|
-
case "sqlite":
|
|
2171
|
-
return "TEXT";
|
|
2172
|
-
case "mysql":
|
|
2173
|
-
return "TEXT";
|
|
2174
|
-
}
|
|
2281
|
+
function resolveApplicationEventBus() {
|
|
2282
|
+
return requireActiveApplicationContext().container.resolve(CORE_EVENT_BUS_TOKEN);
|
|
2175
2283
|
}
|
|
2176
|
-
function
|
|
2177
|
-
|
|
2178
|
-
case "pgsql":
|
|
2179
|
-
return "BOOLEAN";
|
|
2180
|
-
case "mysql":
|
|
2181
|
-
return "BOOLEAN";
|
|
2182
|
-
case "sqlite":
|
|
2183
|
-
return "INTEGER";
|
|
2184
|
-
}
|
|
2284
|
+
function resolveApplicationAuth() {
|
|
2285
|
+
return requireActiveApplicationContext().container.resolve(CORE_AUTH_TOKEN);
|
|
2185
2286
|
}
|
|
2186
|
-
function
|
|
2187
|
-
|
|
2188
|
-
case "pgsql":
|
|
2189
|
-
return "INTEGER";
|
|
2190
|
-
case "mysql":
|
|
2191
|
-
return "INT";
|
|
2192
|
-
case "sqlite":
|
|
2193
|
-
return "INTEGER";
|
|
2194
|
-
}
|
|
2287
|
+
function resolveApplicationPolicyGate() {
|
|
2288
|
+
return requireActiveApplicationContext().container.resolve(CORE_POLICY_GATE_TOKEN);
|
|
2195
2289
|
}
|
|
2196
|
-
function
|
|
2197
|
-
|
|
2198
|
-
case "pgsql":
|
|
2199
|
-
return "BIGINT";
|
|
2200
|
-
case "mysql":
|
|
2201
|
-
return "BIGINT";
|
|
2202
|
-
case "sqlite":
|
|
2203
|
-
return "INTEGER";
|
|
2204
|
-
}
|
|
2290
|
+
function resolveApplicationConfig() {
|
|
2291
|
+
return requireActiveApplicationContext().config;
|
|
2205
2292
|
}
|
|
2206
|
-
function
|
|
2207
|
-
|
|
2208
|
-
case "pgsql":
|
|
2209
|
-
return "TIMESTAMPTZ";
|
|
2210
|
-
case "mysql":
|
|
2211
|
-
return "TIMESTAMP";
|
|
2212
|
-
case "sqlite":
|
|
2213
|
-
return "TEXT";
|
|
2214
|
-
}
|
|
2293
|
+
function resolveApplicationLogger() {
|
|
2294
|
+
return appLogger;
|
|
2215
2295
|
}
|
|
2216
|
-
function
|
|
2217
|
-
|
|
2218
|
-
case "pgsql":
|
|
2219
|
-
return "JSONB";
|
|
2220
|
-
case "mysql":
|
|
2221
|
-
return "JSON";
|
|
2222
|
-
case "sqlite":
|
|
2223
|
-
return "TEXT";
|
|
2224
|
-
}
|
|
2296
|
+
function resolveApplicationDependencies() {
|
|
2297
|
+
return requireActiveApplicationContext().dependencies;
|
|
2225
2298
|
}
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
case "sqlite":
|
|
2233
|
-
return "TEXT";
|
|
2299
|
+
|
|
2300
|
+
// ../../src/core/auth/resolveMembershipService.ts
|
|
2301
|
+
function resolveMembershipService() {
|
|
2302
|
+
const dependencies = resolveApplicationDependencies();
|
|
2303
|
+
if (dependencies.container.has("core.membership")) {
|
|
2304
|
+
return dependencies.container.resolve("core.membership");
|
|
2234
2305
|
}
|
|
2306
|
+
return new membershipService_default;
|
|
2235
2307
|
}
|
|
2236
2308
|
|
|
2237
|
-
// ../../src/core/
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
if (index.kind === "unique" && index.columns.length > 1) {
|
|
2243
|
-
const columns = index.columns.map((column) => quoteIdentifier(column)).join(", ");
|
|
2244
|
-
parts.push(`UNIQUE (${columns})`);
|
|
2245
|
-
}
|
|
2309
|
+
// ../../src/core/auth/membershipService.ts
|
|
2310
|
+
class MembershipService {
|
|
2311
|
+
members;
|
|
2312
|
+
constructor(members = membershipRepository) {
|
|
2313
|
+
this.members = members;
|
|
2246
2314
|
}
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2315
|
+
async listOrganizationIdsForUser(userId) {
|
|
2316
|
+
const memberships = await this.members.listForUser(userId);
|
|
2317
|
+
return memberships.map((membership) => membership.organization_id);
|
|
2318
|
+
}
|
|
2319
|
+
async getOrgRole(userId, organizationId) {
|
|
2320
|
+
const membership = await this.members.findMembership(userId, organizationId);
|
|
2321
|
+
return membership?.role ?? null;
|
|
2322
|
+
}
|
|
2323
|
+
async requireOrgAccess(organizationId, minimumRole = "member", user = currentAuthUser()) {
|
|
2324
|
+
if (!user) {
|
|
2325
|
+
throw new ForbiddenError("Authentication required.");
|
|
2254
2326
|
}
|
|
2255
|
-
if (
|
|
2256
|
-
|
|
2257
|
-
} else if (index.kind === "partial" || index.kind === "uniquePartial" || index.kind === "gin" || index.kind === "fullText") {
|
|
2258
|
-
statements.push(...compileSpecialIndex(driver, blueprint.table, index));
|
|
2327
|
+
if (isGlobalAdmin(user)) {
|
|
2328
|
+
return "owner";
|
|
2259
2329
|
}
|
|
2330
|
+
const role = await this.getOrgRole(resolveUserId(user), organizationId);
|
|
2331
|
+
if (!role || !hasMinimumOrgRole(role, minimumRole)) {
|
|
2332
|
+
throw new ForbiddenError("Organization membership required.");
|
|
2333
|
+
}
|
|
2334
|
+
return role;
|
|
2260
2335
|
}
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
}
|
|
2271
|
-
for (const columnName of blueprint.droppedColumns) {
|
|
2272
|
-
const dropPrefix = driver === "pgsql" ? "DROP COLUMN IF EXISTS" : "DROP COLUMN";
|
|
2273
|
-
statements.push(`ALTER TABLE ${table} ${dropPrefix} ${quoteIdentifier(columnName)}`);
|
|
2336
|
+
async filterAccessibleOrganizationIds(organizationIds, user = currentAuthUser()) {
|
|
2337
|
+
if (!user) {
|
|
2338
|
+
return [];
|
|
2339
|
+
}
|
|
2340
|
+
if (isGlobalAdmin(user)) {
|
|
2341
|
+
return organizationIds;
|
|
2342
|
+
}
|
|
2343
|
+
const allowed = new Set(await this.listOrganizationIdsForUser(resolveUserId(user)));
|
|
2344
|
+
return organizationIds.filter((organizationId) => allowed.has(organizationId));
|
|
2274
2345
|
}
|
|
2275
|
-
|
|
2276
|
-
|
|
2346
|
+
async addOwnerOnOrganizationCreate(organizationId, userId) {
|
|
2347
|
+
await this.members.addMember({
|
|
2348
|
+
organizationId,
|
|
2349
|
+
userId,
|
|
2350
|
+
role: "owner"
|
|
2351
|
+
});
|
|
2277
2352
|
}
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
statements.push(compileIndex(driver, blueprint.table, index));
|
|
2281
|
-
} else {
|
|
2282
|
-
statements.push(...compileSpecialIndex(driver, blueprint.table, index));
|
|
2283
|
-
}
|
|
2353
|
+
listMembersForOrganization(organizationId) {
|
|
2354
|
+
return this.members.listForOrganization(organizationId);
|
|
2284
2355
|
}
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
function compileDropTable(driver, tableName) {
|
|
2288
|
-
const cascade = driver === "pgsql" ? " CASCADE" : "";
|
|
2289
|
-
return [`DROP TABLE IF EXISTS ${quoteIdentifier(tableName)}${cascade}`];
|
|
2290
|
-
}
|
|
2291
|
-
function compileColumn(driver, column, mode) {
|
|
2292
|
-
const parts = [quoteIdentifier(column.name), compileColumnType(driver, column)];
|
|
2293
|
-
if (column.autoIncrement && driver === "mysql") {
|
|
2294
|
-
parts[1] = `${parts[1]} AUTO_INCREMENT`;
|
|
2356
|
+
addMember(input) {
|
|
2357
|
+
return this.members.addMember(input);
|
|
2295
2358
|
}
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
parts.push("PRIMARY KEY AUTOINCREMENT");
|
|
2299
|
-
} else {
|
|
2300
|
-
parts.push("PRIMARY KEY");
|
|
2301
|
-
}
|
|
2302
|
-
} else if (!column.isNullable) {
|
|
2303
|
-
parts.push("NOT NULL");
|
|
2304
|
-
} else if (column.isNullable) {
|
|
2305
|
-
parts.push("NULL");
|
|
2359
|
+
removeMember(organizationId, userId) {
|
|
2360
|
+
return this.members.removeMember(organizationId, userId);
|
|
2306
2361
|
}
|
|
2307
|
-
|
|
2308
|
-
|
|
2362
|
+
}
|
|
2363
|
+
var membershipService_default = MembershipService;
|
|
2364
|
+
// ../../src/core/auth/policy.ts
|
|
2365
|
+
class Policy {
|
|
2366
|
+
constructor() {}
|
|
2367
|
+
view(_user, _resource) {
|
|
2368
|
+
return false;
|
|
2309
2369
|
}
|
|
2310
|
-
|
|
2311
|
-
|
|
2370
|
+
create(_user) {
|
|
2371
|
+
return false;
|
|
2312
2372
|
}
|
|
2313
|
-
|
|
2314
|
-
|
|
2373
|
+
update(_user, _resource) {
|
|
2374
|
+
return false;
|
|
2315
2375
|
}
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
const reference = `${quoteIdentifier(referencesTable)}(${quoteIdentifier(referencesColumn)})`;
|
|
2319
|
-
let clause = `REFERENCES ${reference}`;
|
|
2320
|
-
if (onDelete === "cascade") {
|
|
2321
|
-
clause += " ON DELETE CASCADE";
|
|
2322
|
-
} else if (onDelete === "set null") {
|
|
2323
|
-
clause += " ON DELETE SET NULL";
|
|
2324
|
-
}
|
|
2325
|
-
parts.push(clause);
|
|
2376
|
+
delete(_user, _resource) {
|
|
2377
|
+
return false;
|
|
2326
2378
|
}
|
|
2327
|
-
return parts.join(" ");
|
|
2328
2379
|
}
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2380
|
+
var BLOCKED_POLICY_ACTIONS = new Set([
|
|
2381
|
+
"constructor",
|
|
2382
|
+
"toString",
|
|
2383
|
+
"valueOf",
|
|
2384
|
+
"hasOwnProperty",
|
|
2385
|
+
"isPrototypeOf",
|
|
2386
|
+
"propertyIsEnumerable",
|
|
2387
|
+
"__proto__"
|
|
2388
|
+
]);
|
|
2389
|
+
|
|
2390
|
+
class PolicyGate {
|
|
2391
|
+
constructor() {}
|
|
2392
|
+
policies = new Map;
|
|
2393
|
+
register(resource, policy) {
|
|
2394
|
+
this.policies.set(resource, policy);
|
|
2395
|
+
}
|
|
2396
|
+
allows(resource, action, user, model) {
|
|
2397
|
+
const policy = this.policies.get(resource);
|
|
2398
|
+
if (!policy) {
|
|
2399
|
+
return false;
|
|
2335
2400
|
}
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
const unique = index.kind === "unique" ? "UNIQUE " : "";
|
|
2339
|
-
return `CREATE ${unique}INDEX IF NOT EXISTS ${quoteIdentifier(indexName)} ON ${quoteIdentifier(tableName)}(${columns})`;
|
|
2340
|
-
}
|
|
2341
|
-
function compileSpecialIndex(driver, tableName, index) {
|
|
2342
|
-
const indexName = index.name ?? defaultIndexName(tableName, index.columns, index.kind);
|
|
2343
|
-
const columns = index.columns.map((column) => quoteIdentifier(column)).join(", ");
|
|
2344
|
-
switch (index.kind) {
|
|
2345
|
-
case "partial":
|
|
2346
|
-
case "uniquePartial": {
|
|
2347
|
-
if (driver !== "pgsql") {
|
|
2348
|
-
throw new UnsupportedSchemaFeatureError("partialIndex()", driver);
|
|
2349
|
-
}
|
|
2350
|
-
const unique = index.kind === "uniquePartial" ? "UNIQUE " : "";
|
|
2351
|
-
return [
|
|
2352
|
-
`CREATE ${unique}INDEX IF NOT EXISTS ${quoteIdentifier(indexName)} ON ${quoteIdentifier(tableName)}(${columns}) WHERE ${index.where}`
|
|
2353
|
-
];
|
|
2401
|
+
if (BLOCKED_POLICY_ACTIONS.has(action) || !(action in policy)) {
|
|
2402
|
+
return false;
|
|
2354
2403
|
}
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
}
|
|
2359
|
-
return [
|
|
2360
|
-
`CREATE INDEX IF NOT EXISTS ${quoteIdentifier(indexName)} ON ${quoteIdentifier(tableName)} USING GIN(${columns})`
|
|
2361
|
-
];
|
|
2404
|
+
const handler = policy[action];
|
|
2405
|
+
if (typeof handler !== "function") {
|
|
2406
|
+
return false;
|
|
2362
2407
|
}
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
if (driver === "pgsql") {
|
|
2370
|
-
throw new UnsupportedSchemaFeatureError("fullText(); use ginIndex() with a tsvector column on PostgreSQL", driver);
|
|
2371
|
-
}
|
|
2372
|
-
throw new UnsupportedSchemaFeatureError("fullText()", driver);
|
|
2408
|
+
const resolvedUser = user === undefined ? currentAuthUser() : user;
|
|
2409
|
+
return model === undefined ? handler.call(policy, resolvedUser) : handler.call(policy, resolvedUser, model);
|
|
2410
|
+
}
|
|
2411
|
+
authorize(resource, action, user, model) {
|
|
2412
|
+
if (!this.allows(resource, action, user, model)) {
|
|
2413
|
+
throw new ForbiddenError;
|
|
2373
2414
|
}
|
|
2374
|
-
default:
|
|
2375
|
-
return [];
|
|
2376
2415
|
}
|
|
2377
2416
|
}
|
|
2378
|
-
|
|
2379
|
-
|
|
2417
|
+
// ../../src/core/database/bindConnection.ts
|
|
2418
|
+
function bindDatabaseConnection2(connection) {
|
|
2419
|
+
bindDatabaseConnection(connection);
|
|
2380
2420
|
}
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2421
|
+
|
|
2422
|
+
// ../../src/domain/scim.ts
|
|
2423
|
+
var TEST_SCIM_BEARER_TOKEN = "workhub-scim-test-token";
|
|
2424
|
+
|
|
2425
|
+
// ../../src/core/security/timingSafeCompare.ts
|
|
2426
|
+
import { timingSafeEqual } from "crypto";
|
|
2427
|
+
function timingSafeCompareString(left, right) {
|
|
2428
|
+
const leftBuffer = Buffer.from(left);
|
|
2429
|
+
const rightBuffer = Buffer.from(right);
|
|
2430
|
+
if (leftBuffer.length !== rightBuffer.length) {
|
|
2431
|
+
return false;
|
|
2391
2432
|
}
|
|
2433
|
+
return timingSafeEqual(leftBuffer, rightBuffer);
|
|
2392
2434
|
}
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2435
|
+
|
|
2436
|
+
// ../../src/core/security/scimTenantTokens.ts
|
|
2437
|
+
function parseScimTenantTokens(raw) {
|
|
2438
|
+
const tokens = new Map;
|
|
2439
|
+
if (!raw?.trim()) {
|
|
2440
|
+
return tokens;
|
|
2441
|
+
}
|
|
2442
|
+
for (const entry of raw.split(",")) {
|
|
2443
|
+
const [tenantPart, tokenPart] = entry.split(":");
|
|
2444
|
+
if (!tenantPart || !tokenPart) {
|
|
2445
|
+
continue;
|
|
2399
2446
|
}
|
|
2400
|
-
|
|
2447
|
+
const tenantId = Number.parseInt(tenantPart.trim(), 10);
|
|
2448
|
+
const token = tokenPart.trim();
|
|
2449
|
+
if (Number.isInteger(tenantId) && tenantId > 0 && token.length > 0) {
|
|
2450
|
+
tokens.set(tenantId, token);
|
|
2451
|
+
}
|
|
2452
|
+
}
|
|
2453
|
+
return tokens;
|
|
2454
|
+
}
|
|
2455
|
+
function resolveScimTenantFromToken(token) {
|
|
2456
|
+
const tenantTokens = parseScimTenantTokens(process.env.SCIM_TENANT_TOKENS);
|
|
2457
|
+
for (const [tenantId, expectedToken] of tenantTokens) {
|
|
2458
|
+
if (timingSafeCompareString(token, expectedToken)) {
|
|
2459
|
+
return tenantId;
|
|
2460
|
+
}
|
|
2461
|
+
}
|
|
2462
|
+
const fallbackToken = process.env.SCIM_BEARER_TOKEN ?? TEST_SCIM_BEARER_TOKEN;
|
|
2463
|
+
if (timingSafeCompareString(token, fallbackToken)) {
|
|
2464
|
+
return 1;
|
|
2465
|
+
}
|
|
2466
|
+
return null;
|
|
2401
2467
|
}
|
|
2402
2468
|
|
|
2403
|
-
// ../../src/core/
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
function grammarForDriver(driver) {
|
|
2414
|
-
switch (driver) {
|
|
2415
|
-
case "pgsql":
|
|
2416
|
-
return PostgresGrammar;
|
|
2417
|
-
case "mysql":
|
|
2418
|
-
return MySqlGrammar;
|
|
2419
|
-
case "sqlite":
|
|
2420
|
-
return SqliteGrammar;
|
|
2421
|
-
default:
|
|
2422
|
-
throw new Error(`Unsupported database driver: ${driver}`);
|
|
2423
|
-
}
|
|
2424
|
-
}
|
|
2425
|
-
// ../../src/core/database/schema/schema.ts
|
|
2426
|
-
class SchemaBuilder {
|
|
2427
|
-
#driver;
|
|
2428
|
-
#statements = [];
|
|
2429
|
-
constructor(driver) {
|
|
2430
|
-
this.#driver = driver;
|
|
2431
|
-
}
|
|
2432
|
-
create(table, callback) {
|
|
2433
|
-
const blueprint = new Blueprint(table, "create");
|
|
2434
|
-
callback(blueprint);
|
|
2435
|
-
this.#statements.push(...grammarForDriver(this.#driver).compile(blueprint));
|
|
2436
|
-
return this;
|
|
2437
|
-
}
|
|
2438
|
-
table(table, callback) {
|
|
2439
|
-
const blueprint = new Blueprint(table, "alter");
|
|
2440
|
-
callback(blueprint);
|
|
2441
|
-
this.#statements.push(...grammarForDriver(this.#driver).compile(blueprint));
|
|
2442
|
-
return this;
|
|
2443
|
-
}
|
|
2444
|
-
drop(table) {
|
|
2445
|
-
const blueprint = new Blueprint(table, "drop");
|
|
2446
|
-
this.#statements.push(...grammarForDriver(this.#driver).compile(blueprint));
|
|
2447
|
-
return this;
|
|
2448
|
-
}
|
|
2449
|
-
toSql() {
|
|
2450
|
-
return [...this.#statements];
|
|
2451
|
-
}
|
|
2452
|
-
async execute(db) {
|
|
2453
|
-
for (const statement of this.#statements) {
|
|
2454
|
-
await db.unsafe(statement);
|
|
2455
|
-
}
|
|
2456
|
-
}
|
|
2469
|
+
// ../../src/core/tenant/resolveTenant.ts
|
|
2470
|
+
async function resolveTenant(tenantId) {
|
|
2471
|
+
const rows = await repositoryConnection`
|
|
2472
|
+
SELECT id, slug, plan, region
|
|
2473
|
+
FROM tenant
|
|
2474
|
+
WHERE id = ${tenantId}
|
|
2475
|
+
LIMIT 1
|
|
2476
|
+
`;
|
|
2477
|
+
const row = rows[0];
|
|
2478
|
+
return row ? { id: row.id, slug: row.slug, plan: row.plan, region: row.region ?? "eu" } : null;
|
|
2457
2479
|
}
|
|
2458
2480
|
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
static async run(db, driver, callback) {
|
|
2464
|
-
const schema = Schema.builder(driver);
|
|
2465
|
-
await callback(schema);
|
|
2466
|
-
await schema.execute(db);
|
|
2467
|
-
}
|
|
2468
|
-
}
|
|
2469
|
-
function createSchemaBuilder(db, driver) {
|
|
2470
|
-
const builder = Schema.builder(driver);
|
|
2471
|
-
return Object.assign(builder, {
|
|
2472
|
-
async commit() {
|
|
2473
|
-
await builder.execute(db);
|
|
2474
|
-
}
|
|
2475
|
-
});
|
|
2476
|
-
}
|
|
2477
|
-
// ../../src/core/database/table.ts
|
|
2478
|
-
function defineTable(definition) {
|
|
2479
|
-
return definition;
|
|
2480
|
-
}
|
|
2481
|
-
// ../../src/core/database/transaction.ts
|
|
2482
|
-
function supportsTransactions(connection) {
|
|
2483
|
-
return typeof connection.begin === "function";
|
|
2481
|
+
// ../../src/core/tenant/tenantDatabaseScope.ts
|
|
2482
|
+
async function applyTenantContextToTransaction(transaction, tenantId) {
|
|
2483
|
+
await transaction.unsafe(`SELECT set_config('app.tenant_id', $1, true)`, [String(tenantId)]);
|
|
2484
|
+
await transaction.unsafe(`SELECT set_config('app.bypass_rls', $1, true)`, ["false"]);
|
|
2484
2485
|
}
|
|
2485
|
-
async function
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2486
|
+
async function runWithTenantDatabase(tenant, callback) {
|
|
2487
|
+
if (hasActiveDatabaseConnection()) {
|
|
2488
|
+
const activeConnection2 = getActiveDatabaseConnection(getDefaultDatabasePool());
|
|
2489
|
+
await applyTenantContextToTransaction(activeConnection2, tenant.id);
|
|
2490
|
+
return await runWithTenant(tenant, callback);
|
|
2489
2491
|
}
|
|
2490
|
-
return await
|
|
2491
|
-
|
|
2492
|
+
return await getDefaultDatabasePool().begin(async (transaction) => {
|
|
2493
|
+
await applyTenantContextToTransaction(transaction, tenant.id);
|
|
2494
|
+
return await runWithDatabaseConnection(transaction, async () => {
|
|
2495
|
+
return await runWithTenant(tenant, callback);
|
|
2496
|
+
});
|
|
2492
2497
|
});
|
|
2493
2498
|
}
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
const parsed = Number.parseInt(process.env[name] ?? String(fallback), 10);
|
|
2497
|
-
return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback;
|
|
2499
|
+
function isInsideTenantDatabaseScope(tenantId = currentTenant()?.id) {
|
|
2500
|
+
return hasActiveDatabaseConnection() && currentTenant()?.id === tenantId;
|
|
2498
2501
|
}
|
|
2499
|
-
var databaseConfig = {
|
|
2500
|
-
url: process.env.DATABASE_URL ?? "",
|
|
2501
|
-
poolMax: readInteger("DB_POOL_MAX", 10),
|
|
2502
|
-
idleTimeoutSeconds: readInteger("DB_POOL_IDLE_TIMEOUT", 30),
|
|
2503
|
-
maxLifetimeSeconds: readInteger("DB_POOL_MAX_LIFETIME", 3600),
|
|
2504
|
-
connectionTimeoutSeconds: readInteger("DB_CONNECTION_TIMEOUT", 10)
|
|
2505
|
-
};
|
|
2506
2502
|
|
|
2507
|
-
// ../../src/
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2503
|
+
// ../../src/core/auth/scimAuthMiddleware.ts
|
|
2504
|
+
function createScimAuthMiddleware() {
|
|
2505
|
+
return async (request, next) => {
|
|
2506
|
+
const authorization = request.headers.get("authorization");
|
|
2507
|
+
if (!authorization?.startsWith("Bearer ")) {
|
|
2508
|
+
return jsonScimError("SCIM bearer token required.", 401);
|
|
2509
|
+
}
|
|
2510
|
+
const token = authorization.slice("Bearer ".length).trim();
|
|
2511
|
+
const tenantId = resolveScimTenantFromToken(token);
|
|
2512
|
+
if (tenantId === null) {
|
|
2513
|
+
return jsonScimError("Invalid SCIM bearer token.", 401);
|
|
2514
|
+
}
|
|
2515
|
+
const tenant = await resolveTenant(tenantId);
|
|
2516
|
+
if (!tenant) {
|
|
2517
|
+
return jsonScimError("SCIM tenant not found.", 401);
|
|
2518
|
+
}
|
|
2519
|
+
return await runWithTenantDatabase(tenant, async () => {
|
|
2520
|
+
bindDatabaseConnection2(getActiveDatabaseConnection(getDefaultDatabasePool()));
|
|
2521
|
+
try {
|
|
2522
|
+
return await next();
|
|
2523
|
+
} finally {
|
|
2524
|
+
resetBoundDatabaseConnection();
|
|
2525
|
+
}
|
|
2526
|
+
});
|
|
2527
|
+
};
|
|
2528
|
+
}
|
|
2529
|
+
function jsonScimError(detail, status) {
|
|
2530
|
+
return Response.json({
|
|
2531
|
+
schemas: ["urn:ietf:params:scim:api:messages:2.0:Error"],
|
|
2532
|
+
detail,
|
|
2533
|
+
status: String(status)
|
|
2534
|
+
}, {
|
|
2535
|
+
status,
|
|
2536
|
+
headers: { "content-type": "application/scim+json" }
|
|
2519
2537
|
});
|
|
2520
2538
|
}
|
|
2539
|
+
// ../../src/core/cache/redisCacheStore.ts
|
|
2540
|
+
var {RedisClient } = globalThis.Bun;
|
|
2541
|
+
var KEY_PREFIX = "workhub:cache:";
|
|
2542
|
+
var TAG_PREFIX = "workhub:cache:tag:";
|
|
2521
2543
|
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2544
|
+
class RedisCacheStore {
|
|
2545
|
+
ttlMs;
|
|
2546
|
+
maxEntries;
|
|
2547
|
+
client;
|
|
2548
|
+
inflight = new Map;
|
|
2549
|
+
keyTags = new Map;
|
|
2550
|
+
constructor(redisUrl, ttlMs, maxEntries) {
|
|
2551
|
+
this.ttlMs = ttlMs;
|
|
2552
|
+
this.maxEntries = maxEntries;
|
|
2553
|
+
this.client = new RedisClient(redisUrl);
|
|
2530
2554
|
}
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
var db = new Proxy(function database() {}, {
|
|
2538
|
-
apply(_target, _thisArg, args) {
|
|
2539
|
-
return getDb()(...args);
|
|
2540
|
-
},
|
|
2541
|
-
get(_target, property) {
|
|
2542
|
-
const connection = getDb();
|
|
2543
|
-
const value = connection[property];
|
|
2544
|
-
return typeof value === "function" ? value.bind(connection) : value;
|
|
2555
|
+
async get(key) {
|
|
2556
|
+
const raw = await this.client.get(this.storageKey(key));
|
|
2557
|
+
if (raw === null) {
|
|
2558
|
+
return;
|
|
2559
|
+
}
|
|
2560
|
+
return JSON.parse(raw);
|
|
2545
2561
|
}
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
"user_id",
|
|
2556
|
-
"name",
|
|
2557
|
-
"token_hash",
|
|
2558
|
-
"abilities",
|
|
2559
|
-
"last_used_at",
|
|
2560
|
-
"expires_at",
|
|
2561
|
-
"created_at"
|
|
2562
|
-
],
|
|
2563
|
-
defaultOrderBy: { column: "id", direction: "ASC" }
|
|
2564
|
-
});
|
|
2565
|
-
|
|
2566
|
-
// ../../src/core/auth/password.ts
|
|
2567
|
-
async function verifyPassword(password, passwordHash) {
|
|
2568
|
-
return await Bun.password.verify(password, passwordHash);
|
|
2569
|
-
}
|
|
2570
|
-
|
|
2571
|
-
// ../../src/core/crypto/fieldEncryption.ts
|
|
2572
|
-
import { createCipheriv, createDecipheriv, createHmac, randomBytes } from "crypto";
|
|
2573
|
-
var ENCRYPTION_PREFIX = "enc:v1:";
|
|
2574
|
-
var IV_LENGTH = 12;
|
|
2575
|
-
var TAG_LENGTH = 16;
|
|
2576
|
-
function resolveEncryptionKey() {
|
|
2577
|
-
const raw = process.env.KMS_ENCRYPTION_KEY?.trim();
|
|
2578
|
-
if (!raw) {
|
|
2579
|
-
return null;
|
|
2562
|
+
async set(key, value, ttlMs) {
|
|
2563
|
+
const resolvedTtlMs = ttlMs ?? this.ttlMs;
|
|
2564
|
+
const payload = JSON.stringify(value);
|
|
2565
|
+
if (resolvedTtlMs > 0) {
|
|
2566
|
+
await this.client.psetex(this.storageKey(key), resolvedTtlMs, payload);
|
|
2567
|
+
} else {
|
|
2568
|
+
await this.client.set(this.storageKey(key), payload);
|
|
2569
|
+
}
|
|
2570
|
+
await this.enforceMaxEntries();
|
|
2580
2571
|
}
|
|
2581
|
-
|
|
2582
|
-
|
|
2572
|
+
async getOrSet(key, loader, ttlMs) {
|
|
2573
|
+
const cached = await this.get(key);
|
|
2574
|
+
if (cached !== undefined) {
|
|
2575
|
+
return cached;
|
|
2576
|
+
}
|
|
2577
|
+
const inflightRequest = this.inflight.get(key);
|
|
2578
|
+
if (inflightRequest) {
|
|
2579
|
+
return inflightRequest;
|
|
2580
|
+
}
|
|
2581
|
+
const pendingRequest = loader().then(async (value) => {
|
|
2582
|
+
await this.set(key, value, ttlMs);
|
|
2583
|
+
return value;
|
|
2584
|
+
}).finally(() => {
|
|
2585
|
+
this.inflight.delete(key);
|
|
2586
|
+
});
|
|
2587
|
+
this.inflight.set(key, pendingRequest);
|
|
2588
|
+
return pendingRequest;
|
|
2583
2589
|
}
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2590
|
+
async attachTags(key, tags) {
|
|
2591
|
+
if (tags.length === 0) {
|
|
2592
|
+
return;
|
|
2593
|
+
}
|
|
2594
|
+
let tagsForKey = this.keyTags.get(key);
|
|
2595
|
+
if (!tagsForKey) {
|
|
2596
|
+
tagsForKey = new Set;
|
|
2597
|
+
this.keyTags.set(key, tagsForKey);
|
|
2598
|
+
}
|
|
2599
|
+
for (const tag of tags) {
|
|
2600
|
+
tagsForKey.add(tag);
|
|
2601
|
+
await this.client.sadd(this.tagKey(tag), key);
|
|
2602
|
+
}
|
|
2587
2603
|
}
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2604
|
+
async flushTags(tags) {
|
|
2605
|
+
const keysToRemove = new Set;
|
|
2606
|
+
for (const tag of tags) {
|
|
2607
|
+
const members = await this.client.smembers(this.tagKey(tag));
|
|
2608
|
+
for (const member of members) {
|
|
2609
|
+
keysToRemove.add(member);
|
|
2610
|
+
}
|
|
2611
|
+
}
|
|
2612
|
+
let removed = 0;
|
|
2613
|
+
for (const key of keysToRemove) {
|
|
2614
|
+
if (await this.invalidate(key)) {
|
|
2615
|
+
removed += 1;
|
|
2616
|
+
}
|
|
2617
|
+
}
|
|
2618
|
+
for (const tag of tags) {
|
|
2619
|
+
await this.client.del(this.tagKey(tag));
|
|
2620
|
+
}
|
|
2621
|
+
return removed;
|
|
2622
|
+
}
|
|
2623
|
+
async invalidate(key) {
|
|
2624
|
+
const deleted = await this.client.del(this.storageKey(key));
|
|
2625
|
+
await this.detachKeyFromTags(key);
|
|
2626
|
+
return deleted > 0;
|
|
2627
|
+
}
|
|
2628
|
+
async invalidateByPrefix(prefix) {
|
|
2629
|
+
const keys = await this.client.keys(`${KEY_PREFIX}*`);
|
|
2630
|
+
let removed = 0;
|
|
2631
|
+
for (const storageKey of keys) {
|
|
2632
|
+
const key = storageKey.slice(KEY_PREFIX.length);
|
|
2633
|
+
if (key === prefix || key.startsWith(`${prefix}?`)) {
|
|
2634
|
+
if (await this.invalidate(key)) {
|
|
2635
|
+
removed += 1;
|
|
2636
|
+
}
|
|
2637
|
+
}
|
|
2638
|
+
}
|
|
2639
|
+
return removed;
|
|
2594
2640
|
}
|
|
2595
|
-
|
|
2596
|
-
|
|
2641
|
+
async clear() {
|
|
2642
|
+
const keys = await this.client.keys(`${KEY_PREFIX}*`);
|
|
2643
|
+
if (keys.length > 0) {
|
|
2644
|
+
await this.client.del(...keys);
|
|
2645
|
+
}
|
|
2646
|
+
const tagKeys = await this.client.keys(`${TAG_PREFIX}*`);
|
|
2647
|
+
if (tagKeys.length > 0) {
|
|
2648
|
+
await this.client.del(...tagKeys);
|
|
2649
|
+
}
|
|
2650
|
+
this.inflight.clear();
|
|
2651
|
+
this.keyTags.clear();
|
|
2597
2652
|
}
|
|
2598
|
-
|
|
2599
|
-
}
|
|
2600
|
-
|
|
2601
|
-
if (!value.startsWith(ENCRYPTION_PREFIX)) {
|
|
2602
|
-
return value;
|
|
2653
|
+
async size() {
|
|
2654
|
+
const keys = await this.client.keys(`${KEY_PREFIX}*`);
|
|
2655
|
+
return keys.length;
|
|
2603
2656
|
}
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
const tag = payload.subarray(payload.length - TAG_LENGTH);
|
|
2607
|
-
const ciphertext = payload.subarray(IV_LENGTH, payload.length - TAG_LENGTH);
|
|
2608
|
-
const decipher = createDecipheriv("aes-256-gcm", key, iv);
|
|
2609
|
-
decipher.setAuthTag(tag);
|
|
2610
|
-
return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf8");
|
|
2611
|
-
}
|
|
2612
|
-
|
|
2613
|
-
// ../../src/core/crypto/mfaSecret.ts
|
|
2614
|
-
function revealMfaSecret(stored) {
|
|
2615
|
-
if (!stored) {
|
|
2616
|
-
return null;
|
|
2657
|
+
storageKey(key) {
|
|
2658
|
+
return `${KEY_PREFIX}${key}`;
|
|
2617
2659
|
}
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
return stored;
|
|
2660
|
+
tagKey(tag) {
|
|
2661
|
+
return `${TAG_PREFIX}${tag}`;
|
|
2621
2662
|
}
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
}
|
|
2630
|
-
|
|
2631
|
-
return requestMetaContext.getStore() ?? {
|
|
2632
|
-
ipAddress: null,
|
|
2633
|
-
userAgent: null
|
|
2634
|
-
};
|
|
2635
|
-
}
|
|
2636
|
-
|
|
2637
|
-
// ../../src/core/security/securityEvents.ts
|
|
2638
|
-
function logSecurityEvent(event, details = {}) {
|
|
2639
|
-
const meta = currentRequestMeta();
|
|
2640
|
-
const user = currentAuthUser();
|
|
2641
|
-
console.log(JSON.stringify({
|
|
2642
|
-
level: "security",
|
|
2643
|
-
event,
|
|
2644
|
-
timestamp: new Date().toISOString(),
|
|
2645
|
-
ip_address: meta.ipAddress ?? null,
|
|
2646
|
-
user_agent: meta.userAgent ?? null,
|
|
2647
|
-
user_id: user?.id ?? null,
|
|
2648
|
-
...details
|
|
2649
|
-
}));
|
|
2650
|
-
}
|
|
2651
|
-
|
|
2652
|
-
// ../../src/core/security/tokenExpiry.ts
|
|
2653
|
-
function resolveDefaultTokenExpiryDays() {
|
|
2654
|
-
const raw = process.env.API_TOKEN_DEFAULT_EXPIRY_DAYS?.trim();
|
|
2655
|
-
if (!raw) {
|
|
2656
|
-
return null;
|
|
2663
|
+
async detachKeyFromTags(key) {
|
|
2664
|
+
const tags = this.keyTags.get(key);
|
|
2665
|
+
if (!tags) {
|
|
2666
|
+
return;
|
|
2667
|
+
}
|
|
2668
|
+
for (const tag of tags) {
|
|
2669
|
+
await this.client.srem(this.tagKey(tag), key);
|
|
2670
|
+
}
|
|
2671
|
+
this.keyTags.delete(key);
|
|
2657
2672
|
}
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2673
|
+
async enforceMaxEntries() {
|
|
2674
|
+
const keys = await this.client.keys(`${KEY_PREFIX}*`);
|
|
2675
|
+
if (keys.length <= this.maxEntries) {
|
|
2676
|
+
return;
|
|
2677
|
+
}
|
|
2678
|
+
const overflow = keys.length - this.maxEntries;
|
|
2679
|
+
const keysToRemove = keys.slice(0, overflow);
|
|
2680
|
+
if (keysToRemove.length > 0) {
|
|
2681
|
+
await this.client.del(...keysToRemove);
|
|
2682
|
+
}
|
|
2661
2683
|
}
|
|
2662
|
-
return parsed;
|
|
2663
2684
|
}
|
|
2685
|
+
var redisCacheStore_default = RedisCacheStore;
|
|
2664
2686
|
|
|
2665
|
-
// ../../src/core/
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2687
|
+
// ../../src/core/cache/simpleCache.ts
|
|
2688
|
+
class SimpleCache {
|
|
2689
|
+
ttlMs;
|
|
2690
|
+
maxEntries;
|
|
2691
|
+
cache = new Map;
|
|
2692
|
+
inflight = new Map;
|
|
2693
|
+
tagIndex = new Map;
|
|
2694
|
+
keyTags = new Map;
|
|
2695
|
+
constructor(ttlMs = 3600000, maxEntries = 100) {
|
|
2696
|
+
this.ttlMs = ttlMs;
|
|
2697
|
+
this.maxEntries = maxEntries;
|
|
2698
|
+
if (!Number.isFinite(ttlMs) || ttlMs < 0) {
|
|
2699
|
+
throw new RangeError("ttlMs must be a non-negative number.");
|
|
2700
|
+
}
|
|
2701
|
+
if (!Number.isInteger(maxEntries) || maxEntries < 1) {
|
|
2702
|
+
throw new RangeError("maxEntries must be a positive integer.");
|
|
2675
2703
|
}
|
|
2676
|
-
bits += value.toString(2).padStart(5, "0");
|
|
2677
2704
|
}
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
bytes.push(Number.parseInt(bits.slice(index, index + 8), 2));
|
|
2705
|
+
get(key) {
|
|
2706
|
+
return this.getFreshEntry(key)?.value;
|
|
2681
2707
|
}
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
const b0 = digest[offset] ?? 0;
|
|
2692
|
-
const b1 = digest[offset + 1] ?? 0;
|
|
2693
|
-
const b2 = digest[offset + 2] ?? 0;
|
|
2694
|
-
const b3 = digest[offset + 3] ?? 0;
|
|
2695
|
-
const code = (b0 & 127) << 24 | (b1 & 255) << 16 | (b2 & 255) << 8 | b3 & 255;
|
|
2696
|
-
return String(code % 10 ** digits).padStart(digits, "0");
|
|
2697
|
-
}
|
|
2698
|
-
function verifyTotp(secret, token, window = 1) {
|
|
2699
|
-
const normalized = token.trim();
|
|
2700
|
-
if (!/^\d{6}$/u.test(normalized)) {
|
|
2701
|
-
return false;
|
|
2708
|
+
set(key, value, ttlMs) {
|
|
2709
|
+
const now = Date.now();
|
|
2710
|
+
const resolvedTtlMs = ttlMs ?? this.ttlMs;
|
|
2711
|
+
this.cache.set(key, {
|
|
2712
|
+
value,
|
|
2713
|
+
expiresAt: now + resolvedTtlMs,
|
|
2714
|
+
lastAccessedAt: now
|
|
2715
|
+
});
|
|
2716
|
+
this.evictOverflow();
|
|
2702
2717
|
}
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2718
|
+
async getOrSet(key, loader, ttlMs) {
|
|
2719
|
+
this.pruneExpired();
|
|
2720
|
+
const cachedEntry = this.getFreshEntry(key);
|
|
2721
|
+
if (cachedEntry) {
|
|
2722
|
+
return cachedEntry.value;
|
|
2723
|
+
}
|
|
2724
|
+
const inflightRequest = this.inflight.get(key);
|
|
2725
|
+
if (inflightRequest) {
|
|
2726
|
+
return inflightRequest;
|
|
2727
|
+
}
|
|
2728
|
+
const pendingRequest = loader().then((value) => {
|
|
2729
|
+
this.set(key, value, ttlMs);
|
|
2730
|
+
return value;
|
|
2731
|
+
}).finally(() => {
|
|
2732
|
+
this.inflight.delete(key);
|
|
2733
|
+
});
|
|
2734
|
+
this.inflight.set(key, pendingRequest);
|
|
2735
|
+
return pendingRequest;
|
|
2736
|
+
}
|
|
2737
|
+
attachTags(key, tags) {
|
|
2738
|
+
if (tags.length === 0) {
|
|
2739
|
+
return;
|
|
2740
|
+
}
|
|
2741
|
+
let tagsForKey = this.keyTags.get(key);
|
|
2742
|
+
if (!tagsForKey) {
|
|
2743
|
+
tagsForKey = new Set;
|
|
2744
|
+
this.keyTags.set(key, tagsForKey);
|
|
2745
|
+
}
|
|
2746
|
+
for (const tag of tags) {
|
|
2747
|
+
tagsForKey.add(tag);
|
|
2748
|
+
let keysForTag = this.tagIndex.get(tag);
|
|
2749
|
+
if (!keysForTag) {
|
|
2750
|
+
keysForTag = new Set;
|
|
2751
|
+
this.tagIndex.set(tag, keysForTag);
|
|
2752
|
+
}
|
|
2753
|
+
keysForTag.add(key);
|
|
2707
2754
|
}
|
|
2708
2755
|
}
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
}
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
return 1;
|
|
2756
|
+
flushTags(tags) {
|
|
2757
|
+
const keysToRemove = new Set;
|
|
2758
|
+
for (const tag of tags) {
|
|
2759
|
+
const keys = this.tagIndex.get(tag);
|
|
2760
|
+
if (!keys) {
|
|
2761
|
+
continue;
|
|
2762
|
+
}
|
|
2763
|
+
for (const key of keys) {
|
|
2764
|
+
keysToRemove.add(key);
|
|
2765
|
+
}
|
|
2766
|
+
}
|
|
2767
|
+
let removed = 0;
|
|
2768
|
+
for (const key of keysToRemove) {
|
|
2769
|
+
if (this.invalidate(key)) {
|
|
2770
|
+
removed += 1;
|
|
2771
|
+
}
|
|
2772
|
+
}
|
|
2773
|
+
for (const tag of tags) {
|
|
2774
|
+
this.tagIndex.delete(tag);
|
|
2775
|
+
}
|
|
2776
|
+
return removed;
|
|
2731
2777
|
}
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
oauthIdentities;
|
|
2739
|
-
oauthProviders = new Map;
|
|
2740
|
-
constructor(users, tokens, oauthIdentities) {
|
|
2741
|
-
this.users = users;
|
|
2742
|
-
this.tokens = tokens;
|
|
2743
|
-
this.oauthIdentities = oauthIdentities;
|
|
2778
|
+
invalidate(key) {
|
|
2779
|
+
const removed = this.cache.delete(key);
|
|
2780
|
+
if (removed) {
|
|
2781
|
+
this.detachKeyFromTags(key);
|
|
2782
|
+
}
|
|
2783
|
+
return removed;
|
|
2744
2784
|
}
|
|
2745
|
-
|
|
2746
|
-
|
|
2785
|
+
invalidateByPrefix(prefix) {
|
|
2786
|
+
let removed = 0;
|
|
2787
|
+
for (const key of [...this.cache.keys()]) {
|
|
2788
|
+
if (key === prefix || key.startsWith(`${prefix}?`)) {
|
|
2789
|
+
if (this.invalidate(key)) {
|
|
2790
|
+
removed += 1;
|
|
2791
|
+
}
|
|
2792
|
+
}
|
|
2793
|
+
}
|
|
2794
|
+
return removed;
|
|
2747
2795
|
}
|
|
2748
|
-
|
|
2749
|
-
|
|
2796
|
+
clear() {
|
|
2797
|
+
this.cache.clear();
|
|
2798
|
+
this.inflight.clear();
|
|
2799
|
+
this.tagIndex.clear();
|
|
2800
|
+
this.keyTags.clear();
|
|
2750
2801
|
}
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
logSecurityEvent("auth_login_failed", { reason: "invalid_password", user_id: user.id });
|
|
2760
|
-
throw new UnauthorizedError("Invalid credentials.");
|
|
2761
|
-
}
|
|
2762
|
-
if (isFeatureEnabled("emailVerification") && !user.email_verified_at) {
|
|
2763
|
-
logSecurityEvent("auth_login_blocked", { reason: "email_unverified", user_id: user.id });
|
|
2764
|
-
throw new UnauthorizedError("Email address is not verified.");
|
|
2802
|
+
size() {
|
|
2803
|
+
this.pruneExpired();
|
|
2804
|
+
return this.cache.size;
|
|
2805
|
+
}
|
|
2806
|
+
detachKeyFromTags(key) {
|
|
2807
|
+
const tags = this.keyTags.get(key);
|
|
2808
|
+
if (!tags) {
|
|
2809
|
+
return;
|
|
2765
2810
|
}
|
|
2766
|
-
|
|
2767
|
-
const
|
|
2768
|
-
if (!
|
|
2769
|
-
|
|
2770
|
-
|
|
2811
|
+
for (const tag of tags) {
|
|
2812
|
+
const keys = this.tagIndex.get(tag);
|
|
2813
|
+
if (!keys) {
|
|
2814
|
+
continue;
|
|
2815
|
+
}
|
|
2816
|
+
keys.delete(key);
|
|
2817
|
+
if (keys.size === 0) {
|
|
2818
|
+
this.tagIndex.delete(tag);
|
|
2771
2819
|
}
|
|
2772
2820
|
}
|
|
2773
|
-
|
|
2774
|
-
return await this.tokens.createToken(user.id, {
|
|
2775
|
-
name: "password-login",
|
|
2776
|
-
abilities: resolveAbilitiesForRole(user.role),
|
|
2777
|
-
expiresInDays: resolveDefaultTokenExpiryDays() ?? undefined
|
|
2778
|
-
});
|
|
2821
|
+
this.keyTags.delete(key);
|
|
2779
2822
|
}
|
|
2780
|
-
|
|
2781
|
-
const
|
|
2782
|
-
if (!
|
|
2783
|
-
|
|
2823
|
+
getFreshEntry(key) {
|
|
2824
|
+
const entry = this.cache.get(key);
|
|
2825
|
+
if (!entry) {
|
|
2826
|
+
return;
|
|
2784
2827
|
}
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
expiresInDays: resolveDefaultTokenExpiryDays() ?? undefined
|
|
2792
|
-
});
|
|
2828
|
+
if (entry.expiresAt <= Date.now()) {
|
|
2829
|
+
this.invalidate(key);
|
|
2830
|
+
return;
|
|
2831
|
+
}
|
|
2832
|
+
entry.lastAccessedAt = Date.now();
|
|
2833
|
+
return entry;
|
|
2793
2834
|
}
|
|
2794
|
-
|
|
2795
|
-
const
|
|
2796
|
-
|
|
2797
|
-
|
|
2835
|
+
pruneExpired() {
|
|
2836
|
+
const now = Date.now();
|
|
2837
|
+
for (const [key, entry] of this.cache.entries()) {
|
|
2838
|
+
if (entry.expiresAt <= now) {
|
|
2839
|
+
this.invalidate(key);
|
|
2840
|
+
}
|
|
2798
2841
|
}
|
|
2799
|
-
return provider.getAuthorizationUrl(state);
|
|
2800
2842
|
}
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2843
|
+
evictOverflow() {
|
|
2844
|
+
while (this.cache.size > this.maxEntries) {
|
|
2845
|
+
let oldestKey;
|
|
2846
|
+
let oldestAccessTime = Number.POSITIVE_INFINITY;
|
|
2847
|
+
for (const [key, entry] of this.cache.entries()) {
|
|
2848
|
+
if (entry.lastAccessedAt < oldestAccessTime) {
|
|
2849
|
+
oldestAccessTime = entry.lastAccessedAt;
|
|
2850
|
+
oldestKey = key;
|
|
2851
|
+
}
|
|
2852
|
+
}
|
|
2853
|
+
if (!oldestKey) {
|
|
2854
|
+
return;
|
|
2855
|
+
}
|
|
2856
|
+
this.invalidate(oldestKey);
|
|
2805
2857
|
}
|
|
2806
|
-
const existingUser = await this.users.findByEmail(profile.email);
|
|
2807
|
-
const user = existingUser ?? await this.users.create({
|
|
2808
|
-
name: profile.name,
|
|
2809
|
-
email: profile.email,
|
|
2810
|
-
role: "member",
|
|
2811
|
-
tenant_id: currentTenantId(),
|
|
2812
|
-
email_verified_at: new Date,
|
|
2813
|
-
created_at: new Date,
|
|
2814
|
-
updated_at: new Date
|
|
2815
|
-
});
|
|
2816
|
-
await this.oauthIdentities.create({
|
|
2817
|
-
user_id: user.id,
|
|
2818
|
-
provider: providerName,
|
|
2819
|
-
provider_user_id: profile.providerUserId,
|
|
2820
|
-
email: profile.email,
|
|
2821
|
-
created_at: new Date
|
|
2822
|
-
});
|
|
2823
|
-
return user;
|
|
2824
2858
|
}
|
|
2825
2859
|
}
|
|
2860
|
+
var simpleCache_default = SimpleCache;
|
|
2826
2861
|
|
|
2827
|
-
// ../../src/
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
// ../../src/modules/user/provider.ts
|
|
2864
|
-
var tokenServiceToken = CORE_TOKEN_SERVICE_TOKEN;
|
|
2865
|
-
|
|
2866
|
-
// ../../src/core/auth/guard.ts
|
|
2867
|
-
function devHeaderAbilities(role) {
|
|
2868
|
-
if (role === "admin") {
|
|
2869
|
-
return [...ADMIN_ABILITIES];
|
|
2862
|
+
// ../../src/core/cache/simpleCacheStore.ts
|
|
2863
|
+
class SimpleCacheStore {
|
|
2864
|
+
cache;
|
|
2865
|
+
constructor(cache) {
|
|
2866
|
+
this.cache = cache;
|
|
2867
|
+
}
|
|
2868
|
+
get(key) {
|
|
2869
|
+
return Promise.resolve(this.cache.get(key));
|
|
2870
|
+
}
|
|
2871
|
+
set(key, value, ttlMs) {
|
|
2872
|
+
this.cache.set(key, value, ttlMs);
|
|
2873
|
+
return Promise.resolve();
|
|
2874
|
+
}
|
|
2875
|
+
getOrSet(key, loader, ttlMs) {
|
|
2876
|
+
return this.cache.getOrSet(key, loader, ttlMs);
|
|
2877
|
+
}
|
|
2878
|
+
attachTags(key, tags) {
|
|
2879
|
+
this.cache.attachTags(key, tags);
|
|
2880
|
+
return Promise.resolve();
|
|
2881
|
+
}
|
|
2882
|
+
flushTags(tags) {
|
|
2883
|
+
return Promise.resolve(this.cache.flushTags(tags));
|
|
2884
|
+
}
|
|
2885
|
+
invalidate(key) {
|
|
2886
|
+
return Promise.resolve(this.cache.invalidate(key));
|
|
2887
|
+
}
|
|
2888
|
+
invalidateByPrefix(prefix) {
|
|
2889
|
+
return Promise.resolve(this.cache.invalidateByPrefix(prefix));
|
|
2890
|
+
}
|
|
2891
|
+
clear() {
|
|
2892
|
+
this.cache.clear();
|
|
2893
|
+
return Promise.resolve();
|
|
2894
|
+
}
|
|
2895
|
+
size() {
|
|
2896
|
+
return Promise.resolve(this.cache.size());
|
|
2870
2897
|
}
|
|
2871
|
-
return [...MEMBER_ABILITIES];
|
|
2872
2898
|
}
|
|
2899
|
+
var simpleCacheStore_default = SimpleCacheStore;
|
|
2873
2900
|
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
if (!
|
|
2878
|
-
|
|
2901
|
+
// ../../src/core/cache/createCacheStore.ts
|
|
2902
|
+
function createCacheStore(options) {
|
|
2903
|
+
if (options.driver === "redis") {
|
|
2904
|
+
if (!options.redisUrl) {
|
|
2905
|
+
throw new Error('CACHE_DRIVER="redis" requires REDIS_URL to be set.');
|
|
2879
2906
|
}
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2907
|
+
return new redisCacheStore_default(options.redisUrl, options.ttlMs, options.maxEntries);
|
|
2908
|
+
}
|
|
2909
|
+
return new simpleCacheStore_default(new simpleCache_default(options.ttlMs, options.maxEntries));
|
|
2910
|
+
}
|
|
2911
|
+
// ../../src/core/cache/taggedCache.ts
|
|
2912
|
+
class TaggedCache {
|
|
2913
|
+
store;
|
|
2914
|
+
tags;
|
|
2915
|
+
constructor(store, tags) {
|
|
2916
|
+
this.store = store;
|
|
2917
|
+
this.tags = tags;
|
|
2918
|
+
}
|
|
2919
|
+
async remember(key, callback, ttlMs) {
|
|
2920
|
+
const value = await this.store.getOrSet(key, callback, ttlMs);
|
|
2921
|
+
await this.store.attachTags(key, this.tags);
|
|
2922
|
+
return value;
|
|
2923
|
+
}
|
|
2924
|
+
async flush() {
|
|
2925
|
+
return this.store.flushTags(this.tags);
|
|
2886
2926
|
}
|
|
2887
2927
|
}
|
|
2928
|
+
var taggedCache_default = TaggedCache;
|
|
2888
2929
|
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2930
|
+
// ../../src/core/cache/repository.ts
|
|
2931
|
+
class CacheRepository {
|
|
2932
|
+
store;
|
|
2933
|
+
constructor(store) {
|
|
2934
|
+
this.store = store;
|
|
2893
2935
|
}
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2936
|
+
async get(key) {
|
|
2937
|
+
return this.store.get(key);
|
|
2938
|
+
}
|
|
2939
|
+
async remember(key, callback, ttlMs) {
|
|
2940
|
+
return this.store.getOrSet(key, callback, ttlMs);
|
|
2941
|
+
}
|
|
2942
|
+
async forget(key) {
|
|
2943
|
+
return this.store.invalidate(key);
|
|
2944
|
+
}
|
|
2945
|
+
async flush() {
|
|
2946
|
+
await this.store.clear();
|
|
2947
|
+
}
|
|
2948
|
+
tags(...names) {
|
|
2949
|
+
return new taggedCache_default(this.store, names);
|
|
2950
|
+
}
|
|
2951
|
+
async getOrSet(key, loader, ttlMs) {
|
|
2952
|
+
return this.remember(key, loader, ttlMs);
|
|
2953
|
+
}
|
|
2954
|
+
async invalidate(key) {
|
|
2955
|
+
return this.forget(key);
|
|
2956
|
+
}
|
|
2957
|
+
async invalidateByPrefix(prefix) {
|
|
2958
|
+
return this.store.invalidateByPrefix(prefix);
|
|
2959
|
+
}
|
|
2960
|
+
async clear() {
|
|
2961
|
+
await this.flush();
|
|
2962
|
+
}
|
|
2963
|
+
async size() {
|
|
2964
|
+
return this.store.size();
|
|
2904
2965
|
}
|
|
2905
2966
|
}
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2967
|
+
var repository_default2 = CacheRepository;
|
|
2968
|
+
// ../../src/core/cache/tags.ts
|
|
2969
|
+
var CACHE_TAGS = {
|
|
2970
|
+
organizations: "organizations",
|
|
2971
|
+
projects: "projects",
|
|
2972
|
+
tasks: "tasks",
|
|
2973
|
+
comments: "comments",
|
|
2974
|
+
attachments: "attachments",
|
|
2975
|
+
reports: "reports"
|
|
2976
|
+
};
|
|
2977
|
+
// ../../src/core/contracts/container.ts
|
|
2978
|
+
class ServiceContainer {
|
|
2979
|
+
services = new Map;
|
|
2980
|
+
singletonFactories = new Map;
|
|
2981
|
+
bindings = new Map;
|
|
2982
|
+
set(key, value) {
|
|
2983
|
+
this.singletonFactories.delete(key);
|
|
2984
|
+
this.bindings.delete(key);
|
|
2985
|
+
this.services.set(key, value);
|
|
2986
|
+
return value;
|
|
2911
2987
|
}
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2988
|
+
singleton(key, factory) {
|
|
2989
|
+
this.bindings.delete(key);
|
|
2990
|
+
this.services.delete(key);
|
|
2991
|
+
this.singletonFactories.set(key, factory);
|
|
2992
|
+
}
|
|
2993
|
+
bind(key, factory) {
|
|
2994
|
+
this.singletonFactories.delete(key);
|
|
2995
|
+
this.services.delete(key);
|
|
2996
|
+
this.bindings.set(key, factory);
|
|
2997
|
+
}
|
|
2998
|
+
get(key) {
|
|
2999
|
+
if (this.services.has(key)) {
|
|
3000
|
+
return this.services.get(key);
|
|
2916
3001
|
}
|
|
2917
|
-
const
|
|
2918
|
-
if (
|
|
2919
|
-
|
|
3002
|
+
const singletonFactory = this.singletonFactories.get(key);
|
|
3003
|
+
if (singletonFactory) {
|
|
3004
|
+
const value = singletonFactory(this);
|
|
3005
|
+
this.services.set(key, value);
|
|
3006
|
+
return value;
|
|
2920
3007
|
}
|
|
2921
|
-
|
|
2922
|
-
|
|
3008
|
+
const binding = this.bindings.get(key);
|
|
3009
|
+
if (binding) {
|
|
3010
|
+
return binding(this);
|
|
2923
3011
|
}
|
|
2924
|
-
|
|
2925
|
-
return await tokenService.resolveUserFromToken(token);
|
|
3012
|
+
throw new Error(`Service "${key}" is not registered.`);
|
|
2926
3013
|
}
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
class CompositeGuard {
|
|
2930
|
-
guards;
|
|
2931
|
-
constructor(guards) {
|
|
2932
|
-
this.guards = guards;
|
|
3014
|
+
resolve(key) {
|
|
3015
|
+
return this.get(key);
|
|
2933
3016
|
}
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
const user = await Promise.resolve(guard.resolve(request));
|
|
2937
|
-
if (user) {
|
|
2938
|
-
return user;
|
|
2939
|
-
}
|
|
2940
|
-
}
|
|
2941
|
-
return null;
|
|
3017
|
+
has(key) {
|
|
3018
|
+
return this.services.has(key) || this.singletonFactories.has(key) || this.bindings.has(key);
|
|
2942
3019
|
}
|
|
2943
3020
|
}
|
|
2944
3021
|
|
|
2945
|
-
class
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
this.
|
|
3022
|
+
class ConfigStore {
|
|
3023
|
+
values = new Map;
|
|
3024
|
+
set(key, value) {
|
|
3025
|
+
this.values.set(key, value);
|
|
3026
|
+
return value;
|
|
2949
3027
|
}
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
return await Promise.resolve(this.guard.resolve(request));
|
|
2953
|
-
}
|
|
2954
|
-
return currentAuthUser();
|
|
3028
|
+
get(key) {
|
|
3029
|
+
return this.values.get(key);
|
|
2955
3030
|
}
|
|
2956
|
-
|
|
2957
|
-
|
|
3031
|
+
require(key) {
|
|
3032
|
+
if (!this.values.has(key)) {
|
|
3033
|
+
throw new Error(`Config key "${key}" is not defined.`);
|
|
3034
|
+
}
|
|
3035
|
+
return this.values.get(key);
|
|
2958
3036
|
}
|
|
2959
|
-
|
|
2960
|
-
return
|
|
3037
|
+
has(key) {
|
|
3038
|
+
return this.values.has(key);
|
|
2961
3039
|
}
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
3040
|
+
}
|
|
3041
|
+
// ../../src/core/database/connection.ts
|
|
3042
|
+
function createDatabaseConnection2(source) {
|
|
3043
|
+
return {
|
|
3044
|
+
async unsafe(query, params = []) {
|
|
3045
|
+
return await source.unsafe(query, params);
|
|
2966
3046
|
}
|
|
2967
|
-
|
|
2968
|
-
}
|
|
3047
|
+
};
|
|
2969
3048
|
}
|
|
2970
|
-
// ../../src/
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
LIMIT 1
|
|
2979
|
-
`;
|
|
2980
|
-
return rows[0] ?? null;
|
|
2981
|
-
}
|
|
2982
|
-
async listForUser(userId) {
|
|
2983
|
-
return await connection_default`
|
|
2984
|
-
SELECT id, organization_id, user_id, role, created_at
|
|
2985
|
-
FROM organization_member
|
|
2986
|
-
WHERE user_id = ${userId}
|
|
2987
|
-
ORDER BY organization_id
|
|
2988
|
-
`;
|
|
2989
|
-
}
|
|
2990
|
-
async listForOrganization(organizationId) {
|
|
2991
|
-
return await connection_default`
|
|
2992
|
-
SELECT id, organization_id, user_id, role, created_at
|
|
2993
|
-
FROM organization_member
|
|
2994
|
-
WHERE organization_id = ${organizationId}
|
|
2995
|
-
ORDER BY id
|
|
2996
|
-
`;
|
|
3049
|
+
// ../../src/core/database/migrations/advisoryLock.ts
|
|
3050
|
+
var MIGRATION_LOCK_KEY = 42424242;
|
|
3051
|
+
async function withMigrationLock(db2, callback, lockKey = MIGRATION_LOCK_KEY) {
|
|
3052
|
+
await db2.unsafe("SELECT pg_advisory_lock($1)", [lockKey]);
|
|
3053
|
+
try {
|
|
3054
|
+
return await callback();
|
|
3055
|
+
} finally {
|
|
3056
|
+
await db2.unsafe("SELECT pg_advisory_unlock($1)", [lockKey]);
|
|
2997
3057
|
}
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3058
|
+
}
|
|
3059
|
+
// ../../src/core/database/migrations/runner.ts
|
|
3060
|
+
import { readdir } from "fs/promises";
|
|
3061
|
+
import { join } from "path";
|
|
3062
|
+
import { pathToFileURL } from "url";
|
|
3063
|
+
var MIGRATIONS_TABLE = "framework_migrations";
|
|
3064
|
+
async function ensureMigrationsTable(db2) {
|
|
3065
|
+
await db2.unsafe(`
|
|
3066
|
+
CREATE TABLE IF NOT EXISTS ${MIGRATIONS_TABLE} (
|
|
3067
|
+
name TEXT PRIMARY KEY,
|
|
3068
|
+
batch INTEGER NOT NULL,
|
|
3069
|
+
run_on TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
3070
|
+
)
|
|
3071
|
+
`);
|
|
3072
|
+
}
|
|
3073
|
+
async function getAppliedMigrations(db2) {
|
|
3074
|
+
await ensureMigrationsTable(db2);
|
|
3075
|
+
return await db2.unsafe(`
|
|
3076
|
+
SELECT name, batch
|
|
3077
|
+
FROM ${MIGRATIONS_TABLE}
|
|
3078
|
+
ORDER BY batch ASC, name ASC
|
|
3079
|
+
`);
|
|
3080
|
+
}
|
|
3081
|
+
async function loadMigrationsFromDirectory(directory) {
|
|
3082
|
+
const entries = await readdir(directory);
|
|
3083
|
+
const migrationFiles = entries.filter((entry) => (entry.endsWith(".ts") || entry.endsWith(".js")) && entry !== "types.ts" && entry !== "index.ts" && entry !== "runner.ts").sort();
|
|
3084
|
+
const loadedMigrations = await Promise.all(migrationFiles.map(async (fileName) => {
|
|
3085
|
+
const moduleUrl = pathToFileURL(join(directory, fileName)).href;
|
|
3086
|
+
const module = await import(moduleUrl);
|
|
3087
|
+
return module.default;
|
|
3088
|
+
}));
|
|
3089
|
+
return loadedMigrations.filter((migration) => migration?.name !== undefined);
|
|
3090
|
+
}
|
|
3091
|
+
async function getMigrationStatus(db2, migrations) {
|
|
3092
|
+
const applied = await getAppliedMigrations(db2);
|
|
3093
|
+
const appliedByName = new Map(applied.map(({ name, batch }) => [name, Number(batch)]));
|
|
3094
|
+
return migrations.map(({ name }) => ({
|
|
3095
|
+
name,
|
|
3096
|
+
status: appliedByName.has(name) ? "up" : "pending",
|
|
3097
|
+
batch: appliedByName.get(name) ?? null
|
|
3098
|
+
}));
|
|
3099
|
+
}
|
|
3100
|
+
async function runPendingMigrations(db2, migrations, options = {}) {
|
|
3101
|
+
const applied = await getAppliedMigrations(db2);
|
|
3102
|
+
const appliedNames = new Set(applied.map(({ name }) => name));
|
|
3103
|
+
const nextBatch = applied.reduce((currentMax, { batch }) => Math.max(currentMax, Number(batch)), 0) + 1;
|
|
3104
|
+
const pendingMigrations = migrations.filter(({ name }) => !appliedNames.has(name));
|
|
3105
|
+
for (const migration of pendingMigrations) {
|
|
3106
|
+
options.onMigration?.(migration.name);
|
|
3107
|
+
await migration.up(db2);
|
|
3108
|
+
const inserted = await db2.unsafe(`INSERT INTO ${MIGRATIONS_TABLE} (name, batch) VALUES ($1, $2) ON CONFLICT (name) DO NOTHING RETURNING name`, [migration.name, nextBatch]);
|
|
3109
|
+
if (inserted.length === 0) {
|
|
3110
|
+
throw new Error(`Migration ${migration.name} was applied but not recorded.`);
|
|
3007
3111
|
}
|
|
3008
|
-
return row;
|
|
3009
|
-
}
|
|
3010
|
-
async removeMember(organizationId, userId) {
|
|
3011
|
-
const rows = await connection_default`
|
|
3012
|
-
DELETE FROM organization_member
|
|
3013
|
-
WHERE organization_id = ${organizationId} AND user_id = ${userId}
|
|
3014
|
-
RETURNING id
|
|
3015
|
-
`;
|
|
3016
|
-
return rows.length > 0;
|
|
3017
3112
|
}
|
|
3113
|
+
return pendingMigrations.length;
|
|
3018
3114
|
}
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
var membershipRepository = new memberRepository_default;
|
|
3024
|
-
async function runWithMembershipContext(callback) {
|
|
3025
|
-
const user = currentAuthUser();
|
|
3026
|
-
if (!user || isGlobalAdmin(user)) {
|
|
3027
|
-
return await callback();
|
|
3115
|
+
async function migrateDatabase(db2, migrations, options = {}) {
|
|
3116
|
+
const { advisoryLock = false, onMigration } = options;
|
|
3117
|
+
if (advisoryLock) {
|
|
3118
|
+
return withMigrationLock(db2, () => runPendingMigrations(db2, migrations, { onMigration }));
|
|
3028
3119
|
}
|
|
3029
|
-
|
|
3030
|
-
const context = {
|
|
3031
|
-
organizationIds: memberships.map((membership) => membership.organization_id),
|
|
3032
|
-
rolesByOrganizationId: new Map(memberships.map((membership) => [membership.organization_id, membership.role]))
|
|
3033
|
-
};
|
|
3034
|
-
return await membershipContext.run(context, callback);
|
|
3035
|
-
}
|
|
3036
|
-
function currentOrgRole(organizationId) {
|
|
3037
|
-
return membershipContext.getStore()?.rolesByOrganizationId.get(organizationId) ?? null;
|
|
3038
|
-
}
|
|
3039
|
-
function hasOrgMembership(organizationId) {
|
|
3040
|
-
return currentOrgRole(organizationId) !== null;
|
|
3041
|
-
}
|
|
3042
|
-
function currentOrganizationIds() {
|
|
3043
|
-
return membershipContext.getStore()?.organizationIds ?? [];
|
|
3120
|
+
return runPendingMigrations(db2, migrations, { onMigration });
|
|
3044
3121
|
}
|
|
3045
|
-
function
|
|
3046
|
-
const
|
|
3047
|
-
if (
|
|
3048
|
-
return
|
|
3122
|
+
async function rollbackDatabase(db2, migrations, options = {}) {
|
|
3123
|
+
const applied = await getAppliedMigrations(db2);
|
|
3124
|
+
if (applied.length === 0) {
|
|
3125
|
+
return 0;
|
|
3049
3126
|
}
|
|
3050
|
-
const
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3127
|
+
const lastBatch = applied.reduce((currentMax, { batch }) => Math.max(currentMax, Number(batch)), 0);
|
|
3128
|
+
const migrationsToRollback = new Set(applied.filter(({ batch }) => Number(batch) === lastBatch).map(({ name }) => name));
|
|
3129
|
+
let rolledBack = 0;
|
|
3130
|
+
for (const migration of [...migrations].reverse()) {
|
|
3131
|
+
if (!migrationsToRollback.has(migration.name)) {
|
|
3132
|
+
continue;
|
|
3133
|
+
}
|
|
3134
|
+
options.onMigration?.(migration.name);
|
|
3135
|
+
await migration.down(db2);
|
|
3136
|
+
await db2.unsafe(`DELETE FROM ${MIGRATIONS_TABLE} WHERE name = $1`, [migration.name]);
|
|
3137
|
+
rolledBack += 1;
|
|
3138
|
+
}
|
|
3139
|
+
return rolledBack;
|
|
3056
3140
|
}
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3141
|
+
async function freshDatabase(db2, migrations, options = {}) {
|
|
3142
|
+
const runFresh = async () => {
|
|
3143
|
+
const applied = await getAppliedMigrations(db2);
|
|
3144
|
+
const appliedNames = new Set(applied.map(({ name }) => name));
|
|
3145
|
+
const appliedMigrations = migrations.filter(({ name }) => appliedNames.has(name));
|
|
3146
|
+
for (const migration of [...appliedMigrations].reverse()) {
|
|
3147
|
+
options.onMigration?.(migration.name);
|
|
3148
|
+
await migration.down(db2);
|
|
3149
|
+
}
|
|
3150
|
+
if (appliedMigrations.length > 0) {
|
|
3151
|
+
await db2.unsafe(`DELETE FROM ${MIGRATIONS_TABLE}`);
|
|
3152
|
+
}
|
|
3153
|
+
await runPendingMigrations(db2, migrations, options);
|
|
3061
3154
|
};
|
|
3155
|
+
if (options.advisoryLock) {
|
|
3156
|
+
await withMigrationLock(db2, runFresh);
|
|
3157
|
+
return;
|
|
3158
|
+
}
|
|
3159
|
+
await runFresh();
|
|
3062
3160
|
}
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3161
|
+
// ../../src/core/database/model.ts
|
|
3162
|
+
var modelRepositories = new WeakMap;
|
|
3163
|
+
var modelGlobalScopes = new WeakMap;
|
|
3164
|
+
var modelBooted = new WeakSet;
|
|
3165
|
+
function resolveModelRepository(model) {
|
|
3166
|
+
const repository = modelRepositories.get(model);
|
|
3167
|
+
if (!repository) {
|
|
3168
|
+
throw new Error(`${model.name}.repository() is not implemented.`);
|
|
3169
|
+
}
|
|
3170
|
+
return repository;
|
|
3067
3171
|
}
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3172
|
+
function modelStatics(model) {
|
|
3173
|
+
return model;
|
|
3174
|
+
}
|
|
3175
|
+
function ensureBooted(model) {
|
|
3176
|
+
if (modelBooted.has(model)) {
|
|
3177
|
+
return;
|
|
3073
3178
|
}
|
|
3074
|
-
|
|
3075
|
-
|
|
3179
|
+
modelBooted.add(model);
|
|
3180
|
+
const boot = model.boot;
|
|
3181
|
+
if (typeof boot === "function") {
|
|
3182
|
+
boot.call(model);
|
|
3076
3183
|
}
|
|
3077
|
-
return currentOrganizationIds();
|
|
3078
3184
|
}
|
|
3079
|
-
function
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3185
|
+
function getGlobalScopes(model) {
|
|
3186
|
+
return modelGlobalScopes.get(model) ?? [];
|
|
3187
|
+
}
|
|
3188
|
+
function hydrateValue(value, cast) {
|
|
3189
|
+
if (value === null || value === undefined) {
|
|
3190
|
+
return value;
|
|
3083
3191
|
}
|
|
3084
|
-
|
|
3085
|
-
|
|
3192
|
+
switch (cast) {
|
|
3193
|
+
case "date":
|
|
3194
|
+
case "datetime":
|
|
3195
|
+
return value instanceof Date ? value : new Date(String(value));
|
|
3196
|
+
case "json":
|
|
3197
|
+
return typeof value === "string" ? JSON.parse(value) : value;
|
|
3198
|
+
case "bool":
|
|
3199
|
+
case "boolean":
|
|
3200
|
+
return value === true || value === 1 || value === "1" || value === "true";
|
|
3201
|
+
default:
|
|
3202
|
+
return value;
|
|
3086
3203
|
}
|
|
3087
|
-
return scope;
|
|
3088
3204
|
}
|
|
3089
|
-
function
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
return where;
|
|
3205
|
+
function dehydrateValue(value, cast) {
|
|
3206
|
+
if (value === null || value === undefined) {
|
|
3207
|
+
return value;
|
|
3093
3208
|
}
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3209
|
+
switch (cast) {
|
|
3210
|
+
case "date":
|
|
3211
|
+
case "datetime":
|
|
3212
|
+
return value instanceof Date ? value : new Date(String(value));
|
|
3213
|
+
case "json":
|
|
3214
|
+
return typeof value === "string" ? value : JSON.stringify(value);
|
|
3215
|
+
case "bool":
|
|
3216
|
+
case "boolean":
|
|
3217
|
+
return Boolean(value);
|
|
3218
|
+
default:
|
|
3219
|
+
return value;
|
|
3099
3220
|
}
|
|
3100
|
-
return {
|
|
3101
|
-
...where,
|
|
3102
|
-
organization_id: organizationIds.length === 1 ? organizationIds[0] : organizationIds
|
|
3103
|
-
};
|
|
3104
3221
|
}
|
|
3105
|
-
function
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
return {
|
|
3111
|
-
...where,
|
|
3112
|
-
project_id: requestedProjectId
|
|
3113
|
-
};
|
|
3114
|
-
}
|
|
3115
|
-
if (accessibleProjectIds.length === 0) {
|
|
3116
|
-
return {
|
|
3117
|
-
...where,
|
|
3118
|
-
project_id: [-1]
|
|
3119
|
-
};
|
|
3222
|
+
function filterMassAssignable(fillable, guarded, input) {
|
|
3223
|
+
const resolvedGuarded = guarded ?? true;
|
|
3224
|
+
if (fillable && fillable.length > 0) {
|
|
3225
|
+
const allowed = new Set(fillable);
|
|
3226
|
+
return Object.fromEntries(Object.entries(input).filter(([key]) => allowed.has(key)));
|
|
3120
3227
|
}
|
|
3121
|
-
if (
|
|
3122
|
-
return {
|
|
3123
|
-
...where,
|
|
3124
|
-
project_id: accessibleProjectIds.includes(requestedProjectId) ? requestedProjectId : -1
|
|
3125
|
-
};
|
|
3228
|
+
if (resolvedGuarded === true || resolvedGuarded.includes("*")) {
|
|
3229
|
+
return {};
|
|
3126
3230
|
}
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
project_id: accessibleProjectIds
|
|
3130
|
-
};
|
|
3231
|
+
const blocked = new Set(resolvedGuarded);
|
|
3232
|
+
return Object.fromEntries(Object.entries(input).filter(([key]) => !blocked.has(key)));
|
|
3131
3233
|
}
|
|
3132
|
-
function
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3234
|
+
function applyCasts(values, casts, direction) {
|
|
3235
|
+
if (Object.keys(casts).length === 0) {
|
|
3236
|
+
return values;
|
|
3237
|
+
}
|
|
3238
|
+
const result = { ...values };
|
|
3239
|
+
const castFn = direction === "hydrate" ? hydrateValue : dehydrateValue;
|
|
3240
|
+
for (const [key, cast] of Object.entries(casts)) {
|
|
3241
|
+
if (key in result && cast) {
|
|
3242
|
+
result[key] = castFn(result[key], cast);
|
|
3140
3243
|
}
|
|
3141
|
-
};
|
|
3142
|
-
}
|
|
3143
|
-
function assertResourceInCurrentTenant(resourceTenantId, resourceLabel, resourceId) {
|
|
3144
|
-
if (resourceTenantId !== currentTenantId()) {
|
|
3145
|
-
throw new NotFoundError(`${resourceLabel} ${resourceId} not found.`);
|
|
3146
3244
|
}
|
|
3245
|
+
return result;
|
|
3147
3246
|
}
|
|
3148
|
-
function
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
return;
|
|
3247
|
+
function applyTimestampsOnCreate(columns, values, enabled) {
|
|
3248
|
+
if (!enabled) {
|
|
3249
|
+
return values;
|
|
3152
3250
|
}
|
|
3153
|
-
const
|
|
3154
|
-
|
|
3155
|
-
|
|
3251
|
+
const now = new Date;
|
|
3252
|
+
const result = { ...values };
|
|
3253
|
+
if (columns.includes("created_at")) {
|
|
3254
|
+
result.created_at = now;
|
|
3255
|
+
}
|
|
3256
|
+
if (columns.includes("updated_at")) {
|
|
3257
|
+
result.updated_at = now;
|
|
3156
3258
|
}
|
|
3259
|
+
return result;
|
|
3157
3260
|
}
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
if (dependency === undefined) {
|
|
3162
|
-
throw new Error(`Required dependency "${key}" is not registered.`);
|
|
3261
|
+
function applyTimestampsOnUpdate(columns, values, enabled) {
|
|
3262
|
+
if (!enabled) {
|
|
3263
|
+
return values;
|
|
3163
3264
|
}
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3265
|
+
const result = { ...values };
|
|
3266
|
+
if (columns.includes("updated_at")) {
|
|
3267
|
+
result.updated_at = new Date;
|
|
3268
|
+
}
|
|
3269
|
+
return result;
|
|
3168
3270
|
}
|
|
3169
3271
|
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
}
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
...context
|
|
3183
|
-
};
|
|
3184
|
-
const line = JSON.stringify(entry);
|
|
3185
|
-
if (level === "error") {
|
|
3186
|
-
console.error(line);
|
|
3187
|
-
return;
|
|
3188
|
-
}
|
|
3189
|
-
console.log(line);
|
|
3272
|
+
class Model {
|
|
3273
|
+
attributes;
|
|
3274
|
+
repository;
|
|
3275
|
+
static $fillable;
|
|
3276
|
+
static $guarded;
|
|
3277
|
+
static $casts = {};
|
|
3278
|
+
static $timestamps = true;
|
|
3279
|
+
_exists;
|
|
3280
|
+
constructor(attributes, repository, exists = true) {
|
|
3281
|
+
this.attributes = attributes;
|
|
3282
|
+
this.repository = repository;
|
|
3283
|
+
this._exists = exists;
|
|
3190
3284
|
}
|
|
3191
|
-
|
|
3192
|
-
this.
|
|
3285
|
+
get $exists() {
|
|
3286
|
+
return this._exists;
|
|
3193
3287
|
}
|
|
3194
|
-
|
|
3195
|
-
this.
|
|
3288
|
+
get(key) {
|
|
3289
|
+
return this.attributes[key];
|
|
3196
3290
|
}
|
|
3197
|
-
|
|
3198
|
-
this.
|
|
3291
|
+
get id() {
|
|
3292
|
+
return this.attributes[this.primaryKey()];
|
|
3199
3293
|
}
|
|
3200
|
-
|
|
3201
|
-
this.
|
|
3294
|
+
toObject() {
|
|
3295
|
+
return { ...this.attributes };
|
|
3202
3296
|
}
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
// ../../src/core/runtime/applicationRegistry.ts
|
|
3207
|
-
var APPLICATION_CONTEXT_KEY = Symbol.for("@getstrata/applicationContext");
|
|
3208
|
-
var activeContext;
|
|
3209
|
-
function readStoredApplicationContext() {
|
|
3210
|
-
if (activeContext) {
|
|
3211
|
-
return activeContext;
|
|
3297
|
+
primaryKey() {
|
|
3298
|
+
throw new Error(`${this.constructor.name}.primaryKey() is not implemented.`);
|
|
3212
3299
|
}
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
activeContext = globalContext;
|
|
3300
|
+
static primaryKeyField() {
|
|
3301
|
+
return resolveModelRepository(this).getTable().primaryKey;
|
|
3216
3302
|
}
|
|
3217
|
-
|
|
3218
|
-
}
|
|
3219
|
-
|
|
3220
|
-
activeContext = context;
|
|
3221
|
-
globalThis[APPLICATION_CONTEXT_KEY] = context;
|
|
3222
|
-
}
|
|
3223
|
-
function requireActiveApplicationContext() {
|
|
3224
|
-
const context = readStoredApplicationContext();
|
|
3225
|
-
if (!context) {
|
|
3226
|
-
throw new Error("The application context has not been bootstrapped.");
|
|
3303
|
+
static hydrateAttributes(attributes) {
|
|
3304
|
+
const casts = modelStatics(this).$casts ?? {};
|
|
3305
|
+
return applyCasts(attributes, casts, "hydrate");
|
|
3227
3306
|
}
|
|
3228
|
-
|
|
3229
|
-
}
|
|
3230
|
-
|
|
3231
|
-
return getRequiredDependency(requireActiveApplicationContext().dependencies, "cache");
|
|
3232
|
-
}
|
|
3233
|
-
function resolveApplicationQueue() {
|
|
3234
|
-
return requireActiveApplicationContext().container.resolve(CORE_QUEUE_TOKEN);
|
|
3235
|
-
}
|
|
3236
|
-
function resolveApplicationEventBus() {
|
|
3237
|
-
return requireActiveApplicationContext().container.resolve(CORE_EVENT_BUS_TOKEN);
|
|
3238
|
-
}
|
|
3239
|
-
function resolveApplicationAuth() {
|
|
3240
|
-
return requireActiveApplicationContext().container.resolve(CORE_AUTH_TOKEN);
|
|
3241
|
-
}
|
|
3242
|
-
function resolveApplicationPolicyGate() {
|
|
3243
|
-
return requireActiveApplicationContext().container.resolve(CORE_POLICY_GATE_TOKEN);
|
|
3244
|
-
}
|
|
3245
|
-
function resolveApplicationConfig() {
|
|
3246
|
-
return requireActiveApplicationContext().config;
|
|
3247
|
-
}
|
|
3248
|
-
function resolveApplicationLogger() {
|
|
3249
|
-
return appLogger;
|
|
3250
|
-
}
|
|
3251
|
-
function resolveApplicationDependencies() {
|
|
3252
|
-
return requireActiveApplicationContext().dependencies;
|
|
3253
|
-
}
|
|
3254
|
-
|
|
3255
|
-
// ../../src/core/auth/resolveMembershipService.ts
|
|
3256
|
-
function resolveMembershipService() {
|
|
3257
|
-
const dependencies = resolveApplicationDependencies();
|
|
3258
|
-
if (dependencies.container.has("core.membership")) {
|
|
3259
|
-
return dependencies.container.resolve("core.membership");
|
|
3307
|
+
static dehydrateAttributes(attributes) {
|
|
3308
|
+
const casts = modelStatics(this).$casts ?? {};
|
|
3309
|
+
return applyCasts(attributes, casts, "dehydrate");
|
|
3260
3310
|
}
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
class MembershipService {
|
|
3266
|
-
members;
|
|
3267
|
-
constructor(members = membershipRepository) {
|
|
3268
|
-
this.members = members;
|
|
3311
|
+
static fromRecord(record, repository, exists = true) {
|
|
3312
|
+
const statics = modelStatics(this);
|
|
3313
|
+
const hydrated = statics.hydrateAttributes(record);
|
|
3314
|
+
return new statics(hydrated, repository, exists);
|
|
3269
3315
|
}
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3316
|
+
static boot() {}
|
|
3317
|
+
static addGlobalScope(_name, scope) {
|
|
3318
|
+
ensureBooted(this);
|
|
3319
|
+
const existing = modelGlobalScopes.get(this) ?? [];
|
|
3320
|
+
modelGlobalScopes.set(this, [
|
|
3321
|
+
...existing,
|
|
3322
|
+
scope
|
|
3323
|
+
]);
|
|
3273
3324
|
}
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
return membership?.role ?? null;
|
|
3325
|
+
static repository() {
|
|
3326
|
+
return resolveModelRepository(this);
|
|
3277
3327
|
}
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3328
|
+
static query() {
|
|
3329
|
+
ensureBooted(this);
|
|
3330
|
+
const repository = resolveModelRepository(this);
|
|
3331
|
+
let query = repository.query();
|
|
3332
|
+
for (const scope of getGlobalScopes(this)) {
|
|
3333
|
+
query = scope(query);
|
|
3284
3334
|
}
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3335
|
+
return query;
|
|
3336
|
+
}
|
|
3337
|
+
static async create(attributes) {
|
|
3338
|
+
const statics = modelStatics(this);
|
|
3339
|
+
ensureBooted(this);
|
|
3340
|
+
const repository = resolveModelRepository(this);
|
|
3341
|
+
const table = repository.getTable();
|
|
3342
|
+
const timestamps = statics.$timestamps ?? true;
|
|
3343
|
+
const assignable = filterMassAssignable(statics.$fillable, statics.$guarded, attributes);
|
|
3344
|
+
const withTimestamps = applyTimestampsOnCreate(table.columns, assignable, timestamps);
|
|
3345
|
+
const payload = statics.dehydrateAttributes(withTimestamps);
|
|
3346
|
+
const record = await repository.create(payload);
|
|
3347
|
+
return statics.fromRecord(record, repository, true);
|
|
3348
|
+
}
|
|
3349
|
+
static async find(id) {
|
|
3350
|
+
const statics = modelStatics(this);
|
|
3351
|
+
const repository = resolveModelRepository(this);
|
|
3352
|
+
const primaryKey = repository.getTable().primaryKey;
|
|
3353
|
+
const record = await Model.query.call(this).where({ [primaryKey]: id }).first();
|
|
3354
|
+
return record ? statics.fromRecord(record, repository, true) : null;
|
|
3355
|
+
}
|
|
3356
|
+
static async findOrFail(id, errorFactory) {
|
|
3357
|
+
const model = await Model.find.call(this, id);
|
|
3358
|
+
if (model) {
|
|
3359
|
+
return model;
|
|
3288
3360
|
}
|
|
3289
|
-
|
|
3361
|
+
throw errorFactory?.(id) ?? new NotFoundError(`${this.name} ${String(id)} not found.`);
|
|
3290
3362
|
}
|
|
3291
|
-
async
|
|
3292
|
-
|
|
3293
|
-
|
|
3363
|
+
static async all(options = {}) {
|
|
3364
|
+
const statics = modelStatics(this);
|
|
3365
|
+
const repository = resolveModelRepository(this);
|
|
3366
|
+
let query = Model.query.call(this);
|
|
3367
|
+
if (options.orderBy) {
|
|
3368
|
+
query = query.orderBy(options.orderBy);
|
|
3294
3369
|
}
|
|
3295
|
-
if (
|
|
3296
|
-
|
|
3370
|
+
if (options.limit !== undefined) {
|
|
3371
|
+
query = query.limit(options.limit);
|
|
3297
3372
|
}
|
|
3298
|
-
const
|
|
3299
|
-
return
|
|
3373
|
+
const rows = await query.get();
|
|
3374
|
+
return rows.map((row) => statics.fromRecord(row, repository, true));
|
|
3300
3375
|
}
|
|
3301
|
-
async
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3376
|
+
static async firstWhere(where, options = {}) {
|
|
3377
|
+
const statics = modelStatics(this);
|
|
3378
|
+
const repository = resolveModelRepository(this);
|
|
3379
|
+
let query = Model.query.call(this).where(where);
|
|
3380
|
+
if (options.orderBy) {
|
|
3381
|
+
query = query.orderBy(options.orderBy);
|
|
3382
|
+
}
|
|
3383
|
+
const record = await query.first();
|
|
3384
|
+
return record ? statics.fromRecord(record, repository, true) : null;
|
|
3307
3385
|
}
|
|
3308
|
-
|
|
3309
|
-
|
|
3386
|
+
async save() {
|
|
3387
|
+
const ModelClass = modelStatics(this.constructor);
|
|
3388
|
+
const timestamps = ModelClass.$timestamps ?? true;
|
|
3389
|
+
const casts = ModelClass.$casts ?? {};
|
|
3390
|
+
const table = this.repository.getTable();
|
|
3391
|
+
if (this.$exists) {
|
|
3392
|
+
const changes = applyTimestampsOnUpdate(table.columns, applyCasts(this.attributes, casts, "dehydrate"), timestamps);
|
|
3393
|
+
const record2 = await this.repository.updateByIdOrThrow(this.id, changes);
|
|
3394
|
+
this.attributes = ModelClass.hydrateAttributes(record2);
|
|
3395
|
+
return this;
|
|
3396
|
+
}
|
|
3397
|
+
const assignable = filterMassAssignable(ModelClass.$fillable, ModelClass.$guarded, this.attributes);
|
|
3398
|
+
const withTimestamps = applyTimestampsOnCreate(table.columns, assignable, timestamps);
|
|
3399
|
+
const payload = ModelClass.dehydrateAttributes(withTimestamps);
|
|
3400
|
+
const record = await this.repository.create(payload);
|
|
3401
|
+
this.attributes = ModelClass.hydrateAttributes(record);
|
|
3402
|
+
this._exists = true;
|
|
3403
|
+
return this;
|
|
3310
3404
|
}
|
|
3311
|
-
|
|
3312
|
-
|
|
3405
|
+
async update(changes) {
|
|
3406
|
+
const ModelClass = modelStatics(this.constructor);
|
|
3407
|
+
const assignable = filterMassAssignable(ModelClass.$fillable, ModelClass.$guarded, changes);
|
|
3408
|
+
Object.assign(this.attributes, assignable);
|
|
3409
|
+
return await this.save();
|
|
3313
3410
|
}
|
|
3314
|
-
|
|
3315
|
-
|
|
3411
|
+
async delete() {
|
|
3412
|
+
if (resolveSoftDeleteColumn(this.repository.getTable())) {
|
|
3413
|
+
return await this.repository.deleteById(this.id);
|
|
3414
|
+
}
|
|
3415
|
+
return await this.repository.forceDeleteById(this.id);
|
|
3316
3416
|
}
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
// ../../src/core/auth/policy.ts
|
|
3320
|
-
class Policy {
|
|
3321
|
-
constructor() {}
|
|
3322
|
-
view(_user, _resource) {
|
|
3323
|
-
return false;
|
|
3417
|
+
async forceDelete() {
|
|
3418
|
+
return await this.repository.forceDeleteById(this.id);
|
|
3324
3419
|
}
|
|
3325
|
-
|
|
3326
|
-
|
|
3420
|
+
async restore() {
|
|
3421
|
+
const ModelClass = modelStatics(this.constructor);
|
|
3422
|
+
const record = await this.repository.restoreById(this.id);
|
|
3423
|
+
if (!record) {
|
|
3424
|
+
return null;
|
|
3425
|
+
}
|
|
3426
|
+
this.attributes = ModelClass.hydrateAttributes(record);
|
|
3427
|
+
return this;
|
|
3327
3428
|
}
|
|
3328
|
-
|
|
3329
|
-
|
|
3429
|
+
async loadHasMany(as, relation, childRepository, options = {}) {
|
|
3430
|
+
const grouped = await childRepository.withConnection(this.repository.getConnection()).loadHasManyForParents([this.attributes], relation, options);
|
|
3431
|
+
const loaded = grouped.get(this.attributes[relation.localKey]) ?? [];
|
|
3432
|
+
return Object.assign(this, { [as]: loaded });
|
|
3330
3433
|
}
|
|
3331
|
-
|
|
3332
|
-
|
|
3434
|
+
async loadHasOne(as, relation, childRepository, options = {}) {
|
|
3435
|
+
const loaded = await this.loadHasMany(as, relation, childRepository, { ...options, limit: 1 });
|
|
3436
|
+
const value = loaded[as]?.[0];
|
|
3437
|
+
return Object.assign(this, { [as]: value });
|
|
3333
3438
|
}
|
|
3334
|
-
}
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
"valueOf",
|
|
3339
|
-
"hasOwnProperty",
|
|
3340
|
-
"isPrototypeOf",
|
|
3341
|
-
"propertyIsEnumerable",
|
|
3342
|
-
"__proto__"
|
|
3343
|
-
]);
|
|
3344
|
-
|
|
3345
|
-
class PolicyGate {
|
|
3346
|
-
constructor() {}
|
|
3347
|
-
policies = new Map;
|
|
3348
|
-
register(resource, policy) {
|
|
3349
|
-
this.policies.set(resource, policy);
|
|
3439
|
+
async loadBelongsTo(as, relation, parentRepository, options = {}) {
|
|
3440
|
+
const grouped = await this.repository.loadBelongsToForParents([this.attributes], relation, parentRepository, options);
|
|
3441
|
+
const loaded = grouped.get(this.attributes[relation.foreignKey]);
|
|
3442
|
+
return Object.assign(this, { [as]: loaded });
|
|
3350
3443
|
}
|
|
3351
|
-
|
|
3352
|
-
const
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
return false;
|
|
3358
|
-
}
|
|
3359
|
-
const handler = policy[action];
|
|
3360
|
-
if (typeof handler !== "function") {
|
|
3361
|
-
return false;
|
|
3444
|
+
async loadBelongsToMany(as, relation, relatedRepository, options = {}) {
|
|
3445
|
+
const connection = this.repository.getConnection();
|
|
3446
|
+
const parentId = this.attributes[relation.parentKey];
|
|
3447
|
+
const pivotRows = await connection.unsafe(`SELECT * FROM ${relation.pivotTable} WHERE ${String(relation.foreignPivotKey)} = $1`, [parentId]);
|
|
3448
|
+
if (pivotRows.length === 0) {
|
|
3449
|
+
return Object.assign(this, { [as]: [] });
|
|
3362
3450
|
}
|
|
3363
|
-
const
|
|
3364
|
-
|
|
3451
|
+
const relatedIds = [
|
|
3452
|
+
...new Set(pivotRows.map((row) => row[relation.relatedPivotKey]))
|
|
3453
|
+
];
|
|
3454
|
+
const relatedRows = await relatedRepository.withConnection(connection).findAll({
|
|
3455
|
+
...options,
|
|
3456
|
+
where: {
|
|
3457
|
+
[relation.relatedKey]: relatedIds
|
|
3458
|
+
}
|
|
3459
|
+
});
|
|
3460
|
+
const grouped = indexBelongsToManyRelation([this.attributes], pivotRows, relatedRows, relation);
|
|
3461
|
+
const loaded = grouped.get(parentId) ?? [];
|
|
3462
|
+
return Object.assign(this, { [as]: loaded });
|
|
3365
3463
|
}
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
}
|
|
3464
|
+
mergeAttributes(patch) {
|
|
3465
|
+
Object.assign(this.attributes, patch);
|
|
3466
|
+
return this;
|
|
3370
3467
|
}
|
|
3371
3468
|
}
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3469
|
+
function registerModelRepository(model, repository) {
|
|
3470
|
+
modelRepositories.set(model, repository);
|
|
3471
|
+
ensureBooted(model);
|
|
3472
|
+
return model;
|
|
3375
3473
|
}
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3474
|
+
// ../../src/core/database/schema/columnDefinition.ts
|
|
3475
|
+
class ColumnDefinition {
|
|
3476
|
+
name;
|
|
3477
|
+
kind;
|
|
3478
|
+
length;
|
|
3479
|
+
isNullable = false;
|
|
3480
|
+
isPrimary = false;
|
|
3481
|
+
isUnique = false;
|
|
3482
|
+
autoIncrement = false;
|
|
3483
|
+
defaultValue;
|
|
3484
|
+
checkExpression;
|
|
3485
|
+
foreignKey;
|
|
3486
|
+
constructor(name, kind) {
|
|
3487
|
+
this.name = name;
|
|
3488
|
+
this.kind = kind;
|
|
3387
3489
|
}
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
// ../../src/core/security/scimTenantTokens.ts
|
|
3392
|
-
function parseScimTenantTokens(raw) {
|
|
3393
|
-
const tokens = new Map;
|
|
3394
|
-
if (!raw?.trim()) {
|
|
3395
|
-
return tokens;
|
|
3490
|
+
nullable() {
|
|
3491
|
+
this.isNullable = true;
|
|
3492
|
+
return this;
|
|
3396
3493
|
}
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3494
|
+
notNullable() {
|
|
3495
|
+
this.isNullable = false;
|
|
3496
|
+
return this;
|
|
3497
|
+
}
|
|
3498
|
+
default(value) {
|
|
3499
|
+
if (typeof value === "boolean") {
|
|
3500
|
+
this.defaultValue = value ? "TRUE" : "FALSE";
|
|
3501
|
+
return this;
|
|
3401
3502
|
}
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
tokens.set(tenantId, token);
|
|
3503
|
+
if (typeof value === "number") {
|
|
3504
|
+
this.defaultValue = String(value);
|
|
3505
|
+
return this;
|
|
3406
3506
|
}
|
|
3507
|
+
this.defaultValue = `'${value.replace(/'/g, "''")}'`;
|
|
3508
|
+
return this;
|
|
3407
3509
|
}
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
const tenantTokens = parseScimTenantTokens(process.env.SCIM_TENANT_TOKENS);
|
|
3412
|
-
for (const [tenantId, expectedToken] of tenantTokens) {
|
|
3413
|
-
if (timingSafeCompareString(token, expectedToken)) {
|
|
3414
|
-
return tenantId;
|
|
3415
|
-
}
|
|
3510
|
+
defaultRaw(expression) {
|
|
3511
|
+
this.defaultValue = expression;
|
|
3512
|
+
return this;
|
|
3416
3513
|
}
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
return
|
|
3514
|
+
unique() {
|
|
3515
|
+
this.isUnique = true;
|
|
3516
|
+
return this;
|
|
3420
3517
|
}
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
FROM tenant
|
|
3429
|
-
WHERE id = ${tenantId}
|
|
3430
|
-
LIMIT 1
|
|
3431
|
-
`;
|
|
3432
|
-
const row = rows[0];
|
|
3433
|
-
return row ? { id: row.id, slug: row.slug, plan: row.plan, region: row.region ?? "eu" } : null;
|
|
3434
|
-
}
|
|
3435
|
-
|
|
3436
|
-
// ../../src/core/tenant/tenantDatabaseScope.ts
|
|
3437
|
-
async function applyTenantContextToTransaction(transaction, tenantId) {
|
|
3438
|
-
await transaction.unsafe(`SELECT set_config('app.tenant_id', $1, true)`, [String(tenantId)]);
|
|
3439
|
-
await transaction.unsafe(`SELECT set_config('app.bypass_rls', $1, true)`, ["false"]);
|
|
3440
|
-
}
|
|
3441
|
-
async function runWithTenantDatabase(tenant, callback) {
|
|
3442
|
-
if (hasActiveDatabaseConnection()) {
|
|
3443
|
-
const activeConnection2 = getActiveDatabaseConnection(getDefaultDatabasePool());
|
|
3444
|
-
await applyTenantContextToTransaction(activeConnection2, tenant.id);
|
|
3445
|
-
return await runWithTenant(tenant, callback);
|
|
3518
|
+
primary() {
|
|
3519
|
+
this.isPrimary = true;
|
|
3520
|
+
return this;
|
|
3521
|
+
}
|
|
3522
|
+
check(expression) {
|
|
3523
|
+
this.checkExpression = expression;
|
|
3524
|
+
return this;
|
|
3446
3525
|
}
|
|
3447
|
-
return await getDefaultDatabasePool().begin(async (transaction) => {
|
|
3448
|
-
await applyTenantContextToTransaction(transaction, tenant.id);
|
|
3449
|
-
return await runWithDatabaseConnection(transaction, async () => {
|
|
3450
|
-
return await runWithTenant(tenant, callback);
|
|
3451
|
-
});
|
|
3452
|
-
});
|
|
3453
|
-
}
|
|
3454
|
-
function isInsideTenantDatabaseScope(tenantId = currentTenant()?.id) {
|
|
3455
|
-
return hasActiveDatabaseConnection() && currentTenant()?.id === tenantId;
|
|
3456
3526
|
}
|
|
3457
3527
|
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3528
|
+
class ForeignIdColumnDefinition extends ColumnDefinition {
|
|
3529
|
+
constructor(name) {
|
|
3530
|
+
super(name, "foreignId");
|
|
3531
|
+
this.notNullable();
|
|
3532
|
+
}
|
|
3533
|
+
references(table, column = "id") {
|
|
3534
|
+
this.foreignKey = {
|
|
3535
|
+
referencesTable: table,
|
|
3536
|
+
referencesColumn: column
|
|
3537
|
+
};
|
|
3538
|
+
return this;
|
|
3539
|
+
}
|
|
3540
|
+
constrained(table) {
|
|
3541
|
+
const referencesTable = table ?? inferReferencedTable(this.name);
|
|
3542
|
+
return this.references(referencesTable);
|
|
3543
|
+
}
|
|
3544
|
+
cascadeOnDelete() {
|
|
3545
|
+
if (!this.foreignKey) {
|
|
3546
|
+
throw new Error(`Foreign key is not defined for column ${this.name}`);
|
|
3469
3547
|
}
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3548
|
+
this.foreignKey.onDelete = "cascade";
|
|
3549
|
+
return this;
|
|
3550
|
+
}
|
|
3551
|
+
nullOnDelete() {
|
|
3552
|
+
if (!this.foreignKey) {
|
|
3553
|
+
throw new Error(`Foreign key is not defined for column ${this.name}`);
|
|
3473
3554
|
}
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
return await next();
|
|
3478
|
-
} finally {
|
|
3479
|
-
resetBoundDatabaseConnection();
|
|
3480
|
-
}
|
|
3481
|
-
});
|
|
3482
|
-
};
|
|
3555
|
+
this.foreignKey.onDelete = "set null";
|
|
3556
|
+
return this;
|
|
3557
|
+
}
|
|
3483
3558
|
}
|
|
3484
|
-
function
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
}, {
|
|
3490
|
-
status,
|
|
3491
|
-
headers: { "content-type": "application/scim+json" }
|
|
3492
|
-
});
|
|
3559
|
+
function inferReferencedTable(columnName) {
|
|
3560
|
+
if (!columnName.endsWith("_id")) {
|
|
3561
|
+
throw new Error(`Cannot infer referenced table from column ${columnName}`);
|
|
3562
|
+
}
|
|
3563
|
+
return columnName.slice(0, -3);
|
|
3493
3564
|
}
|
|
3494
|
-
// ../../src/core/cache/redisCacheStore.ts
|
|
3495
|
-
var {RedisClient } = globalThis.Bun;
|
|
3496
|
-
var KEY_PREFIX = "workhub:cache:";
|
|
3497
|
-
var TAG_PREFIX = "workhub:cache:tag:";
|
|
3498
3565
|
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
this.
|
|
3566
|
+
// ../../src/core/database/schema/blueprint.ts
|
|
3567
|
+
class Blueprint {
|
|
3568
|
+
table;
|
|
3569
|
+
action;
|
|
3570
|
+
columns = [];
|
|
3571
|
+
indexes = [];
|
|
3572
|
+
droppedColumns = [];
|
|
3573
|
+
droppedIndexes = [];
|
|
3574
|
+
constructor(table, action) {
|
|
3575
|
+
this.table = table;
|
|
3576
|
+
this.action = action;
|
|
3509
3577
|
}
|
|
3510
|
-
|
|
3511
|
-
const
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
return
|
|
3578
|
+
id(name = "id") {
|
|
3579
|
+
const column = new ColumnDefinition(name, "id");
|
|
3580
|
+
column.primary();
|
|
3581
|
+
column.autoIncrement = true;
|
|
3582
|
+
this.columns.push(column);
|
|
3583
|
+
return column;
|
|
3516
3584
|
}
|
|
3517
|
-
|
|
3518
|
-
const
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
await this.client.set(this.storageKey(key), payload);
|
|
3524
|
-
}
|
|
3525
|
-
await this.enforceMaxEntries();
|
|
3585
|
+
string(name, length) {
|
|
3586
|
+
const column = new ColumnDefinition(name, "string");
|
|
3587
|
+
column.length = length;
|
|
3588
|
+
column.notNullable();
|
|
3589
|
+
this.columns.push(column);
|
|
3590
|
+
return column;
|
|
3526
3591
|
}
|
|
3527
|
-
|
|
3528
|
-
const
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
const inflightRequest = this.inflight.get(key);
|
|
3533
|
-
if (inflightRequest) {
|
|
3534
|
-
return inflightRequest;
|
|
3535
|
-
}
|
|
3536
|
-
const pendingRequest = loader().then(async (value) => {
|
|
3537
|
-
await this.set(key, value, ttlMs);
|
|
3538
|
-
return value;
|
|
3539
|
-
}).finally(() => {
|
|
3540
|
-
this.inflight.delete(key);
|
|
3541
|
-
});
|
|
3542
|
-
this.inflight.set(key, pendingRequest);
|
|
3543
|
-
return pendingRequest;
|
|
3592
|
+
text(name) {
|
|
3593
|
+
const column = new ColumnDefinition(name, "text");
|
|
3594
|
+
column.notNullable();
|
|
3595
|
+
this.columns.push(column);
|
|
3596
|
+
return column;
|
|
3544
3597
|
}
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
if (!tagsForKey) {
|
|
3551
|
-
tagsForKey = new Set;
|
|
3552
|
-
this.keyTags.set(key, tagsForKey);
|
|
3553
|
-
}
|
|
3554
|
-
for (const tag of tags) {
|
|
3555
|
-
tagsForKey.add(tag);
|
|
3556
|
-
await this.client.sadd(this.tagKey(tag), key);
|
|
3557
|
-
}
|
|
3598
|
+
boolean(name) {
|
|
3599
|
+
const column = new ColumnDefinition(name, "boolean");
|
|
3600
|
+
column.notNullable();
|
|
3601
|
+
this.columns.push(column);
|
|
3602
|
+
return column;
|
|
3558
3603
|
}
|
|
3559
|
-
|
|
3560
|
-
const
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
keysToRemove.add(member);
|
|
3565
|
-
}
|
|
3566
|
-
}
|
|
3567
|
-
let removed = 0;
|
|
3568
|
-
for (const key of keysToRemove) {
|
|
3569
|
-
if (await this.invalidate(key)) {
|
|
3570
|
-
removed += 1;
|
|
3571
|
-
}
|
|
3572
|
-
}
|
|
3573
|
-
for (const tag of tags) {
|
|
3574
|
-
await this.client.del(this.tagKey(tag));
|
|
3575
|
-
}
|
|
3576
|
-
return removed;
|
|
3604
|
+
integer(name) {
|
|
3605
|
+
const column = new ColumnDefinition(name, "integer");
|
|
3606
|
+
column.notNullable();
|
|
3607
|
+
this.columns.push(column);
|
|
3608
|
+
return column;
|
|
3577
3609
|
}
|
|
3578
|
-
|
|
3579
|
-
const
|
|
3580
|
-
|
|
3581
|
-
|
|
3610
|
+
bigInteger(name) {
|
|
3611
|
+
const column = new ColumnDefinition(name, "bigInteger");
|
|
3612
|
+
column.notNullable();
|
|
3613
|
+
this.columns.push(column);
|
|
3614
|
+
return column;
|
|
3582
3615
|
}
|
|
3583
|
-
|
|
3584
|
-
const
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
if (key === prefix || key.startsWith(`${prefix}?`)) {
|
|
3589
|
-
if (await this.invalidate(key)) {
|
|
3590
|
-
removed += 1;
|
|
3591
|
-
}
|
|
3592
|
-
}
|
|
3593
|
-
}
|
|
3594
|
-
return removed;
|
|
3616
|
+
timestamp(name) {
|
|
3617
|
+
const column = new ColumnDefinition(name, "timestamp");
|
|
3618
|
+
column.notNullable();
|
|
3619
|
+
this.columns.push(column);
|
|
3620
|
+
return column;
|
|
3595
3621
|
}
|
|
3596
|
-
|
|
3597
|
-
const
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
const tagKeys = await this.client.keys(`${TAG_PREFIX}*`);
|
|
3602
|
-
if (tagKeys.length > 0) {
|
|
3603
|
-
await this.client.del(...tagKeys);
|
|
3604
|
-
}
|
|
3605
|
-
this.inflight.clear();
|
|
3606
|
-
this.keyTags.clear();
|
|
3622
|
+
json(name) {
|
|
3623
|
+
const column = new ColumnDefinition(name, "json");
|
|
3624
|
+
column.notNullable();
|
|
3625
|
+
this.columns.push(column);
|
|
3626
|
+
return column;
|
|
3607
3627
|
}
|
|
3608
|
-
|
|
3609
|
-
const
|
|
3610
|
-
|
|
3628
|
+
jsonb(name) {
|
|
3629
|
+
const column = new ColumnDefinition(name, "jsonb");
|
|
3630
|
+
column.notNullable();
|
|
3631
|
+
this.columns.push(column);
|
|
3632
|
+
return column;
|
|
3611
3633
|
}
|
|
3612
|
-
|
|
3613
|
-
|
|
3634
|
+
foreignId(name) {
|
|
3635
|
+
const column = new ForeignIdColumnDefinition(name);
|
|
3636
|
+
this.columns.push(column);
|
|
3637
|
+
return column;
|
|
3614
3638
|
}
|
|
3615
|
-
|
|
3616
|
-
|
|
3639
|
+
timestamps() {
|
|
3640
|
+
this.timestamp("created_at").defaultRaw("NOW()");
|
|
3641
|
+
this.timestamp("updated_at").defaultRaw("NOW()");
|
|
3617
3642
|
}
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
if (!tags) {
|
|
3621
|
-
return;
|
|
3622
|
-
}
|
|
3623
|
-
for (const tag of tags) {
|
|
3624
|
-
await this.client.srem(this.tagKey(tag), key);
|
|
3625
|
-
}
|
|
3626
|
-
this.keyTags.delete(key);
|
|
3643
|
+
softDeletes() {
|
|
3644
|
+
this.timestamp("deleted_at").nullable();
|
|
3627
3645
|
}
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
if (keys.length <= this.maxEntries) {
|
|
3631
|
-
return;
|
|
3632
|
-
}
|
|
3633
|
-
const overflow = keys.length - this.maxEntries;
|
|
3634
|
-
const keysToRemove = keys.slice(0, overflow);
|
|
3635
|
-
if (keysToRemove.length > 0) {
|
|
3636
|
-
await this.client.del(...keysToRemove);
|
|
3637
|
-
}
|
|
3646
|
+
dropColumn(name) {
|
|
3647
|
+
this.droppedColumns.push(name);
|
|
3638
3648
|
}
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
// ../../src/core/cache/simpleCache.ts
|
|
3643
|
-
class SimpleCache {
|
|
3644
|
-
ttlMs;
|
|
3645
|
-
maxEntries;
|
|
3646
|
-
cache = new Map;
|
|
3647
|
-
inflight = new Map;
|
|
3648
|
-
tagIndex = new Map;
|
|
3649
|
-
keyTags = new Map;
|
|
3650
|
-
constructor(ttlMs = 3600000, maxEntries = 100) {
|
|
3651
|
-
this.ttlMs = ttlMs;
|
|
3652
|
-
this.maxEntries = maxEntries;
|
|
3653
|
-
if (!Number.isFinite(ttlMs) || ttlMs < 0) {
|
|
3654
|
-
throw new RangeError("ttlMs must be a non-negative number.");
|
|
3655
|
-
}
|
|
3656
|
-
if (!Number.isInteger(maxEntries) || maxEntries < 1) {
|
|
3657
|
-
throw new RangeError("maxEntries must be a positive integer.");
|
|
3658
|
-
}
|
|
3649
|
+
dropSoftDeletes() {
|
|
3650
|
+
this.dropColumn("deleted_at");
|
|
3651
|
+
this.dropIndex(`idx_${this.table}_deleted_at`);
|
|
3659
3652
|
}
|
|
3660
|
-
|
|
3661
|
-
|
|
3653
|
+
dropIndex(name) {
|
|
3654
|
+
this.droppedIndexes.push(name);
|
|
3662
3655
|
}
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
expiresAt: now + resolvedTtlMs,
|
|
3669
|
-
lastAccessedAt: now
|
|
3656
|
+
unique(columns, name) {
|
|
3657
|
+
this.indexes.push({
|
|
3658
|
+
name,
|
|
3659
|
+
columns: Array.isArray(columns) ? columns : [columns],
|
|
3660
|
+
kind: "unique"
|
|
3670
3661
|
});
|
|
3671
|
-
this.evictOverflow();
|
|
3672
3662
|
}
|
|
3673
|
-
|
|
3674
|
-
this.
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
const inflightRequest = this.inflight.get(key);
|
|
3680
|
-
if (inflightRequest) {
|
|
3681
|
-
return inflightRequest;
|
|
3682
|
-
}
|
|
3683
|
-
const pendingRequest = loader().then((value) => {
|
|
3684
|
-
this.set(key, value, ttlMs);
|
|
3685
|
-
return value;
|
|
3686
|
-
}).finally(() => {
|
|
3687
|
-
this.inflight.delete(key);
|
|
3663
|
+
index(columns, options = {}) {
|
|
3664
|
+
this.indexes.push({
|
|
3665
|
+
name: options.name,
|
|
3666
|
+
columns: Array.isArray(columns) ? columns : [columns],
|
|
3667
|
+
kind: "index",
|
|
3668
|
+
order: options.order
|
|
3688
3669
|
});
|
|
3689
|
-
this.inflight.set(key, pendingRequest);
|
|
3690
|
-
return pendingRequest;
|
|
3691
|
-
}
|
|
3692
|
-
attachTags(key, tags) {
|
|
3693
|
-
if (tags.length === 0) {
|
|
3694
|
-
return;
|
|
3695
|
-
}
|
|
3696
|
-
let tagsForKey = this.keyTags.get(key);
|
|
3697
|
-
if (!tagsForKey) {
|
|
3698
|
-
tagsForKey = new Set;
|
|
3699
|
-
this.keyTags.set(key, tagsForKey);
|
|
3700
|
-
}
|
|
3701
|
-
for (const tag of tags) {
|
|
3702
|
-
tagsForKey.add(tag);
|
|
3703
|
-
let keysForTag = this.tagIndex.get(tag);
|
|
3704
|
-
if (!keysForTag) {
|
|
3705
|
-
keysForTag = new Set;
|
|
3706
|
-
this.tagIndex.set(tag, keysForTag);
|
|
3707
|
-
}
|
|
3708
|
-
keysForTag.add(key);
|
|
3709
|
-
}
|
|
3710
3670
|
}
|
|
3711
|
-
|
|
3712
|
-
const
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
keysToRemove.add(key);
|
|
3720
|
-
}
|
|
3721
|
-
}
|
|
3722
|
-
let removed = 0;
|
|
3723
|
-
for (const key of keysToRemove) {
|
|
3724
|
-
if (this.invalidate(key)) {
|
|
3725
|
-
removed += 1;
|
|
3726
|
-
}
|
|
3727
|
-
}
|
|
3728
|
-
for (const tag of tags) {
|
|
3729
|
-
this.tagIndex.delete(tag);
|
|
3730
|
-
}
|
|
3731
|
-
return removed;
|
|
3671
|
+
partialIndex(columns, where, nameOrOptions) {
|
|
3672
|
+
const options = typeof nameOrOptions === "string" ? { name: nameOrOptions } : nameOrOptions ?? {};
|
|
3673
|
+
this.indexes.push({
|
|
3674
|
+
name: options.name,
|
|
3675
|
+
columns: Array.isArray(columns) ? columns : [columns],
|
|
3676
|
+
kind: options.unique ? "uniquePartial" : "partial",
|
|
3677
|
+
where
|
|
3678
|
+
});
|
|
3732
3679
|
}
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3680
|
+
fullText(columns, name) {
|
|
3681
|
+
this.indexes.push({
|
|
3682
|
+
name,
|
|
3683
|
+
columns: Array.isArray(columns) ? columns : [columns],
|
|
3684
|
+
kind: "fullText"
|
|
3685
|
+
});
|
|
3739
3686
|
}
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
}
|
|
3747
|
-
}
|
|
3748
|
-
}
|
|
3749
|
-
return removed;
|
|
3687
|
+
ginIndex(column, name) {
|
|
3688
|
+
this.indexes.push({
|
|
3689
|
+
name,
|
|
3690
|
+
columns: [column],
|
|
3691
|
+
kind: "gin"
|
|
3692
|
+
});
|
|
3750
3693
|
}
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3694
|
+
}
|
|
3695
|
+
// ../../src/core/database/schema/driver.ts
|
|
3696
|
+
function normalizeConnectionName(connection) {
|
|
3697
|
+
const normalized = connection.trim().toLowerCase();
|
|
3698
|
+
if (normalized === "pgsql" || normalized === "postgres" || normalized === "postgresql") {
|
|
3699
|
+
return "pgsql";
|
|
3756
3700
|
}
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
return this.cache.size;
|
|
3701
|
+
if (normalized === "mysql" || normalized === "mariadb") {
|
|
3702
|
+
return "mysql";
|
|
3760
3703
|
}
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
if (!tags) {
|
|
3764
|
-
return;
|
|
3765
|
-
}
|
|
3766
|
-
for (const tag of tags) {
|
|
3767
|
-
const keys = this.tagIndex.get(tag);
|
|
3768
|
-
if (!keys) {
|
|
3769
|
-
continue;
|
|
3770
|
-
}
|
|
3771
|
-
keys.delete(key);
|
|
3772
|
-
if (keys.size === 0) {
|
|
3773
|
-
this.tagIndex.delete(tag);
|
|
3774
|
-
}
|
|
3775
|
-
}
|
|
3776
|
-
this.keyTags.delete(key);
|
|
3704
|
+
if (normalized === "sqlite") {
|
|
3705
|
+
return "sqlite";
|
|
3777
3706
|
}
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
this.invalidate(key);
|
|
3785
|
-
return;
|
|
3786
|
-
}
|
|
3787
|
-
entry.lastAccessedAt = Date.now();
|
|
3788
|
-
return entry;
|
|
3707
|
+
throw new Error(`Unsupported DB_CONNECTION: ${connection}`);
|
|
3708
|
+
}
|
|
3709
|
+
function resolveDriverFromUrl(url) {
|
|
3710
|
+
const normalized = url.trim().toLowerCase();
|
|
3711
|
+
if (normalized.startsWith("postgres://") || normalized.startsWith("postgresql://") || normalized.startsWith("postgres:")) {
|
|
3712
|
+
return "pgsql";
|
|
3789
3713
|
}
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
for (const [key, entry] of this.cache.entries()) {
|
|
3793
|
-
if (entry.expiresAt <= now) {
|
|
3794
|
-
this.invalidate(key);
|
|
3795
|
-
}
|
|
3796
|
-
}
|
|
3714
|
+
if (normalized.startsWith("mysql://") || normalized.startsWith("mysql:")) {
|
|
3715
|
+
return "mysql";
|
|
3797
3716
|
}
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
let oldestKey;
|
|
3801
|
-
let oldestAccessTime = Number.POSITIVE_INFINITY;
|
|
3802
|
-
for (const [key, entry] of this.cache.entries()) {
|
|
3803
|
-
if (entry.lastAccessedAt < oldestAccessTime) {
|
|
3804
|
-
oldestAccessTime = entry.lastAccessedAt;
|
|
3805
|
-
oldestKey = key;
|
|
3806
|
-
}
|
|
3807
|
-
}
|
|
3808
|
-
if (!oldestKey) {
|
|
3809
|
-
return;
|
|
3810
|
-
}
|
|
3811
|
-
this.invalidate(oldestKey);
|
|
3812
|
-
}
|
|
3717
|
+
if (normalized.startsWith("sqlite:")) {
|
|
3718
|
+
return "sqlite";
|
|
3813
3719
|
}
|
|
3720
|
+
return null;
|
|
3814
3721
|
}
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
cache;
|
|
3820
|
-
constructor(cache) {
|
|
3821
|
-
this.cache = cache;
|
|
3822
|
-
}
|
|
3823
|
-
get(key) {
|
|
3824
|
-
return Promise.resolve(this.cache.get(key));
|
|
3722
|
+
function resolveDatabaseDriver(options = {}) {
|
|
3723
|
+
const connection = options.connection ?? process.env.DB_CONNECTION;
|
|
3724
|
+
if (connection) {
|
|
3725
|
+
return normalizeConnectionName(connection);
|
|
3825
3726
|
}
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
3727
|
+
const url = options.url ?? process.env.DATABASE_URL ?? "";
|
|
3728
|
+
const fromUrl = resolveDriverFromUrl(url);
|
|
3729
|
+
if (fromUrl) {
|
|
3730
|
+
return fromUrl;
|
|
3829
3731
|
}
|
|
3830
|
-
|
|
3831
|
-
|
|
3732
|
+
return "pgsql";
|
|
3733
|
+
}
|
|
3734
|
+
// ../../src/core/database/schema/errors.ts
|
|
3735
|
+
class UnsupportedSchemaFeatureError extends Error {
|
|
3736
|
+
constructor(feature, driver) {
|
|
3737
|
+
super(`${feature} is not supported for the ${driver} driver`);
|
|
3738
|
+
this.name = "UnsupportedSchemaFeatureError";
|
|
3832
3739
|
}
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3740
|
+
}
|
|
3741
|
+
// ../../src/core/database/schema/grammars/grammar.ts
|
|
3742
|
+
function compileColumnType(driver, column) {
|
|
3743
|
+
switch (column.kind) {
|
|
3744
|
+
case "id":
|
|
3745
|
+
return compileIdType(driver);
|
|
3746
|
+
case "string":
|
|
3747
|
+
return compileStringType(driver, column.length);
|
|
3748
|
+
case "text":
|
|
3749
|
+
return compileTextType(driver);
|
|
3750
|
+
case "boolean":
|
|
3751
|
+
return compileBooleanType(driver);
|
|
3752
|
+
case "integer":
|
|
3753
|
+
case "foreignId":
|
|
3754
|
+
return compileIntegerType(driver);
|
|
3755
|
+
case "bigInteger":
|
|
3756
|
+
return compileBigIntegerType(driver);
|
|
3757
|
+
case "timestamp":
|
|
3758
|
+
return compileTimestampType(driver);
|
|
3759
|
+
case "json":
|
|
3760
|
+
return compileJsonType(driver);
|
|
3761
|
+
case "jsonb":
|
|
3762
|
+
return compileJsonbType(driver);
|
|
3763
|
+
default:
|
|
3764
|
+
throw new Error(`Unsupported column kind: ${column.kind}`);
|
|
3836
3765
|
}
|
|
3837
|
-
|
|
3838
|
-
|
|
3766
|
+
}
|
|
3767
|
+
function compileIdType(driver) {
|
|
3768
|
+
switch (driver) {
|
|
3769
|
+
case "pgsql":
|
|
3770
|
+
return "SERIAL";
|
|
3771
|
+
case "mysql":
|
|
3772
|
+
return "BIGINT UNSIGNED";
|
|
3773
|
+
case "sqlite":
|
|
3774
|
+
return "INTEGER";
|
|
3839
3775
|
}
|
|
3840
|
-
|
|
3841
|
-
|
|
3776
|
+
}
|
|
3777
|
+
function compileStringType(driver, length) {
|
|
3778
|
+
switch (driver) {
|
|
3779
|
+
case "pgsql":
|
|
3780
|
+
return "TEXT";
|
|
3781
|
+
case "mysql":
|
|
3782
|
+
return length ? `VARCHAR(${length})` : "VARCHAR(255)";
|
|
3783
|
+
case "sqlite":
|
|
3784
|
+
return "TEXT";
|
|
3842
3785
|
}
|
|
3843
|
-
|
|
3844
|
-
|
|
3786
|
+
}
|
|
3787
|
+
function compileTextType(driver) {
|
|
3788
|
+
switch (driver) {
|
|
3789
|
+
case "pgsql":
|
|
3790
|
+
case "sqlite":
|
|
3791
|
+
return "TEXT";
|
|
3792
|
+
case "mysql":
|
|
3793
|
+
return "TEXT";
|
|
3845
3794
|
}
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
|
|
3795
|
+
}
|
|
3796
|
+
function compileBooleanType(driver) {
|
|
3797
|
+
switch (driver) {
|
|
3798
|
+
case "pgsql":
|
|
3799
|
+
return "BOOLEAN";
|
|
3800
|
+
case "mysql":
|
|
3801
|
+
return "BOOLEAN";
|
|
3802
|
+
case "sqlite":
|
|
3803
|
+
return "INTEGER";
|
|
3849
3804
|
}
|
|
3850
|
-
|
|
3851
|
-
|
|
3805
|
+
}
|
|
3806
|
+
function compileIntegerType(driver) {
|
|
3807
|
+
switch (driver) {
|
|
3808
|
+
case "pgsql":
|
|
3809
|
+
return "INTEGER";
|
|
3810
|
+
case "mysql":
|
|
3811
|
+
return "INT";
|
|
3812
|
+
case "sqlite":
|
|
3813
|
+
return "INTEGER";
|
|
3852
3814
|
}
|
|
3853
3815
|
}
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
return new redisCacheStore_default(options.redisUrl, options.ttlMs, options.maxEntries);
|
|
3816
|
+
function compileBigIntegerType(driver) {
|
|
3817
|
+
switch (driver) {
|
|
3818
|
+
case "pgsql":
|
|
3819
|
+
return "BIGINT";
|
|
3820
|
+
case "mysql":
|
|
3821
|
+
return "BIGINT";
|
|
3822
|
+
case "sqlite":
|
|
3823
|
+
return "INTEGER";
|
|
3863
3824
|
}
|
|
3864
|
-
return new simpleCacheStore_default(new simpleCache_default(options.ttlMs, options.maxEntries));
|
|
3865
3825
|
}
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
|
|
3826
|
+
function compileTimestampType(driver) {
|
|
3827
|
+
switch (driver) {
|
|
3828
|
+
case "pgsql":
|
|
3829
|
+
return "TIMESTAMPTZ";
|
|
3830
|
+
case "mysql":
|
|
3831
|
+
return "TIMESTAMP";
|
|
3832
|
+
case "sqlite":
|
|
3833
|
+
return "TEXT";
|
|
3873
3834
|
}
|
|
3874
|
-
|
|
3875
|
-
|
|
3876
|
-
|
|
3877
|
-
|
|
3835
|
+
}
|
|
3836
|
+
function compileJsonType(driver) {
|
|
3837
|
+
switch (driver) {
|
|
3838
|
+
case "pgsql":
|
|
3839
|
+
return "JSONB";
|
|
3840
|
+
case "mysql":
|
|
3841
|
+
return "JSON";
|
|
3842
|
+
case "sqlite":
|
|
3843
|
+
return "TEXT";
|
|
3878
3844
|
}
|
|
3879
|
-
|
|
3880
|
-
|
|
3845
|
+
}
|
|
3846
|
+
function compileJsonbType(driver) {
|
|
3847
|
+
switch (driver) {
|
|
3848
|
+
case "pgsql":
|
|
3849
|
+
return "JSONB";
|
|
3850
|
+
case "mysql":
|
|
3851
|
+
return "JSON";
|
|
3852
|
+
case "sqlite":
|
|
3853
|
+
return "TEXT";
|
|
3881
3854
|
}
|
|
3882
3855
|
}
|
|
3883
|
-
var taggedCache_default = TaggedCache;
|
|
3884
3856
|
|
|
3885
|
-
// ../../src/core/
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
async remember(key, callback, ttlMs) {
|
|
3895
|
-
return this.store.getOrSet(key, callback, ttlMs);
|
|
3857
|
+
// ../../src/core/database/schema/grammars/compileStatements.ts
|
|
3858
|
+
function compileCreateTable(driver, blueprint) {
|
|
3859
|
+
const table = quoteIdentifier(blueprint.table);
|
|
3860
|
+
const parts = blueprint.columns.map((column) => compileColumn(driver, column, "create"));
|
|
3861
|
+
for (const index of blueprint.indexes) {
|
|
3862
|
+
if (index.kind === "unique" && index.columns.length > 1) {
|
|
3863
|
+
const columns = index.columns.map((column) => quoteIdentifier(column)).join(", ");
|
|
3864
|
+
parts.push(`UNIQUE (${columns})`);
|
|
3865
|
+
}
|
|
3896
3866
|
}
|
|
3897
|
-
|
|
3898
|
-
|
|
3867
|
+
const statements = [`CREATE TABLE IF NOT EXISTS ${table} (
|
|
3868
|
+
${parts.join(`,
|
|
3869
|
+
`)}
|
|
3870
|
+
)`];
|
|
3871
|
+
for (const index of blueprint.indexes) {
|
|
3872
|
+
if (index.kind === "unique" && index.columns.length === 1) {
|
|
3873
|
+
continue;
|
|
3874
|
+
}
|
|
3875
|
+
if (index.kind === "index") {
|
|
3876
|
+
statements.push(compileIndex(driver, blueprint.table, index));
|
|
3877
|
+
} else if (index.kind === "partial" || index.kind === "uniquePartial" || index.kind === "gin" || index.kind === "fullText") {
|
|
3878
|
+
statements.push(...compileSpecialIndex(driver, blueprint.table, index));
|
|
3879
|
+
}
|
|
3899
3880
|
}
|
|
3900
|
-
|
|
3901
|
-
|
|
3881
|
+
return statements;
|
|
3882
|
+
}
|
|
3883
|
+
function compileAlterTable(driver, blueprint) {
|
|
3884
|
+
const statements = [];
|
|
3885
|
+
const table = quoteIdentifier(blueprint.table);
|
|
3886
|
+
for (const column of blueprint.columns) {
|
|
3887
|
+
const addPrefix = driver === "pgsql" ? "ADD COLUMN IF NOT EXISTS" : "ADD COLUMN";
|
|
3888
|
+
statements.push(`ALTER TABLE ${table}
|
|
3889
|
+
${addPrefix} ${compileColumn(driver, column, "alter")}`);
|
|
3902
3890
|
}
|
|
3903
|
-
|
|
3904
|
-
|
|
3891
|
+
for (const columnName of blueprint.droppedColumns) {
|
|
3892
|
+
const dropPrefix = driver === "pgsql" ? "DROP COLUMN IF EXISTS" : "DROP COLUMN";
|
|
3893
|
+
statements.push(`ALTER TABLE ${table} ${dropPrefix} ${quoteIdentifier(columnName)}`);
|
|
3905
3894
|
}
|
|
3906
|
-
|
|
3907
|
-
|
|
3895
|
+
for (const indexName of blueprint.droppedIndexes) {
|
|
3896
|
+
statements.push(`DROP INDEX IF EXISTS ${quoteIdentifier(indexName)}`);
|
|
3908
3897
|
}
|
|
3909
|
-
|
|
3910
|
-
|
|
3898
|
+
for (const index of blueprint.indexes) {
|
|
3899
|
+
if (index.kind === "index" || index.kind === "unique") {
|
|
3900
|
+
statements.push(compileIndex(driver, blueprint.table, index));
|
|
3901
|
+
} else {
|
|
3902
|
+
statements.push(...compileSpecialIndex(driver, blueprint.table, index));
|
|
3903
|
+
}
|
|
3911
3904
|
}
|
|
3912
|
-
|
|
3913
|
-
|
|
3905
|
+
return statements;
|
|
3906
|
+
}
|
|
3907
|
+
function compileDropTable(driver, tableName) {
|
|
3908
|
+
const cascade = driver === "pgsql" ? " CASCADE" : "";
|
|
3909
|
+
return [`DROP TABLE IF EXISTS ${quoteIdentifier(tableName)}${cascade}`];
|
|
3910
|
+
}
|
|
3911
|
+
function compileColumn(driver, column, mode) {
|
|
3912
|
+
const parts = [quoteIdentifier(column.name), compileColumnType(driver, column)];
|
|
3913
|
+
if (column.autoIncrement && driver === "mysql") {
|
|
3914
|
+
parts[1] = `${parts[1]} AUTO_INCREMENT`;
|
|
3914
3915
|
}
|
|
3915
|
-
|
|
3916
|
-
|
|
3916
|
+
if (column.isPrimary && mode === "create") {
|
|
3917
|
+
if (driver === "sqlite") {
|
|
3918
|
+
parts.push("PRIMARY KEY AUTOINCREMENT");
|
|
3919
|
+
} else {
|
|
3920
|
+
parts.push("PRIMARY KEY");
|
|
3921
|
+
}
|
|
3922
|
+
} else if (!column.isNullable) {
|
|
3923
|
+
parts.push("NOT NULL");
|
|
3924
|
+
} else if (column.isNullable) {
|
|
3925
|
+
parts.push("NULL");
|
|
3917
3926
|
}
|
|
3918
|
-
|
|
3919
|
-
|
|
3927
|
+
if (column.defaultValue !== undefined) {
|
|
3928
|
+
parts.push(`DEFAULT ${column.defaultValue}`);
|
|
3920
3929
|
}
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
// ../../src/core/cache/tags.ts
|
|
3924
|
-
var CACHE_TAGS = {
|
|
3925
|
-
organizations: "organizations",
|
|
3926
|
-
projects: "projects",
|
|
3927
|
-
tasks: "tasks",
|
|
3928
|
-
comments: "comments",
|
|
3929
|
-
attachments: "attachments",
|
|
3930
|
-
reports: "reports"
|
|
3931
|
-
};
|
|
3932
|
-
// ../../src/core/contracts/container.ts
|
|
3933
|
-
class ServiceContainer {
|
|
3934
|
-
services = new Map;
|
|
3935
|
-
singletonFactories = new Map;
|
|
3936
|
-
bindings = new Map;
|
|
3937
|
-
set(key, value) {
|
|
3938
|
-
this.singletonFactories.delete(key);
|
|
3939
|
-
this.bindings.delete(key);
|
|
3940
|
-
this.services.set(key, value);
|
|
3941
|
-
return value;
|
|
3930
|
+
if (column.isUnique) {
|
|
3931
|
+
parts.push("UNIQUE");
|
|
3942
3932
|
}
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
this.services.delete(key);
|
|
3946
|
-
this.singletonFactories.set(key, factory);
|
|
3933
|
+
if (column.checkExpression) {
|
|
3934
|
+
parts.push(`CHECK (${column.checkExpression})`);
|
|
3947
3935
|
}
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
|
|
3936
|
+
if (column.foreignKey) {
|
|
3937
|
+
const { referencesTable, referencesColumn, onDelete } = column.foreignKey;
|
|
3938
|
+
const reference = `${quoteIdentifier(referencesTable)}(${quoteIdentifier(referencesColumn)})`;
|
|
3939
|
+
let clause = `REFERENCES ${reference}`;
|
|
3940
|
+
if (onDelete === "cascade") {
|
|
3941
|
+
clause += " ON DELETE CASCADE";
|
|
3942
|
+
} else if (onDelete === "set null") {
|
|
3943
|
+
clause += " ON DELETE SET NULL";
|
|
3944
|
+
}
|
|
3945
|
+
parts.push(clause);
|
|
3952
3946
|
}
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
3947
|
+
return parts.join(" ");
|
|
3948
|
+
}
|
|
3949
|
+
function compileIndex(_driver, tableName, index) {
|
|
3950
|
+
const indexName = index.name ?? defaultIndexName(tableName, index.columns, index.kind === "unique" ? "unique" : "index");
|
|
3951
|
+
const columns = index.columns.map((column) => {
|
|
3952
|
+
const quoted = quoteIdentifier(column);
|
|
3953
|
+
if (index.order === "desc") {
|
|
3954
|
+
return `${quoted} DESC`;
|
|
3955
|
+
}
|
|
3956
|
+
return quoted;
|
|
3957
|
+
}).join(", ");
|
|
3958
|
+
const unique = index.kind === "unique" ? "UNIQUE " : "";
|
|
3959
|
+
return `CREATE ${unique}INDEX IF NOT EXISTS ${quoteIdentifier(indexName)} ON ${quoteIdentifier(tableName)}(${columns})`;
|
|
3960
|
+
}
|
|
3961
|
+
function compileSpecialIndex(driver, tableName, index) {
|
|
3962
|
+
const indexName = index.name ?? defaultIndexName(tableName, index.columns, index.kind);
|
|
3963
|
+
const columns = index.columns.map((column) => quoteIdentifier(column)).join(", ");
|
|
3964
|
+
switch (index.kind) {
|
|
3965
|
+
case "partial":
|
|
3966
|
+
case "uniquePartial": {
|
|
3967
|
+
if (driver !== "pgsql") {
|
|
3968
|
+
throw new UnsupportedSchemaFeatureError("partialIndex()", driver);
|
|
3969
|
+
}
|
|
3970
|
+
const unique = index.kind === "uniquePartial" ? "UNIQUE " : "";
|
|
3971
|
+
return [
|
|
3972
|
+
`CREATE ${unique}INDEX IF NOT EXISTS ${quoteIdentifier(indexName)} ON ${quoteIdentifier(tableName)}(${columns}) WHERE ${index.where}`
|
|
3973
|
+
];
|
|
3956
3974
|
}
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
return
|
|
3975
|
+
case "gin": {
|
|
3976
|
+
if (driver !== "pgsql") {
|
|
3977
|
+
throw new UnsupportedSchemaFeatureError("ginIndex()", driver);
|
|
3978
|
+
}
|
|
3979
|
+
return [
|
|
3980
|
+
`CREATE INDEX IF NOT EXISTS ${quoteIdentifier(indexName)} ON ${quoteIdentifier(tableName)} USING GIN(${columns})`
|
|
3981
|
+
];
|
|
3962
3982
|
}
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
3983
|
+
case "fullText": {
|
|
3984
|
+
if (driver === "mysql") {
|
|
3985
|
+
return [
|
|
3986
|
+
`CREATE FULLTEXT INDEX ${quoteIdentifier(indexName)} ON ${quoteIdentifier(tableName)}(${columns})`
|
|
3987
|
+
];
|
|
3988
|
+
}
|
|
3989
|
+
if (driver === "pgsql") {
|
|
3990
|
+
throw new UnsupportedSchemaFeatureError("fullText(); use ginIndex() with a tsvector column on PostgreSQL", driver);
|
|
3991
|
+
}
|
|
3992
|
+
throw new UnsupportedSchemaFeatureError("fullText()", driver);
|
|
3966
3993
|
}
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
resolve(key) {
|
|
3970
|
-
return this.get(key);
|
|
3994
|
+
default:
|
|
3995
|
+
return [];
|
|
3971
3996
|
}
|
|
3972
|
-
|
|
3973
|
-
|
|
3997
|
+
}
|
|
3998
|
+
function defaultIndexName(tableName, columns, kind) {
|
|
3999
|
+
return `idx_${tableName}_${columns.join("_")}_${kind}`;
|
|
4000
|
+
}
|
|
4001
|
+
function compileBlueprint(driver, blueprint) {
|
|
4002
|
+
switch (blueprint.action) {
|
|
4003
|
+
case "create":
|
|
4004
|
+
return compileCreateTable(driver, blueprint);
|
|
4005
|
+
case "alter":
|
|
4006
|
+
return compileAlterTable(driver, blueprint);
|
|
4007
|
+
case "drop":
|
|
4008
|
+
return compileDropTable(driver, blueprint.table);
|
|
4009
|
+
default:
|
|
4010
|
+
throw new Error(`Unsupported blueprint action: ${blueprint.action}`);
|
|
3974
4011
|
}
|
|
3975
4012
|
}
|
|
4013
|
+
// ../../src/core/database/schema/grammars/createGrammar.ts
|
|
4014
|
+
function createGrammar(driver) {
|
|
4015
|
+
return {
|
|
4016
|
+
driver,
|
|
4017
|
+
compile(blueprint) {
|
|
4018
|
+
return compileBlueprint(driver, blueprint);
|
|
4019
|
+
}
|
|
4020
|
+
};
|
|
4021
|
+
}
|
|
3976
4022
|
|
|
3977
|
-
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
|
|
4023
|
+
// ../../src/core/database/schema/grammars/mysqlGrammar.ts
|
|
4024
|
+
var MySqlGrammar = createGrammar("mysql");
|
|
4025
|
+
|
|
4026
|
+
// ../../src/core/database/schema/grammars/postgresGrammar.ts
|
|
4027
|
+
var PostgresGrammar = createGrammar("pgsql");
|
|
4028
|
+
|
|
4029
|
+
// ../../src/core/database/schema/grammars/sqliteGrammar.ts
|
|
4030
|
+
var SqliteGrammar = createGrammar("sqlite");
|
|
4031
|
+
|
|
4032
|
+
// ../../src/core/database/schema/grammars/index.ts
|
|
4033
|
+
function grammarForDriver(driver) {
|
|
4034
|
+
switch (driver) {
|
|
4035
|
+
case "pgsql":
|
|
4036
|
+
return PostgresGrammar;
|
|
4037
|
+
case "mysql":
|
|
4038
|
+
return MySqlGrammar;
|
|
4039
|
+
case "sqlite":
|
|
4040
|
+
return SqliteGrammar;
|
|
4041
|
+
default:
|
|
4042
|
+
throw new Error(`Unsupported database driver: ${driver}`);
|
|
3982
4043
|
}
|
|
3983
|
-
|
|
3984
|
-
|
|
4044
|
+
}
|
|
4045
|
+
// ../../src/core/database/schema/schema.ts
|
|
4046
|
+
class SchemaBuilder {
|
|
4047
|
+
#driver;
|
|
4048
|
+
#statements = [];
|
|
4049
|
+
constructor(driver) {
|
|
4050
|
+
this.#driver = driver;
|
|
3985
4051
|
}
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
return this
|
|
4052
|
+
create(table, callback) {
|
|
4053
|
+
const blueprint = new Blueprint(table, "create");
|
|
4054
|
+
callback(blueprint);
|
|
4055
|
+
this.#statements.push(...grammarForDriver(this.#driver).compile(blueprint));
|
|
4056
|
+
return this;
|
|
3991
4057
|
}
|
|
3992
|
-
|
|
3993
|
-
|
|
4058
|
+
table(table, callback) {
|
|
4059
|
+
const blueprint = new Blueprint(table, "alter");
|
|
4060
|
+
callback(blueprint);
|
|
4061
|
+
this.#statements.push(...grammarForDriver(this.#driver).compile(blueprint));
|
|
4062
|
+
return this;
|
|
3994
4063
|
}
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
await db2.unsafe("SELECT pg_advisory_lock($1)", [lockKey]);
|
|
4000
|
-
try {
|
|
4001
|
-
return await callback();
|
|
4002
|
-
} finally {
|
|
4003
|
-
await db2.unsafe("SELECT pg_advisory_unlock($1)", [lockKey]);
|
|
4064
|
+
drop(table) {
|
|
4065
|
+
const blueprint = new Blueprint(table, "drop");
|
|
4066
|
+
this.#statements.push(...grammarForDriver(this.#driver).compile(blueprint));
|
|
4067
|
+
return this;
|
|
4004
4068
|
}
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
import { readdir } from "fs/promises";
|
|
4008
|
-
import { join } from "path";
|
|
4009
|
-
import { pathToFileURL } from "url";
|
|
4010
|
-
var MIGRATIONS_TABLE = "framework_migrations";
|
|
4011
|
-
async function ensureMigrationsTable(db2) {
|
|
4012
|
-
await db2.unsafe(`
|
|
4013
|
-
CREATE TABLE IF NOT EXISTS ${MIGRATIONS_TABLE} (
|
|
4014
|
-
name TEXT PRIMARY KEY,
|
|
4015
|
-
batch INTEGER NOT NULL,
|
|
4016
|
-
run_on TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
4017
|
-
)
|
|
4018
|
-
`);
|
|
4019
|
-
}
|
|
4020
|
-
async function getAppliedMigrations(db2) {
|
|
4021
|
-
await ensureMigrationsTable(db2);
|
|
4022
|
-
return await db2.unsafe(`
|
|
4023
|
-
SELECT name, batch
|
|
4024
|
-
FROM ${MIGRATIONS_TABLE}
|
|
4025
|
-
ORDER BY batch ASC, name ASC
|
|
4026
|
-
`);
|
|
4027
|
-
}
|
|
4028
|
-
async function loadMigrationsFromDirectory(directory) {
|
|
4029
|
-
const entries = await readdir(directory);
|
|
4030
|
-
const migrationFiles = entries.filter((entry) => (entry.endsWith(".ts") || entry.endsWith(".js")) && entry !== "types.ts" && entry !== "index.ts" && entry !== "runner.ts").sort();
|
|
4031
|
-
const loadedMigrations = await Promise.all(migrationFiles.map(async (fileName) => {
|
|
4032
|
-
const moduleUrl = pathToFileURL(join(directory, fileName)).href;
|
|
4033
|
-
const module = await import(moduleUrl);
|
|
4034
|
-
return module.default;
|
|
4035
|
-
}));
|
|
4036
|
-
return loadedMigrations.filter((migration) => migration?.name !== undefined);
|
|
4037
|
-
}
|
|
4038
|
-
async function getMigrationStatus(db2, migrations) {
|
|
4039
|
-
const applied = await getAppliedMigrations(db2);
|
|
4040
|
-
const appliedByName = new Map(applied.map(({ name, batch }) => [name, Number(batch)]));
|
|
4041
|
-
return migrations.map(({ name }) => ({
|
|
4042
|
-
name,
|
|
4043
|
-
status: appliedByName.has(name) ? "up" : "pending",
|
|
4044
|
-
batch: appliedByName.get(name) ?? null
|
|
4045
|
-
}));
|
|
4046
|
-
}
|
|
4047
|
-
async function runPendingMigrations(db2, migrations, options = {}) {
|
|
4048
|
-
const applied = await getAppliedMigrations(db2);
|
|
4049
|
-
const appliedNames = new Set(applied.map(({ name }) => name));
|
|
4050
|
-
const nextBatch = applied.reduce((currentMax, { batch }) => Math.max(currentMax, Number(batch)), 0) + 1;
|
|
4051
|
-
const pendingMigrations = migrations.filter(({ name }) => !appliedNames.has(name));
|
|
4052
|
-
for (const migration of pendingMigrations) {
|
|
4053
|
-
options.onMigration?.(migration.name);
|
|
4054
|
-
await migration.up(db2);
|
|
4055
|
-
const inserted = await db2.unsafe(`INSERT INTO ${MIGRATIONS_TABLE} (name, batch) VALUES ($1, $2) ON CONFLICT (name) DO NOTHING RETURNING name`, [migration.name, nextBatch]);
|
|
4056
|
-
if (inserted.length === 0) {
|
|
4057
|
-
throw new Error(`Migration ${migration.name} was applied but not recorded.`);
|
|
4058
|
-
}
|
|
4069
|
+
toSql() {
|
|
4070
|
+
return [...this.#statements];
|
|
4059
4071
|
}
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
|
|
4064
|
-
if (advisoryLock) {
|
|
4065
|
-
return withMigrationLock(db2, () => runPendingMigrations(db2, migrations, { onMigration }));
|
|
4072
|
+
async execute(db2) {
|
|
4073
|
+
for (const statement of this.#statements) {
|
|
4074
|
+
await db2.unsafe(statement);
|
|
4075
|
+
}
|
|
4066
4076
|
}
|
|
4067
|
-
return runPendingMigrations(db2, migrations, { onMigration });
|
|
4068
4077
|
}
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
return
|
|
4078
|
+
|
|
4079
|
+
class Schema {
|
|
4080
|
+
static builder(driver) {
|
|
4081
|
+
return new SchemaBuilder(driver ?? resolveDatabaseDriver());
|
|
4073
4082
|
}
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
|
|
4078
|
-
if (!migrationsToRollback.has(migration.name)) {
|
|
4079
|
-
continue;
|
|
4080
|
-
}
|
|
4081
|
-
options.onMigration?.(migration.name);
|
|
4082
|
-
await migration.down(db2);
|
|
4083
|
-
await db2.unsafe(`DELETE FROM ${MIGRATIONS_TABLE} WHERE name = $1`, [migration.name]);
|
|
4084
|
-
rolledBack += 1;
|
|
4083
|
+
static async run(db2, driver, callback) {
|
|
4084
|
+
const schema = Schema.builder(driver);
|
|
4085
|
+
await callback(schema);
|
|
4086
|
+
await schema.execute(db2);
|
|
4085
4087
|
}
|
|
4086
|
-
return rolledBack;
|
|
4087
4088
|
}
|
|
4088
|
-
|
|
4089
|
-
const
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
for (const migration of [...appliedMigrations].reverse()) {
|
|
4094
|
-
options.onMigration?.(migration.name);
|
|
4095
|
-
await migration.down(db2);
|
|
4096
|
-
}
|
|
4097
|
-
if (appliedMigrations.length > 0) {
|
|
4098
|
-
await db2.unsafe(`DELETE FROM ${MIGRATIONS_TABLE}`);
|
|
4089
|
+
function createSchemaBuilder(db2, driver) {
|
|
4090
|
+
const builder = Schema.builder(driver);
|
|
4091
|
+
return Object.assign(builder, {
|
|
4092
|
+
async commit() {
|
|
4093
|
+
await builder.execute(db2);
|
|
4099
4094
|
}
|
|
4100
|
-
|
|
4101
|
-
};
|
|
4102
|
-
if (options.advisoryLock) {
|
|
4103
|
-
await withMigrationLock(db2, runFresh);
|
|
4104
|
-
return;
|
|
4105
|
-
}
|
|
4106
|
-
await runFresh();
|
|
4095
|
+
});
|
|
4107
4096
|
}
|
|
4108
4097
|
// ../../src/core/database/seeders/runner.ts
|
|
4109
4098
|
import { readdir as readdir2 } from "fs/promises";
|
|
@@ -4130,6 +4119,19 @@ async function runSeedersFromDirectory(directory, db2, options) {
|
|
|
4130
4119
|
}
|
|
4131
4120
|
return seeders.length;
|
|
4132
4121
|
}
|
|
4122
|
+
// ../../src/core/database/transaction.ts
|
|
4123
|
+
function supportsTransactions(connection) {
|
|
4124
|
+
return typeof connection.begin === "function";
|
|
4125
|
+
}
|
|
4126
|
+
async function runInTransaction(operation) {
|
|
4127
|
+
const pool = resolveRepositoryConnection();
|
|
4128
|
+
if (!supportsTransactions(pool)) {
|
|
4129
|
+
throw new Error("Active database connection does not support transactions. Bind a client with begin() via bindDatabaseConnection.");
|
|
4130
|
+
}
|
|
4131
|
+
return await pool.begin(async (transaction) => {
|
|
4132
|
+
return await operation(createDatabaseConnection2(transaction));
|
|
4133
|
+
});
|
|
4134
|
+
}
|
|
4133
4135
|
// ../../src/core/mail/mailer.ts
|
|
4134
4136
|
function resolveSmtpConfig() {
|
|
4135
4137
|
const host = process.env.MAIL_HOST?.trim();
|
|
@@ -6060,7 +6062,7 @@ var failedJobTable = defineTable({
|
|
|
6060
6062
|
});
|
|
6061
6063
|
|
|
6062
6064
|
// ../../src/core/queue/failedJobRepository.ts
|
|
6063
|
-
class FailedJobRepository extends
|
|
6065
|
+
class FailedJobRepository extends BaseRepository {
|
|
6064
6066
|
constructor() {
|
|
6065
6067
|
super(failedJobTable);
|
|
6066
6068
|
}
|
|
@@ -6853,7 +6855,7 @@ export {
|
|
|
6853
6855
|
createFlashMiddleware,
|
|
6854
6856
|
createFailedJobService,
|
|
6855
6857
|
createDatabaseQueryProxy,
|
|
6856
|
-
createDatabaseConnection,
|
|
6858
|
+
createDatabaseConnection2 as createDatabaseConnection,
|
|
6857
6859
|
createCsrfTokenCookie,
|
|
6858
6860
|
createCsrfProtection,
|
|
6859
6861
|
createCsrfMiddleware,
|