@getstrata/core 0.5.41 → 0.5.43
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/database/baseRepository.d.ts +1 -0
- package/dist/core/queue/failedJobRepository.d.ts +1 -0
- package/dist/entries/admin/formatValue.js +32 -0
- package/dist/entries/admin/registry.js +32 -0
- package/dist/entries/audit/exportAuditLogs.js +18 -0
- package/dist/entries/audit/siemFormatter.js +37 -0
- package/dist/entries/auth/scimAuthMiddleware.js +241 -0
- package/dist/entries/auth/sessionGuard.js +501 -0
- package/dist/entries/database/baseRepository.js +1388 -0
- package/dist/entries/database/bindConnection.js +22 -0
- package/dist/entries/database/boundConnection.js +19 -0
- package/dist/entries/database/connection.js +12 -0
- package/dist/entries/database/errors.js +128 -0
- package/dist/entries/database/model.js +948 -0
- package/dist/entries/database/query.js +436 -0
- package/dist/entries/database/relationships.js +162 -0
- package/dist/entries/database/schema.js +1054 -0
- package/dist/entries/database/table.js +8 -0
- package/dist/entries/database/transaction.js +129 -0
- package/dist/entries/http/authMiddleware.js +47 -0
- package/dist/entries/http/authorizeMiddleware.js +104 -0
- package/dist/entries/http/metricsMiddleware.js +91 -0
- package/dist/entries/http/parseMultipartUpload.js +144 -0
- package/dist/entries/http/securedRouteModelBinding.js +6 -0
- package/dist/entries/http/webErrorResponse.js +501 -0
- package/dist/entries/http/webFormRequest.js +6 -0
- package/dist/entries/jobs/dispatchWebhookJob.js +18 -0
- package/dist/entries/mail/mailer.js +208 -0
- package/dist/entries/mail/markdownMail.js +63 -0
- package/dist/entries/mail/markdownMailable.js +78 -0
- package/dist/entries/notifications.js +152 -0
- package/dist/entries/openapi/generator.js +178 -0
- package/dist/entries/openapi/validate.js +28 -0
- package/dist/entries/queue/createAppQueue.js +507 -0
- package/dist/entries/queue/failedJobRepository.js +2364 -0
- package/dist/entries/queue/publicQueue.js +507 -0
- package/dist/entries/queue/queueMetrics.js +507 -0
- package/dist/entries/queue/redisQueue.js +232 -0
- package/dist/entries/runtime/asyncContextStore.js +17 -0
- package/dist/entries/security/safeFetch.js +211 -0
- package/dist/entries/security/scimTenantTokens.js +51 -0
- package/dist/entries/security/timingSafeCompare.js +14 -0
- package/dist/entries/tenant/databaseTenantContext.js +116 -0
- package/dist/entries/tenant/tenantDatabaseScope.js +113 -0
- package/dist/entries/view.js +501 -0
- package/package.json +167 -2
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// ../../src/core/runtime/asyncContextStore.ts
|
|
3
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
4
|
+
function createAsyncContextStore(key) {
|
|
5
|
+
const symbol = Symbol.for(key);
|
|
6
|
+
const globalRecord = globalThis;
|
|
7
|
+
const existing = globalRecord[symbol];
|
|
8
|
+
if (existing) {
|
|
9
|
+
return existing;
|
|
10
|
+
}
|
|
11
|
+
const store = new AsyncLocalStorage;
|
|
12
|
+
globalRecord[symbol] = store;
|
|
13
|
+
return store;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// ../../src/core/database/connectionContext.ts
|
|
17
|
+
var activeConnection = createAsyncContextStore("@getstrata/databaseConnectionContext");
|
|
18
|
+
function runWithDatabaseConnection(connection, callback) {
|
|
19
|
+
return activeConnection.run(connection, callback);
|
|
20
|
+
}
|
|
21
|
+
function getActiveDatabaseConnection(fallback) {
|
|
22
|
+
return activeConnection.getStore() ?? fallback;
|
|
23
|
+
}
|
|
24
|
+
function hasActiveDatabaseConnection() {
|
|
25
|
+
return activeConnection.getStore() !== undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ../../src/core/database/queryProxy.ts
|
|
29
|
+
var POOL_CONNECTION_METHODS = new Set(["begin", "close", "connect"]);
|
|
30
|
+
function createDatabaseQueryProxy(pool) {
|
|
31
|
+
function resolveDatabase() {
|
|
32
|
+
return getActiveDatabaseConnection(pool);
|
|
33
|
+
}
|
|
34
|
+
function resolveDatabaseForProperty(property) {
|
|
35
|
+
if (typeof property === "string" && POOL_CONNECTION_METHODS.has(property)) {
|
|
36
|
+
return pool;
|
|
37
|
+
}
|
|
38
|
+
return resolveDatabase();
|
|
39
|
+
}
|
|
40
|
+
return new Proxy(function database() {}, {
|
|
41
|
+
apply(_target, _thisArg, args) {
|
|
42
|
+
return resolveDatabase()(...args);
|
|
43
|
+
},
|
|
44
|
+
get(_target, property) {
|
|
45
|
+
const connection = resolveDatabaseForProperty(property);
|
|
46
|
+
const value = connection[property];
|
|
47
|
+
return typeof value === "function" ? value.bind(connection) : value;
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ../../src/core/database/defaultConnection.ts
|
|
53
|
+
var defaultPool = {
|
|
54
|
+
connection: null
|
|
55
|
+
};
|
|
56
|
+
var defaultQuery = {
|
|
57
|
+
connection: null
|
|
58
|
+
};
|
|
59
|
+
function registerDefaultDatabasePool(connection) {
|
|
60
|
+
defaultPool.connection = connection;
|
|
61
|
+
defaultQuery.connection = createDatabaseQueryProxy(connection);
|
|
62
|
+
}
|
|
63
|
+
function getDefaultDatabasePool() {
|
|
64
|
+
if (!defaultPool.connection) {
|
|
65
|
+
throw new Error("Default database pool is not registered. Call registerDefaultDatabasePool() during app bootstrap.");
|
|
66
|
+
}
|
|
67
|
+
return defaultPool.connection;
|
|
68
|
+
}
|
|
69
|
+
function getDefaultDatabaseQuery() {
|
|
70
|
+
if (!defaultQuery.connection) {
|
|
71
|
+
throw new Error("Default database query handle is not registered. Call registerDefaultDatabasePool() during app bootstrap.");
|
|
72
|
+
}
|
|
73
|
+
return defaultQuery.connection;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ../../src/core/tenant/tenantContext.ts
|
|
77
|
+
var tenantContext = createAsyncContextStore("@getstrata/tenantContext");
|
|
78
|
+
function runWithTenant(tenant, callback) {
|
|
79
|
+
return tenantContext.run(tenant, callback);
|
|
80
|
+
}
|
|
81
|
+
function currentTenant() {
|
|
82
|
+
return tenantContext.getStore() ?? null;
|
|
83
|
+
}
|
|
84
|
+
function currentTenantId() {
|
|
85
|
+
return currentTenant()?.id ?? 1;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ../../src/core/tenant/tenantDatabaseScope.ts
|
|
89
|
+
async function applyTenantContextToTransaction(transaction, tenantId) {
|
|
90
|
+
await transaction.unsafe(`SELECT set_config('app.tenant_id', $1, true)`, [String(tenantId)]);
|
|
91
|
+
await transaction.unsafe(`SELECT set_config('app.bypass_rls', $1, true)`, ["false"]);
|
|
92
|
+
}
|
|
93
|
+
async function runWithTenantDatabase(tenant, callback) {
|
|
94
|
+
if (hasActiveDatabaseConnection()) {
|
|
95
|
+
const activeConnection2 = getActiveDatabaseConnection(getDefaultDatabasePool());
|
|
96
|
+
await applyTenantContextToTransaction(activeConnection2, tenant.id);
|
|
97
|
+
return await runWithTenant(tenant, callback);
|
|
98
|
+
}
|
|
99
|
+
return await getDefaultDatabasePool().begin(async (transaction) => {
|
|
100
|
+
await applyTenantContextToTransaction(transaction, tenant.id);
|
|
101
|
+
return await runWithDatabaseConnection(transaction, async () => {
|
|
102
|
+
return await runWithTenant(tenant, callback);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
function isInsideTenantDatabaseScope(tenantId = currentTenant()?.id) {
|
|
107
|
+
return hasActiveDatabaseConnection() && currentTenant()?.id === tenantId;
|
|
108
|
+
}
|
|
109
|
+
export {
|
|
110
|
+
runWithTenantDatabase,
|
|
111
|
+
isInsideTenantDatabaseScope,
|
|
112
|
+
applyTenantContextToTransaction
|
|
113
|
+
};
|
package/dist/entries/view.js
CHANGED
|
@@ -483,6 +483,14 @@ function appendWhereParts(tableName, where, params) {
|
|
|
483
483
|
}
|
|
484
484
|
return clauses.join(" AND ");
|
|
485
485
|
}
|
|
486
|
+
function buildWhereClause(tableName, where = {}) {
|
|
487
|
+
const params = [];
|
|
488
|
+
const body = appendWhereParts(tableName, where, params);
|
|
489
|
+
return {
|
|
490
|
+
clause: body.length > 0 ? ` WHERE ${body}` : "",
|
|
491
|
+
params
|
|
492
|
+
};
|
|
493
|
+
}
|
|
486
494
|
function buildWhereNodeClause(tableName, node, params) {
|
|
487
495
|
if ("where" in node) {
|
|
488
496
|
return appendWhereParts(tableName, node.where, params);
|
|
@@ -785,6 +793,30 @@ function buildDeleteByIdQuery(table, id) {
|
|
|
785
793
|
}
|
|
786
794
|
|
|
787
795
|
// ../../src/core/database/relationships.ts
|
|
796
|
+
function hasMany(definition) {
|
|
797
|
+
return {
|
|
798
|
+
type: "hasMany",
|
|
799
|
+
...definition
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
function hasOne(definition) {
|
|
803
|
+
return {
|
|
804
|
+
type: "hasOne",
|
|
805
|
+
...definition
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
function belongsTo(definition) {
|
|
809
|
+
return {
|
|
810
|
+
type: "belongsTo",
|
|
811
|
+
...definition
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
function belongsToMany(definition) {
|
|
815
|
+
return {
|
|
816
|
+
type: "belongsToMany",
|
|
817
|
+
...definition
|
|
818
|
+
};
|
|
819
|
+
}
|
|
788
820
|
function indexHasManyRelation(parents, children, relation) {
|
|
789
821
|
const groups = new Map;
|
|
790
822
|
for (const parent of parents) {
|
|
@@ -800,6 +832,15 @@ function indexHasManyRelation(parents, children, relation) {
|
|
|
800
832
|
}
|
|
801
833
|
return groups;
|
|
802
834
|
}
|
|
835
|
+
function indexHasOneRelation(parents, children, relation) {
|
|
836
|
+
const grouped = indexHasManyRelation(parents, children, relation);
|
|
837
|
+
const result = new Map;
|
|
838
|
+
for (const parent of parents) {
|
|
839
|
+
const matches = grouped.get(parent[relation.localKey]) ?? [];
|
|
840
|
+
result.set(parent[relation.localKey], matches[0]);
|
|
841
|
+
}
|
|
842
|
+
return result;
|
|
843
|
+
}
|
|
803
844
|
function indexBelongsToRelation(children, parents, relation) {
|
|
804
845
|
const parentsById = new Map;
|
|
805
846
|
for (const parent of parents) {
|
|
@@ -815,6 +856,45 @@ function indexBelongsToRelation(children, parents, relation) {
|
|
|
815
856
|
}
|
|
816
857
|
return result;
|
|
817
858
|
}
|
|
859
|
+
function indexBelongsToManyRelation(parents, pivotRows, relatedRows, relation) {
|
|
860
|
+
const relatedById = new Map;
|
|
861
|
+
for (const related of relatedRows) {
|
|
862
|
+
relatedById.set(related[relation.relatedKey], related);
|
|
863
|
+
}
|
|
864
|
+
const groups = new Map;
|
|
865
|
+
for (const parent of parents) {
|
|
866
|
+
groups.set(parent[relation.parentKey], []);
|
|
867
|
+
}
|
|
868
|
+
for (const pivot of pivotRows) {
|
|
869
|
+
const parentId = pivot[relation.foreignPivotKey];
|
|
870
|
+
const relatedId = pivot[relation.relatedPivotKey];
|
|
871
|
+
const group = groups.get(parentId);
|
|
872
|
+
const related = relatedById.get(relatedId);
|
|
873
|
+
if (!group || !related) {
|
|
874
|
+
continue;
|
|
875
|
+
}
|
|
876
|
+
group.push(related);
|
|
877
|
+
}
|
|
878
|
+
return groups;
|
|
879
|
+
}
|
|
880
|
+
function morphMany(definition) {
|
|
881
|
+
return {
|
|
882
|
+
type: "morphMany",
|
|
883
|
+
...definition
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
function morphOne(definition) {
|
|
887
|
+
return {
|
|
888
|
+
type: "morphOne",
|
|
889
|
+
...definition
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
function morphTo(definition) {
|
|
893
|
+
return {
|
|
894
|
+
type: "morphTo",
|
|
895
|
+
...definition
|
|
896
|
+
};
|
|
897
|
+
}
|
|
818
898
|
function indexMorphManyRelation(parents, children, relation) {
|
|
819
899
|
const groups = new Map;
|
|
820
900
|
for (const parent of parents) {
|
|
@@ -833,6 +913,15 @@ function indexMorphManyRelation(parents, children, relation) {
|
|
|
833
913
|
}
|
|
834
914
|
return groups;
|
|
835
915
|
}
|
|
916
|
+
function indexMorphOneRelation(parents, children, relation) {
|
|
917
|
+
const grouped = indexMorphManyRelation(parents, children, relation);
|
|
918
|
+
const result = new Map;
|
|
919
|
+
for (const parent of parents) {
|
|
920
|
+
const matches = grouped.get(parent[relation.localKey]) ?? [];
|
|
921
|
+
result.set(parent[relation.localKey], matches[0]);
|
|
922
|
+
}
|
|
923
|
+
return result;
|
|
924
|
+
}
|
|
836
925
|
function indexMorphToRelation(children, parentsByType, relation) {
|
|
837
926
|
const result = new Map;
|
|
838
927
|
for (const child of children) {
|
|
@@ -853,9 +942,15 @@ function indexMorphToRelation(children, parentsByType, relation) {
|
|
|
853
942
|
var boundConnectionHolder = {
|
|
854
943
|
connection: null
|
|
855
944
|
};
|
|
945
|
+
function bindDatabaseConnection(connection) {
|
|
946
|
+
boundConnectionHolder.connection = connection;
|
|
947
|
+
}
|
|
856
948
|
function getBoundDatabaseConnection() {
|
|
857
949
|
return boundConnectionHolder.connection;
|
|
858
950
|
}
|
|
951
|
+
function resetBoundDatabaseConnection() {
|
|
952
|
+
boundConnectionHolder.connection = null;
|
|
953
|
+
}
|
|
859
954
|
|
|
860
955
|
// ../../src/core/runtime/asyncContextStore.ts
|
|
861
956
|
import { AsyncLocalStorage } from "async_hooks";
|
|
@@ -873,9 +968,15 @@ function createAsyncContextStore(key) {
|
|
|
873
968
|
|
|
874
969
|
// ../../src/core/database/connectionContext.ts
|
|
875
970
|
var activeConnection = createAsyncContextStore("@getstrata/databaseConnectionContext");
|
|
971
|
+
function runWithDatabaseConnection(connection, callback) {
|
|
972
|
+
return activeConnection.run(connection, callback);
|
|
973
|
+
}
|
|
876
974
|
function getActiveDatabaseConnection(fallback) {
|
|
877
975
|
return activeConnection.getStore() ?? fallback;
|
|
878
976
|
}
|
|
977
|
+
function hasActiveDatabaseConnection() {
|
|
978
|
+
return activeConnection.getStore() !== undefined;
|
|
979
|
+
}
|
|
879
980
|
|
|
880
981
|
// ../../src/core/database/queryProxy.ts
|
|
881
982
|
var POOL_CONNECTION_METHODS = new Set(["begin", "close", "connect"]);
|
|
@@ -912,6 +1013,12 @@ function registerDefaultDatabasePool(connection) {
|
|
|
912
1013
|
defaultPool.connection = connection;
|
|
913
1014
|
defaultQuery.connection = createDatabaseQueryProxy(connection);
|
|
914
1015
|
}
|
|
1016
|
+
function getDefaultDatabasePool() {
|
|
1017
|
+
if (!defaultPool.connection) {
|
|
1018
|
+
throw new Error("Default database pool is not registered. Call registerDefaultDatabasePool() during app bootstrap.");
|
|
1019
|
+
}
|
|
1020
|
+
return defaultPool.connection;
|
|
1021
|
+
}
|
|
915
1022
|
function getDefaultDatabaseQuery() {
|
|
916
1023
|
if (!defaultQuery.connection) {
|
|
917
1024
|
throw new Error("Default database query handle is not registered. Call registerDefaultDatabasePool() during app bootstrap.");
|
|
@@ -1489,10 +1596,327 @@ class BaseRepository {
|
|
|
1489
1596
|
}
|
|
1490
1597
|
}
|
|
1491
1598
|
var baseRepository_default = BaseRepository;
|
|
1599
|
+
// ../../src/core/database/connection.ts
|
|
1600
|
+
function createDatabaseConnection(source) {
|
|
1601
|
+
return {
|
|
1602
|
+
async unsafe(query, params = []) {
|
|
1603
|
+
return await source.unsafe(query, params);
|
|
1604
|
+
}
|
|
1605
|
+
};
|
|
1606
|
+
}
|
|
1492
1607
|
// ../../src/core/database/model.ts
|
|
1493
1608
|
var modelRepositories = new WeakMap;
|
|
1494
1609
|
var modelGlobalScopes = new WeakMap;
|
|
1495
1610
|
var modelBooted = new WeakSet;
|
|
1611
|
+
function resolveModelRepository(model) {
|
|
1612
|
+
const repository = modelRepositories.get(model);
|
|
1613
|
+
if (!repository) {
|
|
1614
|
+
throw new Error(`${model.name}.repository() is not implemented.`);
|
|
1615
|
+
}
|
|
1616
|
+
return repository;
|
|
1617
|
+
}
|
|
1618
|
+
function modelStatics(model) {
|
|
1619
|
+
return model;
|
|
1620
|
+
}
|
|
1621
|
+
function ensureBooted(model) {
|
|
1622
|
+
if (modelBooted.has(model)) {
|
|
1623
|
+
return;
|
|
1624
|
+
}
|
|
1625
|
+
modelBooted.add(model);
|
|
1626
|
+
const boot = model.boot;
|
|
1627
|
+
if (typeof boot === "function") {
|
|
1628
|
+
boot.call(model);
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
function getGlobalScopes(model) {
|
|
1632
|
+
return modelGlobalScopes.get(model) ?? [];
|
|
1633
|
+
}
|
|
1634
|
+
function hydrateValue(value, cast) {
|
|
1635
|
+
if (value === null || value === undefined) {
|
|
1636
|
+
return value;
|
|
1637
|
+
}
|
|
1638
|
+
switch (cast) {
|
|
1639
|
+
case "date":
|
|
1640
|
+
case "datetime":
|
|
1641
|
+
return value instanceof Date ? value : new Date(String(value));
|
|
1642
|
+
case "json":
|
|
1643
|
+
return typeof value === "string" ? JSON.parse(value) : value;
|
|
1644
|
+
case "bool":
|
|
1645
|
+
case "boolean":
|
|
1646
|
+
return value === true || value === 1 || value === "1" || value === "true";
|
|
1647
|
+
default:
|
|
1648
|
+
return value;
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
function dehydrateValue(value, cast) {
|
|
1652
|
+
if (value === null || value === undefined) {
|
|
1653
|
+
return value;
|
|
1654
|
+
}
|
|
1655
|
+
switch (cast) {
|
|
1656
|
+
case "date":
|
|
1657
|
+
case "datetime":
|
|
1658
|
+
return value instanceof Date ? value : new Date(String(value));
|
|
1659
|
+
case "json":
|
|
1660
|
+
return typeof value === "string" ? value : JSON.stringify(value);
|
|
1661
|
+
case "bool":
|
|
1662
|
+
case "boolean":
|
|
1663
|
+
return Boolean(value);
|
|
1664
|
+
default:
|
|
1665
|
+
return value;
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
function filterMassAssignable(fillable, guarded, input) {
|
|
1669
|
+
const resolvedGuarded = guarded ?? true;
|
|
1670
|
+
if (fillable && fillable.length > 0) {
|
|
1671
|
+
const allowed = new Set(fillable);
|
|
1672
|
+
return Object.fromEntries(Object.entries(input).filter(([key]) => allowed.has(key)));
|
|
1673
|
+
}
|
|
1674
|
+
if (resolvedGuarded === true || resolvedGuarded.includes("*")) {
|
|
1675
|
+
return {};
|
|
1676
|
+
}
|
|
1677
|
+
const blocked = new Set(resolvedGuarded);
|
|
1678
|
+
return Object.fromEntries(Object.entries(input).filter(([key]) => !blocked.has(key)));
|
|
1679
|
+
}
|
|
1680
|
+
function applyCasts(values, casts, direction) {
|
|
1681
|
+
if (Object.keys(casts).length === 0) {
|
|
1682
|
+
return values;
|
|
1683
|
+
}
|
|
1684
|
+
const result = { ...values };
|
|
1685
|
+
const castFn = direction === "hydrate" ? hydrateValue : dehydrateValue;
|
|
1686
|
+
for (const [key, cast] of Object.entries(casts)) {
|
|
1687
|
+
if (key in result && cast) {
|
|
1688
|
+
result[key] = castFn(result[key], cast);
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
return result;
|
|
1692
|
+
}
|
|
1693
|
+
function applyTimestampsOnCreate(columns, values, enabled) {
|
|
1694
|
+
if (!enabled) {
|
|
1695
|
+
return values;
|
|
1696
|
+
}
|
|
1697
|
+
const now = new Date;
|
|
1698
|
+
const result = { ...values };
|
|
1699
|
+
if (columns.includes("created_at")) {
|
|
1700
|
+
result.created_at = now;
|
|
1701
|
+
}
|
|
1702
|
+
if (columns.includes("updated_at")) {
|
|
1703
|
+
result.updated_at = now;
|
|
1704
|
+
}
|
|
1705
|
+
return result;
|
|
1706
|
+
}
|
|
1707
|
+
function applyTimestampsOnUpdate(columns, values, enabled) {
|
|
1708
|
+
if (!enabled) {
|
|
1709
|
+
return values;
|
|
1710
|
+
}
|
|
1711
|
+
const result = { ...values };
|
|
1712
|
+
if (columns.includes("updated_at")) {
|
|
1713
|
+
result.updated_at = new Date;
|
|
1714
|
+
}
|
|
1715
|
+
return result;
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
class Model {
|
|
1719
|
+
attributes;
|
|
1720
|
+
repository;
|
|
1721
|
+
static $fillable;
|
|
1722
|
+
static $guarded;
|
|
1723
|
+
static $casts = {};
|
|
1724
|
+
static $timestamps = true;
|
|
1725
|
+
_exists;
|
|
1726
|
+
constructor(attributes, repository, exists = true) {
|
|
1727
|
+
this.attributes = attributes;
|
|
1728
|
+
this.repository = repository;
|
|
1729
|
+
this._exists = exists;
|
|
1730
|
+
}
|
|
1731
|
+
get $exists() {
|
|
1732
|
+
return this._exists;
|
|
1733
|
+
}
|
|
1734
|
+
get(key) {
|
|
1735
|
+
return this.attributes[key];
|
|
1736
|
+
}
|
|
1737
|
+
get id() {
|
|
1738
|
+
return this.attributes[this.primaryKey()];
|
|
1739
|
+
}
|
|
1740
|
+
toObject() {
|
|
1741
|
+
return { ...this.attributes };
|
|
1742
|
+
}
|
|
1743
|
+
primaryKey() {
|
|
1744
|
+
throw new Error(`${this.constructor.name}.primaryKey() is not implemented.`);
|
|
1745
|
+
}
|
|
1746
|
+
static primaryKeyField() {
|
|
1747
|
+
return resolveModelRepository(this).getTable().primaryKey;
|
|
1748
|
+
}
|
|
1749
|
+
static hydrateAttributes(attributes) {
|
|
1750
|
+
const casts = modelStatics(this).$casts ?? {};
|
|
1751
|
+
return applyCasts(attributes, casts, "hydrate");
|
|
1752
|
+
}
|
|
1753
|
+
static dehydrateAttributes(attributes) {
|
|
1754
|
+
const casts = modelStatics(this).$casts ?? {};
|
|
1755
|
+
return applyCasts(attributes, casts, "dehydrate");
|
|
1756
|
+
}
|
|
1757
|
+
static fromRecord(record, repository, exists = true) {
|
|
1758
|
+
const statics = modelStatics(this);
|
|
1759
|
+
const hydrated = statics.hydrateAttributes(record);
|
|
1760
|
+
return new statics(hydrated, repository, exists);
|
|
1761
|
+
}
|
|
1762
|
+
static boot() {}
|
|
1763
|
+
static addGlobalScope(_name, scope) {
|
|
1764
|
+
ensureBooted(this);
|
|
1765
|
+
const existing = modelGlobalScopes.get(this) ?? [];
|
|
1766
|
+
modelGlobalScopes.set(this, [
|
|
1767
|
+
...existing,
|
|
1768
|
+
scope
|
|
1769
|
+
]);
|
|
1770
|
+
}
|
|
1771
|
+
static repository() {
|
|
1772
|
+
return resolveModelRepository(this);
|
|
1773
|
+
}
|
|
1774
|
+
static query() {
|
|
1775
|
+
ensureBooted(this);
|
|
1776
|
+
const repository = resolveModelRepository(this);
|
|
1777
|
+
let query = repository.query();
|
|
1778
|
+
for (const scope of getGlobalScopes(this)) {
|
|
1779
|
+
query = scope(query);
|
|
1780
|
+
}
|
|
1781
|
+
return query;
|
|
1782
|
+
}
|
|
1783
|
+
static async create(attributes) {
|
|
1784
|
+
const statics = modelStatics(this);
|
|
1785
|
+
ensureBooted(this);
|
|
1786
|
+
const repository = resolveModelRepository(this);
|
|
1787
|
+
const table = repository.getTable();
|
|
1788
|
+
const timestamps = statics.$timestamps ?? true;
|
|
1789
|
+
const assignable = filterMassAssignable(statics.$fillable, statics.$guarded, attributes);
|
|
1790
|
+
const withTimestamps = applyTimestampsOnCreate(table.columns, assignable, timestamps);
|
|
1791
|
+
const payload = statics.dehydrateAttributes(withTimestamps);
|
|
1792
|
+
const record = await repository.create(payload);
|
|
1793
|
+
return statics.fromRecord(record, repository, true);
|
|
1794
|
+
}
|
|
1795
|
+
static async find(id) {
|
|
1796
|
+
const statics = modelStatics(this);
|
|
1797
|
+
const repository = resolveModelRepository(this);
|
|
1798
|
+
const primaryKey = repository.getTable().primaryKey;
|
|
1799
|
+
const record = await Model.query.call(this).where({ [primaryKey]: id }).first();
|
|
1800
|
+
return record ? statics.fromRecord(record, repository, true) : null;
|
|
1801
|
+
}
|
|
1802
|
+
static async findOrFail(id, errorFactory) {
|
|
1803
|
+
const model = await Model.find.call(this, id);
|
|
1804
|
+
if (model) {
|
|
1805
|
+
return model;
|
|
1806
|
+
}
|
|
1807
|
+
throw errorFactory?.(id) ?? new NotFoundError(`${this.name} ${String(id)} not found.`);
|
|
1808
|
+
}
|
|
1809
|
+
static async all(options = {}) {
|
|
1810
|
+
const statics = modelStatics(this);
|
|
1811
|
+
const repository = resolveModelRepository(this);
|
|
1812
|
+
let query = Model.query.call(this);
|
|
1813
|
+
if (options.orderBy) {
|
|
1814
|
+
query = query.orderBy(options.orderBy);
|
|
1815
|
+
}
|
|
1816
|
+
if (options.limit !== undefined) {
|
|
1817
|
+
query = query.limit(options.limit);
|
|
1818
|
+
}
|
|
1819
|
+
const rows = await query.get();
|
|
1820
|
+
return rows.map((row) => statics.fromRecord(row, repository, true));
|
|
1821
|
+
}
|
|
1822
|
+
static async firstWhere(where, options = {}) {
|
|
1823
|
+
const statics = modelStatics(this);
|
|
1824
|
+
const repository = resolveModelRepository(this);
|
|
1825
|
+
let query = Model.query.call(this).where(where);
|
|
1826
|
+
if (options.orderBy) {
|
|
1827
|
+
query = query.orderBy(options.orderBy);
|
|
1828
|
+
}
|
|
1829
|
+
const record = await query.first();
|
|
1830
|
+
return record ? statics.fromRecord(record, repository, true) : null;
|
|
1831
|
+
}
|
|
1832
|
+
async save() {
|
|
1833
|
+
const ModelClass = modelStatics(this.constructor);
|
|
1834
|
+
const timestamps = ModelClass.$timestamps ?? true;
|
|
1835
|
+
const casts = ModelClass.$casts ?? {};
|
|
1836
|
+
const table = this.repository.getTable();
|
|
1837
|
+
if (this.$exists) {
|
|
1838
|
+
const changes = applyTimestampsOnUpdate(table.columns, applyCasts(this.attributes, casts, "dehydrate"), timestamps);
|
|
1839
|
+
const record2 = await this.repository.updateByIdOrThrow(this.id, changes);
|
|
1840
|
+
this.attributes = ModelClass.hydrateAttributes(record2);
|
|
1841
|
+
return this;
|
|
1842
|
+
}
|
|
1843
|
+
const assignable = filterMassAssignable(ModelClass.$fillable, ModelClass.$guarded, this.attributes);
|
|
1844
|
+
const withTimestamps = applyTimestampsOnCreate(table.columns, assignable, timestamps);
|
|
1845
|
+
const payload = ModelClass.dehydrateAttributes(withTimestamps);
|
|
1846
|
+
const record = await this.repository.create(payload);
|
|
1847
|
+
this.attributes = ModelClass.hydrateAttributes(record);
|
|
1848
|
+
this._exists = true;
|
|
1849
|
+
return this;
|
|
1850
|
+
}
|
|
1851
|
+
async update(changes) {
|
|
1852
|
+
const ModelClass = modelStatics(this.constructor);
|
|
1853
|
+
const assignable = filterMassAssignable(ModelClass.$fillable, ModelClass.$guarded, changes);
|
|
1854
|
+
Object.assign(this.attributes, assignable);
|
|
1855
|
+
return await this.save();
|
|
1856
|
+
}
|
|
1857
|
+
async delete() {
|
|
1858
|
+
if (resolveSoftDeleteColumn(this.repository.getTable())) {
|
|
1859
|
+
return await this.repository.deleteById(this.id);
|
|
1860
|
+
}
|
|
1861
|
+
return await this.repository.forceDeleteById(this.id);
|
|
1862
|
+
}
|
|
1863
|
+
async forceDelete() {
|
|
1864
|
+
return await this.repository.forceDeleteById(this.id);
|
|
1865
|
+
}
|
|
1866
|
+
async restore() {
|
|
1867
|
+
const ModelClass = modelStatics(this.constructor);
|
|
1868
|
+
const record = await this.repository.restoreById(this.id);
|
|
1869
|
+
if (!record) {
|
|
1870
|
+
return null;
|
|
1871
|
+
}
|
|
1872
|
+
this.attributes = ModelClass.hydrateAttributes(record);
|
|
1873
|
+
return this;
|
|
1874
|
+
}
|
|
1875
|
+
async loadHasMany(as, relation, childRepository, options = {}) {
|
|
1876
|
+
const grouped = await childRepository.withConnection(this.repository.getConnection()).loadHasManyForParents([this.attributes], relation, options);
|
|
1877
|
+
const loaded = grouped.get(this.attributes[relation.localKey]) ?? [];
|
|
1878
|
+
return Object.assign(this, { [as]: loaded });
|
|
1879
|
+
}
|
|
1880
|
+
async loadHasOne(as, relation, childRepository, options = {}) {
|
|
1881
|
+
const loaded = await this.loadHasMany(as, relation, childRepository, { ...options, limit: 1 });
|
|
1882
|
+
const value = loaded[as]?.[0];
|
|
1883
|
+
return Object.assign(this, { [as]: value });
|
|
1884
|
+
}
|
|
1885
|
+
async loadBelongsTo(as, relation, parentRepository, options = {}) {
|
|
1886
|
+
const grouped = await this.repository.loadBelongsToForParents([this.attributes], relation, parentRepository, options);
|
|
1887
|
+
const loaded = grouped.get(this.attributes[relation.foreignKey]);
|
|
1888
|
+
return Object.assign(this, { [as]: loaded });
|
|
1889
|
+
}
|
|
1890
|
+
async loadBelongsToMany(as, relation, relatedRepository, options = {}) {
|
|
1891
|
+
const connection = this.repository.getConnection();
|
|
1892
|
+
const parentId = this.attributes[relation.parentKey];
|
|
1893
|
+
const pivotRows = await connection.unsafe(`SELECT * FROM ${relation.pivotTable} WHERE ${String(relation.foreignPivotKey)} = $1`, [parentId]);
|
|
1894
|
+
if (pivotRows.length === 0) {
|
|
1895
|
+
return Object.assign(this, { [as]: [] });
|
|
1896
|
+
}
|
|
1897
|
+
const relatedIds = [
|
|
1898
|
+
...new Set(pivotRows.map((row) => row[relation.relatedPivotKey]))
|
|
1899
|
+
];
|
|
1900
|
+
const relatedRows = await relatedRepository.withConnection(connection).findAll({
|
|
1901
|
+
...options,
|
|
1902
|
+
where: {
|
|
1903
|
+
[relation.relatedKey]: relatedIds
|
|
1904
|
+
}
|
|
1905
|
+
});
|
|
1906
|
+
const grouped = indexBelongsToManyRelation([this.attributes], pivotRows, relatedRows, relation);
|
|
1907
|
+
const loaded = grouped.get(parentId) ?? [];
|
|
1908
|
+
return Object.assign(this, { [as]: loaded });
|
|
1909
|
+
}
|
|
1910
|
+
mergeAttributes(patch) {
|
|
1911
|
+
Object.assign(this.attributes, patch);
|
|
1912
|
+
return this;
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
function registerModelRepository(model, repository) {
|
|
1916
|
+
modelRepositories.set(model, repository);
|
|
1917
|
+
ensureBooted(model);
|
|
1918
|
+
return model;
|
|
1919
|
+
}
|
|
1496
1920
|
// ../../src/core/database/schema/columnDefinition.ts
|
|
1497
1921
|
class ColumnDefinition {
|
|
1498
1922
|
name;
|
|
@@ -1714,6 +2138,45 @@ class Blueprint {
|
|
|
1714
2138
|
});
|
|
1715
2139
|
}
|
|
1716
2140
|
}
|
|
2141
|
+
// ../../src/core/database/schema/driver.ts
|
|
2142
|
+
function normalizeConnectionName(connection) {
|
|
2143
|
+
const normalized = connection.trim().toLowerCase();
|
|
2144
|
+
if (normalized === "pgsql" || normalized === "postgres" || normalized === "postgresql") {
|
|
2145
|
+
return "pgsql";
|
|
2146
|
+
}
|
|
2147
|
+
if (normalized === "mysql" || normalized === "mariadb") {
|
|
2148
|
+
return "mysql";
|
|
2149
|
+
}
|
|
2150
|
+
if (normalized === "sqlite") {
|
|
2151
|
+
return "sqlite";
|
|
2152
|
+
}
|
|
2153
|
+
throw new Error(`Unsupported DB_CONNECTION: ${connection}`);
|
|
2154
|
+
}
|
|
2155
|
+
function resolveDriverFromUrl(url) {
|
|
2156
|
+
const normalized = url.trim().toLowerCase();
|
|
2157
|
+
if (normalized.startsWith("postgres://") || normalized.startsWith("postgresql://") || normalized.startsWith("postgres:")) {
|
|
2158
|
+
return "pgsql";
|
|
2159
|
+
}
|
|
2160
|
+
if (normalized.startsWith("mysql://") || normalized.startsWith("mysql:")) {
|
|
2161
|
+
return "mysql";
|
|
2162
|
+
}
|
|
2163
|
+
if (normalized.startsWith("sqlite:")) {
|
|
2164
|
+
return "sqlite";
|
|
2165
|
+
}
|
|
2166
|
+
return null;
|
|
2167
|
+
}
|
|
2168
|
+
function resolveDatabaseDriver(options = {}) {
|
|
2169
|
+
const connection = options.connection ?? process.env.DB_CONNECTION;
|
|
2170
|
+
if (connection) {
|
|
2171
|
+
return normalizeConnectionName(connection);
|
|
2172
|
+
}
|
|
2173
|
+
const url = options.url ?? process.env.DATABASE_URL ?? "";
|
|
2174
|
+
const fromUrl = resolveDriverFromUrl(url);
|
|
2175
|
+
if (fromUrl) {
|
|
2176
|
+
return fromUrl;
|
|
2177
|
+
}
|
|
2178
|
+
return "pgsql";
|
|
2179
|
+
}
|
|
1717
2180
|
// ../../src/core/database/schema/errors.ts
|
|
1718
2181
|
class UnsupportedSchemaFeatureError extends Error {
|
|
1719
2182
|
constructor(feature, driver) {
|
|
@@ -2058,10 +2521,42 @@ class SchemaBuilder {
|
|
|
2058
2521
|
}
|
|
2059
2522
|
}
|
|
2060
2523
|
}
|
|
2524
|
+
|
|
2525
|
+
class Schema {
|
|
2526
|
+
static builder(driver) {
|
|
2527
|
+
return new SchemaBuilder(driver ?? resolveDatabaseDriver());
|
|
2528
|
+
}
|
|
2529
|
+
static async run(db, driver, callback) {
|
|
2530
|
+
const schema = Schema.builder(driver);
|
|
2531
|
+
await callback(schema);
|
|
2532
|
+
await schema.execute(db);
|
|
2533
|
+
}
|
|
2534
|
+
}
|
|
2535
|
+
function createSchemaBuilder(db, driver) {
|
|
2536
|
+
const builder = Schema.builder(driver);
|
|
2537
|
+
return Object.assign(builder, {
|
|
2538
|
+
async commit() {
|
|
2539
|
+
await builder.execute(db);
|
|
2540
|
+
}
|
|
2541
|
+
});
|
|
2542
|
+
}
|
|
2061
2543
|
// ../../src/core/database/table.ts
|
|
2062
2544
|
function defineTable(definition) {
|
|
2063
2545
|
return definition;
|
|
2064
2546
|
}
|
|
2547
|
+
// ../../src/core/database/transaction.ts
|
|
2548
|
+
function supportsTransactions(connection) {
|
|
2549
|
+
return typeof connection.begin === "function";
|
|
2550
|
+
}
|
|
2551
|
+
async function runInTransaction(operation) {
|
|
2552
|
+
const pool = resolveRepositoryConnection();
|
|
2553
|
+
if (!supportsTransactions(pool)) {
|
|
2554
|
+
throw new Error("Active database connection does not support transactions. Bind a client with begin() via bindDatabaseConnection.");
|
|
2555
|
+
}
|
|
2556
|
+
return await pool.begin(async (transaction) => {
|
|
2557
|
+
return await operation(createDatabaseConnection(transaction));
|
|
2558
|
+
});
|
|
2559
|
+
}
|
|
2065
2560
|
// ../../src/config/database.ts
|
|
2066
2561
|
function readInteger(name, fallback) {
|
|
2067
2562
|
const parsed = Number.parseInt(process.env[name] ?? String(fallback), 10);
|
|
@@ -2247,6 +2742,9 @@ function revealMfaSecret(stored) {
|
|
|
2247
2742
|
|
|
2248
2743
|
// ../../src/core/auth/authContext.ts
|
|
2249
2744
|
var authContext = createAsyncContextStore("@getstrata/authContext");
|
|
2745
|
+
function runWithAuthUser(user, callback) {
|
|
2746
|
+
return authContext.run(user, callback);
|
|
2747
|
+
}
|
|
2250
2748
|
function currentAuthUser() {
|
|
2251
2749
|
return authContext.getStore() ?? null;
|
|
2252
2750
|
}
|
|
@@ -2337,6 +2835,9 @@ function verifyTotp(secret, token, window = 1) {
|
|
|
2337
2835
|
|
|
2338
2836
|
// ../../src/core/tenant/tenantContext.ts
|
|
2339
2837
|
var tenantContext = createAsyncContextStore("@getstrata/tenantContext");
|
|
2838
|
+
function runWithTenant(tenant, callback) {
|
|
2839
|
+
return tenantContext.run(tenant, callback);
|
|
2840
|
+
}
|
|
2340
2841
|
function currentTenant() {
|
|
2341
2842
|
return tenantContext.getStore() ?? null;
|
|
2342
2843
|
}
|