@getstrata/bootstrap 0.2.26 → 0.2.29
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/bootstrap/http/securedRouteModelBinding.d.ts +2 -2
- package/dist/bootstrap/listeners/invalidateCacheOnModelWrite.d.ts +1 -1
- package/dist/bootstrap/schedule.d.ts +2 -1
- package/dist/bootstrap/web/forms.d.ts +1 -1
- package/dist/entries/applicationRegistry.js +2 -0
- package/dist/entries/buildModuleRoutes.js +5 -0
- package/dist/entries/buildWebModuleRoutes.js +5 -0
- package/dist/entries/cache/modelCacheTags.js +2 -0
- package/dist/entries/config.js +2 -0
- package/dist/entries/context.js +99 -3156
- package/dist/entries/contracts.js +2 -0
- package/dist/entries/createRoutes.js +1468 -0
- package/dist/entries/createSpaRoutes.js +64 -0
- package/dist/entries/createWebRoutes.js +5 -0
- package/dist/entries/dependencies.js +99 -3156
- package/dist/entries/discoverModules.js +2 -0
- package/dist/entries/health.js +215 -0
- package/dist/entries/http/securedRouteModelBinding.js +6 -293
- package/dist/entries/httpKernel.js +5 -0
- package/dist/entries/listeners/invalidateCacheOnModelWrite.js +120 -0
- package/dist/entries/membershipService.js +2 -0
- package/dist/entries/metricsRoutes.js +18 -0
- package/dist/entries/providers/view.js +8 -603
- package/dist/entries/providers.js +94 -3158
- package/dist/entries/queue/defaultJobs.js +7 -465
- package/dist/entries/routeRegistry.js +2 -0
- package/dist/entries/schedule.js +49 -0
- package/dist/entries/secretsGuard.js +9 -0
- package/dist/entries/web/forms.js +4 -18
- package/dist/entries/web/routing.js +21 -304
- package/dist/entries/web/server.js +2 -0
- package/dist/entries/web/session.js +3 -26
- package/dist/entries/web/slug.js +2 -0
- package/dist/index-sfreg6q3.js +0 -0
- package/dist/index.js +176 -3543
- package/package.json +32 -2
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// ../../src/bootstrap/public-api.ts
|
|
3
|
-
import { appSchedule as
|
|
3
|
+
import { appSchedule as appSchedule3, runDueScheduledTasks as runDueScheduledTasks2, Schedule as Schedule2 } from "@getstrata/core/scheduler/schedule";
|
|
4
4
|
|
|
5
5
|
// ../../src/core/scheduler/schedule.ts
|
|
6
6
|
class Schedule {
|
|
@@ -35,6 +35,11 @@ async function runDueScheduledTasks(schedule = appSchedule, now = new Date) {
|
|
|
35
35
|
return due.length;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
// ../../src/bootstrap/schedule.ts
|
|
39
|
+
import { exportPendingAuditLogs } from "@getstrata/core/audit/exportAuditLogs";
|
|
40
|
+
import { appLogger } from "@getstrata/core/logging/logger";
|
|
41
|
+
import { appSchedule as appSchedule2 } from "@getstrata/core/scheduler/schedule";
|
|
42
|
+
|
|
38
43
|
// ../../src/config/features.ts
|
|
39
44
|
function readFeatureFlags() {
|
|
40
45
|
return {
|
|
@@ -56,431 +61,11 @@ function isFeatureEnabled(feature) {
|
|
|
56
61
|
return readFeatureFlags()[feature];
|
|
57
62
|
}
|
|
58
63
|
|
|
59
|
-
// ../../src/config/app.ts
|
|
60
|
-
var appConfig = {
|
|
61
|
-
name: "WorkHub",
|
|
62
|
-
env: process.env.APP_ENV ?? "local",
|
|
63
|
-
debug: (process.env.APP_DEBUG ?? "true") !== "false",
|
|
64
|
-
url: process.env.APP_URL ?? "http://localhost:3000",
|
|
65
|
-
apiPrefix: process.env.API_PREFIX ?? "/api/v1"
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
// ../../src/core/database/boundConnection.ts
|
|
69
|
-
var boundConnectionHolder = {
|
|
70
|
-
connection: null
|
|
71
|
-
};
|
|
72
|
-
function getBoundDatabaseConnection() {
|
|
73
|
-
return boundConnectionHolder.connection;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// ../../src/core/runtime/asyncContextStore.ts
|
|
77
|
-
import { AsyncLocalStorage } from "async_hooks";
|
|
78
|
-
function createAsyncContextStore(key) {
|
|
79
|
-
const symbol = Symbol.for(key);
|
|
80
|
-
const globalRecord = globalThis;
|
|
81
|
-
const existing = globalRecord[symbol];
|
|
82
|
-
if (existing) {
|
|
83
|
-
return existing;
|
|
84
|
-
}
|
|
85
|
-
const store = new AsyncLocalStorage;
|
|
86
|
-
globalRecord[symbol] = store;
|
|
87
|
-
return store;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// ../../src/core/database/connectionContext.ts
|
|
91
|
-
var activeConnection = createAsyncContextStore("@getstrata/databaseConnectionContext");
|
|
92
|
-
function getActiveDatabaseConnection(fallback) {
|
|
93
|
-
return activeConnection.getStore() ?? fallback;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// ../../src/core/database/queryProxy.ts
|
|
97
|
-
var POOL_CONNECTION_METHODS = new Set(["begin", "close", "connect"]);
|
|
98
|
-
function createDatabaseQueryProxy(pool) {
|
|
99
|
-
function resolveDatabase() {
|
|
100
|
-
return getActiveDatabaseConnection(pool);
|
|
101
|
-
}
|
|
102
|
-
function resolveDatabaseForProperty(property) {
|
|
103
|
-
if (typeof property === "string" && POOL_CONNECTION_METHODS.has(property)) {
|
|
104
|
-
return pool;
|
|
105
|
-
}
|
|
106
|
-
return resolveDatabase();
|
|
107
|
-
}
|
|
108
|
-
return new Proxy(function database() {}, {
|
|
109
|
-
apply(_target, _thisArg, args) {
|
|
110
|
-
return resolveDatabase()(...args);
|
|
111
|
-
},
|
|
112
|
-
get(_target, property) {
|
|
113
|
-
const connection = resolveDatabaseForProperty(property);
|
|
114
|
-
const value = connection[property];
|
|
115
|
-
return typeof value === "function" ? value.bind(connection) : value;
|
|
116
|
-
}
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// ../../src/core/database/defaultConnection.ts
|
|
121
|
-
var defaultPool = {
|
|
122
|
-
connection: null
|
|
123
|
-
};
|
|
124
|
-
var defaultQuery = {
|
|
125
|
-
connection: null
|
|
126
|
-
};
|
|
127
|
-
function registerDefaultDatabasePool(connection) {
|
|
128
|
-
defaultPool.connection = connection;
|
|
129
|
-
defaultQuery.connection = createDatabaseQueryProxy(connection);
|
|
130
|
-
}
|
|
131
|
-
function getDefaultDatabaseQuery() {
|
|
132
|
-
if (!defaultQuery.connection) {
|
|
133
|
-
throw new Error("Default database query handle is not registered. Call registerDefaultDatabasePool() during app bootstrap.");
|
|
134
|
-
}
|
|
135
|
-
return defaultQuery.connection;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// ../../src/core/database/repositoryConnection.ts
|
|
139
|
-
function resolveRepositoryConnection() {
|
|
140
|
-
return getBoundDatabaseConnection() ?? getDefaultDatabaseQuery();
|
|
141
|
-
}
|
|
142
|
-
var repositoryConnection = new Proxy(function repositoryConnection2() {}, {
|
|
143
|
-
apply(_target, _thisArg, args) {
|
|
144
|
-
return resolveRepositoryConnection()(...args);
|
|
145
|
-
},
|
|
146
|
-
get(_target, property) {
|
|
147
|
-
const connection = resolveRepositoryConnection();
|
|
148
|
-
const value = connection[property];
|
|
149
|
-
return typeof value === "function" ? value.bind(connection) : value;
|
|
150
|
-
}
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
// ../../src/core/security/safeUrl.ts
|
|
154
|
-
import { lookup as dnsLookupImpl } from "dns/promises";
|
|
155
|
-
|
|
156
|
-
// ../../src/core/errors/http.ts
|
|
157
|
-
class HttpError extends Error {
|
|
158
|
-
status;
|
|
159
|
-
details;
|
|
160
|
-
constructor(status, message, details) {
|
|
161
|
-
super(message);
|
|
162
|
-
this.name = new.target.name;
|
|
163
|
-
this.status = status;
|
|
164
|
-
this.details = details;
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
class BadRequestError extends HttpError {
|
|
169
|
-
constructor(message = "Bad Request", details) {
|
|
170
|
-
super(400, message, details);
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
class ConflictError extends HttpError {
|
|
174
|
-
constructor(message = "Conflict", details) {
|
|
175
|
-
super(409, message, details);
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
class UnprocessableEntityError extends HttpError {
|
|
180
|
-
constructor(message = "Unprocessable Entity", details) {
|
|
181
|
-
super(422, message, details);
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
class ForbiddenError extends HttpError {
|
|
185
|
-
constructor(message = "Forbidden", details) {
|
|
186
|
-
super(403, message, details);
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
class UnauthorizedError extends HttpError {
|
|
191
|
-
constructor(message = "Unauthorized", details) {
|
|
192
|
-
super(401, message, details);
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
class PreconditionFailedError extends HttpError {
|
|
196
|
-
constructor(message = "Precondition Failed", details) {
|
|
197
|
-
super(412, message, details);
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// ../../src/core/security/safeUrl.ts
|
|
202
|
-
var dnsLookup = dnsLookupImpl;
|
|
203
|
-
var BLOCKED_HOSTNAMES = new Set([
|
|
204
|
-
"localhost",
|
|
205
|
-
"127.0.0.1",
|
|
206
|
-
"0.0.0.0",
|
|
207
|
-
"::1",
|
|
208
|
-
"metadata.google.internal"
|
|
209
|
-
]);
|
|
210
|
-
function isPrivateIpv4(hostname) {
|
|
211
|
-
const match = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(hostname);
|
|
212
|
-
if (!match) {
|
|
213
|
-
return false;
|
|
214
|
-
}
|
|
215
|
-
const octets = match.slice(1, 5).map((part) => Number.parseInt(part, 10));
|
|
216
|
-
if (octets.some((octet) => octet < 0 || octet > 255)) {
|
|
217
|
-
return true;
|
|
218
|
-
}
|
|
219
|
-
const [a = 0, b = 0] = octets;
|
|
220
|
-
if (a === 10) {
|
|
221
|
-
return true;
|
|
222
|
-
}
|
|
223
|
-
if (a === 127) {
|
|
224
|
-
return true;
|
|
225
|
-
}
|
|
226
|
-
if (a === 0) {
|
|
227
|
-
return true;
|
|
228
|
-
}
|
|
229
|
-
if (a === 169 && b === 254) {
|
|
230
|
-
return true;
|
|
231
|
-
}
|
|
232
|
-
if (a === 172 && b >= 16 && b <= 31) {
|
|
233
|
-
return true;
|
|
234
|
-
}
|
|
235
|
-
if (a === 192 && b === 168) {
|
|
236
|
-
return true;
|
|
237
|
-
}
|
|
238
|
-
return false;
|
|
239
|
-
}
|
|
240
|
-
function isBlockedHostname(hostname) {
|
|
241
|
-
const normalized = hostname.trim().toLowerCase();
|
|
242
|
-
if (normalized.length === 0) {
|
|
243
|
-
return true;
|
|
244
|
-
}
|
|
245
|
-
if (BLOCKED_HOSTNAMES.has(normalized)) {
|
|
246
|
-
return true;
|
|
247
|
-
}
|
|
248
|
-
if (normalized.endsWith(".local") || normalized.endsWith(".internal")) {
|
|
249
|
-
return true;
|
|
250
|
-
}
|
|
251
|
-
if (normalized.includes(":")) {
|
|
252
|
-
return true;
|
|
253
|
-
}
|
|
254
|
-
return isPrivateIpv4(normalized);
|
|
255
|
-
}
|
|
256
|
-
function assertSafeOutboundUrl(rawUrl, options = {}) {
|
|
257
|
-
let parsed;
|
|
258
|
-
try {
|
|
259
|
-
parsed = new URL(rawUrl);
|
|
260
|
-
} catch {
|
|
261
|
-
throw new BadRequestError("Webhook URL is invalid.");
|
|
262
|
-
}
|
|
263
|
-
if (parsed.protocol !== "https:" && !(options.allowHttp && parsed.protocol === "http:")) {
|
|
264
|
-
throw new BadRequestError("Webhook URL must use HTTPS.");
|
|
265
|
-
}
|
|
266
|
-
if (parsed.username || parsed.password) {
|
|
267
|
-
throw new BadRequestError("Webhook URL must not include credentials.");
|
|
268
|
-
}
|
|
269
|
-
if (isBlockedHostname(parsed.hostname)) {
|
|
270
|
-
throw new BadRequestError("Webhook URL targets a blocked host.");
|
|
271
|
-
}
|
|
272
|
-
return parsed;
|
|
273
|
-
}
|
|
274
|
-
function isBlockedIpAddress(address) {
|
|
275
|
-
return isBlockedHostname(address.trim().toLowerCase());
|
|
276
|
-
}
|
|
277
|
-
async function assertSafeOutboundUrlResolved(rawUrl, options = {}) {
|
|
278
|
-
const parsed = assertSafeOutboundUrl(rawUrl, options);
|
|
279
|
-
if (options.resolveDns === false) {
|
|
280
|
-
return parsed;
|
|
281
|
-
}
|
|
282
|
-
const hostname = parsed.hostname.trim().toLowerCase();
|
|
283
|
-
const results = await dnsLookup(hostname, { all: true, verbatim: true });
|
|
284
|
-
if (results.some((result) => isBlockedIpAddress(result.address))) {
|
|
285
|
-
throw new BadRequestError("Webhook URL targets a blocked host.");
|
|
286
|
-
}
|
|
287
|
-
return parsed;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
// ../../src/core/security/safeFetch.ts
|
|
291
|
-
var DEFAULT_FETCH_TIMEOUT_MS = 1e4;
|
|
292
|
-
async function safeFetch(input, init = {}, options = {}) {
|
|
293
|
-
const timeoutMs = options.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;
|
|
294
|
-
const maxRedirects = options.maxRedirects ?? 0;
|
|
295
|
-
const resolveDns = options.resolveDns ?? appConfig.env === "production";
|
|
296
|
-
const urlOptions = { allowHttp: options.allowHttp, resolveDns };
|
|
297
|
-
const controller = new AbortController;
|
|
298
|
-
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
299
|
-
try {
|
|
300
|
-
let currentUrl = (await assertSafeOutboundUrlResolved(input, urlOptions)).toString();
|
|
301
|
-
let redirectCount = 0;
|
|
302
|
-
while (true) {
|
|
303
|
-
const response = await fetch(currentUrl, {
|
|
304
|
-
...init,
|
|
305
|
-
signal: controller.signal,
|
|
306
|
-
redirect: "manual"
|
|
307
|
-
});
|
|
308
|
-
if (response.status >= 300 && response.status < 400) {
|
|
309
|
-
const location = response.headers.get("location");
|
|
310
|
-
if (!location || redirectCount >= maxRedirects) {
|
|
311
|
-
return response;
|
|
312
|
-
}
|
|
313
|
-
currentUrl = (await assertSafeOutboundUrlResolved(new URL(location, currentUrl).toString(), urlOptions)).toString();
|
|
314
|
-
redirectCount += 1;
|
|
315
|
-
continue;
|
|
316
|
-
}
|
|
317
|
-
return response;
|
|
318
|
-
}
|
|
319
|
-
} finally {
|
|
320
|
-
clearTimeout(timeout);
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
// ../../src/core/tenant/databaseTenantContext.ts
|
|
325
|
-
async function runWithMigrationBypass(callback) {
|
|
326
|
-
await repositoryConnection`SELECT set_config('app.bypass_rls', 'true', false)`;
|
|
327
|
-
try {
|
|
328
|
-
return await callback();
|
|
329
|
-
} finally {
|
|
330
|
-
await repositoryConnection`SELECT set_config('app.bypass_rls', 'false', false)`;
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
// ../../src/core/audit/siemFormatter.ts
|
|
335
|
-
function formatSiemAuditEvent(input) {
|
|
336
|
-
return {
|
|
337
|
-
timestamp: input.created_at.toISOString(),
|
|
338
|
-
event_type: "workhub.audit",
|
|
339
|
-
actor_user_id: input.user_id,
|
|
340
|
-
tenant_id: input.tenant_id ?? null,
|
|
341
|
-
trace_id: input.trace_id ?? null,
|
|
342
|
-
action: input.action,
|
|
343
|
-
subject_type: input.subject_type,
|
|
344
|
-
subject_id: input.subject_id,
|
|
345
|
-
ip_address: input.ip_address ?? null,
|
|
346
|
-
user_agent: input.user_agent ?? null,
|
|
347
|
-
checksum: input.checksum ?? null,
|
|
348
|
-
payload: input.payload ?? {}
|
|
349
|
-
};
|
|
350
|
-
}
|
|
351
|
-
function formatCefLine(event) {
|
|
352
|
-
const extension = [
|
|
353
|
-
`rt=${event.timestamp}`,
|
|
354
|
-
`suid=${event.actor_user_id ?? "unknown"}`,
|
|
355
|
-
`cs1=${event.action}`,
|
|
356
|
-
`cs1Label=Action`,
|
|
357
|
-
`cs2=${event.subject_type}`,
|
|
358
|
-
`cs2Label=SubjectType`,
|
|
359
|
-
`cs3=${event.subject_id ?? ""}`,
|
|
360
|
-
`cs3Label=SubjectId`,
|
|
361
|
-
`src=${event.ip_address ?? ""}`,
|
|
362
|
-
`request=${event.trace_id ?? ""}`
|
|
363
|
-
].join(" ");
|
|
364
|
-
return `CEF:0|WorkHub|API|1.0|${event.action}|${event.subject_type}|5|${extension}`;
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
// ../../src/core/audit/exportAuditLogs.ts
|
|
368
|
-
function resolveAuditExportConfig() {
|
|
369
|
-
const endpoint = process.env.SIEM_EXPORT_URL?.trim();
|
|
370
|
-
if (!endpoint) {
|
|
371
|
-
return null;
|
|
372
|
-
}
|
|
373
|
-
assertSafeOutboundUrl(endpoint, { allowHttp: appConfig.env !== "production" });
|
|
374
|
-
const batchSize = Number(process.env.SIEM_EXPORT_BATCH_SIZE ?? "100");
|
|
375
|
-
return {
|
|
376
|
-
endpoint,
|
|
377
|
-
format: process.env.SIEM_EXPORT_FORMAT === "cef" ? "cef" : "json",
|
|
378
|
-
batchSize: Number.isFinite(batchSize) ? batchSize : 100
|
|
379
|
-
};
|
|
380
|
-
}
|
|
381
|
-
async function exportPendingAuditLogs() {
|
|
382
|
-
const config = resolveAuditExportConfig();
|
|
383
|
-
if (!config) {
|
|
384
|
-
return 0;
|
|
385
|
-
}
|
|
386
|
-
return await runWithMigrationBypass(async () => {
|
|
387
|
-
const rows = await repositoryConnection`
|
|
388
|
-
SELECT
|
|
389
|
-
id,
|
|
390
|
-
user_id,
|
|
391
|
-
action,
|
|
392
|
-
subject_type,
|
|
393
|
-
subject_id,
|
|
394
|
-
payload,
|
|
395
|
-
ip_address,
|
|
396
|
-
user_agent,
|
|
397
|
-
checksum,
|
|
398
|
-
tenant_id,
|
|
399
|
-
trace_id,
|
|
400
|
-
created_at
|
|
401
|
-
FROM audit_log
|
|
402
|
-
WHERE exported_at IS NULL
|
|
403
|
-
ORDER BY id
|
|
404
|
-
LIMIT ${config.batchSize}
|
|
405
|
-
`;
|
|
406
|
-
if (rows.length === 0) {
|
|
407
|
-
return 0;
|
|
408
|
-
}
|
|
409
|
-
const events = rows.map((row) => formatSiemAuditEvent({
|
|
410
|
-
action: row.action,
|
|
411
|
-
subject_type: row.subject_type,
|
|
412
|
-
subject_id: row.subject_id,
|
|
413
|
-
user_id: row.user_id,
|
|
414
|
-
tenant_id: row.tenant_id,
|
|
415
|
-
trace_id: row.trace_id,
|
|
416
|
-
ip_address: row.ip_address,
|
|
417
|
-
user_agent: row.user_agent,
|
|
418
|
-
checksum: row.checksum,
|
|
419
|
-
payload: row.payload,
|
|
420
|
-
created_at: row.created_at
|
|
421
|
-
}));
|
|
422
|
-
const body = config.format === "cef" ? events.map((event) => formatCefLine(event)).join(`
|
|
423
|
-
`) : JSON.stringify({ events });
|
|
424
|
-
const response = await safeFetch(config.endpoint, {
|
|
425
|
-
method: "POST",
|
|
426
|
-
headers: {
|
|
427
|
-
"content-type": config.format === "cef" ? "text/plain" : "application/json",
|
|
428
|
-
...process.env.SIEM_EXPORT_TOKEN ? { authorization: `Bearer ${process.env.SIEM_EXPORT_TOKEN}` } : {}
|
|
429
|
-
},
|
|
430
|
-
body
|
|
431
|
-
}, { allowHttp: appConfig.env !== "production" });
|
|
432
|
-
if (!response.ok) {
|
|
433
|
-
throw new Error(`SIEM export failed with status ${response.status}.`);
|
|
434
|
-
}
|
|
435
|
-
const ids = rows.map((row) => row.id);
|
|
436
|
-
for (const id of ids) {
|
|
437
|
-
await repositoryConnection`UPDATE audit_log SET exported_at = NOW() WHERE id = ${id}`;
|
|
438
|
-
}
|
|
439
|
-
return rows.length;
|
|
440
|
-
});
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
// ../../src/core/logging/logger.ts
|
|
444
|
-
class Logger {
|
|
445
|
-
channel;
|
|
446
|
-
constructor(channel = "app") {
|
|
447
|
-
this.channel = channel;
|
|
448
|
-
}
|
|
449
|
-
write(level, message, context = {}) {
|
|
450
|
-
const entry = {
|
|
451
|
-
level,
|
|
452
|
-
channel: this.channel,
|
|
453
|
-
message,
|
|
454
|
-
timestamp: new Date().toISOString(),
|
|
455
|
-
...context
|
|
456
|
-
};
|
|
457
|
-
const line = JSON.stringify(entry);
|
|
458
|
-
if (level === "error") {
|
|
459
|
-
console.error(line);
|
|
460
|
-
return;
|
|
461
|
-
}
|
|
462
|
-
console.log(line);
|
|
463
|
-
}
|
|
464
|
-
debug(message, context) {
|
|
465
|
-
this.write("debug", message, context);
|
|
466
|
-
}
|
|
467
|
-
info(message, context) {
|
|
468
|
-
this.write("info", message, context);
|
|
469
|
-
}
|
|
470
|
-
warn(message, context) {
|
|
471
|
-
this.write("warn", message, context);
|
|
472
|
-
}
|
|
473
|
-
error(message, context) {
|
|
474
|
-
this.write("error", message, context);
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
var appLogger = new Logger("app");
|
|
478
|
-
|
|
479
64
|
// ../../src/bootstrap/schedule.ts
|
|
480
|
-
|
|
65
|
+
appSchedule2.command("* * * * *", "heartbeat", () => {
|
|
481
66
|
appLogger.debug("Scheduler heartbeat");
|
|
482
67
|
});
|
|
483
|
-
|
|
68
|
+
appSchedule2.command("* * * * *", "audit-export", async () => {
|
|
484
69
|
if (!isFeatureEnabled("siemExport")) {
|
|
485
70
|
return;
|
|
486
71
|
}
|
|
@@ -967,6 +552,14 @@ import {
|
|
|
967
552
|
resolveService
|
|
968
553
|
} from "@getstrata/core/contracts/di";
|
|
969
554
|
|
|
555
|
+
// ../../src/bootstrap/providers/auth.ts
|
|
556
|
+
import {
|
|
557
|
+
AuthManager,
|
|
558
|
+
CompositeGuard,
|
|
559
|
+
DatabaseTokenGuard,
|
|
560
|
+
GuestGuard
|
|
561
|
+
} from "@getstrata/core/auth/guard";
|
|
562
|
+
|
|
970
563
|
// ../../src/config/auth.ts
|
|
971
564
|
var authConfig = {
|
|
972
565
|
allowDevHeaders: (process.env.AUTH_DEV_HEADERS ?? "true") !== "false",
|
|
@@ -1032,6 +625,68 @@ var databaseConfig = {
|
|
|
1032
625
|
connectionTimeoutSeconds: readInteger("DB_CONNECTION_TIMEOUT", 10)
|
|
1033
626
|
};
|
|
1034
627
|
|
|
628
|
+
// ../../src/core/runtime/asyncContextStore.ts
|
|
629
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
630
|
+
function createAsyncContextStore(key) {
|
|
631
|
+
const symbol = Symbol.for(key);
|
|
632
|
+
const globalRecord = globalThis;
|
|
633
|
+
const existing = globalRecord[symbol];
|
|
634
|
+
if (existing) {
|
|
635
|
+
return existing;
|
|
636
|
+
}
|
|
637
|
+
const store = new AsyncLocalStorage;
|
|
638
|
+
globalRecord[symbol] = store;
|
|
639
|
+
return store;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// ../../src/core/database/connectionContext.ts
|
|
643
|
+
var activeConnection = createAsyncContextStore("@getstrata/databaseConnectionContext");
|
|
644
|
+
function getActiveDatabaseConnection(fallback) {
|
|
645
|
+
return activeConnection.getStore() ?? fallback;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// ../../src/core/database/queryProxy.ts
|
|
649
|
+
var POOL_CONNECTION_METHODS = new Set(["begin", "close", "connect"]);
|
|
650
|
+
function createDatabaseQueryProxy(pool) {
|
|
651
|
+
function resolveDatabase() {
|
|
652
|
+
return getActiveDatabaseConnection(pool);
|
|
653
|
+
}
|
|
654
|
+
function resolveDatabaseForProperty(property) {
|
|
655
|
+
if (typeof property === "string" && POOL_CONNECTION_METHODS.has(property)) {
|
|
656
|
+
return pool;
|
|
657
|
+
}
|
|
658
|
+
return resolveDatabase();
|
|
659
|
+
}
|
|
660
|
+
return new Proxy(function database() {}, {
|
|
661
|
+
apply(_target, _thisArg, args) {
|
|
662
|
+
return resolveDatabase()(...args);
|
|
663
|
+
},
|
|
664
|
+
get(_target, property) {
|
|
665
|
+
const connection = resolveDatabaseForProperty(property);
|
|
666
|
+
const value = connection[property];
|
|
667
|
+
return typeof value === "function" ? value.bind(connection) : value;
|
|
668
|
+
}
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// ../../src/core/database/defaultConnection.ts
|
|
673
|
+
var defaultPool = {
|
|
674
|
+
connection: null
|
|
675
|
+
};
|
|
676
|
+
var defaultQuery = {
|
|
677
|
+
connection: null
|
|
678
|
+
};
|
|
679
|
+
function registerDefaultDatabasePool(connection) {
|
|
680
|
+
defaultPool.connection = connection;
|
|
681
|
+
defaultQuery.connection = createDatabaseQueryProxy(connection);
|
|
682
|
+
}
|
|
683
|
+
function getDefaultDatabaseQuery() {
|
|
684
|
+
if (!defaultQuery.connection) {
|
|
685
|
+
throw new Error("Default database query handle is not registered. Call registerDefaultDatabasePool() during app bootstrap.");
|
|
686
|
+
}
|
|
687
|
+
return defaultQuery.connection;
|
|
688
|
+
}
|
|
689
|
+
|
|
1035
690
|
// ../../src/db/connection/createConnection.ts
|
|
1036
691
|
var {SQL } = globalThis.Bun;
|
|
1037
692
|
function createDatabaseConnection(config) {
|
|
@@ -1094,7 +749,7 @@ var apiTokenTable = defineTable({
|
|
|
1094
749
|
// ../../src/modules/user/authService.ts
|
|
1095
750
|
import { verifyPassword } from "@getstrata/core/auth/password";
|
|
1096
751
|
import { revealMfaSecret } from "@getstrata/core/crypto/mfaSecret";
|
|
1097
|
-
import { UnauthorizedError
|
|
752
|
+
import { UnauthorizedError } from "@getstrata/core/errors/http";
|
|
1098
753
|
import { logSecurityEvent } from "@getstrata/core/security/securityEvents";
|
|
1099
754
|
import { resolveDefaultTokenExpiryDays } from "@getstrata/core/security/tokenExpiry";
|
|
1100
755
|
import { verifyTotp } from "@getstrata/core/security/totp";
|
|
@@ -1119,22 +774,22 @@ class AuthService {
|
|
|
1119
774
|
const user = await this.users.findByEmail(email);
|
|
1120
775
|
if (!user?.password_hash) {
|
|
1121
776
|
logSecurityEvent("auth_login_failed", { reason: "unknown_user", email });
|
|
1122
|
-
throw new
|
|
777
|
+
throw new UnauthorizedError("Invalid credentials.");
|
|
1123
778
|
}
|
|
1124
779
|
const valid = await verifyPassword(password, user.password_hash);
|
|
1125
780
|
if (!valid) {
|
|
1126
781
|
logSecurityEvent("auth_login_failed", { reason: "invalid_password", user_id: user.id });
|
|
1127
|
-
throw new
|
|
782
|
+
throw new UnauthorizedError("Invalid credentials.");
|
|
1128
783
|
}
|
|
1129
784
|
if (isFeatureEnabled("emailVerification") && !user.email_verified_at) {
|
|
1130
785
|
logSecurityEvent("auth_login_blocked", { reason: "email_unverified", user_id: user.id });
|
|
1131
|
-
throw new
|
|
786
|
+
throw new UnauthorizedError("Email address is not verified.");
|
|
1132
787
|
}
|
|
1133
788
|
if (isFeatureEnabled("mfa") && user.mfa_enabled) {
|
|
1134
789
|
const mfaSecret = revealMfaSecret(user.mfa_secret);
|
|
1135
790
|
if (!mfaSecret || !options.mfaCode || !verifyTotp(mfaSecret, options.mfaCode)) {
|
|
1136
791
|
logSecurityEvent("auth_login_failed", { reason: "invalid_mfa", user_id: user.id });
|
|
1137
|
-
throw new
|
|
792
|
+
throw new UnauthorizedError("Invalid MFA code.");
|
|
1138
793
|
}
|
|
1139
794
|
}
|
|
1140
795
|
logSecurityEvent("auth_login_success", { user_id: user.id, method: "password" });
|
|
@@ -1147,7 +802,7 @@ class AuthService {
|
|
|
1147
802
|
async loginWithOAuth(providerName, code) {
|
|
1148
803
|
const provider = this.oauthProviders.get(providerName);
|
|
1149
804
|
if (!provider) {
|
|
1150
|
-
throw new
|
|
805
|
+
throw new UnauthorizedError("Unsupported OAuth provider.");
|
|
1151
806
|
}
|
|
1152
807
|
const profile = await provider.exchangeCode(code);
|
|
1153
808
|
const user = await this.findOrCreateOAuthUser(providerName, profile);
|
|
@@ -1161,7 +816,7 @@ class AuthService {
|
|
|
1161
816
|
buildOAuthAuthorizationUrl(providerName, state) {
|
|
1162
817
|
const provider = this.oauthProviders.get(providerName);
|
|
1163
818
|
if (!provider) {
|
|
1164
|
-
throw new
|
|
819
|
+
throw new UnauthorizedError("Unsupported OAuth provider.");
|
|
1165
820
|
}
|
|
1166
821
|
return provider.getAuthorizationUrl(state);
|
|
1167
822
|
}
|
|
@@ -1248,115 +903,23 @@ var userTable = defineTable4({
|
|
|
1248
903
|
|
|
1249
904
|
// ../../src/modules/user/tokenService.ts
|
|
1250
905
|
import { hashApiToken } from "@getstrata/core/auth/tokenHash";
|
|
1251
|
-
import { ForbiddenError
|
|
906
|
+
import { ForbiddenError, NotFoundError as NotFoundError2 } from "@getstrata/core/errors/http";
|
|
1252
907
|
import { resolveDefaultTokenExpiryDays as resolveDefaultTokenExpiryDays2 } from "@getstrata/core/security/tokenExpiry";
|
|
1253
908
|
|
|
1254
909
|
// ../../src/modules/user/provider.ts
|
|
1255
910
|
var tokenServiceToken = CORE_TOKEN_SERVICE_TOKEN;
|
|
1256
911
|
|
|
1257
|
-
// ../../src/core/auth/
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
912
|
+
// ../../src/core/auth/sessionCookie.ts
|
|
913
|
+
import { createHmac, timingSafeEqual } from "crypto";
|
|
914
|
+
var SESSION_COOKIE = "workhub_session";
|
|
915
|
+
var SESSION_TTL_SECONDS = 60 * 60 * 24 * 7;
|
|
916
|
+
function resolveSessionSecret() {
|
|
917
|
+
return process.env.SESSION_SECRET?.trim() || process.env.OAUTH_STATE_SECRET?.trim() || process.env.ADMIN_API_TOKEN?.trim() || "workhub-dev-session-secret";
|
|
1261
918
|
}
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
return [...ADMIN_ABILITIES];
|
|
1267
|
-
}
|
|
1268
|
-
return [...MEMBER_ABILITIES];
|
|
1269
|
-
}
|
|
1270
|
-
|
|
1271
|
-
class GuestGuard {
|
|
1272
|
-
resolve(request) {
|
|
1273
|
-
const userId = request.headers.get("x-authenticated-user-id");
|
|
1274
|
-
if (!userId) {
|
|
1275
|
-
return null;
|
|
1276
|
-
}
|
|
1277
|
-
const role = request.headers.get("x-authenticated-user-role");
|
|
1278
|
-
return {
|
|
1279
|
-
id: userId,
|
|
1280
|
-
abilities: devHeaderAbilities(role),
|
|
1281
|
-
...role ? { role } : {}
|
|
1282
|
-
};
|
|
1283
|
-
}
|
|
1284
|
-
}
|
|
1285
|
-
class DatabaseTokenGuard {
|
|
1286
|
-
container;
|
|
1287
|
-
constructor(container) {
|
|
1288
|
-
this.container = container;
|
|
1289
|
-
}
|
|
1290
|
-
async resolve(request) {
|
|
1291
|
-
const authorization = request.headers.get("authorization");
|
|
1292
|
-
if (!authorization?.startsWith("Bearer ")) {
|
|
1293
|
-
return null;
|
|
1294
|
-
}
|
|
1295
|
-
const token = authorization.slice("Bearer ".length).trim();
|
|
1296
|
-
if (!token) {
|
|
1297
|
-
return null;
|
|
1298
|
-
}
|
|
1299
|
-
if (!this.container.has(tokenServiceToken)) {
|
|
1300
|
-
return null;
|
|
1301
|
-
}
|
|
1302
|
-
const tokenService = this.container.resolve(tokenServiceToken);
|
|
1303
|
-
return await tokenService.resolveUserFromToken(token);
|
|
1304
|
-
}
|
|
1305
|
-
}
|
|
1306
|
-
|
|
1307
|
-
class CompositeGuard {
|
|
1308
|
-
guards;
|
|
1309
|
-
constructor(guards) {
|
|
1310
|
-
this.guards = guards;
|
|
1311
|
-
}
|
|
1312
|
-
async resolve(request) {
|
|
1313
|
-
for (const guard of this.guards) {
|
|
1314
|
-
const user = await Promise.resolve(guard.resolve(request));
|
|
1315
|
-
if (user) {
|
|
1316
|
-
return user;
|
|
1317
|
-
}
|
|
1318
|
-
}
|
|
1319
|
-
return null;
|
|
1320
|
-
}
|
|
1321
|
-
}
|
|
1322
|
-
|
|
1323
|
-
class AuthManager {
|
|
1324
|
-
guard;
|
|
1325
|
-
constructor(guard) {
|
|
1326
|
-
this.guard = guard;
|
|
1327
|
-
}
|
|
1328
|
-
async resolve(request) {
|
|
1329
|
-
if (request) {
|
|
1330
|
-
return await Promise.resolve(this.guard.resolve(request));
|
|
1331
|
-
}
|
|
1332
|
-
return currentAuthUser();
|
|
1333
|
-
}
|
|
1334
|
-
user(request) {
|
|
1335
|
-
return this.resolve(request);
|
|
1336
|
-
}
|
|
1337
|
-
async check(request) {
|
|
1338
|
-
return await this.user(request) !== null;
|
|
1339
|
-
}
|
|
1340
|
-
async requireUser(request) {
|
|
1341
|
-
const user = await this.user(request);
|
|
1342
|
-
if (!user) {
|
|
1343
|
-
throw new UnauthorizedError;
|
|
1344
|
-
}
|
|
1345
|
-
return user;
|
|
1346
|
-
}
|
|
1347
|
-
}
|
|
1348
|
-
|
|
1349
|
-
// ../../src/core/auth/sessionCookie.ts
|
|
1350
|
-
import { createHmac, timingSafeEqual } from "crypto";
|
|
1351
|
-
var SESSION_COOKIE = "workhub_session";
|
|
1352
|
-
var SESSION_TTL_SECONDS = 60 * 60 * 24 * 7;
|
|
1353
|
-
function resolveSessionSecret() {
|
|
1354
|
-
return process.env.SESSION_SECRET?.trim() || process.env.OAUTH_STATE_SECRET?.trim() || process.env.ADMIN_API_TOKEN?.trim() || "workhub-dev-session-secret";
|
|
1355
|
-
}
|
|
1356
|
-
function signSession(userId, issuedAt) {
|
|
1357
|
-
const payload = `${userId}.${issuedAt}`;
|
|
1358
|
-
const signature = createHmac("sha256", resolveSessionSecret()).update(payload).digest("hex");
|
|
1359
|
-
return `${payload}.${signature}`;
|
|
919
|
+
function signSession(userId, issuedAt) {
|
|
920
|
+
const payload = `${userId}.${issuedAt}`;
|
|
921
|
+
const signature = createHmac("sha256", resolveSessionSecret()).update(payload).digest("hex");
|
|
922
|
+
return `${payload}.${signature}`;
|
|
1360
923
|
}
|
|
1361
924
|
function readCookieValue(request, cookieName) {
|
|
1362
925
|
const cookieHeader = request.headers.get("cookie");
|
|
@@ -1446,378 +1009,8 @@ var authProvider = {
|
|
|
1446
1009
|
};
|
|
1447
1010
|
var auth_default = authProvider;
|
|
1448
1011
|
|
|
1449
|
-
// ../../src/
|
|
1450
|
-
|
|
1451
|
-
var KEY_PREFIX = "workhub:cache:";
|
|
1452
|
-
var TAG_PREFIX = "workhub:cache:tag:";
|
|
1453
|
-
|
|
1454
|
-
class RedisCacheStore {
|
|
1455
|
-
ttlMs;
|
|
1456
|
-
maxEntries;
|
|
1457
|
-
client;
|
|
1458
|
-
inflight = new Map;
|
|
1459
|
-
keyTags = new Map;
|
|
1460
|
-
constructor(redisUrl, ttlMs, maxEntries) {
|
|
1461
|
-
this.ttlMs = ttlMs;
|
|
1462
|
-
this.maxEntries = maxEntries;
|
|
1463
|
-
this.client = new RedisClient(redisUrl);
|
|
1464
|
-
}
|
|
1465
|
-
async get(key) {
|
|
1466
|
-
const raw = await this.client.get(this.storageKey(key));
|
|
1467
|
-
if (raw === null) {
|
|
1468
|
-
return;
|
|
1469
|
-
}
|
|
1470
|
-
return JSON.parse(raw);
|
|
1471
|
-
}
|
|
1472
|
-
async set(key, value, ttlMs) {
|
|
1473
|
-
const resolvedTtlMs = ttlMs ?? this.ttlMs;
|
|
1474
|
-
const payload = JSON.stringify(value);
|
|
1475
|
-
if (resolvedTtlMs > 0) {
|
|
1476
|
-
await this.client.psetex(this.storageKey(key), resolvedTtlMs, payload);
|
|
1477
|
-
} else {
|
|
1478
|
-
await this.client.set(this.storageKey(key), payload);
|
|
1479
|
-
}
|
|
1480
|
-
await this.enforceMaxEntries();
|
|
1481
|
-
}
|
|
1482
|
-
async getOrSet(key, loader, ttlMs) {
|
|
1483
|
-
const cached = await this.get(key);
|
|
1484
|
-
if (cached !== undefined) {
|
|
1485
|
-
return cached;
|
|
1486
|
-
}
|
|
1487
|
-
const inflightRequest = this.inflight.get(key);
|
|
1488
|
-
if (inflightRequest) {
|
|
1489
|
-
return inflightRequest;
|
|
1490
|
-
}
|
|
1491
|
-
const pendingRequest = loader().then(async (value) => {
|
|
1492
|
-
await this.set(key, value, ttlMs);
|
|
1493
|
-
return value;
|
|
1494
|
-
}).finally(() => {
|
|
1495
|
-
this.inflight.delete(key);
|
|
1496
|
-
});
|
|
1497
|
-
this.inflight.set(key, pendingRequest);
|
|
1498
|
-
return pendingRequest;
|
|
1499
|
-
}
|
|
1500
|
-
async attachTags(key, tags) {
|
|
1501
|
-
if (tags.length === 0) {
|
|
1502
|
-
return;
|
|
1503
|
-
}
|
|
1504
|
-
let tagsForKey = this.keyTags.get(key);
|
|
1505
|
-
if (!tagsForKey) {
|
|
1506
|
-
tagsForKey = new Set;
|
|
1507
|
-
this.keyTags.set(key, tagsForKey);
|
|
1508
|
-
}
|
|
1509
|
-
for (const tag of tags) {
|
|
1510
|
-
tagsForKey.add(tag);
|
|
1511
|
-
await this.client.sadd(this.tagKey(tag), key);
|
|
1512
|
-
}
|
|
1513
|
-
}
|
|
1514
|
-
async flushTags(tags) {
|
|
1515
|
-
const keysToRemove = new Set;
|
|
1516
|
-
for (const tag of tags) {
|
|
1517
|
-
const members = await this.client.smembers(this.tagKey(tag));
|
|
1518
|
-
for (const member of members) {
|
|
1519
|
-
keysToRemove.add(member);
|
|
1520
|
-
}
|
|
1521
|
-
}
|
|
1522
|
-
let removed = 0;
|
|
1523
|
-
for (const key of keysToRemove) {
|
|
1524
|
-
if (await this.invalidate(key)) {
|
|
1525
|
-
removed += 1;
|
|
1526
|
-
}
|
|
1527
|
-
}
|
|
1528
|
-
for (const tag of tags) {
|
|
1529
|
-
await this.client.del(this.tagKey(tag));
|
|
1530
|
-
}
|
|
1531
|
-
return removed;
|
|
1532
|
-
}
|
|
1533
|
-
async invalidate(key) {
|
|
1534
|
-
const deleted = await this.client.del(this.storageKey(key));
|
|
1535
|
-
await this.detachKeyFromTags(key);
|
|
1536
|
-
return deleted > 0;
|
|
1537
|
-
}
|
|
1538
|
-
async invalidateByPrefix(prefix) {
|
|
1539
|
-
const keys = await this.client.keys(`${KEY_PREFIX}*`);
|
|
1540
|
-
let removed = 0;
|
|
1541
|
-
for (const storageKey of keys) {
|
|
1542
|
-
const key = storageKey.slice(KEY_PREFIX.length);
|
|
1543
|
-
if (key === prefix || key.startsWith(`${prefix}?`)) {
|
|
1544
|
-
if (await this.invalidate(key)) {
|
|
1545
|
-
removed += 1;
|
|
1546
|
-
}
|
|
1547
|
-
}
|
|
1548
|
-
}
|
|
1549
|
-
return removed;
|
|
1550
|
-
}
|
|
1551
|
-
async clear() {
|
|
1552
|
-
const keys = await this.client.keys(`${KEY_PREFIX}*`);
|
|
1553
|
-
if (keys.length > 0) {
|
|
1554
|
-
await this.client.del(...keys);
|
|
1555
|
-
}
|
|
1556
|
-
const tagKeys = await this.client.keys(`${TAG_PREFIX}*`);
|
|
1557
|
-
if (tagKeys.length > 0) {
|
|
1558
|
-
await this.client.del(...tagKeys);
|
|
1559
|
-
}
|
|
1560
|
-
this.inflight.clear();
|
|
1561
|
-
this.keyTags.clear();
|
|
1562
|
-
}
|
|
1563
|
-
async size() {
|
|
1564
|
-
const keys = await this.client.keys(`${KEY_PREFIX}*`);
|
|
1565
|
-
return keys.length;
|
|
1566
|
-
}
|
|
1567
|
-
storageKey(key) {
|
|
1568
|
-
return `${KEY_PREFIX}${key}`;
|
|
1569
|
-
}
|
|
1570
|
-
tagKey(tag) {
|
|
1571
|
-
return `${TAG_PREFIX}${tag}`;
|
|
1572
|
-
}
|
|
1573
|
-
async detachKeyFromTags(key) {
|
|
1574
|
-
const tags = this.keyTags.get(key);
|
|
1575
|
-
if (!tags) {
|
|
1576
|
-
return;
|
|
1577
|
-
}
|
|
1578
|
-
for (const tag of tags) {
|
|
1579
|
-
await this.client.srem(this.tagKey(tag), key);
|
|
1580
|
-
}
|
|
1581
|
-
this.keyTags.delete(key);
|
|
1582
|
-
}
|
|
1583
|
-
async enforceMaxEntries() {
|
|
1584
|
-
const keys = await this.client.keys(`${KEY_PREFIX}*`);
|
|
1585
|
-
if (keys.length <= this.maxEntries) {
|
|
1586
|
-
return;
|
|
1587
|
-
}
|
|
1588
|
-
const overflow = keys.length - this.maxEntries;
|
|
1589
|
-
const keysToRemove = keys.slice(0, overflow);
|
|
1590
|
-
if (keysToRemove.length > 0) {
|
|
1591
|
-
await this.client.del(...keysToRemove);
|
|
1592
|
-
}
|
|
1593
|
-
}
|
|
1594
|
-
}
|
|
1595
|
-
var redisCacheStore_default = RedisCacheStore;
|
|
1596
|
-
|
|
1597
|
-
// ../../src/core/cache/simpleCache.ts
|
|
1598
|
-
class SimpleCache {
|
|
1599
|
-
ttlMs;
|
|
1600
|
-
maxEntries;
|
|
1601
|
-
cache = new Map;
|
|
1602
|
-
inflight = new Map;
|
|
1603
|
-
tagIndex = new Map;
|
|
1604
|
-
keyTags = new Map;
|
|
1605
|
-
constructor(ttlMs = 3600000, maxEntries = 100) {
|
|
1606
|
-
this.ttlMs = ttlMs;
|
|
1607
|
-
this.maxEntries = maxEntries;
|
|
1608
|
-
if (!Number.isFinite(ttlMs) || ttlMs < 0) {
|
|
1609
|
-
throw new RangeError("ttlMs must be a non-negative number.");
|
|
1610
|
-
}
|
|
1611
|
-
if (!Number.isInteger(maxEntries) || maxEntries < 1) {
|
|
1612
|
-
throw new RangeError("maxEntries must be a positive integer.");
|
|
1613
|
-
}
|
|
1614
|
-
}
|
|
1615
|
-
get(key) {
|
|
1616
|
-
return this.getFreshEntry(key)?.value;
|
|
1617
|
-
}
|
|
1618
|
-
set(key, value, ttlMs) {
|
|
1619
|
-
const now = Date.now();
|
|
1620
|
-
const resolvedTtlMs = ttlMs ?? this.ttlMs;
|
|
1621
|
-
this.cache.set(key, {
|
|
1622
|
-
value,
|
|
1623
|
-
expiresAt: now + resolvedTtlMs,
|
|
1624
|
-
lastAccessedAt: now
|
|
1625
|
-
});
|
|
1626
|
-
this.evictOverflow();
|
|
1627
|
-
}
|
|
1628
|
-
async getOrSet(key, loader, ttlMs) {
|
|
1629
|
-
this.pruneExpired();
|
|
1630
|
-
const cachedEntry = this.getFreshEntry(key);
|
|
1631
|
-
if (cachedEntry) {
|
|
1632
|
-
return cachedEntry.value;
|
|
1633
|
-
}
|
|
1634
|
-
const inflightRequest = this.inflight.get(key);
|
|
1635
|
-
if (inflightRequest) {
|
|
1636
|
-
return inflightRequest;
|
|
1637
|
-
}
|
|
1638
|
-
const pendingRequest = loader().then((value) => {
|
|
1639
|
-
this.set(key, value, ttlMs);
|
|
1640
|
-
return value;
|
|
1641
|
-
}).finally(() => {
|
|
1642
|
-
this.inflight.delete(key);
|
|
1643
|
-
});
|
|
1644
|
-
this.inflight.set(key, pendingRequest);
|
|
1645
|
-
return pendingRequest;
|
|
1646
|
-
}
|
|
1647
|
-
attachTags(key, tags) {
|
|
1648
|
-
if (tags.length === 0) {
|
|
1649
|
-
return;
|
|
1650
|
-
}
|
|
1651
|
-
let tagsForKey = this.keyTags.get(key);
|
|
1652
|
-
if (!tagsForKey) {
|
|
1653
|
-
tagsForKey = new Set;
|
|
1654
|
-
this.keyTags.set(key, tagsForKey);
|
|
1655
|
-
}
|
|
1656
|
-
for (const tag of tags) {
|
|
1657
|
-
tagsForKey.add(tag);
|
|
1658
|
-
let keysForTag = this.tagIndex.get(tag);
|
|
1659
|
-
if (!keysForTag) {
|
|
1660
|
-
keysForTag = new Set;
|
|
1661
|
-
this.tagIndex.set(tag, keysForTag);
|
|
1662
|
-
}
|
|
1663
|
-
keysForTag.add(key);
|
|
1664
|
-
}
|
|
1665
|
-
}
|
|
1666
|
-
flushTags(tags) {
|
|
1667
|
-
const keysToRemove = new Set;
|
|
1668
|
-
for (const tag of tags) {
|
|
1669
|
-
const keys = this.tagIndex.get(tag);
|
|
1670
|
-
if (!keys) {
|
|
1671
|
-
continue;
|
|
1672
|
-
}
|
|
1673
|
-
for (const key of keys) {
|
|
1674
|
-
keysToRemove.add(key);
|
|
1675
|
-
}
|
|
1676
|
-
}
|
|
1677
|
-
let removed = 0;
|
|
1678
|
-
for (const key of keysToRemove) {
|
|
1679
|
-
if (this.invalidate(key)) {
|
|
1680
|
-
removed += 1;
|
|
1681
|
-
}
|
|
1682
|
-
}
|
|
1683
|
-
for (const tag of tags) {
|
|
1684
|
-
this.tagIndex.delete(tag);
|
|
1685
|
-
}
|
|
1686
|
-
return removed;
|
|
1687
|
-
}
|
|
1688
|
-
invalidate(key) {
|
|
1689
|
-
const removed = this.cache.delete(key);
|
|
1690
|
-
if (removed) {
|
|
1691
|
-
this.detachKeyFromTags(key);
|
|
1692
|
-
}
|
|
1693
|
-
return removed;
|
|
1694
|
-
}
|
|
1695
|
-
invalidateByPrefix(prefix) {
|
|
1696
|
-
let removed = 0;
|
|
1697
|
-
for (const key of [...this.cache.keys()]) {
|
|
1698
|
-
if (key === prefix || key.startsWith(`${prefix}?`)) {
|
|
1699
|
-
if (this.invalidate(key)) {
|
|
1700
|
-
removed += 1;
|
|
1701
|
-
}
|
|
1702
|
-
}
|
|
1703
|
-
}
|
|
1704
|
-
return removed;
|
|
1705
|
-
}
|
|
1706
|
-
clear() {
|
|
1707
|
-
this.cache.clear();
|
|
1708
|
-
this.inflight.clear();
|
|
1709
|
-
this.tagIndex.clear();
|
|
1710
|
-
this.keyTags.clear();
|
|
1711
|
-
}
|
|
1712
|
-
size() {
|
|
1713
|
-
this.pruneExpired();
|
|
1714
|
-
return this.cache.size;
|
|
1715
|
-
}
|
|
1716
|
-
detachKeyFromTags(key) {
|
|
1717
|
-
const tags = this.keyTags.get(key);
|
|
1718
|
-
if (!tags) {
|
|
1719
|
-
return;
|
|
1720
|
-
}
|
|
1721
|
-
for (const tag of tags) {
|
|
1722
|
-
const keys = this.tagIndex.get(tag);
|
|
1723
|
-
if (!keys) {
|
|
1724
|
-
continue;
|
|
1725
|
-
}
|
|
1726
|
-
keys.delete(key);
|
|
1727
|
-
if (keys.size === 0) {
|
|
1728
|
-
this.tagIndex.delete(tag);
|
|
1729
|
-
}
|
|
1730
|
-
}
|
|
1731
|
-
this.keyTags.delete(key);
|
|
1732
|
-
}
|
|
1733
|
-
getFreshEntry(key) {
|
|
1734
|
-
const entry = this.cache.get(key);
|
|
1735
|
-
if (!entry) {
|
|
1736
|
-
return;
|
|
1737
|
-
}
|
|
1738
|
-
if (entry.expiresAt <= Date.now()) {
|
|
1739
|
-
this.invalidate(key);
|
|
1740
|
-
return;
|
|
1741
|
-
}
|
|
1742
|
-
entry.lastAccessedAt = Date.now();
|
|
1743
|
-
return entry;
|
|
1744
|
-
}
|
|
1745
|
-
pruneExpired() {
|
|
1746
|
-
const now = Date.now();
|
|
1747
|
-
for (const [key, entry] of this.cache.entries()) {
|
|
1748
|
-
if (entry.expiresAt <= now) {
|
|
1749
|
-
this.invalidate(key);
|
|
1750
|
-
}
|
|
1751
|
-
}
|
|
1752
|
-
}
|
|
1753
|
-
evictOverflow() {
|
|
1754
|
-
while (this.cache.size > this.maxEntries) {
|
|
1755
|
-
let oldestKey;
|
|
1756
|
-
let oldestAccessTime = Number.POSITIVE_INFINITY;
|
|
1757
|
-
for (const [key, entry] of this.cache.entries()) {
|
|
1758
|
-
if (entry.lastAccessedAt < oldestAccessTime) {
|
|
1759
|
-
oldestAccessTime = entry.lastAccessedAt;
|
|
1760
|
-
oldestKey = key;
|
|
1761
|
-
}
|
|
1762
|
-
}
|
|
1763
|
-
if (!oldestKey) {
|
|
1764
|
-
return;
|
|
1765
|
-
}
|
|
1766
|
-
this.invalidate(oldestKey);
|
|
1767
|
-
}
|
|
1768
|
-
}
|
|
1769
|
-
}
|
|
1770
|
-
var simpleCache_default = SimpleCache;
|
|
1771
|
-
|
|
1772
|
-
// ../../src/core/cache/simpleCacheStore.ts
|
|
1773
|
-
class SimpleCacheStore {
|
|
1774
|
-
cache;
|
|
1775
|
-
constructor(cache) {
|
|
1776
|
-
this.cache = cache;
|
|
1777
|
-
}
|
|
1778
|
-
get(key) {
|
|
1779
|
-
return Promise.resolve(this.cache.get(key));
|
|
1780
|
-
}
|
|
1781
|
-
set(key, value, ttlMs) {
|
|
1782
|
-
this.cache.set(key, value, ttlMs);
|
|
1783
|
-
return Promise.resolve();
|
|
1784
|
-
}
|
|
1785
|
-
getOrSet(key, loader, ttlMs) {
|
|
1786
|
-
return this.cache.getOrSet(key, loader, ttlMs);
|
|
1787
|
-
}
|
|
1788
|
-
attachTags(key, tags) {
|
|
1789
|
-
this.cache.attachTags(key, tags);
|
|
1790
|
-
return Promise.resolve();
|
|
1791
|
-
}
|
|
1792
|
-
flushTags(tags) {
|
|
1793
|
-
return Promise.resolve(this.cache.flushTags(tags));
|
|
1794
|
-
}
|
|
1795
|
-
invalidate(key) {
|
|
1796
|
-
return Promise.resolve(this.cache.invalidate(key));
|
|
1797
|
-
}
|
|
1798
|
-
invalidateByPrefix(prefix) {
|
|
1799
|
-
return Promise.resolve(this.cache.invalidateByPrefix(prefix));
|
|
1800
|
-
}
|
|
1801
|
-
clear() {
|
|
1802
|
-
this.cache.clear();
|
|
1803
|
-
return Promise.resolve();
|
|
1804
|
-
}
|
|
1805
|
-
size() {
|
|
1806
|
-
return Promise.resolve(this.cache.size());
|
|
1807
|
-
}
|
|
1808
|
-
}
|
|
1809
|
-
var simpleCacheStore_default = SimpleCacheStore;
|
|
1810
|
-
|
|
1811
|
-
// ../../src/core/cache/createCacheStore.ts
|
|
1812
|
-
function createCacheStore(options) {
|
|
1813
|
-
if (options.driver === "redis") {
|
|
1814
|
-
if (!options.redisUrl) {
|
|
1815
|
-
throw new Error('CACHE_DRIVER="redis" requires REDIS_URL to be set.');
|
|
1816
|
-
}
|
|
1817
|
-
return new redisCacheStore_default(options.redisUrl, options.ttlMs, options.maxEntries);
|
|
1818
|
-
}
|
|
1819
|
-
return new simpleCacheStore_default(new simpleCache_default(options.ttlMs, options.maxEntries));
|
|
1820
|
-
}
|
|
1012
|
+
// ../../src/bootstrap/providers/cache.ts
|
|
1013
|
+
import { createCacheStore } from "@getstrata/core/cache/createCacheStore";
|
|
1821
1014
|
|
|
1822
1015
|
// ../../src/core/cache/taggedCache.ts
|
|
1823
1016
|
class TaggedCache {
|
|
@@ -1895,6 +1088,18 @@ var cacheProvider = {
|
|
|
1895
1088
|
};
|
|
1896
1089
|
var cache_default = cacheProvider;
|
|
1897
1090
|
|
|
1091
|
+
// ../../src/bootstrap/providers/config.ts
|
|
1092
|
+
import { validateEnv } from "@getstrata/core/config/envSchema";
|
|
1093
|
+
|
|
1094
|
+
// ../../src/config/app.ts
|
|
1095
|
+
var appConfig = {
|
|
1096
|
+
name: "WorkHub",
|
|
1097
|
+
env: process.env.APP_ENV ?? "local",
|
|
1098
|
+
debug: (process.env.APP_DEBUG ?? "true") !== "false",
|
|
1099
|
+
url: process.env.APP_URL ?? "http://localhost:3000",
|
|
1100
|
+
apiPrefix: process.env.API_PREFIX ?? "/api/v1"
|
|
1101
|
+
};
|
|
1102
|
+
|
|
1898
1103
|
// ../../src/config/queue.ts
|
|
1899
1104
|
var queueConfig = {
|
|
1900
1105
|
driver: process.env.QUEUE_DRIVER === "redis" || process.env.QUEUE_DRIVER === "async" || process.env.QUEUE_DRIVER === "sync" ? process.env.QUEUE_DRIVER : DEFAULT_QUEUE_DRIVER,
|
|
@@ -1902,34 +1107,6 @@ var queueConfig = {
|
|
|
1902
1107
|
backoffMs: Number(process.env.QUEUE_BACKOFF_MS ?? "1000")
|
|
1903
1108
|
};
|
|
1904
1109
|
|
|
1905
|
-
// ../../src/core/config/envSchema.ts
|
|
1906
|
-
function validateEnv(schema, env = process.env) {
|
|
1907
|
-
const resolved = {};
|
|
1908
|
-
for (const [name, rule] of Object.entries(schema)) {
|
|
1909
|
-
const rawValue = env[name];
|
|
1910
|
-
const value = rawValue === undefined || rawValue.trim() === "" ? rule.default : rawValue;
|
|
1911
|
-
if (value === undefined || value.trim() === "") {
|
|
1912
|
-
if (rule.required) {
|
|
1913
|
-
throw new Error(`Missing required environment variable "${name}".`);
|
|
1914
|
-
}
|
|
1915
|
-
continue;
|
|
1916
|
-
}
|
|
1917
|
-
if (rule.integer) {
|
|
1918
|
-
const parsed = Number.parseInt(value, 10);
|
|
1919
|
-
const minimum = rule.minimum ?? Number.NEGATIVE_INFINITY;
|
|
1920
|
-
if (!Number.isInteger(parsed) || parsed < minimum) {
|
|
1921
|
-
const comparison = minimum === Number.NEGATIVE_INFINITY ? "an integer" : `an integer >= ${minimum}`;
|
|
1922
|
-
throw new Error(`Environment variable "${name}" must be ${comparison}.`);
|
|
1923
|
-
}
|
|
1924
|
-
}
|
|
1925
|
-
if (rule.pattern && !rule.pattern.test(value)) {
|
|
1926
|
-
throw new Error(`Environment variable "${name}" has an invalid format.`);
|
|
1927
|
-
}
|
|
1928
|
-
resolved[name] = value;
|
|
1929
|
-
}
|
|
1930
|
-
return resolved;
|
|
1931
|
-
}
|
|
1932
|
-
|
|
1933
1110
|
// ../../src/bootstrap/env.ts
|
|
1934
1111
|
import { defineEnvSchema } from "@getstrata/core/config/envSchema";
|
|
1935
1112
|
var appEnvSchema = defineEnvSchema({
|
|
@@ -2049,1985 +1226,49 @@ var configProvider = {
|
|
|
2049
1226
|
};
|
|
2050
1227
|
var config_default = configProvider;
|
|
2051
1228
|
|
|
2052
|
-
// ../../src/
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
handlers.add(listener);
|
|
2059
|
-
this.listeners.set(event, handlers);
|
|
2060
|
-
return () => {
|
|
2061
|
-
handlers.delete(listener);
|
|
2062
|
-
if (handlers.size === 0) {
|
|
2063
|
-
this.listeners.delete(event);
|
|
2064
|
-
}
|
|
2065
|
-
};
|
|
2066
|
-
}
|
|
2067
|
-
async dispatch(event, payload) {
|
|
2068
|
-
const handlers = this.listeners.get(event);
|
|
2069
|
-
if (!handlers || handlers.size === 0) {
|
|
2070
|
-
return;
|
|
2071
|
-
}
|
|
2072
|
-
for (const handler of handlers) {
|
|
2073
|
-
await handler(payload);
|
|
2074
|
-
}
|
|
2075
|
-
}
|
|
2076
|
-
}
|
|
2077
|
-
var eventBus = new EventBus;
|
|
2078
|
-
|
|
2079
|
-
// ../../src/core/events/index.ts
|
|
2080
|
-
function modelEventName(tableName, action) {
|
|
2081
|
-
return `${tableName}.${action}`;
|
|
2082
|
-
}
|
|
2083
|
-
|
|
2084
|
-
// ../../src/bootstrap/providers/events.ts
|
|
2085
|
-
var eventsProvider = {
|
|
2086
|
-
name: "core.events",
|
|
2087
|
-
register({ container }) {
|
|
2088
|
-
container.set(CORE_EVENT_BUS_TOKEN, eventBus);
|
|
2089
|
-
}
|
|
2090
|
-
};
|
|
2091
|
-
var events_default = eventsProvider;
|
|
2092
|
-
|
|
2093
|
-
// ../../src/bootstrap/discoverListeners.ts
|
|
2094
|
-
import { readdirSync as readdirSync2 } from "fs";
|
|
2095
|
-
import { join as join2 } from "path";
|
|
2096
|
-
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
2097
|
-
async function loadDiscoveredListeners() {
|
|
2098
|
-
const listenersDirectory = join2(import.meta.dir, "../listeners");
|
|
2099
|
-
let entries;
|
|
2100
|
-
try {
|
|
2101
|
-
entries = readdirSync2(listenersDirectory, { withFileTypes: true }).filter((entry) => entry.isFile() && entry.name.endsWith(".ts")).map((entry) => entry.name);
|
|
2102
|
-
} catch (error) {
|
|
2103
|
-
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
2104
|
-
return [];
|
|
2105
|
-
}
|
|
2106
|
-
throw error;
|
|
2107
|
-
}
|
|
2108
|
-
const listeners = await Promise.all(entries.map(async (fileName) => {
|
|
2109
|
-
const moduleUrl = pathToFileURL2(join2(listenersDirectory, fileName)).href;
|
|
2110
|
-
const loaded = await import(moduleUrl);
|
|
2111
|
-
return loaded.default;
|
|
2112
|
-
}));
|
|
2113
|
-
return listeners.filter((listener) => typeof listener === "function");
|
|
2114
|
-
}
|
|
2115
|
-
var appListeners = await loadDiscoveredListeners();
|
|
2116
|
-
function discoverListeners() {
|
|
2117
|
-
return appListeners;
|
|
2118
|
-
}
|
|
2119
|
-
|
|
2120
|
-
// ../../src/core/queue/index.ts
|
|
2121
|
-
class Job {
|
|
2122
|
-
maxAttempts;
|
|
2123
|
-
backoffMs;
|
|
2124
|
-
priority;
|
|
2125
|
-
}
|
|
2126
|
-
|
|
2127
|
-
// ../../src/core/jobs/invalidateCacheTagsJob.ts
|
|
2128
|
-
class InvalidateCacheTagsJob extends Job {
|
|
2129
|
-
cache;
|
|
2130
|
-
constructor(cache) {
|
|
2131
|
-
super();
|
|
2132
|
-
this.cache = cache;
|
|
2133
|
-
}
|
|
2134
|
-
async handle(payload) {
|
|
2135
|
-
await this.cache.tags(...payload.tags).flush();
|
|
2136
|
-
}
|
|
2137
|
-
}
|
|
2138
|
-
var invalidateCacheTagsJob_default = InvalidateCacheTagsJob;
|
|
2139
|
-
|
|
2140
|
-
// ../../src/core/pagination/index.ts
|
|
2141
|
-
function buildPaginationMeta(input) {
|
|
2142
|
-
const lastPage = Math.max(1, Math.ceil(input.total / input.perPage));
|
|
2143
|
-
return {
|
|
2144
|
-
page: input.page,
|
|
2145
|
-
per_page: input.perPage,
|
|
2146
|
-
total: input.total,
|
|
2147
|
-
last_page: lastPage
|
|
2148
|
-
};
|
|
2149
|
-
}
|
|
2150
|
-
|
|
2151
|
-
// ../../src/core/database/errors.ts
|
|
2152
|
-
function isPostgresError(error) {
|
|
2153
|
-
return typeof error === "object" && error !== null && (("errno" in error) || ("code" in error));
|
|
2154
|
-
}
|
|
2155
|
-
function getPostgresSqlState(error) {
|
|
2156
|
-
if (typeof error.errno === "string" && /^\d{5}$/.test(error.errno)) {
|
|
2157
|
-
return error.errno;
|
|
2158
|
-
}
|
|
2159
|
-
if (typeof error.errno === "number") {
|
|
2160
|
-
return String(error.errno).padStart(5, "0");
|
|
2161
|
-
}
|
|
2162
|
-
if (typeof error.code === "string" && /^\d{5}$/.test(error.code)) {
|
|
2163
|
-
return error.code;
|
|
2164
|
-
}
|
|
2165
|
-
return;
|
|
2166
|
-
}
|
|
2167
|
-
function mapDatabaseError(error) {
|
|
2168
|
-
if (error instanceof HttpError) {
|
|
2169
|
-
return error;
|
|
2170
|
-
}
|
|
2171
|
-
if (!isPostgresError(error)) {
|
|
2172
|
-
const message = error instanceof Error ? error.message : "Database operation failed.";
|
|
2173
|
-
return new BadRequestError(message);
|
|
2174
|
-
}
|
|
2175
|
-
const sqlState = getPostgresSqlState(error);
|
|
2176
|
-
switch (sqlState) {
|
|
2177
|
-
case "23505":
|
|
2178
|
-
return new ConflictError(error.detail ?? "A record with these values already exists.", {
|
|
2179
|
-
constraint: error.constraint
|
|
2180
|
-
});
|
|
2181
|
-
case "23503":
|
|
2182
|
-
return new UnprocessableEntityError(error.detail ?? "Referenced record does not exist.", {
|
|
2183
|
-
constraint: error.constraint
|
|
2184
|
-
});
|
|
2185
|
-
case "23502":
|
|
2186
|
-
return new BadRequestError(error.detail ?? "Required field is missing.", {
|
|
2187
|
-
constraint: error.constraint
|
|
2188
|
-
});
|
|
2189
|
-
case "23514":
|
|
2190
|
-
return new BadRequestError(error.detail ?? "Value violates a database constraint.", {
|
|
2191
|
-
constraint: error.constraint
|
|
2192
|
-
});
|
|
2193
|
-
default:
|
|
2194
|
-
return new BadRequestError(error.message ?? "Database operation failed.", {
|
|
2195
|
-
code: error.code,
|
|
2196
|
-
sqlState
|
|
2197
|
-
});
|
|
2198
|
-
}
|
|
2199
|
-
}
|
|
2200
|
-
async function withDatabaseErrorHandling(operation) {
|
|
2201
|
-
try {
|
|
2202
|
-
return await operation();
|
|
2203
|
-
} catch (error) {
|
|
2204
|
-
throw mapDatabaseError(error);
|
|
2205
|
-
}
|
|
2206
|
-
}
|
|
2207
|
-
|
|
2208
|
-
// ../../src/core/database/query.ts
|
|
2209
|
-
function quoteIdentifier(identifier) {
|
|
2210
|
-
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(identifier)) {
|
|
2211
|
-
throw new Error(`Invalid SQL identifier: ${identifier}`);
|
|
2212
|
-
}
|
|
2213
|
-
return `"${identifier}"`;
|
|
2214
|
-
}
|
|
2215
|
-
function qualifyColumn(tableName, column) {
|
|
2216
|
-
return `${quoteIdentifier(tableName)}.${quoteIdentifier(column)}`;
|
|
2217
|
-
}
|
|
2218
|
-
function resolveQualifiedColumn(defaultTable, columnName) {
|
|
2219
|
-
if (columnName.includes(".")) {
|
|
2220
|
-
const [table, column] = columnName.split(".", 2);
|
|
2221
|
-
if (!table || !column) {
|
|
2222
|
-
throw new Error(`Invalid qualified column: ${columnName}`);
|
|
2223
|
-
}
|
|
2224
|
-
return qualifyColumn(table, column);
|
|
2225
|
-
}
|
|
2226
|
-
return qualifyColumn(defaultTable, columnName);
|
|
2227
|
-
}
|
|
2228
|
-
function parseQualifiedColumn(reference) {
|
|
2229
|
-
const [table, column] = reference.split(".", 2);
|
|
2230
|
-
if (!table || !column) {
|
|
2231
|
-
throw new Error(`Join columns must be qualified as table.column: ${reference}`);
|
|
2232
|
-
}
|
|
2233
|
-
return { table, column };
|
|
2234
|
-
}
|
|
2235
|
-
function normalizeDirection(direction = "ASC") {
|
|
2236
|
-
return direction.toUpperCase() === "DESC" ? "DESC" : "ASC";
|
|
2237
|
-
}
|
|
2238
|
-
function isQueryOperator(value) {
|
|
2239
|
-
return value !== null && !Array.isArray(value) && !(value instanceof Date) && typeof value === "object";
|
|
2240
|
-
}
|
|
2241
|
-
function pushParam(values, value) {
|
|
2242
|
-
values.push(value);
|
|
2243
|
-
return `$${values.length}`;
|
|
2244
|
-
}
|
|
2245
|
-
function buildInClause(column, values, params) {
|
|
2246
|
-
if (values.length === 0) {
|
|
2247
|
-
return "1 = 0";
|
|
2248
|
-
}
|
|
2249
|
-
const placeholders = values.map((value) => pushParam(params, value)).join(", ");
|
|
2250
|
-
return `${column} IN (${placeholders})`;
|
|
2251
|
-
}
|
|
2252
|
-
function buildOperatorClauses(column, operator, params) {
|
|
2253
|
-
const clauses = [];
|
|
2254
|
-
if (operator.isNull === true) {
|
|
2255
|
-
clauses.push(`${column} IS NULL`);
|
|
2256
|
-
}
|
|
2257
|
-
if (operator.isNull === false) {
|
|
2258
|
-
clauses.push(`${column} IS NOT NULL`);
|
|
2259
|
-
}
|
|
2260
|
-
if (operator.eq !== undefined) {
|
|
2261
|
-
if (operator.eq === null) {
|
|
2262
|
-
clauses.push(`${column} IS NULL`);
|
|
2263
|
-
} else {
|
|
2264
|
-
clauses.push(`${column} = ${pushParam(params, operator.eq)}`);
|
|
2265
|
-
}
|
|
2266
|
-
}
|
|
2267
|
-
if (operator.in !== undefined) {
|
|
2268
|
-
clauses.push(buildInClause(column, operator.in, params));
|
|
2269
|
-
}
|
|
2270
|
-
if (operator.gt !== undefined) {
|
|
2271
|
-
clauses.push(`${column} > ${pushParam(params, operator.gt)}`);
|
|
2272
|
-
}
|
|
2273
|
-
if (operator.gte !== undefined) {
|
|
2274
|
-
clauses.push(`${column} >= ${pushParam(params, operator.gte)}`);
|
|
2275
|
-
}
|
|
2276
|
-
if (operator.lt !== undefined) {
|
|
2277
|
-
clauses.push(`${column} < ${pushParam(params, operator.lt)}`);
|
|
2278
|
-
}
|
|
2279
|
-
if (operator.lte !== undefined) {
|
|
2280
|
-
clauses.push(`${column} <= ${pushParam(params, operator.lte)}`);
|
|
2281
|
-
}
|
|
2282
|
-
if (operator.ilike !== undefined) {
|
|
2283
|
-
clauses.push(`${column} ILIKE ${pushParam(params, `%${operator.ilike}%`)}`);
|
|
2284
|
-
}
|
|
2285
|
-
if (operator.tsMatch !== undefined) {
|
|
2286
|
-
clauses.push(`${column} @@ plainto_tsquery('english', ${pushParam(params, operator.tsMatch)})`);
|
|
2287
|
-
}
|
|
2288
|
-
return clauses;
|
|
2289
|
-
}
|
|
2290
|
-
function appendWhereParts(tableName, where, params) {
|
|
2291
|
-
const clauses = [];
|
|
2292
|
-
for (const [columnName, filterValue] of Object.entries(where)) {
|
|
2293
|
-
if (filterValue === undefined) {
|
|
2294
|
-
continue;
|
|
2295
|
-
}
|
|
2296
|
-
const column = resolveQualifiedColumn(tableName, columnName);
|
|
2297
|
-
if (Array.isArray(filterValue)) {
|
|
2298
|
-
clauses.push(buildInClause(column, filterValue, params));
|
|
2299
|
-
continue;
|
|
2300
|
-
}
|
|
2301
|
-
if (isQueryOperator(filterValue)) {
|
|
2302
|
-
clauses.push(...buildOperatorClauses(column, filterValue, params));
|
|
2303
|
-
continue;
|
|
2304
|
-
}
|
|
2305
|
-
if (filterValue === null) {
|
|
2306
|
-
clauses.push(`${column} IS NULL`);
|
|
2307
|
-
continue;
|
|
2308
|
-
}
|
|
2309
|
-
clauses.push(`${column} = ${pushParam(params, filterValue)}`);
|
|
2310
|
-
}
|
|
2311
|
-
return clauses.join(" AND ");
|
|
2312
|
-
}
|
|
2313
|
-
function buildWhereNodeClause(tableName, node, params) {
|
|
2314
|
-
if ("where" in node) {
|
|
2315
|
-
return appendWhereParts(tableName, node.where, params);
|
|
2316
|
-
}
|
|
2317
|
-
const grouped = buildWhereGroupClause(tableName, node.group, params);
|
|
2318
|
-
if (!grouped) {
|
|
2319
|
-
return "";
|
|
2320
|
-
}
|
|
2321
|
-
return grouped.includes(" OR ") ? `(${grouped})` : grouped;
|
|
2322
|
-
}
|
|
2323
|
-
function buildWhereGroupClause(tableName, nodes, params) {
|
|
2324
|
-
let result = "";
|
|
2325
|
-
for (const node of nodes) {
|
|
2326
|
-
const part = buildWhereNodeClause(tableName, node, params);
|
|
2327
|
-
if (!part) {
|
|
2328
|
-
continue;
|
|
2329
|
-
}
|
|
2330
|
-
if (!result) {
|
|
2331
|
-
result = part;
|
|
2332
|
-
continue;
|
|
2333
|
-
}
|
|
2334
|
-
result = node.kind === "or" ? `${result} OR ${part}` : `${result} AND ${part}`;
|
|
2335
|
-
}
|
|
2336
|
-
if (!result) {
|
|
2337
|
-
return "";
|
|
2338
|
-
}
|
|
2339
|
-
return result;
|
|
2340
|
-
}
|
|
2341
|
-
function buildAdvancedWhereClause(tableName, where = {}, whereNodes = [], params = []) {
|
|
2342
|
-
const nodes = [];
|
|
2343
|
-
if (Object.keys(where).length > 0) {
|
|
2344
|
-
nodes.push({ kind: "and", where });
|
|
2345
|
-
}
|
|
2346
|
-
nodes.push(...whereNodes);
|
|
2347
|
-
const combined = buildWhereGroupClause(tableName, nodes, params);
|
|
2348
|
-
return {
|
|
2349
|
-
clause: combined ? ` WHERE ${combined}` : "",
|
|
2350
|
-
params
|
|
2351
|
-
};
|
|
2352
|
-
}
|
|
2353
|
-
function resolveSoftDeleteColumn(table) {
|
|
2354
|
-
if (!table.softDeletes) {
|
|
2355
|
-
return null;
|
|
2356
|
-
}
|
|
2357
|
-
if (table.softDeletes === true) {
|
|
2358
|
-
return "deleted_at";
|
|
2359
|
-
}
|
|
2360
|
-
return table.softDeletes.column ?? "deleted_at";
|
|
2361
|
-
}
|
|
2362
|
-
function appendSoftDeleteScope(table, options, clauses) {
|
|
2363
|
-
const column = resolveSoftDeleteColumn(table);
|
|
2364
|
-
if (!column) {
|
|
2365
|
-
return;
|
|
2366
|
-
}
|
|
2367
|
-
const qualifiedColumn = qualifyColumn(table.name, column);
|
|
2368
|
-
if (options.onlyTrashed) {
|
|
2369
|
-
clauses.push(`${qualifiedColumn} IS NOT NULL`);
|
|
2370
|
-
return;
|
|
2371
|
-
}
|
|
2372
|
-
if (!options.withTrashed) {
|
|
2373
|
-
clauses.push(`${qualifiedColumn} IS NULL`);
|
|
2374
|
-
}
|
|
2375
|
-
}
|
|
2376
|
-
function buildQueryWhereClause(table, options = {}, whereNodes = [], params = []) {
|
|
2377
|
-
const { clause, params: whereParams } = buildAdvancedWhereClause(table.name, options.where ?? {}, whereNodes, params);
|
|
2378
|
-
const softDeleteClauses = [];
|
|
2379
|
-
appendSoftDeleteScope(table, options, softDeleteClauses);
|
|
2380
|
-
if (softDeleteClauses.length === 0) {
|
|
2381
|
-
return { clause, params: whereParams };
|
|
2382
|
-
}
|
|
2383
|
-
const base = clause.replace(/^ WHERE /, "");
|
|
2384
|
-
const scope = softDeleteClauses.join(" AND ");
|
|
2385
|
-
return {
|
|
2386
|
-
clause: base ? ` WHERE (${base}) AND ${scope}` : ` WHERE ${scope}`,
|
|
2387
|
-
params: whereParams
|
|
2388
|
-
};
|
|
2389
|
-
}
|
|
2390
|
-
function isQueryOrder(value) {
|
|
2391
|
-
return "column" in value;
|
|
2392
|
-
}
|
|
2393
|
-
function normalizeOrderBy(orderBy) {
|
|
2394
|
-
if (!orderBy) {
|
|
2395
|
-
return [];
|
|
2396
|
-
}
|
|
2397
|
-
if (Array.isArray(orderBy)) {
|
|
2398
|
-
return orderBy;
|
|
2399
|
-
}
|
|
2400
|
-
if (isQueryOrder(orderBy)) {
|
|
2401
|
-
return [orderBy];
|
|
2402
|
-
}
|
|
2403
|
-
return Object.entries(orderBy).map(([column, direction]) => ({
|
|
2404
|
-
column,
|
|
2405
|
-
direction
|
|
2406
|
-
}));
|
|
2407
|
-
}
|
|
2408
|
-
function buildOrderByClause(tableName, orderBy) {
|
|
2409
|
-
const parts = normalizeOrderBy(orderBy).map(({ column, direction }) => {
|
|
2410
|
-
return `${resolveQualifiedColumn(tableName, column)} ${normalizeDirection(direction)}`;
|
|
2411
|
-
});
|
|
2412
|
-
return parts.length > 0 ? ` ORDER BY ${parts.join(", ")}` : "";
|
|
2413
|
-
}
|
|
2414
|
-
function buildGroupByClause(tableName, groupBy) {
|
|
2415
|
-
if (!groupBy) {
|
|
2416
|
-
return "";
|
|
2417
|
-
}
|
|
2418
|
-
const columns = (Array.isArray(groupBy) ? groupBy : [groupBy]).map((column) => String(column));
|
|
2419
|
-
const parts = columns.map((column) => resolveQualifiedColumn(tableName, column));
|
|
2420
|
-
return parts.length > 0 ? ` GROUP BY ${parts.join(", ")}` : "";
|
|
2421
|
-
}
|
|
2422
|
-
function buildHavingClause(tableName, having, params) {
|
|
2423
|
-
if (!having) {
|
|
2424
|
-
return "";
|
|
2425
|
-
}
|
|
2426
|
-
const body = appendWhereParts(tableName, having, params);
|
|
2427
|
-
return body.length > 0 ? ` HAVING ${body}` : "";
|
|
2428
|
-
}
|
|
2429
|
-
function buildJoinClause(joins = []) {
|
|
2430
|
-
return joins.map((join3) => {
|
|
2431
|
-
const joinType = join3.type === "left" ? "LEFT JOIN" : "INNER JOIN";
|
|
2432
|
-
const onClause = join3.on.map(({ left, right }) => `${qualifyColumn(left.table, left.column)} = ${qualifyColumn(right.table, right.column)}`).join(" AND ");
|
|
2433
|
-
return ` ${joinType} ${quoteIdentifier(join3.table)} ON ${onClause}`;
|
|
2434
|
-
}).join("");
|
|
2435
|
-
}
|
|
2436
|
-
function buildLimitClause(limit) {
|
|
2437
|
-
if (limit === undefined) {
|
|
2438
|
-
return "";
|
|
2439
|
-
}
|
|
2440
|
-
if (!Number.isInteger(limit) || limit <= 0) {
|
|
2441
|
-
throw new Error("Query limit must be a positive integer.");
|
|
2442
|
-
}
|
|
2443
|
-
return ` LIMIT ${limit}`;
|
|
2444
|
-
}
|
|
2445
|
-
function buildOffsetClause(offset) {
|
|
2446
|
-
if (offset === undefined) {
|
|
2447
|
-
return "";
|
|
2448
|
-
}
|
|
2449
|
-
if (!Number.isInteger(offset) || offset < 0) {
|
|
2450
|
-
throw new Error("Query offset must be a non-negative integer.");
|
|
2451
|
-
}
|
|
2452
|
-
return ` OFFSET ${offset}`;
|
|
2453
|
-
}
|
|
2454
|
-
function buildReturningColumns(table) {
|
|
2455
|
-
return table.columns.map((column) => qualifyColumn(table.name, column)).join(", ");
|
|
2456
|
-
}
|
|
2457
|
-
function buildSelectList(table, select, params = []) {
|
|
2458
|
-
if (!select || select.length === 0) {
|
|
2459
|
-
return buildReturningColumns(table);
|
|
2460
|
-
}
|
|
2461
|
-
return select.map((item) => {
|
|
2462
|
-
if (item.kind === "column") {
|
|
2463
|
-
const column2 = qualifyColumn(item.table, item.column);
|
|
2464
|
-
return item.as ? `${column2} AS ${quoteIdentifier(item.as)}` : column2;
|
|
2465
|
-
}
|
|
2466
|
-
if (item.kind === "literalText") {
|
|
2467
|
-
return `${pushParam(params, item.value)}::text AS ${quoteIdentifier(item.as)}`;
|
|
2468
|
-
}
|
|
2469
|
-
const column = qualifyColumn(item.table, item.column);
|
|
2470
|
-
const placeholder = pushParam(params, item.query);
|
|
2471
|
-
return `ts_rank(${column}, plainto_tsquery('english', ${placeholder})) AS ${quoteIdentifier(item.as)}`;
|
|
2472
|
-
}).join(", ");
|
|
2473
|
-
}
|
|
2474
|
-
function getDefinedColumnEntries(table, values, options = {}) {
|
|
2475
|
-
const record = values;
|
|
2476
|
-
const excluded = new Set(options.exclude ?? []);
|
|
2477
|
-
return table.columns.flatMap((column) => {
|
|
2478
|
-
if (excluded.has(column) || !Object.hasOwn(record, column)) {
|
|
2479
|
-
return [];
|
|
2480
|
-
}
|
|
2481
|
-
const value = record[column];
|
|
2482
|
-
if (value === undefined) {
|
|
2483
|
-
return [];
|
|
2484
|
-
}
|
|
2485
|
-
return [[column, value]];
|
|
2486
|
-
});
|
|
2487
|
-
}
|
|
2488
|
-
function buildSelectQuery(table, options = {}, whereNodes = []) {
|
|
2489
|
-
const params = [];
|
|
2490
|
-
const columns = buildSelectList(table, options.select, params);
|
|
2491
|
-
const { clause } = buildQueryWhereClause(table, options, whereNodes, params);
|
|
2492
|
-
const joins = buildJoinClause(options.joins);
|
|
2493
|
-
const groupBy = buildGroupByClause(table.name, options.groupBy);
|
|
2494
|
-
const havingClause = buildHavingClause(table.name, options.having, params);
|
|
2495
|
-
const orderBy = buildOrderByClause(table.name, options.orderBy ?? table.defaultOrderBy);
|
|
2496
|
-
const limit = buildLimitClause(options.limit);
|
|
2497
|
-
const offset = buildOffsetClause(options.offset);
|
|
2498
|
-
return {
|
|
2499
|
-
text: `SELECT ${columns} FROM ${quoteIdentifier(table.name)}${joins}${clause}${groupBy}${havingClause}${orderBy}${limit}${offset}`,
|
|
2500
|
-
params
|
|
2501
|
-
};
|
|
2502
|
-
}
|
|
2503
|
-
function buildCountQuery(table, where = {}, options = {}, whereNodes = []) {
|
|
2504
|
-
const params = [];
|
|
2505
|
-
const { clause, params: whereParams } = buildQueryWhereClause(table, {
|
|
2506
|
-
where,
|
|
2507
|
-
withTrashed: options.withTrashed,
|
|
2508
|
-
onlyTrashed: options.onlyTrashed
|
|
2509
|
-
}, whereNodes);
|
|
2510
|
-
params.push(...whereParams);
|
|
2511
|
-
const joins = buildJoinClause(options.joins);
|
|
2512
|
-
const groupBy = buildGroupByClause(table.name, options.groupBy);
|
|
2513
|
-
return {
|
|
2514
|
-
text: `SELECT COUNT(*) AS count FROM ${quoteIdentifier(table.name)}${joins}${clause}${groupBy}`,
|
|
2515
|
-
params
|
|
2516
|
-
};
|
|
2517
|
-
}
|
|
2518
|
-
function buildProjectionQuery(table, expression, alias, options = {}, whereNodes = []) {
|
|
2519
|
-
assertSafeProjectionExpression(expression);
|
|
2520
|
-
const params = [];
|
|
2521
|
-
const { clause, params: whereParams } = buildQueryWhereClause(table, options, whereNodes);
|
|
2522
|
-
params.push(...whereParams);
|
|
2523
|
-
const joins = buildJoinClause(options.joins);
|
|
2524
|
-
const groupBy = buildGroupByClause(table.name, options.groupBy);
|
|
2525
|
-
const orderBy = buildOrderByClause(table.name, options.orderBy);
|
|
2526
|
-
const limit = buildLimitClause(options.limit);
|
|
2527
|
-
return {
|
|
2528
|
-
text: `SELECT ${expression} AS ${quoteIdentifier(alias)} FROM ${quoteIdentifier(table.name)}${joins}${clause}${groupBy}${orderBy}${limit}`,
|
|
2529
|
-
params
|
|
2530
|
-
};
|
|
2531
|
-
}
|
|
2532
|
-
var SAFE_PROJECTION_EXPRESSION = /^(COUNT\(\*\)|(?:"[\w_]+"(?:\."[\w_]+")?)|(?:AVG|SUM|MIN|MAX)\((?:"[\w_]+"(?:\."[\w_]+")?)\))$/;
|
|
2533
|
-
function assertSafeProjectionExpression(expression) {
|
|
2534
|
-
if (!SAFE_PROJECTION_EXPRESSION.test(expression.trim())) {
|
|
2535
|
-
throw new Error(`Unsafe projection expression: ${expression}`);
|
|
2536
|
-
}
|
|
2537
|
-
}
|
|
2538
|
-
function buildGroupedCountQuery(table, column, where = {}, options = {}) {
|
|
2539
|
-
const qualifiedColumn = qualifyColumn(table.name, column);
|
|
2540
|
-
const { clause, params } = buildQueryWhereClause(table, {
|
|
2541
|
-
where,
|
|
2542
|
-
...options
|
|
2543
|
-
});
|
|
2544
|
-
return {
|
|
2545
|
-
text: `SELECT ${qualifiedColumn} AS ${quoteIdentifier("value")}, COUNT(*) AS ${quoteIdentifier("count")} FROM ${quoteIdentifier(table.name)}${clause} GROUP BY ${qualifiedColumn} ORDER BY ${qualifiedColumn} ASC`,
|
|
2546
|
-
params
|
|
2547
|
-
};
|
|
2548
|
-
}
|
|
2549
|
-
function buildInsertQuery(table, values) {
|
|
2550
|
-
const entries = getDefinedColumnEntries(table, values);
|
|
2551
|
-
if (entries.length === 0) {
|
|
2552
|
-
throw new Error(`Cannot insert into ${table.name} without any column values.`);
|
|
2553
|
-
}
|
|
2554
|
-
const params = [];
|
|
2555
|
-
const columns = entries.map(([column]) => quoteIdentifier(column)).join(", ");
|
|
2556
|
-
const placeholders = entries.map(([, value]) => pushParam(params, value)).join(", ");
|
|
2557
|
-
const returningColumns = buildReturningColumns(table);
|
|
2558
|
-
return {
|
|
2559
|
-
text: `INSERT INTO ${quoteIdentifier(table.name)} (${columns}) VALUES (${placeholders}) RETURNING ${returningColumns}`,
|
|
2560
|
-
params
|
|
2561
|
-
};
|
|
2562
|
-
}
|
|
2563
|
-
function buildUpdateQuery(table, id, changes) {
|
|
2564
|
-
const entries = getDefinedColumnEntries(table, changes, {
|
|
2565
|
-
exclude: [table.primaryKey]
|
|
2566
|
-
});
|
|
2567
|
-
if (entries.length === 0) {
|
|
2568
|
-
throw new Error(`Cannot update ${table.name} without any changed column values.`);
|
|
2569
|
-
}
|
|
2570
|
-
const params = [];
|
|
2571
|
-
const setClause = entries.map(([column, value]) => `${quoteIdentifier(column)} = ${pushParam(params, value)}`).join(", ");
|
|
2572
|
-
const primaryKeyPlaceholder = pushParam(params, id);
|
|
2573
|
-
const returningColumns = buildReturningColumns(table);
|
|
2574
|
-
const scopeClauses = [];
|
|
2575
|
-
appendSoftDeleteScope(table, {}, scopeClauses);
|
|
2576
|
-
const scopeSuffix = scopeClauses.length > 0 ? ` AND ${scopeClauses.join(" AND ")}` : "";
|
|
2577
|
-
return {
|
|
2578
|
-
text: `UPDATE ${quoteIdentifier(table.name)} SET ${setClause} WHERE ${quoteIdentifier(table.primaryKey)} = ${primaryKeyPlaceholder}${scopeSuffix} RETURNING ${returningColumns}`,
|
|
2579
|
-
params
|
|
2580
|
-
};
|
|
2581
|
-
}
|
|
2582
|
-
function buildSoftDeleteByIdQuery(table, id, deletedAt) {
|
|
2583
|
-
const deletedAtColumn = resolveSoftDeleteColumn(table);
|
|
2584
|
-
if (!deletedAtColumn) {
|
|
2585
|
-
throw new Error(`Table ${table.name} does not support soft deletes.`);
|
|
2586
|
-
}
|
|
2587
|
-
const returningColumns = buildReturningColumns(table);
|
|
2588
|
-
const scopeClauses = [];
|
|
2589
|
-
appendSoftDeleteScope(table, {}, scopeClauses);
|
|
2590
|
-
const scopeSuffix = scopeClauses.length > 0 ? ` AND ${scopeClauses.join(" AND ")}` : "";
|
|
2591
|
-
return {
|
|
2592
|
-
text: `UPDATE ${quoteIdentifier(table.name)} SET ${quoteIdentifier(deletedAtColumn)} = $1 WHERE ${quoteIdentifier(table.primaryKey)} = $2${scopeSuffix} RETURNING ${returningColumns}`,
|
|
2593
|
-
params: [deletedAt, id]
|
|
2594
|
-
};
|
|
2595
|
-
}
|
|
2596
|
-
function buildRestoreByIdQuery(table, id) {
|
|
2597
|
-
const deletedAtColumn = resolveSoftDeleteColumn(table);
|
|
2598
|
-
if (!deletedAtColumn) {
|
|
2599
|
-
throw new Error(`Table ${table.name} does not support soft deletes.`);
|
|
2600
|
-
}
|
|
2601
|
-
const returningColumns = buildReturningColumns(table);
|
|
2602
|
-
return {
|
|
2603
|
-
text: `UPDATE ${quoteIdentifier(table.name)} SET ${quoteIdentifier(deletedAtColumn)} = $1 WHERE ${quoteIdentifier(table.primaryKey)} = $2 AND ${qualifyColumn(table.name, deletedAtColumn)} IS NOT NULL RETURNING ${returningColumns}`,
|
|
2604
|
-
params: [null, id]
|
|
2605
|
-
};
|
|
2606
|
-
}
|
|
2607
|
-
function buildDeleteByIdQuery(table, id) {
|
|
2608
|
-
return {
|
|
2609
|
-
text: `DELETE FROM ${quoteIdentifier(table.name)} WHERE ${quoteIdentifier(table.primaryKey)} = $1 RETURNING ${quoteIdentifier(table.primaryKey)} AS ${quoteIdentifier("deleted_id")}`,
|
|
2610
|
-
params: [id]
|
|
2611
|
-
};
|
|
2612
|
-
}
|
|
2613
|
-
|
|
2614
|
-
// ../../src/core/database/relationships.ts
|
|
2615
|
-
function indexHasManyRelation(parents, children, relation) {
|
|
2616
|
-
const groups = new Map;
|
|
2617
|
-
for (const parent of parents) {
|
|
2618
|
-
groups.set(parent[relation.localKey], []);
|
|
2619
|
-
}
|
|
2620
|
-
for (const child of children) {
|
|
2621
|
-
const key = child[relation.foreignKey];
|
|
2622
|
-
const group = groups.get(key);
|
|
2623
|
-
if (!group) {
|
|
2624
|
-
continue;
|
|
2625
|
-
}
|
|
2626
|
-
group.push(child);
|
|
2627
|
-
}
|
|
2628
|
-
return groups;
|
|
2629
|
-
}
|
|
2630
|
-
function indexBelongsToRelation(children, parents, relation) {
|
|
2631
|
-
const parentsById = new Map;
|
|
2632
|
-
for (const parent of parents) {
|
|
2633
|
-
parentsById.set(parent[relation.ownerKey], parent);
|
|
2634
|
-
}
|
|
2635
|
-
const result = new Map;
|
|
2636
|
-
for (const child of children) {
|
|
2637
|
-
const foreignKey = child[relation.foreignKey];
|
|
2638
|
-
const parent = parentsById.get(foreignKey);
|
|
2639
|
-
if (parent) {
|
|
2640
|
-
result.set(foreignKey, parent);
|
|
2641
|
-
}
|
|
2642
|
-
}
|
|
2643
|
-
return result;
|
|
2644
|
-
}
|
|
2645
|
-
function indexMorphManyRelation(parents, children, relation) {
|
|
2646
|
-
const groups = new Map;
|
|
2647
|
-
for (const parent of parents) {
|
|
2648
|
-
groups.set(parent[relation.localKey], []);
|
|
2649
|
-
}
|
|
2650
|
-
for (const child of children) {
|
|
2651
|
-
if (child[relation.morphTypeKey] !== relation.morphType) {
|
|
2652
|
-
continue;
|
|
2653
|
-
}
|
|
2654
|
-
const key = child[relation.morphIdKey];
|
|
2655
|
-
const group = groups.get(key);
|
|
2656
|
-
if (!group) {
|
|
2657
|
-
continue;
|
|
2658
|
-
}
|
|
2659
|
-
group.push(child);
|
|
2660
|
-
}
|
|
2661
|
-
return groups;
|
|
2662
|
-
}
|
|
2663
|
-
function indexMorphToRelation(children, parentsByType, relation) {
|
|
2664
|
-
const result = new Map;
|
|
2665
|
-
for (const child of children) {
|
|
2666
|
-
const morphType = String(child[relation.morphTypeKey]);
|
|
2667
|
-
const parents = parentsByType.get(morphType);
|
|
2668
|
-
if (!parents) {
|
|
2669
|
-
continue;
|
|
2670
|
-
}
|
|
2671
|
-
const parent = parents.get(child[relation.morphIdKey]);
|
|
2672
|
-
if (parent) {
|
|
2673
|
-
result.set(child[relation.morphIdKey], parent);
|
|
2674
|
-
}
|
|
2675
|
-
}
|
|
2676
|
-
return result;
|
|
2677
|
-
}
|
|
2678
|
-
|
|
2679
|
-
// ../../src/core/database/whereBuilder.ts
|
|
2680
|
-
class WhereBuilder {
|
|
2681
|
-
nodes = [];
|
|
2682
|
-
where(where) {
|
|
2683
|
-
this.nodes.push({ kind: "and", where });
|
|
2684
|
-
return this;
|
|
2685
|
-
}
|
|
2686
|
-
orWhere(where) {
|
|
2687
|
-
this.nodes.push({ kind: "or", where });
|
|
2688
|
-
return this;
|
|
2689
|
-
}
|
|
2690
|
-
whereGroup(fn) {
|
|
2691
|
-
const nested = new WhereBuilder;
|
|
2692
|
-
fn(nested);
|
|
2693
|
-
if (nested.nodes.length > 0) {
|
|
2694
|
-
this.nodes.push({ kind: "and", group: nested.nodes });
|
|
2695
|
-
}
|
|
2696
|
-
return this;
|
|
2697
|
-
}
|
|
2698
|
-
orWhereGroup(fn) {
|
|
2699
|
-
const nested = new WhereBuilder;
|
|
2700
|
-
fn(nested);
|
|
2701
|
-
if (nested.nodes.length > 0) {
|
|
2702
|
-
this.nodes.push({ kind: "or", group: nested.nodes });
|
|
2703
|
-
}
|
|
2704
|
-
return this;
|
|
2705
|
-
}
|
|
2706
|
-
}
|
|
2707
|
-
|
|
2708
|
-
// ../../src/core/database/repositoryQuery.ts
|
|
2709
|
-
class RepositoryQuery {
|
|
2710
|
-
repository;
|
|
2711
|
-
whereClause;
|
|
2712
|
-
queryOptions;
|
|
2713
|
-
eagerLoads = [];
|
|
2714
|
-
whereNodes = [];
|
|
2715
|
-
constructor(repository, whereClause = {}, queryOptions = {}) {
|
|
2716
|
-
this.repository = repository;
|
|
2717
|
-
this.whereClause = whereClause;
|
|
2718
|
-
this.queryOptions = queryOptions;
|
|
2719
|
-
}
|
|
2720
|
-
where(input) {
|
|
2721
|
-
if (typeof input === "function") {
|
|
2722
|
-
const builder = new WhereBuilder;
|
|
2723
|
-
input(builder);
|
|
2724
|
-
this.whereNodes.push(...builder.nodes);
|
|
2725
|
-
return this;
|
|
2726
|
-
}
|
|
2727
|
-
this.whereClause = { ...this.whereClause, ...input };
|
|
2728
|
-
return this;
|
|
2729
|
-
}
|
|
2730
|
-
orWhere(input) {
|
|
2731
|
-
if (typeof input === "function") {
|
|
2732
|
-
const builder = new WhereBuilder;
|
|
2733
|
-
input(builder);
|
|
2734
|
-
if (builder.nodes.length > 0) {
|
|
2735
|
-
this.whereNodes.push({ kind: "or", group: builder.nodes });
|
|
2736
|
-
}
|
|
2737
|
-
return this;
|
|
2738
|
-
}
|
|
2739
|
-
this.whereNodes.push({ kind: "or", where: input });
|
|
2740
|
-
return this;
|
|
2741
|
-
}
|
|
2742
|
-
orderBy(orderBy) {
|
|
2743
|
-
this.queryOptions = { ...this.queryOptions, orderBy };
|
|
2744
|
-
return this;
|
|
2745
|
-
}
|
|
2746
|
-
limit(limit) {
|
|
2747
|
-
this.queryOptions = { ...this.queryOptions, limit };
|
|
2748
|
-
return this;
|
|
2749
|
-
}
|
|
2750
|
-
offset(offset) {
|
|
2751
|
-
this.queryOptions = { ...this.queryOptions, offset };
|
|
2752
|
-
return this;
|
|
2753
|
-
}
|
|
2754
|
-
join(left, right) {
|
|
2755
|
-
return this.addJoin("inner", left, right);
|
|
2756
|
-
}
|
|
2757
|
-
leftJoin(left, right) {
|
|
2758
|
-
return this.addJoin("left", left, right);
|
|
2759
|
-
}
|
|
2760
|
-
groupBy(groupBy) {
|
|
2761
|
-
this.queryOptions = { ...this.queryOptions, groupBy };
|
|
2762
|
-
return this;
|
|
2763
|
-
}
|
|
2764
|
-
having(having) {
|
|
2765
|
-
this.queryOptions = { ...this.queryOptions, having };
|
|
2766
|
-
return this;
|
|
2767
|
-
}
|
|
2768
|
-
withHasMany(as, relation, childRepository, options = {}) {
|
|
2769
|
-
this.eagerLoads.push({
|
|
2770
|
-
kind: "hasMany",
|
|
2771
|
-
as,
|
|
2772
|
-
relation,
|
|
2773
|
-
repository: childRepository,
|
|
2774
|
-
options
|
|
2775
|
-
});
|
|
2776
|
-
return this;
|
|
2777
|
-
}
|
|
2778
|
-
withBelongsTo(as, relation, parentRepository, options = {}) {
|
|
2779
|
-
this.eagerLoads.push({
|
|
2780
|
-
kind: "belongsTo",
|
|
2781
|
-
as,
|
|
2782
|
-
relation,
|
|
2783
|
-
repository: parentRepository,
|
|
2784
|
-
options
|
|
2785
|
-
});
|
|
2786
|
-
return this;
|
|
2787
|
-
}
|
|
2788
|
-
withMorphMany(as, relation, childRepository, options = {}) {
|
|
2789
|
-
this.eagerLoads.push({
|
|
2790
|
-
kind: "morphMany",
|
|
2791
|
-
as,
|
|
2792
|
-
relation,
|
|
2793
|
-
repository: childRepository,
|
|
2794
|
-
options
|
|
2795
|
-
});
|
|
2796
|
-
return this;
|
|
2797
|
-
}
|
|
2798
|
-
withMorphOne(as, relation, childRepository, options = {}) {
|
|
2799
|
-
this.eagerLoads.push({
|
|
2800
|
-
kind: "morphOne",
|
|
2801
|
-
as,
|
|
2802
|
-
relation,
|
|
2803
|
-
repository: childRepository,
|
|
2804
|
-
options
|
|
2805
|
-
});
|
|
2806
|
-
return this;
|
|
2807
|
-
}
|
|
2808
|
-
withMorphTo(as, relation, repositoriesByType, options = {}) {
|
|
2809
|
-
this.eagerLoads.push({
|
|
2810
|
-
kind: "morphTo",
|
|
2811
|
-
as,
|
|
2812
|
-
relation,
|
|
2813
|
-
repository: this.repository,
|
|
2814
|
-
morphRepositories: repositoriesByType,
|
|
2815
|
-
options
|
|
2816
|
-
});
|
|
2817
|
-
return this;
|
|
2818
|
-
}
|
|
2819
|
-
async get() {
|
|
2820
|
-
const rows = await this.repository.findAll(this.buildOptions());
|
|
2821
|
-
return await this.attach(rows);
|
|
2822
|
-
}
|
|
2823
|
-
async first() {
|
|
2824
|
-
const rows = await this.get();
|
|
2825
|
-
return rows[0] ?? null;
|
|
2826
|
-
}
|
|
2827
|
-
async paginate(options) {
|
|
2828
|
-
return await this.repository.paginate({
|
|
2829
|
-
...this.buildOptions(),
|
|
2830
|
-
page: options.page,
|
|
2831
|
-
perPage: options.perPage
|
|
2832
|
-
});
|
|
2833
|
-
}
|
|
2834
|
-
buildOptions() {
|
|
2835
|
-
return {
|
|
2836
|
-
...this.queryOptions,
|
|
2837
|
-
where: this.whereClause,
|
|
2838
|
-
whereNodes: this.whereNodes
|
|
2839
|
-
};
|
|
2840
|
-
}
|
|
2841
|
-
addJoin(type, left, right) {
|
|
2842
|
-
const leftRef = parseQualifiedColumn(left);
|
|
2843
|
-
const rightRef = parseQualifiedColumn(right);
|
|
2844
|
-
const table = type === "inner" ? rightRef.table : rightRef.table;
|
|
2845
|
-
const joins = this.queryOptions.joins ?? [];
|
|
2846
|
-
const existing = joins.find((join3) => join3.table === table && join3.type === type);
|
|
2847
|
-
if (existing) {
|
|
2848
|
-
existing.on.push({ left: leftRef, right: rightRef });
|
|
2849
|
-
return this;
|
|
2850
|
-
}
|
|
2851
|
-
this.queryOptions = {
|
|
2852
|
-
...this.queryOptions,
|
|
2853
|
-
joins: [
|
|
2854
|
-
...joins,
|
|
2855
|
-
{
|
|
2856
|
-
type,
|
|
2857
|
-
table,
|
|
2858
|
-
on: [{ left: leftRef, right: rightRef }]
|
|
2859
|
-
}
|
|
2860
|
-
]
|
|
2861
|
-
};
|
|
2862
|
-
return this;
|
|
2863
|
-
}
|
|
2864
|
-
async attach(rows) {
|
|
2865
|
-
if (rows.length === 0 || this.eagerLoads.length === 0) {
|
|
2866
|
-
return rows.map((row) => ({ ...row }));
|
|
2867
|
-
}
|
|
2868
|
-
let result = rows.map((row) => ({ ...row }));
|
|
2869
|
-
for (const load of this.eagerLoads) {
|
|
2870
|
-
if (load.kind === "hasMany") {
|
|
2871
|
-
const relation2 = load.relation;
|
|
2872
|
-
const grouped2 = await load.repository.withConnection(this.repository.getConnection()).loadHasManyForParents(rows, relation2, load.options);
|
|
2873
|
-
result = result.map((row) => ({
|
|
2874
|
-
...row,
|
|
2875
|
-
[load.as]: grouped2.get(row[relation2.localKey]) ?? []
|
|
2876
|
-
}));
|
|
2877
|
-
continue;
|
|
2878
|
-
}
|
|
2879
|
-
if (load.kind === "morphMany") {
|
|
2880
|
-
const relation2 = load.relation;
|
|
2881
|
-
const grouped2 = await load.repository.withConnection(this.repository.getConnection()).loadMorphManyForParents(rows, relation2, load.options);
|
|
2882
|
-
result = result.map((row) => ({
|
|
2883
|
-
...row,
|
|
2884
|
-
[load.as]: grouped2.get(row[relation2.localKey]) ?? []
|
|
2885
|
-
}));
|
|
2886
|
-
continue;
|
|
2887
|
-
}
|
|
2888
|
-
if (load.kind === "morphOne") {
|
|
2889
|
-
const relation2 = load.relation;
|
|
2890
|
-
const grouped2 = await load.repository.withConnection(this.repository.getConnection()).loadMorphOneForParents(rows, relation2, load.options);
|
|
2891
|
-
result = result.map((row) => ({
|
|
2892
|
-
...row,
|
|
2893
|
-
[load.as]: grouped2.get(row[relation2.localKey])
|
|
2894
|
-
}));
|
|
2895
|
-
continue;
|
|
2896
|
-
}
|
|
2897
|
-
if (load.kind === "morphTo") {
|
|
2898
|
-
const relation2 = load.relation;
|
|
2899
|
-
const grouped2 = await this.repository.loadMorphToForChildren(rows, relation2, load.morphRepositories ?? new Map, load.options);
|
|
2900
|
-
result = result.map((row) => ({
|
|
2901
|
-
...row,
|
|
2902
|
-
[load.as]: grouped2.get(row[relation2.morphIdKey])
|
|
2903
|
-
}));
|
|
2904
|
-
continue;
|
|
2905
|
-
}
|
|
2906
|
-
const relation = load.relation;
|
|
2907
|
-
const grouped = await this.repository.loadBelongsToForParents(rows, relation, load.repository, load.options);
|
|
2908
|
-
result = result.map((row) => ({
|
|
2909
|
-
...row,
|
|
2910
|
-
[load.as]: grouped.get(row[relation.foreignKey])
|
|
2911
|
-
}));
|
|
2912
|
-
}
|
|
2913
|
-
return result;
|
|
2914
|
-
}
|
|
2915
|
-
}
|
|
2916
|
-
|
|
2917
|
-
// ../../src/core/database/baseRepository.ts
|
|
2918
|
-
class BaseRepository5 {
|
|
2919
|
-
table;
|
|
2920
|
-
connection;
|
|
2921
|
-
constructor(table, connection = repositoryConnection) {
|
|
2922
|
-
this.table = table;
|
|
2923
|
-
this.connection = connection;
|
|
2924
|
-
}
|
|
2925
|
-
async findAll(options = {}) {
|
|
2926
|
-
return await withDatabaseErrorHandling(async () => {
|
|
2927
|
-
const { whereNodes, ...queryOptions } = options;
|
|
2928
|
-
const { text, params } = buildSelectQuery(this.table, queryOptions, whereNodes ?? []);
|
|
2929
|
-
return await this.connection.unsafe(text, params);
|
|
2930
|
-
});
|
|
2931
|
-
}
|
|
2932
|
-
async paginate(options) {
|
|
2933
|
-
const { whereNodes, where = {}, page, perPage, ...queryOptions } = options;
|
|
2934
|
-
const total = await this.countWhere(where, {
|
|
2935
|
-
withTrashed: options.withTrashed,
|
|
2936
|
-
onlyTrashed: options.onlyTrashed,
|
|
2937
|
-
joins: options.joins,
|
|
2938
|
-
groupBy: options.groupBy
|
|
2939
|
-
}, whereNodes);
|
|
2940
|
-
const offset = (page - 1) * perPage;
|
|
2941
|
-
const data = await this.findAll({
|
|
2942
|
-
...queryOptions,
|
|
2943
|
-
where,
|
|
2944
|
-
whereNodes,
|
|
2945
|
-
limit: perPage,
|
|
2946
|
-
offset
|
|
2947
|
-
});
|
|
2948
|
-
return {
|
|
2949
|
-
data,
|
|
2950
|
-
meta: buildPaginationMeta({ page, perPage, total })
|
|
2951
|
-
};
|
|
2952
|
-
}
|
|
2953
|
-
async chunk(count, callback, options = {}) {
|
|
2954
|
-
if (!Number.isInteger(count) || count <= 0) {
|
|
2955
|
-
throw new Error("Chunk size must be a positive integer.");
|
|
2956
|
-
}
|
|
2957
|
-
let offset = 0;
|
|
2958
|
-
while (true) {
|
|
2959
|
-
const rows = await this.findAll({
|
|
2960
|
-
...options,
|
|
2961
|
-
limit: count,
|
|
2962
|
-
offset
|
|
2963
|
-
});
|
|
2964
|
-
if (rows.length === 0) {
|
|
2965
|
-
return;
|
|
2966
|
-
}
|
|
2967
|
-
const shouldContinue = await callback(rows);
|
|
2968
|
-
if (shouldContinue === false || rows.length < count) {
|
|
2969
|
-
return;
|
|
2970
|
-
}
|
|
2971
|
-
offset += count;
|
|
2972
|
-
}
|
|
2973
|
-
}
|
|
2974
|
-
async cursorPaginate(options) {
|
|
2975
|
-
const {
|
|
2976
|
-
perPage,
|
|
2977
|
-
cursor,
|
|
2978
|
-
cursorColumn = this.table.primaryKey,
|
|
2979
|
-
direction = "asc",
|
|
2980
|
-
where = {},
|
|
2981
|
-
whereNodes,
|
|
2982
|
-
...queryOptions
|
|
2983
|
-
} = options;
|
|
2984
|
-
if (!Number.isInteger(perPage) || perPage <= 0) {
|
|
2985
|
-
throw new Error("Cursor page size must be a positive integer.");
|
|
2986
|
-
}
|
|
2987
|
-
const cursorWhere = { ...where };
|
|
2988
|
-
if (cursor !== undefined) {
|
|
2989
|
-
cursorWhere[cursorColumn] = direction === "asc" ? { gt: cursor } : { lt: cursor };
|
|
2990
|
-
}
|
|
2991
|
-
const rows = await this.findAll({
|
|
2992
|
-
...queryOptions,
|
|
2993
|
-
where: cursorWhere,
|
|
2994
|
-
whereNodes,
|
|
2995
|
-
orderBy: { [cursorColumn]: direction },
|
|
2996
|
-
limit: perPage + 1
|
|
2997
|
-
});
|
|
2998
|
-
const hasMore = rows.length > perPage;
|
|
2999
|
-
const data = hasMore ? rows.slice(0, perPage) : rows;
|
|
3000
|
-
const nextCursor = hasMore ? data[data.length - 1]?.[cursorColumn] ?? null : null;
|
|
3001
|
-
const prevCursor = cursor ?? null;
|
|
3002
|
-
return {
|
|
3003
|
-
data,
|
|
3004
|
-
meta: {
|
|
3005
|
-
per_page: perPage,
|
|
3006
|
-
next_cursor: nextCursor,
|
|
3007
|
-
prev_cursor: prevCursor,
|
|
3008
|
-
has_more: hasMore
|
|
3009
|
-
}
|
|
3010
|
-
};
|
|
3011
|
-
}
|
|
3012
|
-
async findById(id) {
|
|
3013
|
-
return await this.firstOrNull({
|
|
3014
|
-
[this.table.primaryKey]: id
|
|
3015
|
-
});
|
|
3016
|
-
}
|
|
3017
|
-
async findByIdOrThrow(id, errorFactory) {
|
|
3018
|
-
const record = await this.findById(id);
|
|
3019
|
-
if (record) {
|
|
3020
|
-
return record;
|
|
3021
|
-
}
|
|
3022
|
-
throw errorFactory?.(id) ?? new Error(`${this.table.name} ${String(id)} was not found.`);
|
|
3023
|
-
}
|
|
3024
|
-
async findByIds(ids) {
|
|
3025
|
-
const uniqueIds = [...new Set(ids)];
|
|
3026
|
-
if (uniqueIds.length === 0) {
|
|
3027
|
-
return [];
|
|
3028
|
-
}
|
|
3029
|
-
return await this.findWhere({
|
|
3030
|
-
[this.table.primaryKey]: uniqueIds
|
|
3031
|
-
});
|
|
3032
|
-
}
|
|
3033
|
-
async firstOrNull(where, options = {}) {
|
|
3034
|
-
const [record] = await this.findAll({ ...options, where, limit: 1 });
|
|
3035
|
-
return record ?? null;
|
|
3036
|
-
}
|
|
3037
|
-
async create(values) {
|
|
3038
|
-
return await withDatabaseErrorHandling(async () => {
|
|
3039
|
-
const { text, params } = buildInsertQuery(this.table, values);
|
|
3040
|
-
const [record] = await this.connection.unsafe(text, params);
|
|
3041
|
-
if (!record) {
|
|
3042
|
-
throw new Error(`Insert into ${this.table.name} did not return a record.`);
|
|
3043
|
-
}
|
|
3044
|
-
const entity = record;
|
|
3045
|
-
await eventBus.dispatch(modelEventName(this.table.name, "created"), entity);
|
|
3046
|
-
return entity;
|
|
3047
|
-
});
|
|
3048
|
-
}
|
|
3049
|
-
async updateById(id, changes) {
|
|
3050
|
-
return await withDatabaseErrorHandling(async () => {
|
|
3051
|
-
const { text, params } = buildUpdateQuery(this.table, id, changes);
|
|
3052
|
-
const [record] = await this.connection.unsafe(text, params);
|
|
3053
|
-
const entity = record ?? null;
|
|
3054
|
-
if (entity) {
|
|
3055
|
-
await eventBus.dispatch(modelEventName(this.table.name, "updated"), entity);
|
|
3056
|
-
}
|
|
3057
|
-
return entity;
|
|
3058
|
-
});
|
|
3059
|
-
}
|
|
3060
|
-
async updateByIdOrThrow(id, changes, errorFactory) {
|
|
3061
|
-
const record = await this.updateById(id, changes);
|
|
3062
|
-
if (record) {
|
|
3063
|
-
return record;
|
|
3064
|
-
}
|
|
3065
|
-
throw errorFactory?.(id) ?? new Error(`${this.table.name} ${String(id)} was not found.`);
|
|
3066
|
-
}
|
|
3067
|
-
async deleteById(id) {
|
|
3068
|
-
if (resolveSoftDeleteColumn(this.table)) {
|
|
3069
|
-
return await this.softDeleteById(id);
|
|
3070
|
-
}
|
|
3071
|
-
return await this.forceDeleteById(id);
|
|
3072
|
-
}
|
|
3073
|
-
async softDeleteById(id) {
|
|
3074
|
-
return await withDatabaseErrorHandling(async () => {
|
|
3075
|
-
const { text, params } = buildSoftDeleteByIdQuery(this.table, id, new Date);
|
|
3076
|
-
const [record] = await this.connection.unsafe(text, params);
|
|
3077
|
-
if (!record) {
|
|
3078
|
-
return false;
|
|
3079
|
-
}
|
|
3080
|
-
await eventBus.dispatch(modelEventName(this.table.name, "deleted"), record);
|
|
3081
|
-
return true;
|
|
3082
|
-
});
|
|
3083
|
-
}
|
|
3084
|
-
async forceDeleteById(id) {
|
|
3085
|
-
return await withDatabaseErrorHandling(async () => {
|
|
3086
|
-
const { text, params } = buildDeleteByIdQuery(this.table, id);
|
|
3087
|
-
const [row] = await this.connection.unsafe(text, params);
|
|
3088
|
-
if (!row) {
|
|
3089
|
-
return false;
|
|
3090
|
-
}
|
|
3091
|
-
await eventBus.dispatch(modelEventName(this.table.name, "force-deleted"), {
|
|
3092
|
-
id
|
|
3093
|
-
});
|
|
3094
|
-
return true;
|
|
3095
|
-
});
|
|
3096
|
-
}
|
|
3097
|
-
async restoreById(id) {
|
|
3098
|
-
return await withDatabaseErrorHandling(async () => {
|
|
3099
|
-
const { text, params } = buildRestoreByIdQuery(this.table, id);
|
|
3100
|
-
const [record] = await this.connection.unsafe(text, params);
|
|
3101
|
-
if (!record) {
|
|
3102
|
-
return null;
|
|
3103
|
-
}
|
|
3104
|
-
const entity = record;
|
|
3105
|
-
await eventBus.dispatch(modelEventName(this.table.name, "restored"), entity);
|
|
3106
|
-
return entity;
|
|
3107
|
-
});
|
|
3108
|
-
}
|
|
3109
|
-
withConnection(connection) {
|
|
3110
|
-
const clone = Object.create(Object.getPrototypeOf(this));
|
|
3111
|
-
Object.assign(clone, this);
|
|
3112
|
-
clone.connection = connection;
|
|
3113
|
-
return clone;
|
|
3114
|
-
}
|
|
3115
|
-
getConnection() {
|
|
3116
|
-
return this.connection;
|
|
3117
|
-
}
|
|
3118
|
-
getTable() {
|
|
3119
|
-
return this.table;
|
|
3120
|
-
}
|
|
3121
|
-
query(where = {}) {
|
|
3122
|
-
return new RepositoryQuery(this, where);
|
|
3123
|
-
}
|
|
3124
|
-
async findWhere(where, options = {}) {
|
|
3125
|
-
return await this.findAll({ ...options, where });
|
|
3126
|
-
}
|
|
3127
|
-
async countWhere(where = {}, options = {}, whereNodes = []) {
|
|
3128
|
-
const { text, params } = buildCountQuery(this.table, where, options, whereNodes);
|
|
3129
|
-
const [row] = await this.connection.unsafe(text, params);
|
|
3130
|
-
return Number(row?.count ?? 0);
|
|
3131
|
-
}
|
|
3132
|
-
async averageColumn(column, where = {}) {
|
|
3133
|
-
const qualifiedColumn = qualifyColumn(this.table.name, column);
|
|
3134
|
-
return await this.averageExpression(`AVG(${qualifiedColumn})`, "value", where);
|
|
3135
|
-
}
|
|
3136
|
-
async averageExpression(expression, alias, where = {}) {
|
|
3137
|
-
const { text, params } = buildProjectionQuery(this.table, expression, alias, { where });
|
|
3138
|
-
const [row] = await this.connection.unsafe(text, params);
|
|
3139
|
-
return Math.round(Number(row?.[alias] ?? 0));
|
|
3140
|
-
}
|
|
3141
|
-
async pluckNumberValues(expression, alias, options = {}) {
|
|
3142
|
-
const { text, params } = buildProjectionQuery(this.table, expression, alias, options);
|
|
3143
|
-
const rows = await this.connection.unsafe(text, params);
|
|
3144
|
-
return rows.flatMap((row) => {
|
|
3145
|
-
const value = row[alias];
|
|
3146
|
-
return value === null || value === undefined ? [] : [Number(value)];
|
|
3147
|
-
});
|
|
3148
|
-
}
|
|
3149
|
-
async countGroupedBy(column, where = {}) {
|
|
3150
|
-
const { text, params } = buildGroupedCountQuery(this.table, column, where);
|
|
3151
|
-
const rows = await this.connection.unsafe(text, params);
|
|
3152
|
-
return rows.map(({ value, count }) => ({
|
|
3153
|
-
value,
|
|
3154
|
-
count: Number(count)
|
|
3155
|
-
}));
|
|
3156
|
-
}
|
|
3157
|
-
async findByHasManyRelation(relation, parentId, options = {}) {
|
|
3158
|
-
return await this.findWhere({
|
|
3159
|
-
[relation.foreignKey]: parentId
|
|
3160
|
-
}, options);
|
|
3161
|
-
}
|
|
3162
|
-
async loadHasManyForParents(parents, relation, options = {}) {
|
|
3163
|
-
if (parents.length === 0) {
|
|
3164
|
-
return indexHasManyRelation(parents, [], relation);
|
|
3165
|
-
}
|
|
3166
|
-
const parentIds = [...new Set(parents.map((parent) => parent[relation.localKey]))];
|
|
3167
|
-
const children = await this.findWhere({
|
|
3168
|
-
[relation.foreignKey]: parentIds
|
|
3169
|
-
}, options);
|
|
3170
|
-
return indexHasManyRelation(parents, children, relation);
|
|
3171
|
-
}
|
|
3172
|
-
async loadBelongsToForParents(children, relation, parentRepository, options = {}) {
|
|
3173
|
-
if (children.length === 0) {
|
|
3174
|
-
return new Map;
|
|
3175
|
-
}
|
|
3176
|
-
const ownerIds = [...new Set(children.map((child) => child[relation.foreignKey]))];
|
|
3177
|
-
const parents = await parentRepository.withConnection(this.connection).findWhere({
|
|
3178
|
-
[relation.ownerKey]: ownerIds
|
|
3179
|
-
}, options);
|
|
3180
|
-
return indexBelongsToRelation(children, parents, relation);
|
|
3181
|
-
}
|
|
3182
|
-
async loadMorphManyForParents(parents, relation, options = {}) {
|
|
3183
|
-
if (parents.length === 0) {
|
|
3184
|
-
return indexMorphManyRelation(parents, [], relation);
|
|
3185
|
-
}
|
|
3186
|
-
const parentIds = [...new Set(parents.map((parent) => parent[relation.localKey]))];
|
|
3187
|
-
const children = await this.findWhere({
|
|
3188
|
-
[relation.morphTypeKey]: relation.morphType,
|
|
3189
|
-
[relation.morphIdKey]: parentIds
|
|
3190
|
-
}, options);
|
|
3191
|
-
return indexMorphManyRelation(parents, children, relation);
|
|
3192
|
-
}
|
|
3193
|
-
async loadMorphOneForParents(parents, relation, options = {}) {
|
|
3194
|
-
const grouped = await this.loadMorphManyForParents(parents, relation, options);
|
|
3195
|
-
const result = new Map;
|
|
3196
|
-
for (const parent of parents) {
|
|
3197
|
-
const matches = grouped.get(parent[relation.localKey]) ?? [];
|
|
3198
|
-
result.set(parent[relation.localKey], matches[0]);
|
|
3199
|
-
}
|
|
3200
|
-
return result;
|
|
3201
|
-
}
|
|
3202
|
-
async loadMorphToForChildren(children, relation, repositoriesByType, options = {}) {
|
|
3203
|
-
if (children.length === 0) {
|
|
3204
|
-
return new Map;
|
|
3205
|
-
}
|
|
3206
|
-
const idsByType = new Map;
|
|
3207
|
-
for (const child of children) {
|
|
3208
|
-
const morphType = String(child[relation.morphTypeKey]);
|
|
3209
|
-
const morphId = child[relation.morphIdKey];
|
|
3210
|
-
const ids = idsByType.get(morphType) ?? new Set;
|
|
3211
|
-
ids.add(morphId);
|
|
3212
|
-
idsByType.set(morphType, ids);
|
|
3213
|
-
}
|
|
3214
|
-
const parentsByType = new Map;
|
|
3215
|
-
for (const [morphType, ids] of idsByType) {
|
|
3216
|
-
const repository = repositoriesByType.get(morphType);
|
|
3217
|
-
if (!repository) {
|
|
3218
|
-
continue;
|
|
3219
|
-
}
|
|
3220
|
-
const ownerKey = repository.getTable().primaryKey;
|
|
3221
|
-
const parents = await repository.withConnection(this.connection).findWhere({
|
|
3222
|
-
[ownerKey]: [...ids]
|
|
3223
|
-
}, options);
|
|
3224
|
-
const indexed = new Map;
|
|
3225
|
-
for (const parent of parents) {
|
|
3226
|
-
indexed.set(parent[ownerKey], parent);
|
|
3227
|
-
}
|
|
3228
|
-
parentsByType.set(morphType, indexed);
|
|
3229
|
-
}
|
|
3230
|
-
return indexMorphToRelation(children, parentsByType, relation);
|
|
3231
|
-
}
|
|
3232
|
-
}
|
|
3233
|
-
var baseRepository_default = BaseRepository5;
|
|
3234
|
-
// ../../src/core/database/model.ts
|
|
3235
|
-
var modelRepositories = new WeakMap;
|
|
3236
|
-
var modelGlobalScopes = new WeakMap;
|
|
3237
|
-
var modelBooted = new WeakSet;
|
|
3238
|
-
// ../../src/core/database/schema/columnDefinition.ts
|
|
3239
|
-
class ColumnDefinition {
|
|
3240
|
-
name;
|
|
3241
|
-
kind;
|
|
3242
|
-
length;
|
|
3243
|
-
isNullable = false;
|
|
3244
|
-
isPrimary = false;
|
|
3245
|
-
isUnique = false;
|
|
3246
|
-
autoIncrement = false;
|
|
3247
|
-
defaultValue;
|
|
3248
|
-
checkExpression;
|
|
3249
|
-
foreignKey;
|
|
3250
|
-
constructor(name, kind) {
|
|
3251
|
-
this.name = name;
|
|
3252
|
-
this.kind = kind;
|
|
3253
|
-
}
|
|
3254
|
-
nullable() {
|
|
3255
|
-
this.isNullable = true;
|
|
3256
|
-
return this;
|
|
3257
|
-
}
|
|
3258
|
-
notNullable() {
|
|
3259
|
-
this.isNullable = false;
|
|
3260
|
-
return this;
|
|
3261
|
-
}
|
|
3262
|
-
default(value) {
|
|
3263
|
-
if (typeof value === "boolean") {
|
|
3264
|
-
this.defaultValue = value ? "TRUE" : "FALSE";
|
|
3265
|
-
return this;
|
|
3266
|
-
}
|
|
3267
|
-
if (typeof value === "number") {
|
|
3268
|
-
this.defaultValue = String(value);
|
|
3269
|
-
return this;
|
|
3270
|
-
}
|
|
3271
|
-
this.defaultValue = `'${value.replace(/'/g, "''")}'`;
|
|
3272
|
-
return this;
|
|
3273
|
-
}
|
|
3274
|
-
defaultRaw(expression) {
|
|
3275
|
-
this.defaultValue = expression;
|
|
3276
|
-
return this;
|
|
3277
|
-
}
|
|
3278
|
-
unique() {
|
|
3279
|
-
this.isUnique = true;
|
|
3280
|
-
return this;
|
|
3281
|
-
}
|
|
3282
|
-
primary() {
|
|
3283
|
-
this.isPrimary = true;
|
|
3284
|
-
return this;
|
|
3285
|
-
}
|
|
3286
|
-
check(expression) {
|
|
3287
|
-
this.checkExpression = expression;
|
|
3288
|
-
return this;
|
|
3289
|
-
}
|
|
3290
|
-
}
|
|
3291
|
-
|
|
3292
|
-
class ForeignIdColumnDefinition extends ColumnDefinition {
|
|
3293
|
-
constructor(name) {
|
|
3294
|
-
super(name, "foreignId");
|
|
3295
|
-
this.notNullable();
|
|
3296
|
-
}
|
|
3297
|
-
references(table, column = "id") {
|
|
3298
|
-
this.foreignKey = {
|
|
3299
|
-
referencesTable: table,
|
|
3300
|
-
referencesColumn: column
|
|
3301
|
-
};
|
|
3302
|
-
return this;
|
|
3303
|
-
}
|
|
3304
|
-
constrained(table) {
|
|
3305
|
-
const referencesTable = table ?? inferReferencedTable(this.name);
|
|
3306
|
-
return this.references(referencesTable);
|
|
3307
|
-
}
|
|
3308
|
-
cascadeOnDelete() {
|
|
3309
|
-
if (!this.foreignKey) {
|
|
3310
|
-
throw new Error(`Foreign key is not defined for column ${this.name}`);
|
|
3311
|
-
}
|
|
3312
|
-
this.foreignKey.onDelete = "cascade";
|
|
3313
|
-
return this;
|
|
3314
|
-
}
|
|
3315
|
-
nullOnDelete() {
|
|
3316
|
-
if (!this.foreignKey) {
|
|
3317
|
-
throw new Error(`Foreign key is not defined for column ${this.name}`);
|
|
3318
|
-
}
|
|
3319
|
-
this.foreignKey.onDelete = "set null";
|
|
3320
|
-
return this;
|
|
3321
|
-
}
|
|
3322
|
-
}
|
|
3323
|
-
function inferReferencedTable(columnName) {
|
|
3324
|
-
if (!columnName.endsWith("_id")) {
|
|
3325
|
-
throw new Error(`Cannot infer referenced table from column ${columnName}`);
|
|
3326
|
-
}
|
|
3327
|
-
return columnName.slice(0, -3);
|
|
3328
|
-
}
|
|
3329
|
-
|
|
3330
|
-
// ../../src/core/database/schema/blueprint.ts
|
|
3331
|
-
class Blueprint {
|
|
3332
|
-
table;
|
|
3333
|
-
action;
|
|
3334
|
-
columns = [];
|
|
3335
|
-
indexes = [];
|
|
3336
|
-
droppedColumns = [];
|
|
3337
|
-
droppedIndexes = [];
|
|
3338
|
-
constructor(table, action) {
|
|
3339
|
-
this.table = table;
|
|
3340
|
-
this.action = action;
|
|
3341
|
-
}
|
|
3342
|
-
id(name = "id") {
|
|
3343
|
-
const column = new ColumnDefinition(name, "id");
|
|
3344
|
-
column.primary();
|
|
3345
|
-
column.autoIncrement = true;
|
|
3346
|
-
this.columns.push(column);
|
|
3347
|
-
return column;
|
|
3348
|
-
}
|
|
3349
|
-
string(name, length) {
|
|
3350
|
-
const column = new ColumnDefinition(name, "string");
|
|
3351
|
-
column.length = length;
|
|
3352
|
-
column.notNullable();
|
|
3353
|
-
this.columns.push(column);
|
|
3354
|
-
return column;
|
|
3355
|
-
}
|
|
3356
|
-
text(name) {
|
|
3357
|
-
const column = new ColumnDefinition(name, "text");
|
|
3358
|
-
column.notNullable();
|
|
3359
|
-
this.columns.push(column);
|
|
3360
|
-
return column;
|
|
3361
|
-
}
|
|
3362
|
-
boolean(name) {
|
|
3363
|
-
const column = new ColumnDefinition(name, "boolean");
|
|
3364
|
-
column.notNullable();
|
|
3365
|
-
this.columns.push(column);
|
|
3366
|
-
return column;
|
|
3367
|
-
}
|
|
3368
|
-
integer(name) {
|
|
3369
|
-
const column = new ColumnDefinition(name, "integer");
|
|
3370
|
-
column.notNullable();
|
|
3371
|
-
this.columns.push(column);
|
|
3372
|
-
return column;
|
|
3373
|
-
}
|
|
3374
|
-
bigInteger(name) {
|
|
3375
|
-
const column = new ColumnDefinition(name, "bigInteger");
|
|
3376
|
-
column.notNullable();
|
|
3377
|
-
this.columns.push(column);
|
|
3378
|
-
return column;
|
|
3379
|
-
}
|
|
3380
|
-
timestamp(name) {
|
|
3381
|
-
const column = new ColumnDefinition(name, "timestamp");
|
|
3382
|
-
column.notNullable();
|
|
3383
|
-
this.columns.push(column);
|
|
3384
|
-
return column;
|
|
3385
|
-
}
|
|
3386
|
-
json(name) {
|
|
3387
|
-
const column = new ColumnDefinition(name, "json");
|
|
3388
|
-
column.notNullable();
|
|
3389
|
-
this.columns.push(column);
|
|
3390
|
-
return column;
|
|
3391
|
-
}
|
|
3392
|
-
jsonb(name) {
|
|
3393
|
-
const column = new ColumnDefinition(name, "jsonb");
|
|
3394
|
-
column.notNullable();
|
|
3395
|
-
this.columns.push(column);
|
|
3396
|
-
return column;
|
|
3397
|
-
}
|
|
3398
|
-
foreignId(name) {
|
|
3399
|
-
const column = new ForeignIdColumnDefinition(name);
|
|
3400
|
-
this.columns.push(column);
|
|
3401
|
-
return column;
|
|
3402
|
-
}
|
|
3403
|
-
timestamps() {
|
|
3404
|
-
this.timestamp("created_at").defaultRaw("NOW()");
|
|
3405
|
-
this.timestamp("updated_at").defaultRaw("NOW()");
|
|
3406
|
-
}
|
|
3407
|
-
softDeletes() {
|
|
3408
|
-
this.timestamp("deleted_at").nullable();
|
|
3409
|
-
}
|
|
3410
|
-
dropColumn(name) {
|
|
3411
|
-
this.droppedColumns.push(name);
|
|
3412
|
-
}
|
|
3413
|
-
dropSoftDeletes() {
|
|
3414
|
-
this.dropColumn("deleted_at");
|
|
3415
|
-
this.dropIndex(`idx_${this.table}_deleted_at`);
|
|
3416
|
-
}
|
|
3417
|
-
dropIndex(name) {
|
|
3418
|
-
this.droppedIndexes.push(name);
|
|
3419
|
-
}
|
|
3420
|
-
unique(columns, name) {
|
|
3421
|
-
this.indexes.push({
|
|
3422
|
-
name,
|
|
3423
|
-
columns: Array.isArray(columns) ? columns : [columns],
|
|
3424
|
-
kind: "unique"
|
|
3425
|
-
});
|
|
3426
|
-
}
|
|
3427
|
-
index(columns, options = {}) {
|
|
3428
|
-
this.indexes.push({
|
|
3429
|
-
name: options.name,
|
|
3430
|
-
columns: Array.isArray(columns) ? columns : [columns],
|
|
3431
|
-
kind: "index",
|
|
3432
|
-
order: options.order
|
|
3433
|
-
});
|
|
3434
|
-
}
|
|
3435
|
-
partialIndex(columns, where, nameOrOptions) {
|
|
3436
|
-
const options = typeof nameOrOptions === "string" ? { name: nameOrOptions } : nameOrOptions ?? {};
|
|
3437
|
-
this.indexes.push({
|
|
3438
|
-
name: options.name,
|
|
3439
|
-
columns: Array.isArray(columns) ? columns : [columns],
|
|
3440
|
-
kind: options.unique ? "uniquePartial" : "partial",
|
|
3441
|
-
where
|
|
3442
|
-
});
|
|
3443
|
-
}
|
|
3444
|
-
fullText(columns, name) {
|
|
3445
|
-
this.indexes.push({
|
|
3446
|
-
name,
|
|
3447
|
-
columns: Array.isArray(columns) ? columns : [columns],
|
|
3448
|
-
kind: "fullText"
|
|
3449
|
-
});
|
|
3450
|
-
}
|
|
3451
|
-
ginIndex(column, name) {
|
|
3452
|
-
this.indexes.push({
|
|
3453
|
-
name,
|
|
3454
|
-
columns: [column],
|
|
3455
|
-
kind: "gin"
|
|
3456
|
-
});
|
|
3457
|
-
}
|
|
3458
|
-
}
|
|
3459
|
-
// ../../src/core/database/schema/errors.ts
|
|
3460
|
-
class UnsupportedSchemaFeatureError extends Error {
|
|
3461
|
-
constructor(feature, driver) {
|
|
3462
|
-
super(`${feature} is not supported for the ${driver} driver`);
|
|
3463
|
-
this.name = "UnsupportedSchemaFeatureError";
|
|
3464
|
-
}
|
|
3465
|
-
}
|
|
3466
|
-
// ../../src/core/database/schema/grammars/grammar.ts
|
|
3467
|
-
function compileColumnType(driver, column) {
|
|
3468
|
-
switch (column.kind) {
|
|
3469
|
-
case "id":
|
|
3470
|
-
return compileIdType(driver);
|
|
3471
|
-
case "string":
|
|
3472
|
-
return compileStringType(driver, column.length);
|
|
3473
|
-
case "text":
|
|
3474
|
-
return compileTextType(driver);
|
|
3475
|
-
case "boolean":
|
|
3476
|
-
return compileBooleanType(driver);
|
|
3477
|
-
case "integer":
|
|
3478
|
-
case "foreignId":
|
|
3479
|
-
return compileIntegerType(driver);
|
|
3480
|
-
case "bigInteger":
|
|
3481
|
-
return compileBigIntegerType(driver);
|
|
3482
|
-
case "timestamp":
|
|
3483
|
-
return compileTimestampType(driver);
|
|
3484
|
-
case "json":
|
|
3485
|
-
return compileJsonType(driver);
|
|
3486
|
-
case "jsonb":
|
|
3487
|
-
return compileJsonbType(driver);
|
|
3488
|
-
default:
|
|
3489
|
-
throw new Error(`Unsupported column kind: ${column.kind}`);
|
|
3490
|
-
}
|
|
3491
|
-
}
|
|
3492
|
-
function compileIdType(driver) {
|
|
3493
|
-
switch (driver) {
|
|
3494
|
-
case "pgsql":
|
|
3495
|
-
return "SERIAL";
|
|
3496
|
-
case "mysql":
|
|
3497
|
-
return "BIGINT UNSIGNED";
|
|
3498
|
-
case "sqlite":
|
|
3499
|
-
return "INTEGER";
|
|
3500
|
-
}
|
|
3501
|
-
}
|
|
3502
|
-
function compileStringType(driver, length) {
|
|
3503
|
-
switch (driver) {
|
|
3504
|
-
case "pgsql":
|
|
3505
|
-
return "TEXT";
|
|
3506
|
-
case "mysql":
|
|
3507
|
-
return length ? `VARCHAR(${length})` : "VARCHAR(255)";
|
|
3508
|
-
case "sqlite":
|
|
3509
|
-
return "TEXT";
|
|
3510
|
-
}
|
|
3511
|
-
}
|
|
3512
|
-
function compileTextType(driver) {
|
|
3513
|
-
switch (driver) {
|
|
3514
|
-
case "pgsql":
|
|
3515
|
-
case "sqlite":
|
|
3516
|
-
return "TEXT";
|
|
3517
|
-
case "mysql":
|
|
3518
|
-
return "TEXT";
|
|
3519
|
-
}
|
|
3520
|
-
}
|
|
3521
|
-
function compileBooleanType(driver) {
|
|
3522
|
-
switch (driver) {
|
|
3523
|
-
case "pgsql":
|
|
3524
|
-
return "BOOLEAN";
|
|
3525
|
-
case "mysql":
|
|
3526
|
-
return "BOOLEAN";
|
|
3527
|
-
case "sqlite":
|
|
3528
|
-
return "INTEGER";
|
|
3529
|
-
}
|
|
3530
|
-
}
|
|
3531
|
-
function compileIntegerType(driver) {
|
|
3532
|
-
switch (driver) {
|
|
3533
|
-
case "pgsql":
|
|
3534
|
-
return "INTEGER";
|
|
3535
|
-
case "mysql":
|
|
3536
|
-
return "INT";
|
|
3537
|
-
case "sqlite":
|
|
3538
|
-
return "INTEGER";
|
|
3539
|
-
}
|
|
3540
|
-
}
|
|
3541
|
-
function compileBigIntegerType(driver) {
|
|
3542
|
-
switch (driver) {
|
|
3543
|
-
case "pgsql":
|
|
3544
|
-
return "BIGINT";
|
|
3545
|
-
case "mysql":
|
|
3546
|
-
return "BIGINT";
|
|
3547
|
-
case "sqlite":
|
|
3548
|
-
return "INTEGER";
|
|
3549
|
-
}
|
|
3550
|
-
}
|
|
3551
|
-
function compileTimestampType(driver) {
|
|
3552
|
-
switch (driver) {
|
|
3553
|
-
case "pgsql":
|
|
3554
|
-
return "TIMESTAMPTZ";
|
|
3555
|
-
case "mysql":
|
|
3556
|
-
return "TIMESTAMP";
|
|
3557
|
-
case "sqlite":
|
|
3558
|
-
return "TEXT";
|
|
3559
|
-
}
|
|
3560
|
-
}
|
|
3561
|
-
function compileJsonType(driver) {
|
|
3562
|
-
switch (driver) {
|
|
3563
|
-
case "pgsql":
|
|
3564
|
-
return "JSONB";
|
|
3565
|
-
case "mysql":
|
|
3566
|
-
return "JSON";
|
|
3567
|
-
case "sqlite":
|
|
3568
|
-
return "TEXT";
|
|
3569
|
-
}
|
|
3570
|
-
}
|
|
3571
|
-
function compileJsonbType(driver) {
|
|
3572
|
-
switch (driver) {
|
|
3573
|
-
case "pgsql":
|
|
3574
|
-
return "JSONB";
|
|
3575
|
-
case "mysql":
|
|
3576
|
-
return "JSON";
|
|
3577
|
-
case "sqlite":
|
|
3578
|
-
return "TEXT";
|
|
3579
|
-
}
|
|
3580
|
-
}
|
|
3581
|
-
|
|
3582
|
-
// ../../src/core/database/schema/grammars/compileStatements.ts
|
|
3583
|
-
function compileCreateTable(driver, blueprint) {
|
|
3584
|
-
const table = quoteIdentifier(blueprint.table);
|
|
3585
|
-
const parts = blueprint.columns.map((column) => compileColumn(driver, column, "create"));
|
|
3586
|
-
for (const index of blueprint.indexes) {
|
|
3587
|
-
if (index.kind === "unique" && index.columns.length > 1) {
|
|
3588
|
-
const columns = index.columns.map((column) => quoteIdentifier(column)).join(", ");
|
|
3589
|
-
parts.push(`UNIQUE (${columns})`);
|
|
3590
|
-
}
|
|
3591
|
-
}
|
|
3592
|
-
const statements = [`CREATE TABLE IF NOT EXISTS ${table} (
|
|
3593
|
-
${parts.join(`,
|
|
3594
|
-
`)}
|
|
3595
|
-
)`];
|
|
3596
|
-
for (const index of blueprint.indexes) {
|
|
3597
|
-
if (index.kind === "unique" && index.columns.length === 1) {
|
|
3598
|
-
continue;
|
|
3599
|
-
}
|
|
3600
|
-
if (index.kind === "index") {
|
|
3601
|
-
statements.push(compileIndex(driver, blueprint.table, index));
|
|
3602
|
-
} else if (index.kind === "partial" || index.kind === "uniquePartial" || index.kind === "gin" || index.kind === "fullText") {
|
|
3603
|
-
statements.push(...compileSpecialIndex(driver, blueprint.table, index));
|
|
3604
|
-
}
|
|
3605
|
-
}
|
|
3606
|
-
return statements;
|
|
3607
|
-
}
|
|
3608
|
-
function compileAlterTable(driver, blueprint) {
|
|
3609
|
-
const statements = [];
|
|
3610
|
-
const table = quoteIdentifier(blueprint.table);
|
|
3611
|
-
for (const column of blueprint.columns) {
|
|
3612
|
-
const addPrefix = driver === "pgsql" ? "ADD COLUMN IF NOT EXISTS" : "ADD COLUMN";
|
|
3613
|
-
statements.push(`ALTER TABLE ${table}
|
|
3614
|
-
${addPrefix} ${compileColumn(driver, column, "alter")}`);
|
|
3615
|
-
}
|
|
3616
|
-
for (const columnName of blueprint.droppedColumns) {
|
|
3617
|
-
const dropPrefix = driver === "pgsql" ? "DROP COLUMN IF EXISTS" : "DROP COLUMN";
|
|
3618
|
-
statements.push(`ALTER TABLE ${table} ${dropPrefix} ${quoteIdentifier(columnName)}`);
|
|
3619
|
-
}
|
|
3620
|
-
for (const indexName of blueprint.droppedIndexes) {
|
|
3621
|
-
statements.push(`DROP INDEX IF EXISTS ${quoteIdentifier(indexName)}`);
|
|
3622
|
-
}
|
|
3623
|
-
for (const index of blueprint.indexes) {
|
|
3624
|
-
if (index.kind === "index" || index.kind === "unique") {
|
|
3625
|
-
statements.push(compileIndex(driver, blueprint.table, index));
|
|
3626
|
-
} else {
|
|
3627
|
-
statements.push(...compileSpecialIndex(driver, blueprint.table, index));
|
|
3628
|
-
}
|
|
3629
|
-
}
|
|
3630
|
-
return statements;
|
|
3631
|
-
}
|
|
3632
|
-
function compileDropTable(driver, tableName) {
|
|
3633
|
-
const cascade = driver === "pgsql" ? " CASCADE" : "";
|
|
3634
|
-
return [`DROP TABLE IF EXISTS ${quoteIdentifier(tableName)}${cascade}`];
|
|
3635
|
-
}
|
|
3636
|
-
function compileColumn(driver, column, mode) {
|
|
3637
|
-
const parts = [quoteIdentifier(column.name), compileColumnType(driver, column)];
|
|
3638
|
-
if (column.autoIncrement && driver === "mysql") {
|
|
3639
|
-
parts[1] = `${parts[1]} AUTO_INCREMENT`;
|
|
3640
|
-
}
|
|
3641
|
-
if (column.isPrimary && mode === "create") {
|
|
3642
|
-
if (driver === "sqlite") {
|
|
3643
|
-
parts.push("PRIMARY KEY AUTOINCREMENT");
|
|
3644
|
-
} else {
|
|
3645
|
-
parts.push("PRIMARY KEY");
|
|
3646
|
-
}
|
|
3647
|
-
} else if (!column.isNullable) {
|
|
3648
|
-
parts.push("NOT NULL");
|
|
3649
|
-
} else if (column.isNullable) {
|
|
3650
|
-
parts.push("NULL");
|
|
3651
|
-
}
|
|
3652
|
-
if (column.defaultValue !== undefined) {
|
|
3653
|
-
parts.push(`DEFAULT ${column.defaultValue}`);
|
|
3654
|
-
}
|
|
3655
|
-
if (column.isUnique) {
|
|
3656
|
-
parts.push("UNIQUE");
|
|
3657
|
-
}
|
|
3658
|
-
if (column.checkExpression) {
|
|
3659
|
-
parts.push(`CHECK (${column.checkExpression})`);
|
|
3660
|
-
}
|
|
3661
|
-
if (column.foreignKey) {
|
|
3662
|
-
const { referencesTable, referencesColumn, onDelete } = column.foreignKey;
|
|
3663
|
-
const reference = `${quoteIdentifier(referencesTable)}(${quoteIdentifier(referencesColumn)})`;
|
|
3664
|
-
let clause = `REFERENCES ${reference}`;
|
|
3665
|
-
if (onDelete === "cascade") {
|
|
3666
|
-
clause += " ON DELETE CASCADE";
|
|
3667
|
-
} else if (onDelete === "set null") {
|
|
3668
|
-
clause += " ON DELETE SET NULL";
|
|
3669
|
-
}
|
|
3670
|
-
parts.push(clause);
|
|
3671
|
-
}
|
|
3672
|
-
return parts.join(" ");
|
|
3673
|
-
}
|
|
3674
|
-
function compileIndex(_driver, tableName, index) {
|
|
3675
|
-
const indexName = index.name ?? defaultIndexName(tableName, index.columns, index.kind === "unique" ? "unique" : "index");
|
|
3676
|
-
const columns = index.columns.map((column) => {
|
|
3677
|
-
const quoted = quoteIdentifier(column);
|
|
3678
|
-
if (index.order === "desc") {
|
|
3679
|
-
return `${quoted} DESC`;
|
|
3680
|
-
}
|
|
3681
|
-
return quoted;
|
|
3682
|
-
}).join(", ");
|
|
3683
|
-
const unique = index.kind === "unique" ? "UNIQUE " : "";
|
|
3684
|
-
return `CREATE ${unique}INDEX IF NOT EXISTS ${quoteIdentifier(indexName)} ON ${quoteIdentifier(tableName)}(${columns})`;
|
|
3685
|
-
}
|
|
3686
|
-
function compileSpecialIndex(driver, tableName, index) {
|
|
3687
|
-
const indexName = index.name ?? defaultIndexName(tableName, index.columns, index.kind);
|
|
3688
|
-
const columns = index.columns.map((column) => quoteIdentifier(column)).join(", ");
|
|
3689
|
-
switch (index.kind) {
|
|
3690
|
-
case "partial":
|
|
3691
|
-
case "uniquePartial": {
|
|
3692
|
-
if (driver !== "pgsql") {
|
|
3693
|
-
throw new UnsupportedSchemaFeatureError("partialIndex()", driver);
|
|
3694
|
-
}
|
|
3695
|
-
const unique = index.kind === "uniquePartial" ? "UNIQUE " : "";
|
|
3696
|
-
return [
|
|
3697
|
-
`CREATE ${unique}INDEX IF NOT EXISTS ${quoteIdentifier(indexName)} ON ${quoteIdentifier(tableName)}(${columns}) WHERE ${index.where}`
|
|
3698
|
-
];
|
|
3699
|
-
}
|
|
3700
|
-
case "gin": {
|
|
3701
|
-
if (driver !== "pgsql") {
|
|
3702
|
-
throw new UnsupportedSchemaFeatureError("ginIndex()", driver);
|
|
3703
|
-
}
|
|
3704
|
-
return [
|
|
3705
|
-
`CREATE INDEX IF NOT EXISTS ${quoteIdentifier(indexName)} ON ${quoteIdentifier(tableName)} USING GIN(${columns})`
|
|
3706
|
-
];
|
|
3707
|
-
}
|
|
3708
|
-
case "fullText": {
|
|
3709
|
-
if (driver === "mysql") {
|
|
3710
|
-
return [
|
|
3711
|
-
`CREATE FULLTEXT INDEX ${quoteIdentifier(indexName)} ON ${quoteIdentifier(tableName)}(${columns})`
|
|
3712
|
-
];
|
|
3713
|
-
}
|
|
3714
|
-
if (driver === "pgsql") {
|
|
3715
|
-
throw new UnsupportedSchemaFeatureError("fullText(); use ginIndex() with a tsvector column on PostgreSQL", driver);
|
|
3716
|
-
}
|
|
3717
|
-
throw new UnsupportedSchemaFeatureError("fullText()", driver);
|
|
3718
|
-
}
|
|
3719
|
-
default:
|
|
3720
|
-
return [];
|
|
3721
|
-
}
|
|
3722
|
-
}
|
|
3723
|
-
function defaultIndexName(tableName, columns, kind) {
|
|
3724
|
-
return `idx_${tableName}_${columns.join("_")}_${kind}`;
|
|
3725
|
-
}
|
|
3726
|
-
function compileBlueprint(driver, blueprint) {
|
|
3727
|
-
switch (blueprint.action) {
|
|
3728
|
-
case "create":
|
|
3729
|
-
return compileCreateTable(driver, blueprint);
|
|
3730
|
-
case "alter":
|
|
3731
|
-
return compileAlterTable(driver, blueprint);
|
|
3732
|
-
case "drop":
|
|
3733
|
-
return compileDropTable(driver, blueprint.table);
|
|
3734
|
-
default:
|
|
3735
|
-
throw new Error(`Unsupported blueprint action: ${blueprint.action}`);
|
|
3736
|
-
}
|
|
3737
|
-
}
|
|
3738
|
-
// ../../src/core/database/schema/grammars/createGrammar.ts
|
|
3739
|
-
function createGrammar(driver) {
|
|
3740
|
-
return {
|
|
3741
|
-
driver,
|
|
3742
|
-
compile(blueprint) {
|
|
3743
|
-
return compileBlueprint(driver, blueprint);
|
|
3744
|
-
}
|
|
3745
|
-
};
|
|
3746
|
-
}
|
|
3747
|
-
|
|
3748
|
-
// ../../src/core/database/schema/grammars/mysqlGrammar.ts
|
|
3749
|
-
var MySqlGrammar = createGrammar("mysql");
|
|
3750
|
-
|
|
3751
|
-
// ../../src/core/database/schema/grammars/postgresGrammar.ts
|
|
3752
|
-
var PostgresGrammar = createGrammar("pgsql");
|
|
3753
|
-
|
|
3754
|
-
// ../../src/core/database/schema/grammars/sqliteGrammar.ts
|
|
3755
|
-
var SqliteGrammar = createGrammar("sqlite");
|
|
3756
|
-
|
|
3757
|
-
// ../../src/core/database/schema/grammars/index.ts
|
|
3758
|
-
function grammarForDriver(driver) {
|
|
3759
|
-
switch (driver) {
|
|
3760
|
-
case "pgsql":
|
|
3761
|
-
return PostgresGrammar;
|
|
3762
|
-
case "mysql":
|
|
3763
|
-
return MySqlGrammar;
|
|
3764
|
-
case "sqlite":
|
|
3765
|
-
return SqliteGrammar;
|
|
3766
|
-
default:
|
|
3767
|
-
throw new Error(`Unsupported database driver: ${driver}`);
|
|
3768
|
-
}
|
|
3769
|
-
}
|
|
3770
|
-
// ../../src/core/database/schema/schema.ts
|
|
3771
|
-
class SchemaBuilder {
|
|
3772
|
-
#driver;
|
|
3773
|
-
#statements = [];
|
|
3774
|
-
constructor(driver) {
|
|
3775
|
-
this.#driver = driver;
|
|
3776
|
-
}
|
|
3777
|
-
create(table, callback) {
|
|
3778
|
-
const blueprint = new Blueprint(table, "create");
|
|
3779
|
-
callback(blueprint);
|
|
3780
|
-
this.#statements.push(...grammarForDriver(this.#driver).compile(blueprint));
|
|
3781
|
-
return this;
|
|
3782
|
-
}
|
|
3783
|
-
table(table, callback) {
|
|
3784
|
-
const blueprint = new Blueprint(table, "alter");
|
|
3785
|
-
callback(blueprint);
|
|
3786
|
-
this.#statements.push(...grammarForDriver(this.#driver).compile(blueprint));
|
|
3787
|
-
return this;
|
|
3788
|
-
}
|
|
3789
|
-
drop(table) {
|
|
3790
|
-
const blueprint = new Blueprint(table, "drop");
|
|
3791
|
-
this.#statements.push(...grammarForDriver(this.#driver).compile(blueprint));
|
|
3792
|
-
return this;
|
|
3793
|
-
}
|
|
3794
|
-
toSql() {
|
|
3795
|
-
return [...this.#statements];
|
|
3796
|
-
}
|
|
3797
|
-
async execute(db2) {
|
|
3798
|
-
for (const statement of this.#statements) {
|
|
3799
|
-
await db2.unsafe(statement);
|
|
3800
|
-
}
|
|
3801
|
-
}
|
|
3802
|
-
}
|
|
3803
|
-
// ../../src/core/database/table.ts
|
|
3804
|
-
function defineTable5(definition) {
|
|
3805
|
-
return definition;
|
|
3806
|
-
}
|
|
3807
|
-
// ../../src/core/queue/failedJobTable.ts
|
|
3808
|
-
var failedJobTable = defineTable5({
|
|
3809
|
-
name: "failed_job",
|
|
3810
|
-
primaryKey: "id",
|
|
3811
|
-
columns: ["id", "job_name", "payload", "exception", "failed_at"],
|
|
3812
|
-
defaultOrderBy: { column: "failed_at", direction: "DESC" }
|
|
3813
|
-
});
|
|
3814
|
-
|
|
3815
|
-
// ../../src/core/queue/failedJobRepository.ts
|
|
3816
|
-
class FailedJobRepository extends baseRepository_default {
|
|
3817
|
-
constructor() {
|
|
3818
|
-
super(failedJobTable);
|
|
3819
|
-
}
|
|
3820
|
-
}
|
|
3821
|
-
var failedJobRepository_default = FailedJobRepository;
|
|
3822
|
-
|
|
3823
|
-
// ../../src/core/queue/failedJobService.ts
|
|
3824
|
-
class FailedJobService {
|
|
3825
|
-
repository;
|
|
3826
|
-
constructor(repository) {
|
|
3827
|
-
this.repository = repository;
|
|
3828
|
-
}
|
|
3829
|
-
async recordFailure(input) {
|
|
3830
|
-
return await this.repository.create({
|
|
3831
|
-
job_name: input.jobName,
|
|
3832
|
-
payload: input.payload,
|
|
3833
|
-
exception: input.exception,
|
|
3834
|
-
failed_at: new Date
|
|
3835
|
-
});
|
|
3836
|
-
}
|
|
3837
|
-
listRecent(limit = 50) {
|
|
3838
|
-
return this.repository.findAll({
|
|
3839
|
-
limit,
|
|
3840
|
-
orderBy: { column: "failed_at", direction: "DESC" }
|
|
3841
|
-
});
|
|
3842
|
-
}
|
|
3843
|
-
async retry(id) {
|
|
3844
|
-
const failedJob = await this.repository.findByIdOrThrow(id, (jobId) => new Error(`Failed job ${jobId} not found.`));
|
|
3845
|
-
await this.repository.deleteById(id);
|
|
3846
|
-
return failedJob;
|
|
3847
|
-
}
|
|
3848
|
-
async delete(id) {
|
|
3849
|
-
const deleted = await this.repository.deleteById(id);
|
|
3850
|
-
if (!deleted) {
|
|
3851
|
-
throw new Error(`Failed job ${id} not found.`);
|
|
3852
|
-
}
|
|
3853
|
-
}
|
|
3854
|
-
async flush() {
|
|
3855
|
-
const jobs = await this.repository.findAll();
|
|
3856
|
-
let deleted = 0;
|
|
3857
|
-
for (const job of jobs) {
|
|
3858
|
-
if (await this.repository.deleteById(job.id)) {
|
|
3859
|
-
deleted += 1;
|
|
3860
|
-
}
|
|
3861
|
-
}
|
|
3862
|
-
return deleted;
|
|
3863
|
-
}
|
|
3864
|
-
}
|
|
3865
|
-
var failedJobService_default = FailedJobService;
|
|
3866
|
-
|
|
3867
|
-
// ../../src/core/queue/jobRegistry.ts
|
|
3868
|
-
class JobRegistry {
|
|
3869
|
-
factories = new Map;
|
|
3870
|
-
instances = new WeakMap;
|
|
3871
|
-
register(name, factory) {
|
|
3872
|
-
this.factories.set(name, factory);
|
|
3873
|
-
}
|
|
3874
|
-
resolveName(job) {
|
|
3875
|
-
return this.instances.get(job);
|
|
3876
|
-
}
|
|
3877
|
-
track(name, job) {
|
|
3878
|
-
this.instances.set(job, name);
|
|
3879
|
-
return job;
|
|
3880
|
-
}
|
|
3881
|
-
create(name) {
|
|
3882
|
-
const factory = this.factories.get(name);
|
|
3883
|
-
if (!factory) {
|
|
3884
|
-
return;
|
|
3885
|
-
}
|
|
3886
|
-
return factory();
|
|
3887
|
-
}
|
|
3888
|
-
names() {
|
|
3889
|
-
return [...this.factories.keys()];
|
|
3890
|
-
}
|
|
3891
|
-
}
|
|
3892
|
-
var JOB_REGISTRY_KEY = Symbol.for("@getstrata/jobRegistry");
|
|
3893
|
-
function readSharedJobRegistry() {
|
|
3894
|
-
const globalRegistry = globalThis[JOB_REGISTRY_KEY];
|
|
3895
|
-
if (globalRegistry) {
|
|
3896
|
-
return globalRegistry;
|
|
3897
|
-
}
|
|
3898
|
-
const registry = new JobRegistry;
|
|
3899
|
-
globalThis[JOB_REGISTRY_KEY] = registry;
|
|
3900
|
-
return registry;
|
|
3901
|
-
}
|
|
3902
|
-
var jobRegistry = readSharedJobRegistry();
|
|
3903
|
-
|
|
3904
|
-
// ../../src/core/queue/jobRunner.ts
|
|
3905
|
-
async function runQueueJob(envelope, failedJobs) {
|
|
3906
|
-
const job = jobRegistry.create(envelope.name);
|
|
3907
|
-
if (!job) {
|
|
3908
|
-
throw new Error(`Unknown job "${envelope.name}".`);
|
|
3909
|
-
}
|
|
3910
|
-
const attempts = envelope.attempts ?? 0;
|
|
3911
|
-
try {
|
|
3912
|
-
await job.handle(envelope.payload);
|
|
3913
|
-
} catch (error) {
|
|
3914
|
-
const nextAttempt = attempts + 1;
|
|
3915
|
-
const maxAttempts = job.maxAttempts ?? queueConfig.maxAttempts;
|
|
3916
|
-
if (nextAttempt < maxAttempts) {
|
|
3917
|
-
const backoffMs = job.backoffMs ?? queueConfig.backoffMs;
|
|
3918
|
-
await new Promise((resolve) => setTimeout(resolve, backoffMs * nextAttempt));
|
|
3919
|
-
await runQueueJob({
|
|
3920
|
-
...envelope,
|
|
3921
|
-
attempts: nextAttempt
|
|
3922
|
-
}, failedJobs);
|
|
3923
|
-
return;
|
|
3924
|
-
}
|
|
3925
|
-
await failedJobs.recordFailure({
|
|
3926
|
-
jobName: envelope.name,
|
|
3927
|
-
payload: envelope.payload,
|
|
3928
|
-
exception: error instanceof Error ? error.stack ?? error.message : String(error)
|
|
3929
|
-
});
|
|
3930
|
-
throw error;
|
|
3931
|
-
}
|
|
3932
|
-
}
|
|
3933
|
-
|
|
3934
|
-
// ../../src/core/queue/redisQueue.ts
|
|
3935
|
-
var {RedisClient: RedisClient2 } = globalThis.Bun;
|
|
3936
|
-
var QUEUE_LIST_KEY = "workhub:queue:default";
|
|
3937
|
-
var QUEUE_HIGH_KEY = "workhub:queue:high";
|
|
3938
|
-
var QUEUE_LOW_KEY = "workhub:queue:low";
|
|
3939
|
-
function queueKeyForPriority(priority = "default") {
|
|
3940
|
-
switch (priority) {
|
|
3941
|
-
case "high":
|
|
3942
|
-
return QUEUE_HIGH_KEY;
|
|
3943
|
-
case "low":
|
|
3944
|
-
return QUEUE_LOW_KEY;
|
|
3945
|
-
default:
|
|
3946
|
-
return QUEUE_LIST_KEY;
|
|
3947
|
-
}
|
|
3948
|
-
}
|
|
3949
|
-
class RedisQueue {
|
|
3950
|
-
client;
|
|
3951
|
-
constructor(redisUrl) {
|
|
3952
|
-
this.client = new RedisClient2(redisUrl);
|
|
3953
|
-
}
|
|
3954
|
-
async dispatch(job, payload) {
|
|
3955
|
-
const name = jobRegistry.resolveName(job);
|
|
3956
|
-
if (!name) {
|
|
3957
|
-
throw new Error("Job is not registered with the queue worker registry.");
|
|
3958
|
-
}
|
|
3959
|
-
const envelope = {
|
|
3960
|
-
name,
|
|
3961
|
-
payload,
|
|
3962
|
-
attempts: 0
|
|
3963
|
-
};
|
|
3964
|
-
const queueKey = queueKeyForPriority(job.priority);
|
|
3965
|
-
await this.client.lpush(queueKey, JSON.stringify(envelope));
|
|
3966
|
-
}
|
|
3967
|
-
}
|
|
3968
|
-
|
|
3969
|
-
// ../../src/core/queue/resilientQueue.ts
|
|
3970
|
-
class ResilientQueue {
|
|
3971
|
-
failedJobs;
|
|
3972
|
-
asyncDispatch;
|
|
3973
|
-
constructor(failedJobs, asyncDispatch = false) {
|
|
3974
|
-
this.failedJobs = failedJobs;
|
|
3975
|
-
this.asyncDispatch = asyncDispatch;
|
|
3976
|
-
}
|
|
3977
|
-
async dispatch(job, payload) {
|
|
3978
|
-
const name = jobRegistry.resolveName(job);
|
|
3979
|
-
if (!name) {
|
|
3980
|
-
throw new Error("Job is not registered with the queue worker registry.");
|
|
3981
|
-
}
|
|
3982
|
-
const envelope = {
|
|
3983
|
-
name,
|
|
3984
|
-
payload,
|
|
3985
|
-
attempts: 0
|
|
3986
|
-
};
|
|
3987
|
-
if (this.asyncDispatch) {
|
|
3988
|
-
setTimeout(() => {
|
|
3989
|
-
runQueueJob(envelope, this.failedJobs).catch((error) => {
|
|
3990
|
-
console.error("[ResilientQueue] Job failed:", error);
|
|
3991
|
-
});
|
|
3992
|
-
}, 0);
|
|
3993
|
-
return;
|
|
3994
|
-
}
|
|
3995
|
-
await runQueueJob(envelope, this.failedJobs);
|
|
1229
|
+
// ../../src/bootstrap/providers/events.ts
|
|
1230
|
+
import { eventBus } from "@getstrata/core/events";
|
|
1231
|
+
var eventsProvider = {
|
|
1232
|
+
name: "core.events",
|
|
1233
|
+
register({ container }) {
|
|
1234
|
+
container.set(CORE_EVENT_BUS_TOKEN, eventBus);
|
|
3996
1235
|
}
|
|
3997
|
-
}
|
|
1236
|
+
};
|
|
1237
|
+
var events_default = eventsProvider;
|
|
3998
1238
|
|
|
3999
|
-
// ../../src/
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
}
|
|
4003
|
-
function
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
throw new Error('QUEUE_DRIVER="redis" requires REDIS_URL to be set.');
|
|
1239
|
+
// ../../src/bootstrap/discoverListeners.ts
|
|
1240
|
+
import { readdirSync as readdirSync2 } from "fs";
|
|
1241
|
+
import { join as join2 } from "path";
|
|
1242
|
+
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
1243
|
+
async function loadDiscoveredListeners() {
|
|
1244
|
+
const listenersDirectory = join2(import.meta.dir, "../listeners");
|
|
1245
|
+
let entries;
|
|
1246
|
+
try {
|
|
1247
|
+
entries = readdirSync2(listenersDirectory, { withFileTypes: true }).filter((entry) => entry.isFile() && entry.name.endsWith(".ts")).map((entry) => entry.name);
|
|
1248
|
+
} catch (error) {
|
|
1249
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
1250
|
+
return [];
|
|
4012
1251
|
}
|
|
4013
|
-
|
|
1252
|
+
throw error;
|
|
4014
1253
|
}
|
|
4015
|
-
|
|
1254
|
+
const listeners = await Promise.all(entries.map(async (fileName) => {
|
|
1255
|
+
const moduleUrl = pathToFileURL2(join2(listenersDirectory, fileName)).href;
|
|
1256
|
+
const loaded = await import(moduleUrl);
|
|
1257
|
+
return loaded.default;
|
|
1258
|
+
}));
|
|
1259
|
+
return listeners.filter((listener) => typeof listener === "function");
|
|
4016
1260
|
}
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
function createAppQueue(driver, redisUrl, failedJobs = createFailedJobService(), registerJobs) {
|
|
4021
|
-
return createProductionQueue(driver, {
|
|
4022
|
-
redisUrl,
|
|
4023
|
-
failedJobs,
|
|
4024
|
-
registerJobs
|
|
4025
|
-
});
|
|
1261
|
+
var appListeners = await loadDiscoveredListeners();
|
|
1262
|
+
function discoverListeners() {
|
|
1263
|
+
return appListeners;
|
|
4026
1264
|
}
|
|
4027
1265
|
|
|
4028
1266
|
// ../../src/bootstrap/listeners/invalidateCacheOnModelWrite.ts
|
|
1267
|
+
import { eventBus as eventBus2, modelEventName } from "@getstrata/core/events";
|
|
1268
|
+
import InvalidateCacheTagsJob from "@getstrata/core/jobs/invalidateCacheTagsJob";
|
|
1269
|
+
import { createTrackedJob } from "@getstrata/core/queue/createAppQueue";
|
|
4029
1270
|
var MODEL_WRITE_ACTIONS = ["created", "updated", "deleted", "restored", "force-deleted"];
|
|
4030
|
-
function registerInvalidateCacheOnModelWriteListeners(bus =
|
|
1271
|
+
function registerInvalidateCacheOnModelWriteListeners(bus = eventBus2) {
|
|
4031
1272
|
for (const tableName of discoverModelTableNames()) {
|
|
4032
1273
|
for (const action of MODEL_WRITE_ACTIONS) {
|
|
4033
1274
|
bus.listen(modelEventName(tableName, action), async () => {
|
|
@@ -4043,7 +1284,7 @@ function registerInvalidateCacheOnModelWriteListeners(bus = eventBus) {
|
|
|
4043
1284
|
} catch {
|
|
4044
1285
|
return;
|
|
4045
1286
|
}
|
|
4046
|
-
const job = createTrackedJob("cache.invalidate-tags", new
|
|
1287
|
+
const job = createTrackedJob("cache.invalidate-tags", new InvalidateCacheTagsJob(cache));
|
|
4047
1288
|
await queue.dispatch(job, { tags });
|
|
4048
1289
|
});
|
|
4049
1290
|
}
|
|
@@ -4072,46 +1313,8 @@ var listenersProvider = {
|
|
|
4072
1313
|
};
|
|
4073
1314
|
var listeners_default = listenersProvider;
|
|
4074
1315
|
|
|
4075
|
-
// ../../src/core/auth/policy.ts
|
|
4076
|
-
var BLOCKED_POLICY_ACTIONS = new Set([
|
|
4077
|
-
"constructor",
|
|
4078
|
-
"toString",
|
|
4079
|
-
"valueOf",
|
|
4080
|
-
"hasOwnProperty",
|
|
4081
|
-
"isPrototypeOf",
|
|
4082
|
-
"propertyIsEnumerable",
|
|
4083
|
-
"__proto__"
|
|
4084
|
-
]);
|
|
4085
|
-
|
|
4086
|
-
class PolicyGate {
|
|
4087
|
-
constructor() {}
|
|
4088
|
-
policies = new Map;
|
|
4089
|
-
register(resource, policy) {
|
|
4090
|
-
this.policies.set(resource, policy);
|
|
4091
|
-
}
|
|
4092
|
-
allows(resource, action, user, model) {
|
|
4093
|
-
const policy = this.policies.get(resource);
|
|
4094
|
-
if (!policy) {
|
|
4095
|
-
return false;
|
|
4096
|
-
}
|
|
4097
|
-
if (BLOCKED_POLICY_ACTIONS.has(action) || !(action in policy)) {
|
|
4098
|
-
return false;
|
|
4099
|
-
}
|
|
4100
|
-
const handler = policy[action];
|
|
4101
|
-
if (typeof handler !== "function") {
|
|
4102
|
-
return false;
|
|
4103
|
-
}
|
|
4104
|
-
const resolvedUser = user === undefined ? currentAuthUser() : user;
|
|
4105
|
-
return model === undefined ? handler.call(policy, resolvedUser) : handler.call(policy, resolvedUser, model);
|
|
4106
|
-
}
|
|
4107
|
-
authorize(resource, action, user, model) {
|
|
4108
|
-
if (!this.allows(resource, action, user, model)) {
|
|
4109
|
-
throw new ForbiddenError;
|
|
4110
|
-
}
|
|
4111
|
-
}
|
|
4112
|
-
}
|
|
4113
|
-
|
|
4114
1316
|
// ../../src/bootstrap/providers/policy.ts
|
|
1317
|
+
import { PolicyGate } from "@getstrata/core/auth/policy";
|
|
4115
1318
|
var policyProvider = {
|
|
4116
1319
|
name: "core.policy",
|
|
4117
1320
|
register({ container }) {
|
|
@@ -4120,116 +1323,23 @@ var policyProvider = {
|
|
|
4120
1323
|
};
|
|
4121
1324
|
var policy_default = policyProvider;
|
|
4122
1325
|
|
|
4123
|
-
// ../../src/
|
|
4124
|
-
import {
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
const rows = await repositoryConnection`
|
|
4130
|
-
SELECT id, url, secret
|
|
4131
|
-
FROM webhook
|
|
4132
|
-
WHERE id = ${payload.webhookId} AND active = TRUE
|
|
4133
|
-
LIMIT 1
|
|
4134
|
-
`;
|
|
4135
|
-
const webhook = rows[0];
|
|
4136
|
-
if (!webhook) {
|
|
4137
|
-
return;
|
|
4138
|
-
}
|
|
4139
|
-
const body = JSON.stringify({ event: payload.event, payload: payload.payload });
|
|
4140
|
-
const signature = createHmac2("sha256", webhook.secret).update(body).digest("hex");
|
|
4141
|
-
assertSafeOutboundUrl(webhook.url, { allowHttp: appConfig.env !== "production" });
|
|
4142
|
-
let responseStatus = null;
|
|
4143
|
-
let errorMessage = null;
|
|
4144
|
-
try {
|
|
4145
|
-
const response = await safeFetch(webhook.url, {
|
|
4146
|
-
method: "POST",
|
|
4147
|
-
headers: {
|
|
4148
|
-
"content-type": "application/json",
|
|
4149
|
-
"x-workhub-signature": signature
|
|
4150
|
-
},
|
|
4151
|
-
body
|
|
4152
|
-
}, { allowHttp: appConfig.env !== "production" });
|
|
4153
|
-
responseStatus = response.status;
|
|
4154
|
-
if (!response.ok) {
|
|
4155
|
-
throw new Error(`Webhook delivery failed with status ${response.status}.`);
|
|
4156
|
-
}
|
|
4157
|
-
} catch (error) {
|
|
4158
|
-
errorMessage = error instanceof Error ? error.message : String(error);
|
|
4159
|
-
await repositoryConnection`
|
|
4160
|
-
INSERT INTO webhook_delivery (webhook_id, event, payload, response_status, error)
|
|
4161
|
-
VALUES (
|
|
4162
|
-
${webhook.id},
|
|
4163
|
-
${payload.event},
|
|
4164
|
-
${JSON.stringify(payload.payload)}::jsonb,
|
|
4165
|
-
${responseStatus},
|
|
4166
|
-
${errorMessage}
|
|
4167
|
-
)
|
|
4168
|
-
`;
|
|
4169
|
-
throw error instanceof Error ? error : new Error(errorMessage);
|
|
4170
|
-
}
|
|
4171
|
-
await repositoryConnection`
|
|
4172
|
-
INSERT INTO webhook_delivery (webhook_id, event, payload, response_status)
|
|
4173
|
-
VALUES (
|
|
4174
|
-
${webhook.id},
|
|
4175
|
-
${payload.event},
|
|
4176
|
-
${JSON.stringify(payload.payload)}::jsonb,
|
|
4177
|
-
${responseStatus}
|
|
4178
|
-
)
|
|
4179
|
-
`;
|
|
4180
|
-
}
|
|
4181
|
-
}
|
|
4182
|
-
var dispatchWebhookJob_default = DispatchWebhookJob;
|
|
4183
|
-
|
|
4184
|
-
// ../../src/core/contracts/di.ts
|
|
4185
|
-
function getRequiredDependency2(dependencies, key) {
|
|
4186
|
-
const dependency = dependencies[key];
|
|
4187
|
-
if (dependency === undefined) {
|
|
4188
|
-
throw new Error(`Required dependency "${key}" is not registered.`);
|
|
4189
|
-
}
|
|
4190
|
-
return dependency;
|
|
4191
|
-
}
|
|
4192
|
-
|
|
4193
|
-
// ../../src/core/contracts/serviceTokens.ts
|
|
4194
|
-
var CORE_POLICY_GATE_TOKEN2 = "core.policyGate";
|
|
4195
|
-
var CORE_AUTH_TOKEN2 = "core.auth";
|
|
4196
|
-
|
|
4197
|
-
// ../../src/core/runtime/applicationRegistry.ts
|
|
4198
|
-
var APPLICATION_CONTEXT_KEY = Symbol.for("@getstrata/applicationContext");
|
|
4199
|
-
var activeContext;
|
|
4200
|
-
function readStoredApplicationContext() {
|
|
4201
|
-
if (activeContext) {
|
|
4202
|
-
return activeContext;
|
|
4203
|
-
}
|
|
4204
|
-
const globalContext = globalThis[APPLICATION_CONTEXT_KEY];
|
|
4205
|
-
if (globalContext) {
|
|
4206
|
-
activeContext = globalContext;
|
|
4207
|
-
}
|
|
4208
|
-
return activeContext;
|
|
4209
|
-
}
|
|
4210
|
-
function requireActiveApplicationContext() {
|
|
4211
|
-
const context = readStoredApplicationContext();
|
|
4212
|
-
if (!context) {
|
|
4213
|
-
throw new Error("The application context has not been bootstrapped.");
|
|
4214
|
-
}
|
|
4215
|
-
return context;
|
|
4216
|
-
}
|
|
4217
|
-
function resolveApplicationCache2() {
|
|
4218
|
-
return getRequiredDependency2(requireActiveApplicationContext().dependencies, "cache");
|
|
4219
|
-
}
|
|
4220
|
-
function resolveApplicationAuth2() {
|
|
4221
|
-
return requireActiveApplicationContext().container.resolve(CORE_AUTH_TOKEN2);
|
|
4222
|
-
}
|
|
4223
|
-
function resolveApplicationPolicyGate2() {
|
|
4224
|
-
return requireActiveApplicationContext().container.resolve(CORE_POLICY_GATE_TOKEN2);
|
|
4225
|
-
}
|
|
1326
|
+
// ../../src/bootstrap/providers/queue.ts
|
|
1327
|
+
import {
|
|
1328
|
+
createAppQueue,
|
|
1329
|
+
createFailedJobService,
|
|
1330
|
+
FAILED_JOB_SERVICE_TOKEN
|
|
1331
|
+
} from "@getstrata/core/queue/createAppQueue";
|
|
4226
1332
|
|
|
4227
1333
|
// ../../src/bootstrap/queue/defaultJobs.ts
|
|
1334
|
+
import DispatchWebhookJob from "@getstrata/core/jobs/dispatchWebhookJob";
|
|
1335
|
+
import InvalidateCacheTagsJob2 from "@getstrata/core/jobs/invalidateCacheTagsJob";
|
|
1336
|
+
import { jobRegistry } from "@getstrata/core/queue/jobRegistry";
|
|
1337
|
+
import { resolveApplicationCache as resolveApplicationCache2 } from "@getstrata/core/runtime/applicationRegistry";
|
|
4228
1338
|
function registerDefaultJobs() {
|
|
4229
1339
|
jobRegistry.register("cache.invalidate-tags", () => {
|
|
4230
|
-
return new
|
|
1340
|
+
return new InvalidateCacheTagsJob2(resolveApplicationCache2());
|
|
4231
1341
|
});
|
|
4232
|
-
jobRegistry.register("webhook.dispatch", () => new
|
|
1342
|
+
jobRegistry.register("webhook.dispatch", () => new DispatchWebhookJob);
|
|
4233
1343
|
}
|
|
4234
1344
|
|
|
4235
1345
|
// ../../src/bootstrap/providers/queue.ts
|
|
@@ -4246,120 +1356,8 @@ var queueProvider = {
|
|
|
4246
1356
|
};
|
|
4247
1357
|
var queue_default = queueProvider;
|
|
4248
1358
|
|
|
4249
|
-
// ../../src/core/storage/storage.ts
|
|
4250
|
-
import { mkdir, readFile, unlink, writeFile } from "fs/promises";
|
|
4251
|
-
import { dirname, join as join3 } from "path";
|
|
4252
|
-
var {S3Client } = globalThis.Bun;
|
|
4253
|
-
|
|
4254
|
-
class LocalStorageDriver {
|
|
4255
|
-
rootDirectory;
|
|
4256
|
-
constructor(rootDirectory) {
|
|
4257
|
-
this.rootDirectory = rootDirectory;
|
|
4258
|
-
}
|
|
4259
|
-
resolveRootDirectory() {
|
|
4260
|
-
return this.rootDirectory ?? process.env.STORAGE_PATH ?? "storage";
|
|
4261
|
-
}
|
|
4262
|
-
resolvePath(path) {
|
|
4263
|
-
return join3(this.resolveRootDirectory(), path.replace(/^\/+/, ""));
|
|
4264
|
-
}
|
|
4265
|
-
async put(path, contents) {
|
|
4266
|
-
const absolutePath = this.resolvePath(path);
|
|
4267
|
-
await mkdir(dirname(absolutePath), { recursive: true });
|
|
4268
|
-
await writeFile(absolutePath, contents);
|
|
4269
|
-
return path;
|
|
4270
|
-
}
|
|
4271
|
-
async get(path) {
|
|
4272
|
-
try {
|
|
4273
|
-
return await readFile(this.resolvePath(path));
|
|
4274
|
-
} catch {
|
|
4275
|
-
return null;
|
|
4276
|
-
}
|
|
4277
|
-
}
|
|
4278
|
-
async delete(path) {
|
|
4279
|
-
try {
|
|
4280
|
-
await unlink(this.resolvePath(path));
|
|
4281
|
-
return true;
|
|
4282
|
-
} catch {
|
|
4283
|
-
return false;
|
|
4284
|
-
}
|
|
4285
|
-
}
|
|
4286
|
-
}
|
|
4287
|
-
|
|
4288
|
-
class S3StorageDriver {
|
|
4289
|
-
client;
|
|
4290
|
-
constructor(client) {
|
|
4291
|
-
this.client = client;
|
|
4292
|
-
}
|
|
4293
|
-
async put(path, contents) {
|
|
4294
|
-
await this.client.write(path.replace(/^\/+/, ""), contents);
|
|
4295
|
-
return path;
|
|
4296
|
-
}
|
|
4297
|
-
async get(path) {
|
|
4298
|
-
const normalizedPath = path.replace(/^\/+/, "");
|
|
4299
|
-
const file = this.client.file(normalizedPath);
|
|
4300
|
-
if (!await file.exists()) {
|
|
4301
|
-
return null;
|
|
4302
|
-
}
|
|
4303
|
-
return new Uint8Array(await file.arrayBuffer());
|
|
4304
|
-
}
|
|
4305
|
-
async delete(path) {
|
|
4306
|
-
try {
|
|
4307
|
-
await this.client.unlink(path.replace(/^\/+/, ""));
|
|
4308
|
-
return true;
|
|
4309
|
-
} catch {
|
|
4310
|
-
return false;
|
|
4311
|
-
}
|
|
4312
|
-
}
|
|
4313
|
-
}
|
|
4314
|
-
|
|
4315
|
-
class StorageManager {
|
|
4316
|
-
driver;
|
|
4317
|
-
constructor(driver) {
|
|
4318
|
-
this.driver = driver;
|
|
4319
|
-
}
|
|
4320
|
-
put(path, contents) {
|
|
4321
|
-
return this.driver.put(path, contents);
|
|
4322
|
-
}
|
|
4323
|
-
get(path) {
|
|
4324
|
-
return this.driver.get(path);
|
|
4325
|
-
}
|
|
4326
|
-
delete(path) {
|
|
4327
|
-
return this.driver.delete(path);
|
|
4328
|
-
}
|
|
4329
|
-
}
|
|
4330
|
-
function resolveS3Config() {
|
|
4331
|
-
const accessKeyId = process.env.AWS_ACCESS_KEY_ID?.trim();
|
|
4332
|
-
const secretAccessKey = process.env.AWS_SECRET_ACCESS_KEY?.trim();
|
|
4333
|
-
const bucket = process.env.AWS_BUCKET?.trim();
|
|
4334
|
-
if (!accessKeyId || !secretAccessKey || !bucket) {
|
|
4335
|
-
throw new Error('STORAGE_DRIVER="s3" requires AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and AWS_BUCKET.');
|
|
4336
|
-
}
|
|
4337
|
-
return {
|
|
4338
|
-
accessKeyId,
|
|
4339
|
-
secretAccessKey,
|
|
4340
|
-
bucket,
|
|
4341
|
-
...process.env.AWS_REGION?.trim() ? { region: process.env.AWS_REGION.trim() } : {},
|
|
4342
|
-
...process.env.AWS_ENDPOINT?.trim() ? { endpoint: process.env.AWS_ENDPOINT.trim() } : {}
|
|
4343
|
-
};
|
|
4344
|
-
}
|
|
4345
|
-
function createS3Client(config = resolveS3Config()) {
|
|
4346
|
-
return new S3Client({
|
|
4347
|
-
accessKeyId: config.accessKeyId,
|
|
4348
|
-
secretAccessKey: config.secretAccessKey,
|
|
4349
|
-
bucket: config.bucket,
|
|
4350
|
-
...config.region ? { region: config.region } : {},
|
|
4351
|
-
...config.endpoint ? { endpoint: config.endpoint } : {}
|
|
4352
|
-
});
|
|
4353
|
-
}
|
|
4354
|
-
function createStorageDriver() {
|
|
4355
|
-
const driver = process.env.STORAGE_DRIVER ?? "local";
|
|
4356
|
-
if (driver === "s3") {
|
|
4357
|
-
return new S3StorageDriver(createS3Client());
|
|
4358
|
-
}
|
|
4359
|
-
return new LocalStorageDriver;
|
|
4360
|
-
}
|
|
4361
|
-
|
|
4362
1359
|
// ../../src/bootstrap/providers/storage.ts
|
|
1360
|
+
import { createStorageDriver, StorageManager } from "@getstrata/core/storage/storage";
|
|
4363
1361
|
var storageProvider = {
|
|
4364
1362
|
name: "core.storage",
|
|
4365
1363
|
register({ dependencies }) {
|
|
@@ -4368,215 +1366,9 @@ var storageProvider = {
|
|
|
4368
1366
|
};
|
|
4369
1367
|
var storage_default = storageProvider;
|
|
4370
1368
|
|
|
4371
|
-
// ../../src/core/http/requestMetaContext.ts
|
|
4372
|
-
var requestMetaContext = createAsyncContextStore("@getstrata/requestMetaContext");
|
|
4373
|
-
function currentRequestMeta() {
|
|
4374
|
-
return requestMetaContext.getStore() ?? {
|
|
4375
|
-
ipAddress: null,
|
|
4376
|
-
userAgent: null
|
|
4377
|
-
};
|
|
4378
|
-
}
|
|
4379
|
-
|
|
4380
|
-
// ../../src/core/view/etaViewEngine.ts
|
|
4381
|
-
import { join as join4 } from "path";
|
|
4382
|
-
import { Eta } from "eta";
|
|
4383
|
-
var DEFAULT_VIEWS_DIRECTORY = join4(process.cwd(), "resources/views");
|
|
4384
|
-
var DEFAULT_LAYOUT = "layouts/app.eta";
|
|
4385
|
-
|
|
4386
|
-
class EtaViewEngine {
|
|
4387
|
-
eta;
|
|
4388
|
-
resolveLayoutData;
|
|
4389
|
-
constructor(viewsDirectory = DEFAULT_VIEWS_DIRECTORY, resolveLayoutData) {
|
|
4390
|
-
this.eta = new Eta({
|
|
4391
|
-
views: viewsDirectory,
|
|
4392
|
-
autoTrim: false
|
|
4393
|
-
});
|
|
4394
|
-
this.resolveLayoutData = resolveLayoutData;
|
|
4395
|
-
}
|
|
4396
|
-
async render(name, data = {}, options = {}) {
|
|
4397
|
-
const template = name.endsWith(".eta") ? name : `${name}.eta`;
|
|
4398
|
-
const layoutData = this.resolveLayoutData ? await this.resolveLayoutData() : {};
|
|
4399
|
-
const mergedData = { ...layoutData, ...data };
|
|
4400
|
-
const body = await this.eta.renderAsync(template, mergedData);
|
|
4401
|
-
const layout = options.layout ?? DEFAULT_LAYOUT;
|
|
4402
|
-
if (layout === false) {
|
|
4403
|
-
return body;
|
|
4404
|
-
}
|
|
4405
|
-
const layoutTemplate = layout.endsWith(".eta") ? layout : `${layout}.eta`;
|
|
4406
|
-
return await this.eta.renderAsync(layoutTemplate, {
|
|
4407
|
-
...mergedData,
|
|
4408
|
-
body
|
|
4409
|
-
});
|
|
4410
|
-
}
|
|
4411
|
-
}
|
|
4412
|
-
// ../../src/core/http/cookies.ts
|
|
4413
|
-
function readRequestCookie(request, name) {
|
|
4414
|
-
const cookies = request.cookies;
|
|
4415
|
-
if (cookies && typeof cookies.get === "function") {
|
|
4416
|
-
const value = cookies.get(name);
|
|
4417
|
-
if (value) {
|
|
4418
|
-
return value;
|
|
4419
|
-
}
|
|
4420
|
-
}
|
|
4421
|
-
const header = request.headers.get("cookie");
|
|
4422
|
-
if (!header) {
|
|
4423
|
-
return null;
|
|
4424
|
-
}
|
|
4425
|
-
for (const part of header.split(";")) {
|
|
4426
|
-
const idx = part.indexOf("=");
|
|
4427
|
-
if (idx === -1)
|
|
4428
|
-
continue;
|
|
4429
|
-
const cookieName = part.slice(0, idx).trim();
|
|
4430
|
-
if (cookieName !== name)
|
|
4431
|
-
continue;
|
|
4432
|
-
return decodeURIComponent(part.slice(idx + 1).trim());
|
|
4433
|
-
}
|
|
4434
|
-
return null;
|
|
4435
|
-
}
|
|
4436
|
-
|
|
4437
|
-
// ../../src/core/http/csrfToken.ts
|
|
4438
|
-
var CSRF_COOKIE = "workhub_csrf";
|
|
4439
|
-
var CSRF_TTL_MS = 60 * 60 * 1000;
|
|
4440
|
-
function resolveCsrfSecret() {
|
|
4441
|
-
return process.env.SESSION_SECRET?.trim() || process.env.OAUTH_STATE_SECRET?.trim() || process.env.ADMIN_API_TOKEN?.trim() || "workhub-dev-csrf-secret";
|
|
4442
|
-
}
|
|
4443
|
-
function csrfVerifyOptions() {
|
|
4444
|
-
return { secret: resolveCsrfSecret(), maxAge: CSRF_TTL_MS };
|
|
4445
|
-
}
|
|
4446
|
-
function createCsrfTokenCookie() {
|
|
4447
|
-
const token = Bun.CSRF.generate(resolveCsrfSecret(), { expiresIn: CSRF_TTL_MS });
|
|
4448
|
-
return {
|
|
4449
|
-
token,
|
|
4450
|
-
cookie: `${CSRF_COOKIE}=${encodeURIComponent(token)}; Path=/; SameSite=Lax; Max-Age=${Math.floor(CSRF_TTL_MS / 1000)}`
|
|
4451
|
-
};
|
|
4452
|
-
}
|
|
4453
|
-
function resolveCsrfToken(request) {
|
|
4454
|
-
const cookieValue = readRequestCookie(request, CSRF_COOKIE);
|
|
4455
|
-
if (cookieValue && Bun.CSRF.verify(cookieValue, csrfVerifyOptions())) {
|
|
4456
|
-
return { token: cookieValue };
|
|
4457
|
-
}
|
|
4458
|
-
return createCsrfTokenCookie();
|
|
4459
|
-
}
|
|
4460
|
-
function resolveCsrfTokenForRequest(request) {
|
|
4461
|
-
const metaToken = currentRequestMeta().csrfToken;
|
|
4462
|
-
if (metaToken) {
|
|
4463
|
-
return metaToken;
|
|
4464
|
-
}
|
|
4465
|
-
return resolveCsrfToken(request).token;
|
|
4466
|
-
}
|
|
4467
|
-
|
|
4468
|
-
// ../../src/core/http/flashSession.ts
|
|
4469
|
-
import { createHmac as createHmac3, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
4470
|
-
var FLASH_COOKIE = "workhub_flash";
|
|
4471
|
-
var FLASH_TTL_MS = 60 * 1000;
|
|
4472
|
-
function resolveFlashSecret() {
|
|
4473
|
-
return process.env.SESSION_SECRET?.trim() || process.env.OAUTH_STATE_SECRET?.trim() || "workhub-dev-flash-secret";
|
|
4474
|
-
}
|
|
4475
|
-
function signFlashPayload(payload, issuedAt) {
|
|
4476
|
-
const signature = createHmac3("sha256", resolveFlashSecret()).update(`${payload}.${issuedAt}`).digest("hex");
|
|
4477
|
-
return `${payload}.${issuedAt}.${signature}`;
|
|
4478
|
-
}
|
|
4479
|
-
function readFlashCookie(request) {
|
|
4480
|
-
const cookieHeader = request.headers.get("cookie");
|
|
4481
|
-
if (!cookieHeader) {
|
|
4482
|
-
return null;
|
|
4483
|
-
}
|
|
4484
|
-
for (const part of cookieHeader.split(";")) {
|
|
4485
|
-
const [name, ...rest] = part.trim().split("=");
|
|
4486
|
-
if (name === FLASH_COOKIE) {
|
|
4487
|
-
return decodeURIComponent(rest.join("="));
|
|
4488
|
-
}
|
|
4489
|
-
}
|
|
4490
|
-
return null;
|
|
4491
|
-
}
|
|
4492
|
-
function parseFlashCookie(cookieValue) {
|
|
4493
|
-
const parts = cookieValue.split(".");
|
|
4494
|
-
if (parts.length < 3) {
|
|
4495
|
-
return null;
|
|
4496
|
-
}
|
|
4497
|
-
const signature = parts.pop();
|
|
4498
|
-
const issuedAtRaw = parts.pop();
|
|
4499
|
-
const payload = parts.join(".");
|
|
4500
|
-
if (!signature || !issuedAtRaw || !payload) {
|
|
4501
|
-
return null;
|
|
4502
|
-
}
|
|
4503
|
-
const issuedAt = Number.parseInt(issuedAtRaw, 10);
|
|
4504
|
-
if (!Number.isFinite(issuedAt) || Date.now() - issuedAt > FLASH_TTL_MS) {
|
|
4505
|
-
return null;
|
|
4506
|
-
}
|
|
4507
|
-
const expectedSignature = signFlashPayload(payload, issuedAt).split(".").pop();
|
|
4508
|
-
if (!expectedSignature) {
|
|
4509
|
-
return null;
|
|
4510
|
-
}
|
|
4511
|
-
const expectedBuffer = Buffer.from(expectedSignature);
|
|
4512
|
-
const actualBuffer = Buffer.from(signature);
|
|
4513
|
-
if (expectedBuffer.length !== actualBuffer.length) {
|
|
4514
|
-
return null;
|
|
4515
|
-
}
|
|
4516
|
-
if (!timingSafeEqual2(expectedBuffer, actualBuffer)) {
|
|
4517
|
-
return null;
|
|
4518
|
-
}
|
|
4519
|
-
try {
|
|
4520
|
-
const parsed = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
|
4521
|
-
if (!parsed?.message || typeof parsed.message !== "string") {
|
|
4522
|
-
return null;
|
|
4523
|
-
}
|
|
4524
|
-
if (parsed.level !== "success" && parsed.level !== "error" && parsed.level !== "info") {
|
|
4525
|
-
return null;
|
|
4526
|
-
}
|
|
4527
|
-
return parsed;
|
|
4528
|
-
} catch {
|
|
4529
|
-
return null;
|
|
4530
|
-
}
|
|
4531
|
-
}
|
|
4532
|
-
function pullFlash(request) {
|
|
4533
|
-
const cookieValue = readFlashCookie(request);
|
|
4534
|
-
if (!cookieValue) {
|
|
4535
|
-
return null;
|
|
4536
|
-
}
|
|
4537
|
-
return parseFlashCookie(cookieValue);
|
|
4538
|
-
}
|
|
4539
|
-
|
|
4540
|
-
// ../../src/core/view/webLayoutData.ts
|
|
4541
|
-
async function resolveWebLayoutData(container, request) {
|
|
4542
|
-
const csrfToken = request ? resolveCsrfTokenForRequest(request) : "";
|
|
4543
|
-
const flash = request ? currentRequestMeta().flash ?? pullFlash(request) : null;
|
|
4544
|
-
const authUser = currentAuthUser();
|
|
4545
|
-
if (!authUser) {
|
|
4546
|
-
return { authUser: null, csrfToken, flash };
|
|
4547
|
-
}
|
|
4548
|
-
const userId = Number(authUser.id);
|
|
4549
|
-
if (!Number.isInteger(userId) || userId <= 0) {
|
|
4550
|
-
return { authUser: null, csrfToken, flash };
|
|
4551
|
-
}
|
|
4552
|
-
if (!container.has(tokenServiceToken)) {
|
|
4553
|
-
return {
|
|
4554
|
-
authUser: {
|
|
4555
|
-
id: userId,
|
|
4556
|
-
email: "",
|
|
4557
|
-
role: authUser.role ?? "member"
|
|
4558
|
-
},
|
|
4559
|
-
csrfToken,
|
|
4560
|
-
flash
|
|
4561
|
-
};
|
|
4562
|
-
}
|
|
4563
|
-
const tokenService = container.resolve(tokenServiceToken);
|
|
4564
|
-
try {
|
|
4565
|
-
const user = await tokenService.findByIdOrThrow(userId);
|
|
4566
|
-
return {
|
|
4567
|
-
authUser: {
|
|
4568
|
-
id: userId,
|
|
4569
|
-
email: user.email ?? "",
|
|
4570
|
-
role: authUser.role ?? user.role ?? "member"
|
|
4571
|
-
},
|
|
4572
|
-
csrfToken,
|
|
4573
|
-
flash
|
|
4574
|
-
};
|
|
4575
|
-
} catch {
|
|
4576
|
-
return { authUser: null, csrfToken, flash };
|
|
4577
|
-
}
|
|
4578
|
-
}
|
|
4579
1369
|
// ../../src/bootstrap/providers/view.ts
|
|
1370
|
+
import { currentRequestMeta } from "@getstrata/core/http/requestMetaContext";
|
|
1371
|
+
import { DEFAULT_VIEWS_DIRECTORY, EtaViewEngine, resolveWebLayoutData } from "@getstrata/core/view";
|
|
4580
1372
|
var CORE_VIEW_TOKEN = "core.view";
|
|
4581
1373
|
var VIEW_DIRECTORY_CONFIG_KEY = "view.directory";
|
|
4582
1374
|
var viewProvider = {
|
|
@@ -4696,8 +1488,8 @@ function createAppContext() {
|
|
|
4696
1488
|
return appContext;
|
|
4697
1489
|
}
|
|
4698
1490
|
// ../../src/bootstrap/createWebRoutes.ts
|
|
4699
|
-
import { join as
|
|
4700
|
-
import { htmlResponse
|
|
1491
|
+
import { join as join3 } from "path";
|
|
1492
|
+
import { htmlResponse } from "@getstrata/core/view";
|
|
4701
1493
|
function registerRoute(method, path, middleware) {
|
|
4702
1494
|
routeRegistry.register({ method, path, middleware });
|
|
4703
1495
|
}
|
|
@@ -4713,9 +1505,9 @@ function createWebRoutes(dependencies) {
|
|
|
4713
1505
|
registerRoute("GET", "/assets/*", ["global", "web"]);
|
|
4714
1506
|
const pathname = new URL(request.url).pathname;
|
|
4715
1507
|
const relativePath = pathname.replace(/^\//, "");
|
|
4716
|
-
const file = Bun.file(
|
|
1508
|
+
const file = Bun.file(join3(process.cwd(), "public", relativePath));
|
|
4717
1509
|
if (!await file.exists()) {
|
|
4718
|
-
return
|
|
1510
|
+
return htmlResponse("Not Found", { status: 404 });
|
|
4719
1511
|
}
|
|
4720
1512
|
return new Response(file);
|
|
4721
1513
|
};
|
|
@@ -4735,178 +1527,17 @@ function mergeWebRoutes(dependencies, routes) {
|
|
|
4735
1527
|
function createAppDependencies() {
|
|
4736
1528
|
return createAppContext().dependencies;
|
|
4737
1529
|
}
|
|
4738
|
-
// ../../src/
|
|
4739
|
-
|
|
4740
|
-
|
|
4741
|
-
|
|
4742
|
-
|
|
4743
|
-
// ../../src/core/http/etag.ts
|
|
4744
|
-
function isEtagEnabled() {
|
|
4745
|
-
return (process.env.FEATURE_ETAG ?? "true") !== "false";
|
|
4746
|
-
}
|
|
4747
|
-
function formatWeakEtag(digest) {
|
|
4748
|
-
return `W/"${digest}"`;
|
|
4749
|
-
}
|
|
4750
|
-
function etagFromResource(resource) {
|
|
4751
|
-
const version = resource.updated_at ?? resource.created_at ?? "";
|
|
4752
|
-
const versionText = version instanceof Date ? version.toISOString() : version ? String(version) : "0";
|
|
4753
|
-
const digest = nonCryptographicDigest(`${String(resource.id ?? "0")}:${versionText}`).slice(0, 32);
|
|
4754
|
-
return formatWeakEtag(digest);
|
|
4755
|
-
}
|
|
4756
|
-
function normalizeEtag(value) {
|
|
4757
|
-
return value.trim();
|
|
4758
|
-
}
|
|
4759
|
-
function etagValuesMatch(left, right) {
|
|
4760
|
-
return normalizeEtag(left) === normalizeEtag(right);
|
|
4761
|
-
}
|
|
4762
|
-
function parseEtagList(header) {
|
|
4763
|
-
if (!header) {
|
|
4764
|
-
return [];
|
|
4765
|
-
}
|
|
4766
|
-
return header.split(",").map((value) => normalizeEtag(value)).filter(Boolean);
|
|
4767
|
-
}
|
|
4768
|
-
function ifNoneMatchSatisfied(request, etag) {
|
|
4769
|
-
const header = request.headers.get("if-none-match");
|
|
4770
|
-
if (!header) {
|
|
4771
|
-
return false;
|
|
4772
|
-
}
|
|
4773
|
-
if (header.trim() === "*") {
|
|
4774
|
-
return true;
|
|
4775
|
-
}
|
|
4776
|
-
return parseEtagList(header).some((candidate) => etagValuesMatch(candidate, etag));
|
|
4777
|
-
}
|
|
4778
|
-
function ifMatchSatisfied(request, etag) {
|
|
4779
|
-
const header = request.headers.get("if-match");
|
|
4780
|
-
if (!header) {
|
|
4781
|
-
return false;
|
|
4782
|
-
}
|
|
4783
|
-
if (header.trim() === "*") {
|
|
4784
|
-
return true;
|
|
4785
|
-
}
|
|
4786
|
-
return parseEtagList(header).some((candidate) => etagValuesMatch(candidate, etag));
|
|
4787
|
-
}
|
|
4788
|
-
function assertIfMatch(request, etag, options = {}) {
|
|
4789
|
-
const header = request.headers.get("if-match");
|
|
4790
|
-
if (!header) {
|
|
4791
|
-
if (options.required) {
|
|
4792
|
-
throw new PreconditionFailedError("If-Match header is required.");
|
|
4793
|
-
}
|
|
4794
|
-
return;
|
|
4795
|
-
}
|
|
4796
|
-
if (!ifMatchSatisfied(request, etag)) {
|
|
4797
|
-
throw new PreconditionFailedError("Resource ETag does not match If-Match.");
|
|
4798
|
-
}
|
|
4799
|
-
}
|
|
4800
|
-
function applyEtagHeaders(headers, etag) {
|
|
4801
|
-
const next = new Headers(headers);
|
|
4802
|
-
next.set("ETag", etag);
|
|
4803
|
-
next.set("Cache-Control", "private, must-revalidate");
|
|
4804
|
-
next.append("Vary", "Authorization");
|
|
4805
|
-
next.append("Vary", "X-Tenant-Id");
|
|
4806
|
-
return next;
|
|
4807
|
-
}
|
|
4808
|
-
function notModifiedResponse(etag) {
|
|
4809
|
-
return new Response(null, {
|
|
4810
|
-
status: 304,
|
|
4811
|
-
headers: applyEtagHeaders(new Headers, etag)
|
|
4812
|
-
});
|
|
4813
|
-
}
|
|
4814
|
-
function applyConditionalGet(request, response, etag) {
|
|
4815
|
-
if (!isEtagEnabled()) {
|
|
4816
|
-
return response;
|
|
4817
|
-
}
|
|
4818
|
-
if (ifNoneMatchSatisfied(request, etag)) {
|
|
4819
|
-
return notModifiedResponse(etag);
|
|
4820
|
-
}
|
|
4821
|
-
const headers = applyEtagHeaders(new Headers(response.headers), etag);
|
|
4822
|
-
return new Response(response.body, {
|
|
4823
|
-
status: response.status,
|
|
4824
|
-
statusText: response.statusText,
|
|
4825
|
-
headers
|
|
4826
|
-
});
|
|
4827
|
-
}
|
|
4828
|
-
|
|
4829
|
-
// ../../src/core/tenant/tenantContext.ts
|
|
4830
|
-
var tenantContext = createAsyncContextStore("@getstrata/tenantContext");
|
|
4831
|
-
|
|
4832
|
-
// ../../src/core/http/validation.ts
|
|
4833
|
-
function parsePositiveIntParam(value, name = "id") {
|
|
4834
|
-
const parsed = Number.parseInt(value, 10);
|
|
4835
|
-
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
4836
|
-
throw new BadRequestError(`Invalid ${name}. Expected a positive integer.`);
|
|
4837
|
-
}
|
|
4838
|
-
return parsed;
|
|
4839
|
-
}
|
|
4840
|
-
|
|
4841
|
-
// ../../src/core/http/securedRouteModelBinding.ts
|
|
4842
|
-
function isMutatingPolicyAction(action) {
|
|
4843
|
-
return action === "update" || action === "delete";
|
|
4844
|
-
}
|
|
4845
|
-
function securedBindRouteModel(param, resolver, authorization, handler) {
|
|
4846
|
-
return async (request) => {
|
|
4847
|
-
const id = parsePositiveIntParam(String(request.params[param]), String(param));
|
|
4848
|
-
const model = await resolver(id, request);
|
|
4849
|
-
const gate = resolveApplicationPolicyGate2();
|
|
4850
|
-
const auth = resolveApplicationAuth2();
|
|
4851
|
-
const user = currentAuthUser() ?? await auth.resolve(request);
|
|
4852
|
-
gate.authorize(authorization.resource, authorization.action, user, model);
|
|
4853
|
-
if (isEtagEnabled() && isMutatingPolicyAction(authorization.action)) {
|
|
4854
|
-
assertIfMatch(request, etagFromResource(model), {
|
|
4855
|
-
required: authorization.requireIfMatch ?? true
|
|
4856
|
-
});
|
|
4857
|
-
}
|
|
4858
|
-
const response = await handler(request, model);
|
|
4859
|
-
if (isEtagEnabled() && authorization.action === "view") {
|
|
4860
|
-
return applyConditionalGet(request, response, etagFromResource(model));
|
|
4861
|
-
}
|
|
4862
|
-
return response;
|
|
4863
|
-
};
|
|
4864
|
-
}
|
|
4865
|
-
function securedBindRouteModelByKey(param, resolver, authorization, handler) {
|
|
4866
|
-
return async (request) => {
|
|
4867
|
-
const key = String(request.params[param] ?? "").trim();
|
|
4868
|
-
if (!key) {
|
|
4869
|
-
throw new BadRequestError(`Missing route parameter "${String(param)}".`);
|
|
4870
|
-
}
|
|
4871
|
-
const model = await resolver(key, request);
|
|
4872
|
-
const gate = resolveApplicationPolicyGate2();
|
|
4873
|
-
const auth = resolveApplicationAuth2();
|
|
4874
|
-
const user = currentAuthUser() ?? await auth.resolve(request);
|
|
4875
|
-
gate.authorize(authorization.resource, authorization.action, user, model);
|
|
4876
|
-
if (isEtagEnabled() && isMutatingPolicyAction(authorization.action)) {
|
|
4877
|
-
assertIfMatch(request, etagFromResource(model), {
|
|
4878
|
-
required: authorization.requireIfMatch ?? true
|
|
4879
|
-
});
|
|
4880
|
-
}
|
|
4881
|
-
const response = await handler(request, model);
|
|
4882
|
-
if (isEtagEnabled() && authorization.action === "view") {
|
|
4883
|
-
return applyConditionalGet(request, response, etagFromResource(model));
|
|
4884
|
-
}
|
|
4885
|
-
return response;
|
|
4886
|
-
};
|
|
4887
|
-
}
|
|
1530
|
+
// ../../src/bootstrap/http/securedRouteModelBinding.ts
|
|
1531
|
+
import {
|
|
1532
|
+
securedBindRouteModel,
|
|
1533
|
+
securedBindRouteModelByKey
|
|
1534
|
+
} from "@getstrata/core/http/securedRouteModelBinding";
|
|
4888
1535
|
// ../../src/bootstrap/membershipService.ts
|
|
4889
1536
|
import { resolveMembershipService } from "@getstrata/core/auth/membershipService";
|
|
4890
|
-
// ../../src/core/http/csrfProtection.ts
|
|
4891
|
-
var DEFAULT_CSRF_TTL_MS = 60 * 60 * 1000;
|
|
4892
|
-
function createCsrfProtection(secret, options = {}) {
|
|
4893
|
-
const expiresIn = options.expiresIn ?? DEFAULT_CSRF_TTL_MS;
|
|
4894
|
-
const maxAge = options.maxAge ?? expiresIn;
|
|
4895
|
-
return {
|
|
4896
|
-
generate(_sessionKey) {
|
|
4897
|
-
return Bun.CSRF.generate(secret, { expiresIn });
|
|
4898
|
-
},
|
|
4899
|
-
verify(token, _sessionKey) {
|
|
4900
|
-
if (!token) {
|
|
4901
|
-
return false;
|
|
4902
|
-
}
|
|
4903
|
-
return Bun.CSRF.verify(token, { secret, maxAge });
|
|
4904
|
-
},
|
|
4905
|
-
secret
|
|
4906
|
-
};
|
|
4907
|
-
}
|
|
4908
|
-
|
|
4909
1537
|
// ../../src/bootstrap/web/forms.ts
|
|
1538
|
+
import {
|
|
1539
|
+
createCsrfProtection
|
|
1540
|
+
} from "@getstrata/core/http/csrfProtection";
|
|
4910
1541
|
async function parseFormBody(request) {
|
|
4911
1542
|
const contentType = request.headers.get("content-type") ?? "";
|
|
4912
1543
|
const fields = {};
|
|
@@ -5034,6 +1665,8 @@ function createWebServer(options) {
|
|
|
5034
1665
|
}
|
|
5035
1666
|
// ../../src/bootstrap/web/session.ts
|
|
5036
1667
|
import { createHash, randomBytes } from "crypto";
|
|
1668
|
+
import { readRequestCookie } from "@getstrata/core/http/cookies";
|
|
1669
|
+
|
|
5037
1670
|
class CookieSessionStore {
|
|
5038
1671
|
sql;
|
|
5039
1672
|
secret;
|
|
@@ -5151,7 +1784,7 @@ export {
|
|
|
5151
1784
|
buildModuleRoutes,
|
|
5152
1785
|
assertProductionSecrets,
|
|
5153
1786
|
assertAppDependenciesComplete,
|
|
5154
|
-
|
|
1787
|
+
appSchedule3 as appSchedule,
|
|
5155
1788
|
ServiceContainer,
|
|
5156
1789
|
Schedule2 as Schedule,
|
|
5157
1790
|
RouteRegistry,
|