@getstrata/core 0.5.43 → 0.5.45
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/entries/admin/types.js +1 -0
- package/dist/entries/auth/abilityChecker.js +1 -0
- package/dist/entries/auth/membershipMiddleware.js +298 -0
- package/dist/entries/auth/scimAuthMiddleware.js +10 -0
- package/dist/entries/auth/sessionGuard.js +14 -0
- package/dist/entries/database/bindConnection.js +1 -22
- package/dist/entries/database/boundConnection.js +1 -19
- package/dist/entries/database/connection.js +1 -12
- package/dist/entries/database/defaultConnection.js +1 -0
- package/dist/entries/database/migrations/types.js +1 -0
- package/dist/entries/database/migrations.js +127 -0
- package/dist/entries/database/repositoryConnection.js +1 -0
- package/dist/entries/database/seeders/types.js +1 -0
- package/dist/entries/http/conditionalResponse.js +192 -0
- package/dist/entries/http/corsMiddleware.js +54 -0
- package/dist/entries/http/csrfMiddleware.js +236 -0
- package/dist/entries/http/csrfToken.js +3 -0
- package/dist/entries/http/flashMiddleware.js +143 -0
- package/dist/entries/http/formRequest.js +158 -0
- package/dist/entries/http/loginThrottleMiddleware.js +46 -0
- package/dist/entries/http/memoryThrottleMiddleware.js +30 -0
- package/dist/entries/http/pagination.js +185 -0
- package/dist/entries/http/requireAbilityMiddleware.js +110 -0
- package/dist/entries/http/requireAuthMiddleware.js +80 -0
- package/dist/entries/http/requireGlobalAdminMiddleware.js +147 -0
- package/dist/entries/http/requireWebAuthMiddleware.js +107 -0
- package/dist/entries/http/route.js +8 -0
- package/dist/entries/http/routeMiddleware.js +32 -0
- package/dist/entries/http/routeModelBinding.js +147 -0
- package/dist/entries/http/scimThrottleMiddleware.js +23 -0
- package/dist/entries/http/securedRouteModelBinding.js +16 -0
- package/dist/entries/http/securityHeadersMiddleware.js +77 -0
- package/dist/entries/http/throttleMiddleware.js +87 -0
- package/dist/entries/http/webErrorResponse.js +14 -0
- package/dist/entries/http/webFormRequest.js +16 -0
- package/dist/entries/logging/requestLoggingMiddleware.js +89 -0
- package/dist/entries/tenant/tenantDatabaseScope.js +10 -0
- package/dist/entries/tracing/tracingMiddleware.js +103 -0
- package/dist/entries/view.js +14 -0
- package/package.json +147 -7
|
@@ -0,0 +1 @@
|
|
|
1
|
+
// @bun
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
// @bun
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// ../../src/config/database.ts
|
|
3
|
+
function readInteger(name, fallback) {
|
|
4
|
+
const parsed = Number.parseInt(process.env[name] ?? String(fallback), 10);
|
|
5
|
+
return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback;
|
|
6
|
+
}
|
|
7
|
+
var databaseConfig = {
|
|
8
|
+
url: process.env.DATABASE_URL ?? "",
|
|
9
|
+
poolMax: readInteger("DB_POOL_MAX", 10),
|
|
10
|
+
idleTimeoutSeconds: readInteger("DB_POOL_IDLE_TIMEOUT", 30),
|
|
11
|
+
maxLifetimeSeconds: readInteger("DB_POOL_MAX_LIFETIME", 3600),
|
|
12
|
+
connectionTimeoutSeconds: readInteger("DB_CONNECTION_TIMEOUT", 10)
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
// ../../src/core/runtime/asyncContextStore.ts
|
|
16
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
17
|
+
function createAsyncContextStore(key) {
|
|
18
|
+
const symbol = Symbol.for(key);
|
|
19
|
+
const globalRecord = globalThis;
|
|
20
|
+
const existing = globalRecord[symbol];
|
|
21
|
+
if (existing) {
|
|
22
|
+
return existing;
|
|
23
|
+
}
|
|
24
|
+
const store = new AsyncLocalStorage;
|
|
25
|
+
globalRecord[symbol] = store;
|
|
26
|
+
return store;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ../../src/core/database/connectionContext.ts
|
|
30
|
+
var activeConnection = createAsyncContextStore("@getstrata/databaseConnectionContext");
|
|
31
|
+
function runWithDatabaseConnection(connection, callback) {
|
|
32
|
+
return activeConnection.run(connection, callback);
|
|
33
|
+
}
|
|
34
|
+
function getActiveDatabaseConnection(fallback) {
|
|
35
|
+
return activeConnection.getStore() ?? fallback;
|
|
36
|
+
}
|
|
37
|
+
function hasActiveDatabaseConnection() {
|
|
38
|
+
return activeConnection.getStore() !== undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ../../src/core/database/queryProxy.ts
|
|
42
|
+
var POOL_CONNECTION_METHODS = new Set(["begin", "close", "connect"]);
|
|
43
|
+
function createDatabaseQueryProxy(pool) {
|
|
44
|
+
function resolveDatabase() {
|
|
45
|
+
return getActiveDatabaseConnection(pool);
|
|
46
|
+
}
|
|
47
|
+
function resolveDatabaseForProperty(property) {
|
|
48
|
+
if (typeof property === "string" && POOL_CONNECTION_METHODS.has(property)) {
|
|
49
|
+
return pool;
|
|
50
|
+
}
|
|
51
|
+
return resolveDatabase();
|
|
52
|
+
}
|
|
53
|
+
return new Proxy(function database() {}, {
|
|
54
|
+
apply(_target, _thisArg, args) {
|
|
55
|
+
return resolveDatabase()(...args);
|
|
56
|
+
},
|
|
57
|
+
get(_target, property) {
|
|
58
|
+
const connection = resolveDatabaseForProperty(property);
|
|
59
|
+
const value = connection[property];
|
|
60
|
+
return typeof value === "function" ? value.bind(connection) : value;
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ../../src/core/database/defaultConnection.ts
|
|
66
|
+
var defaultPool = {
|
|
67
|
+
connection: null
|
|
68
|
+
};
|
|
69
|
+
var defaultQuery = {
|
|
70
|
+
connection: null
|
|
71
|
+
};
|
|
72
|
+
function registerDefaultDatabasePool(connection) {
|
|
73
|
+
defaultPool.connection = connection;
|
|
74
|
+
defaultQuery.connection = createDatabaseQueryProxy(connection);
|
|
75
|
+
}
|
|
76
|
+
function getDefaultDatabasePool() {
|
|
77
|
+
if (!defaultPool.connection) {
|
|
78
|
+
throw new Error("Default database pool is not registered. Call registerDefaultDatabasePool() during app bootstrap.");
|
|
79
|
+
}
|
|
80
|
+
return defaultPool.connection;
|
|
81
|
+
}
|
|
82
|
+
function getDefaultDatabaseQuery() {
|
|
83
|
+
if (!defaultQuery.connection) {
|
|
84
|
+
throw new Error("Default database query handle is not registered. Call registerDefaultDatabasePool() during app bootstrap.");
|
|
85
|
+
}
|
|
86
|
+
return defaultQuery.connection;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ../../src/db/connection/createConnection.ts
|
|
90
|
+
var {SQL } = globalThis.Bun;
|
|
91
|
+
function createDatabaseConnection(config) {
|
|
92
|
+
if (!config.url) {
|
|
93
|
+
throw new Error("DATABASE_URL is not configured. Set DATABASE_URL before starting the app or running integration tests.");
|
|
94
|
+
}
|
|
95
|
+
return new SQL({
|
|
96
|
+
url: config.url,
|
|
97
|
+
max: config.poolMax,
|
|
98
|
+
idleTimeout: config.idleTimeoutSeconds,
|
|
99
|
+
maxLifetime: config.maxLifetimeSeconds,
|
|
100
|
+
connectionTimeout: config.connectionTimeoutSeconds
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ../../src/db/connection/index.ts
|
|
105
|
+
var connectionHolder = {
|
|
106
|
+
connection: null
|
|
107
|
+
};
|
|
108
|
+
function getDatabase() {
|
|
109
|
+
if (!connectionHolder.connection) {
|
|
110
|
+
connectionHolder.connection = createDatabaseConnection(databaseConfig);
|
|
111
|
+
registerDefaultDatabasePool(connectionHolder.connection);
|
|
112
|
+
}
|
|
113
|
+
return connectionHolder.connection;
|
|
114
|
+
}
|
|
115
|
+
function getDb() {
|
|
116
|
+
getDatabase();
|
|
117
|
+
return getDefaultDatabaseQuery();
|
|
118
|
+
}
|
|
119
|
+
var db = new Proxy(function database() {}, {
|
|
120
|
+
apply(_target, _thisArg, args) {
|
|
121
|
+
return getDb()(...args);
|
|
122
|
+
},
|
|
123
|
+
get(_target, property) {
|
|
124
|
+
const connection = getDb();
|
|
125
|
+
const value = connection[property];
|
|
126
|
+
return typeof value === "function" ? value.bind(connection) : value;
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
var connection_default = db;
|
|
130
|
+
|
|
131
|
+
// ../../src/modules/organization/memberRepository.ts
|
|
132
|
+
class OrganizationMemberRepository {
|
|
133
|
+
constructor() {}
|
|
134
|
+
async findMembership(userId, organizationId) {
|
|
135
|
+
const rows = await connection_default`
|
|
136
|
+
SELECT id, organization_id, user_id, role, created_at
|
|
137
|
+
FROM organization_member
|
|
138
|
+
WHERE user_id = ${userId} AND organization_id = ${organizationId}
|
|
139
|
+
LIMIT 1
|
|
140
|
+
`;
|
|
141
|
+
return rows[0] ?? null;
|
|
142
|
+
}
|
|
143
|
+
async listForUser(userId) {
|
|
144
|
+
return await connection_default`
|
|
145
|
+
SELECT id, organization_id, user_id, role, created_at
|
|
146
|
+
FROM organization_member
|
|
147
|
+
WHERE user_id = ${userId}
|
|
148
|
+
ORDER BY organization_id
|
|
149
|
+
`;
|
|
150
|
+
}
|
|
151
|
+
async listForOrganization(organizationId) {
|
|
152
|
+
return await connection_default`
|
|
153
|
+
SELECT id, organization_id, user_id, role, created_at
|
|
154
|
+
FROM organization_member
|
|
155
|
+
WHERE organization_id = ${organizationId}
|
|
156
|
+
ORDER BY id
|
|
157
|
+
`;
|
|
158
|
+
}
|
|
159
|
+
async addMember(input) {
|
|
160
|
+
const rows = await connection_default`
|
|
161
|
+
INSERT INTO organization_member (organization_id, user_id, role)
|
|
162
|
+
VALUES (${input.organizationId}, ${input.userId}, ${input.role ?? "member"})
|
|
163
|
+
RETURNING id, organization_id, user_id, role, created_at
|
|
164
|
+
`;
|
|
165
|
+
const row = rows[0];
|
|
166
|
+
if (!row) {
|
|
167
|
+
throw new Error("Organization member insert did not return a row.");
|
|
168
|
+
}
|
|
169
|
+
return row;
|
|
170
|
+
}
|
|
171
|
+
async removeMember(organizationId, userId) {
|
|
172
|
+
const rows = await connection_default`
|
|
173
|
+
DELETE FROM organization_member
|
|
174
|
+
WHERE organization_id = ${organizationId} AND user_id = ${userId}
|
|
175
|
+
RETURNING id
|
|
176
|
+
`;
|
|
177
|
+
return rows.length > 0;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
var memberRepository_default = OrganizationMemberRepository;
|
|
181
|
+
|
|
182
|
+
// ../../src/core/errors/http.ts
|
|
183
|
+
class HttpError extends Error {
|
|
184
|
+
status;
|
|
185
|
+
details;
|
|
186
|
+
constructor(status, message, details) {
|
|
187
|
+
super(message);
|
|
188
|
+
this.name = new.target.name;
|
|
189
|
+
this.status = status;
|
|
190
|
+
this.details = details;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
class BadRequestError extends HttpError {
|
|
195
|
+
constructor(message = "Bad Request", details) {
|
|
196
|
+
super(400, message, details);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
class NotFoundError extends HttpError {
|
|
201
|
+
constructor(message = "Not Found", details) {
|
|
202
|
+
super(404, message, details);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
class ConflictError extends HttpError {
|
|
207
|
+
constructor(message = "Conflict", details) {
|
|
208
|
+
super(409, message, details);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
class UnprocessableEntityError extends HttpError {
|
|
213
|
+
constructor(message = "Unprocessable Entity", details) {
|
|
214
|
+
super(422, message, details);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
class ValidationError extends HttpError {
|
|
219
|
+
constructor(message = "Validation failed", details) {
|
|
220
|
+
super(422, message, details);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
class ForbiddenError extends HttpError {
|
|
225
|
+
constructor(message = "Forbidden", details) {
|
|
226
|
+
super(403, message, details);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
class UnauthorizedError extends HttpError {
|
|
231
|
+
constructor(message = "Unauthorized", details) {
|
|
232
|
+
super(401, message, details);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
class PayloadTooLargeError extends HttpError {
|
|
237
|
+
constructor(message = "Payload Too Large", details) {
|
|
238
|
+
super(413, message, details);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
class PreconditionFailedError extends HttpError {
|
|
243
|
+
constructor(message = "Precondition Failed", details) {
|
|
244
|
+
super(412, message, details);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ../../src/core/auth/authContext.ts
|
|
249
|
+
var authContext = createAsyncContextStore("@getstrata/authContext");
|
|
250
|
+
function runWithAuthUser(user, callback) {
|
|
251
|
+
return authContext.run(user, callback);
|
|
252
|
+
}
|
|
253
|
+
function currentAuthUser() {
|
|
254
|
+
return authContext.getStore() ?? null;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// ../../src/core/auth/accessControl.ts
|
|
258
|
+
function isGlobalAdmin(user) {
|
|
259
|
+
return user?.role === "admin";
|
|
260
|
+
}
|
|
261
|
+
function resolveUserId(user) {
|
|
262
|
+
const userId = typeof user.id === "number" ? user.id : Number(user.id);
|
|
263
|
+
if (!Number.isInteger(userId) || userId <= 0) {
|
|
264
|
+
throw new ForbiddenError("Invalid authenticated user.");
|
|
265
|
+
}
|
|
266
|
+
return userId;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ../../src/core/auth/membershipContext.ts
|
|
270
|
+
var membershipContext = createAsyncContextStore("@getstrata/membershipContext");
|
|
271
|
+
var membershipRepository = new memberRepository_default;
|
|
272
|
+
async function runWithMembershipContext(callback) {
|
|
273
|
+
const user = currentAuthUser();
|
|
274
|
+
if (!user || isGlobalAdmin(user)) {
|
|
275
|
+
return await callback();
|
|
276
|
+
}
|
|
277
|
+
const memberships = await membershipRepository.listForUser(resolveUserId(user));
|
|
278
|
+
const context = {
|
|
279
|
+
organizationIds: memberships.map((membership) => membership.organization_id),
|
|
280
|
+
rolesByOrganizationId: new Map(memberships.map((membership) => [membership.organization_id, membership.role]))
|
|
281
|
+
};
|
|
282
|
+
return await membershipContext.run(context, callback);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// ../../src/core/auth/membershipContextMiddleware.ts
|
|
286
|
+
function createMembershipContextMiddleware() {
|
|
287
|
+
return async (_request, next) => {
|
|
288
|
+
return await runWithMembershipContext(async () => await next());
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ../../src/core/auth/membershipMiddleware.ts
|
|
293
|
+
function createMembershipMiddleware() {
|
|
294
|
+
return createMembershipContextMiddleware();
|
|
295
|
+
}
|
|
296
|
+
export {
|
|
297
|
+
createMembershipMiddleware
|
|
298
|
+
};
|
|
@@ -177,6 +177,16 @@ function currentTenant() {
|
|
|
177
177
|
function currentTenantId() {
|
|
178
178
|
return currentTenant()?.id ?? 1;
|
|
179
179
|
}
|
|
180
|
+
function rateLimitMultiplierForPlan(plan) {
|
|
181
|
+
switch (plan) {
|
|
182
|
+
case "enterprise":
|
|
183
|
+
return 4;
|
|
184
|
+
case "pro":
|
|
185
|
+
return 2;
|
|
186
|
+
default:
|
|
187
|
+
return 1;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
180
190
|
|
|
181
191
|
// ../../src/core/tenant/tenantDatabaseScope.ts
|
|
182
192
|
async function applyTenantContextToTransaction(transaction, tenantId) {
|
|
@@ -2603,6 +2603,7 @@ var db = new Proxy(function database() {}, {
|
|
|
2603
2603
|
return typeof value === "function" ? value.bind(connection) : value;
|
|
2604
2604
|
}
|
|
2605
2605
|
});
|
|
2606
|
+
var connection_default = db;
|
|
2606
2607
|
|
|
2607
2608
|
// ../../src/modules/user/apiTokenTable.ts
|
|
2608
2609
|
var apiTokenTable = defineTable({
|
|
@@ -2744,6 +2745,9 @@ function currentAuthUser() {
|
|
|
2744
2745
|
|
|
2745
2746
|
// ../../src/core/http/requestMetaContext.ts
|
|
2746
2747
|
var requestMetaContext = createAsyncContextStore("@getstrata/requestMetaContext");
|
|
2748
|
+
function runWithRequestMeta(meta, callback) {
|
|
2749
|
+
return requestMetaContext.run(meta, callback);
|
|
2750
|
+
}
|
|
2747
2751
|
function currentRequestMeta() {
|
|
2748
2752
|
return requestMetaContext.getStore() ?? {
|
|
2749
2753
|
ipAddress: null,
|
|
@@ -2837,6 +2841,16 @@ function currentTenant() {
|
|
|
2837
2841
|
function currentTenantId() {
|
|
2838
2842
|
return currentTenant()?.id ?? 1;
|
|
2839
2843
|
}
|
|
2844
|
+
function rateLimitMultiplierForPlan(plan) {
|
|
2845
|
+
switch (plan) {
|
|
2846
|
+
case "enterprise":
|
|
2847
|
+
return 4;
|
|
2848
|
+
case "pro":
|
|
2849
|
+
return 2;
|
|
2850
|
+
default:
|
|
2851
|
+
return 1;
|
|
2852
|
+
}
|
|
2853
|
+
}
|
|
2840
2854
|
|
|
2841
2855
|
// ../../src/modules/user/authService.ts
|
|
2842
2856
|
class AuthService {
|
|
@@ -1,22 +1 @@
|
|
|
1
|
-
|
|
2
|
-
// ../../src/core/database/boundConnection.ts
|
|
3
|
-
var boundConnectionHolder = {
|
|
4
|
-
connection: null
|
|
5
|
-
};
|
|
6
|
-
function bindDatabaseConnection(connection) {
|
|
7
|
-
boundConnectionHolder.connection = connection;
|
|
8
|
-
}
|
|
9
|
-
function getBoundDatabaseConnection() {
|
|
10
|
-
return boundConnectionHolder.connection;
|
|
11
|
-
}
|
|
12
|
-
function resetBoundDatabaseConnection() {
|
|
13
|
-
boundConnectionHolder.connection = null;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
// ../../src/core/database/bindConnection.ts
|
|
17
|
-
function bindDatabaseConnection2(connection) {
|
|
18
|
-
bindDatabaseConnection(connection);
|
|
19
|
-
}
|
|
20
|
-
export {
|
|
21
|
-
bindDatabaseConnection2 as bindDatabaseConnection
|
|
22
|
-
};
|
|
1
|
+
export * from "../../index.js";
|
|
@@ -1,19 +1 @@
|
|
|
1
|
-
|
|
2
|
-
// ../../src/core/database/boundConnection.ts
|
|
3
|
-
var boundConnectionHolder = {
|
|
4
|
-
connection: null
|
|
5
|
-
};
|
|
6
|
-
function bindDatabaseConnection(connection) {
|
|
7
|
-
boundConnectionHolder.connection = connection;
|
|
8
|
-
}
|
|
9
|
-
function getBoundDatabaseConnection() {
|
|
10
|
-
return boundConnectionHolder.connection;
|
|
11
|
-
}
|
|
12
|
-
function resetBoundDatabaseConnection() {
|
|
13
|
-
boundConnectionHolder.connection = null;
|
|
14
|
-
}
|
|
15
|
-
export {
|
|
16
|
-
resetBoundDatabaseConnection,
|
|
17
|
-
getBoundDatabaseConnection,
|
|
18
|
-
bindDatabaseConnection
|
|
19
|
-
};
|
|
1
|
+
export * from "../../index.js";
|
|
@@ -1,12 +1 @@
|
|
|
1
|
-
|
|
2
|
-
// ../../src/core/database/connection.ts
|
|
3
|
-
function createDatabaseConnection(source) {
|
|
4
|
-
return {
|
|
5
|
-
async unsafe(query, params = []) {
|
|
6
|
-
return await source.unsafe(query, params);
|
|
7
|
-
}
|
|
8
|
-
};
|
|
9
|
-
}
|
|
10
|
-
export {
|
|
11
|
-
createDatabaseConnection
|
|
12
|
-
};
|
|
1
|
+
export * from "../../index.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../../index.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
// @bun
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// ../../src/core/database/migrations/runner.ts
|
|
3
|
+
import { readdir } from "fs/promises";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
import { pathToFileURL } from "url";
|
|
6
|
+
|
|
7
|
+
// ../../src/core/database/migrations/advisoryLock.ts
|
|
8
|
+
var MIGRATION_LOCK_KEY = 42424242;
|
|
9
|
+
async function withMigrationLock(db, callback, lockKey = MIGRATION_LOCK_KEY) {
|
|
10
|
+
await db.unsafe("SELECT pg_advisory_lock($1)", [lockKey]);
|
|
11
|
+
try {
|
|
12
|
+
return await callback();
|
|
13
|
+
} finally {
|
|
14
|
+
await db.unsafe("SELECT pg_advisory_unlock($1)", [lockKey]);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// ../../src/core/database/migrations/runner.ts
|
|
19
|
+
var MIGRATIONS_TABLE = "framework_migrations";
|
|
20
|
+
async function ensureMigrationsTable(db) {
|
|
21
|
+
await db.unsafe(`
|
|
22
|
+
CREATE TABLE IF NOT EXISTS ${MIGRATIONS_TABLE} (
|
|
23
|
+
name TEXT PRIMARY KEY,
|
|
24
|
+
batch INTEGER NOT NULL,
|
|
25
|
+
run_on TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
26
|
+
)
|
|
27
|
+
`);
|
|
28
|
+
}
|
|
29
|
+
async function getAppliedMigrations(db) {
|
|
30
|
+
await ensureMigrationsTable(db);
|
|
31
|
+
return await db.unsafe(`
|
|
32
|
+
SELECT name, batch
|
|
33
|
+
FROM ${MIGRATIONS_TABLE}
|
|
34
|
+
ORDER BY batch ASC, name ASC
|
|
35
|
+
`);
|
|
36
|
+
}
|
|
37
|
+
async function loadMigrationsFromDirectory(directory) {
|
|
38
|
+
const entries = await readdir(directory);
|
|
39
|
+
const migrationFiles = entries.filter((entry) => (entry.endsWith(".ts") || entry.endsWith(".js")) && entry !== "types.ts" && entry !== "index.ts" && entry !== "runner.ts").sort();
|
|
40
|
+
const loadedMigrations = await Promise.all(migrationFiles.map(async (fileName) => {
|
|
41
|
+
const moduleUrl = pathToFileURL(join(directory, fileName)).href;
|
|
42
|
+
const module = await import(moduleUrl);
|
|
43
|
+
return module.default;
|
|
44
|
+
}));
|
|
45
|
+
return loadedMigrations.filter((migration) => migration?.name !== undefined);
|
|
46
|
+
}
|
|
47
|
+
async function getMigrationStatus(db, migrations) {
|
|
48
|
+
const applied = await getAppliedMigrations(db);
|
|
49
|
+
const appliedByName = new Map(applied.map(({ name, batch }) => [name, Number(batch)]));
|
|
50
|
+
return migrations.map(({ name }) => ({
|
|
51
|
+
name,
|
|
52
|
+
status: appliedByName.has(name) ? "up" : "pending",
|
|
53
|
+
batch: appliedByName.get(name) ?? null
|
|
54
|
+
}));
|
|
55
|
+
}
|
|
56
|
+
async function runPendingMigrations(db, migrations, options = {}) {
|
|
57
|
+
const applied = await getAppliedMigrations(db);
|
|
58
|
+
const appliedNames = new Set(applied.map(({ name }) => name));
|
|
59
|
+
const nextBatch = applied.reduce((currentMax, { batch }) => Math.max(currentMax, Number(batch)), 0) + 1;
|
|
60
|
+
const pendingMigrations = migrations.filter(({ name }) => !appliedNames.has(name));
|
|
61
|
+
for (const migration of pendingMigrations) {
|
|
62
|
+
options.onMigration?.(migration.name);
|
|
63
|
+
await migration.up(db);
|
|
64
|
+
const inserted = await db.unsafe(`INSERT INTO ${MIGRATIONS_TABLE} (name, batch) VALUES ($1, $2) ON CONFLICT (name) DO NOTHING RETURNING name`, [migration.name, nextBatch]);
|
|
65
|
+
if (inserted.length === 0) {
|
|
66
|
+
throw new Error(`Migration ${migration.name} was applied but not recorded.`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return pendingMigrations.length;
|
|
70
|
+
}
|
|
71
|
+
async function migrateDatabase(db, migrations, options = {}) {
|
|
72
|
+
const { advisoryLock = false, onMigration } = options;
|
|
73
|
+
if (advisoryLock) {
|
|
74
|
+
return withMigrationLock(db, () => runPendingMigrations(db, migrations, { onMigration }));
|
|
75
|
+
}
|
|
76
|
+
return runPendingMigrations(db, migrations, { onMigration });
|
|
77
|
+
}
|
|
78
|
+
async function rollbackDatabase(db, migrations, options = {}) {
|
|
79
|
+
const applied = await getAppliedMigrations(db);
|
|
80
|
+
if (applied.length === 0) {
|
|
81
|
+
return 0;
|
|
82
|
+
}
|
|
83
|
+
const lastBatch = applied.reduce((currentMax, { batch }) => Math.max(currentMax, Number(batch)), 0);
|
|
84
|
+
const migrationsToRollback = new Set(applied.filter(({ batch }) => Number(batch) === lastBatch).map(({ name }) => name));
|
|
85
|
+
let rolledBack = 0;
|
|
86
|
+
for (const migration of [...migrations].reverse()) {
|
|
87
|
+
if (!migrationsToRollback.has(migration.name)) {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
options.onMigration?.(migration.name);
|
|
91
|
+
await migration.down(db);
|
|
92
|
+
await db.unsafe(`DELETE FROM ${MIGRATIONS_TABLE} WHERE name = $1`, [migration.name]);
|
|
93
|
+
rolledBack += 1;
|
|
94
|
+
}
|
|
95
|
+
return rolledBack;
|
|
96
|
+
}
|
|
97
|
+
async function freshDatabase(db, migrations, options = {}) {
|
|
98
|
+
const runFresh = async () => {
|
|
99
|
+
const applied = await getAppliedMigrations(db);
|
|
100
|
+
const appliedNames = new Set(applied.map(({ name }) => name));
|
|
101
|
+
const appliedMigrations = migrations.filter(({ name }) => appliedNames.has(name));
|
|
102
|
+
for (const migration of [...appliedMigrations].reverse()) {
|
|
103
|
+
options.onMigration?.(migration.name);
|
|
104
|
+
await migration.down(db);
|
|
105
|
+
}
|
|
106
|
+
if (appliedMigrations.length > 0) {
|
|
107
|
+
await db.unsafe(`DELETE FROM ${MIGRATIONS_TABLE}`);
|
|
108
|
+
}
|
|
109
|
+
await runPendingMigrations(db, migrations, options);
|
|
110
|
+
};
|
|
111
|
+
if (options.advisoryLock) {
|
|
112
|
+
await withMigrationLock(db, runFresh);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
await runFresh();
|
|
116
|
+
}
|
|
117
|
+
export {
|
|
118
|
+
withMigrationLock,
|
|
119
|
+
runPendingMigrations,
|
|
120
|
+
rollbackDatabase,
|
|
121
|
+
migrateDatabase,
|
|
122
|
+
loadMigrationsFromDirectory,
|
|
123
|
+
getMigrationStatus,
|
|
124
|
+
getAppliedMigrations,
|
|
125
|
+
freshDatabase,
|
|
126
|
+
ensureMigrationsTable
|
|
127
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../../index.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
// @bun
|