@getstrata/core 0.5.47 → 0.5.49
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -0
- package/dist/entries/auth/scimAuthMiddleware.js +0 -3
- package/dist/entries/auth/sessionGuard.js +142 -2751
- package/dist/entries/database/transaction.js +1 -129
- package/dist/entries/http/formRequest.js +0 -3
- package/dist/entries/http/pagination.js +0 -3
- package/dist/entries/http/routeModelBinding.js +0 -3
- package/dist/entries/http/securedRouteModelBinding.js +0 -3
- package/dist/entries/http/throttleMiddleware.js +0 -3
- package/dist/entries/http/webErrorResponse.js +170 -2633
- package/dist/entries/http/webFormRequest.js +0 -3
- package/dist/entries/queue/createAppQueue.js +9 -29
- package/dist/entries/queue/failedJobRepository.js +0 -21
- package/dist/entries/queue/jobRunner.js +9 -8
- package/dist/entries/queue/publicQueue.js +9 -29
- package/dist/entries/queue/queueMetrics.js +9 -29
- package/dist/entries/queue/redisQueue.js +9 -8
- package/dist/entries/tenant/tenantDatabaseScope.js +0 -3
- package/dist/entries/view.js +164 -2750
- package/package.json +2 -2
|
@@ -182,2412 +182,137 @@ function htmlResponse(html, init = {}) {
|
|
|
182
182
|
function isHtmxRequest(request) {
|
|
183
183
|
return request.headers.get("HX-Request") === "true";
|
|
184
184
|
}
|
|
185
|
-
// ../../src/core/contracts/serviceTokens.ts
|
|
186
|
-
var CORE_CONFIG_TOKEN = "core.config";
|
|
187
|
-
var CORE_CACHE_TOKEN = "core.cache";
|
|
188
|
-
var CORE_QUEUE_TOKEN = "core.queue";
|
|
189
|
-
var CORE_EVENT_BUS_TOKEN = "core.eventBus";
|
|
190
|
-
var CORE_POLICY_GATE_TOKEN = "core.policyGate";
|
|
191
|
-
var CORE_AUTH_TOKEN = "core.auth";
|
|
192
|
-
var CORE_TOKEN_SERVICE_TOKEN = "core.tokenService";
|
|
193
185
|
// ../../src/bootstrap/config.ts
|
|
186
|
+
import {
|
|
187
|
+
CORE_AUTH_TOKEN,
|
|
188
|
+
CORE_CACHE_TOKEN,
|
|
189
|
+
CORE_CONFIG_TOKEN,
|
|
190
|
+
CORE_EVENT_BUS_TOKEN,
|
|
191
|
+
CORE_POLICY_GATE_TOKEN,
|
|
192
|
+
CORE_QUEUE_TOKEN,
|
|
193
|
+
CORE_TOKEN_SERVICE_TOKEN
|
|
194
|
+
} from "@getstrata/core/contracts/serviceTokens";
|
|
194
195
|
var DEFAULT_QUEUE_DRIVER = "sync";
|
|
195
196
|
|
|
196
|
-
// ../../src/
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
constructor(options) {
|
|
201
|
-
this.options = options;
|
|
202
|
-
this.name = options.name;
|
|
203
|
-
}
|
|
204
|
-
getAuthorizationUrl(state) {
|
|
205
|
-
const params = new URLSearchParams({
|
|
206
|
-
client_id: this.options.clientId,
|
|
207
|
-
redirect_uri: this.options.redirectUri,
|
|
208
|
-
response_type: "code",
|
|
209
|
-
scope: (this.options.scopes ?? ["openid", "email", "profile"]).join(" "),
|
|
210
|
-
state
|
|
211
|
-
});
|
|
212
|
-
return `${this.options.issuer.replace(/\/$/, "")}/authorize?${params.toString()}`;
|
|
213
|
-
}
|
|
214
|
-
async exchangeCode(code) {
|
|
215
|
-
const tokenResponse = await fetch(`${this.options.issuer.replace(/\/$/, "")}/token`, {
|
|
216
|
-
method: "POST",
|
|
217
|
-
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
218
|
-
body: new URLSearchParams({
|
|
219
|
-
grant_type: "authorization_code",
|
|
220
|
-
code,
|
|
221
|
-
redirect_uri: this.options.redirectUri,
|
|
222
|
-
client_id: this.options.clientId,
|
|
223
|
-
client_secret: this.options.clientSecret
|
|
224
|
-
})
|
|
225
|
-
});
|
|
226
|
-
const tokenBody = await tokenResponse.json();
|
|
227
|
-
if (!tokenBody.access_token) {
|
|
228
|
-
throw new Error("OIDC token exchange failed.");
|
|
229
|
-
}
|
|
230
|
-
const profileResponse = await fetch(`${this.options.issuer.replace(/\/$/, "")}/userinfo`, {
|
|
231
|
-
headers: { authorization: `Bearer ${tokenBody.access_token}` }
|
|
232
|
-
});
|
|
233
|
-
const profile = await profileResponse.json();
|
|
234
|
-
return {
|
|
235
|
-
providerUserId: profile.sub,
|
|
236
|
-
email: profile.email ?? `${profile.sub}@oidc.local`,
|
|
237
|
-
name: profile.name ?? profile.sub
|
|
238
|
-
};
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// ../../src/core/auth/oauth/providers.ts
|
|
243
|
-
class GitHubOAuthProvider {
|
|
244
|
-
options;
|
|
245
|
-
name = "github";
|
|
246
|
-
constructor(options) {
|
|
247
|
-
this.options = options;
|
|
248
|
-
}
|
|
249
|
-
getAuthorizationUrl(state) {
|
|
250
|
-
const params = new URLSearchParams({
|
|
251
|
-
client_id: this.options.clientId,
|
|
252
|
-
redirect_uri: this.options.redirectUri,
|
|
253
|
-
scope: "read:user user:email",
|
|
254
|
-
state
|
|
255
|
-
});
|
|
256
|
-
return `https://github.com/login/oauth/authorize?${params.toString()}`;
|
|
257
|
-
}
|
|
258
|
-
async exchangeCode(code) {
|
|
259
|
-
const tokenResponse = await fetch("https://github.com/login/oauth/access_token", {
|
|
260
|
-
method: "POST",
|
|
261
|
-
headers: {
|
|
262
|
-
accept: "application/json",
|
|
263
|
-
"content-type": "application/json"
|
|
264
|
-
},
|
|
265
|
-
body: JSON.stringify({
|
|
266
|
-
client_id: this.options.clientId,
|
|
267
|
-
client_secret: this.options.clientSecret,
|
|
268
|
-
code,
|
|
269
|
-
redirect_uri: this.options.redirectUri
|
|
270
|
-
})
|
|
271
|
-
});
|
|
272
|
-
const tokenBody = await tokenResponse.json();
|
|
273
|
-
if (!tokenBody.access_token) {
|
|
274
|
-
throw new Error("GitHub OAuth token exchange failed.");
|
|
275
|
-
}
|
|
276
|
-
const profileResponse = await fetch("https://api.github.com/user", {
|
|
277
|
-
headers: {
|
|
278
|
-
authorization: `Bearer ${tokenBody.access_token}`,
|
|
279
|
-
accept: "application/json",
|
|
280
|
-
"user-agent": "workhub"
|
|
281
|
-
}
|
|
282
|
-
});
|
|
283
|
-
const profile = await profileResponse.json();
|
|
284
|
-
return {
|
|
285
|
-
providerUserId: String(profile.id),
|
|
286
|
-
email: profile.email ?? `${profile.login}@users.noreply.github.com`,
|
|
287
|
-
name: profile.name ?? profile.login
|
|
288
|
-
};
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
class MockOAuthProvider {
|
|
293
|
-
profile;
|
|
294
|
-
name = "mock";
|
|
295
|
-
constructor(profile) {
|
|
296
|
-
this.profile = profile;
|
|
297
|
-
}
|
|
298
|
-
getAuthorizationUrl(state) {
|
|
299
|
-
return `https://mock.oauth/authorize?state=${encodeURIComponent(state)}`;
|
|
300
|
-
}
|
|
301
|
-
async exchangeCode(code) {
|
|
302
|
-
if (code !== "valid-code") {
|
|
303
|
-
throw new Error("Invalid OAuth code.");
|
|
304
|
-
}
|
|
305
|
-
return this.profile;
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
// ../../src/core/auth/oauth/samlProvider.ts
|
|
310
|
-
class SamlProvider {
|
|
311
|
-
loginUrl;
|
|
312
|
-
name = "saml";
|
|
313
|
-
constructor(loginUrl) {
|
|
314
|
-
this.loginUrl = loginUrl;
|
|
315
|
-
}
|
|
316
|
-
getAuthorizationUrl(state) {
|
|
317
|
-
return `${this.loginUrl}?state=${encodeURIComponent(state)}`;
|
|
318
|
-
}
|
|
319
|
-
async exchangeCode(code) {
|
|
320
|
-
if (!code.startsWith("saml:")) {
|
|
321
|
-
throw new Error("Invalid SAML assertion reference.");
|
|
322
|
-
}
|
|
323
|
-
const [, email, name] = code.split(":");
|
|
324
|
-
return {
|
|
325
|
-
providerUserId: email ?? "saml-user",
|
|
326
|
-
email: email ?? "saml-user@workhub.test",
|
|
327
|
-
name: name ?? "SAML User"
|
|
328
|
-
};
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
// ../../src/config/features.ts
|
|
333
|
-
function readFeatureFlags() {
|
|
334
|
-
return {
|
|
335
|
-
webhooks: (process.env.FEATURE_WEBHOOKS ?? "true") !== "false",
|
|
336
|
-
fullTextSearch: (process.env.FEATURE_SEARCH ?? "true") !== "false",
|
|
337
|
-
auditLog: (process.env.FEATURE_AUDIT_LOG ?? "true") !== "false",
|
|
338
|
-
oauthLogin: (process.env.FEATURE_OAUTH ?? "true") !== "false",
|
|
339
|
-
samlLogin: (process.env.FEATURE_SAML ?? "false") === "true",
|
|
340
|
-
scim: (process.env.FEATURE_SCIM ?? "true") !== "false",
|
|
341
|
-
billing: (process.env.FEATURE_BILLING ?? "true") !== "false",
|
|
342
|
-
siemExport: (process.env.FEATURE_SIEM_EXPORT ?? "true") !== "false",
|
|
343
|
-
publicReads: (process.env.FEATURE_PUBLIC_READS ?? "true") !== "false",
|
|
344
|
-
emailVerification: (process.env.FEATURE_EMAIL_VERIFICATION ?? "false") === "true",
|
|
345
|
-
mfa: (process.env.FEATURE_MFA ?? "false") === "true"
|
|
346
|
-
};
|
|
347
|
-
}
|
|
348
|
-
var featureFlags = readFeatureFlags();
|
|
349
|
-
function isFeatureEnabled(feature) {
|
|
350
|
-
return readFeatureFlags()[feature];
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
// ../../src/core/events/eventBus.ts
|
|
354
|
-
class EventBus {
|
|
355
|
-
constructor() {}
|
|
356
|
-
listeners = new Map;
|
|
357
|
-
listen(event, listener) {
|
|
358
|
-
const handlers = this.listeners.get(event) ?? new Set;
|
|
359
|
-
handlers.add(listener);
|
|
360
|
-
this.listeners.set(event, handlers);
|
|
361
|
-
return () => {
|
|
362
|
-
handlers.delete(listener);
|
|
363
|
-
if (handlers.size === 0) {
|
|
364
|
-
this.listeners.delete(event);
|
|
365
|
-
}
|
|
366
|
-
};
|
|
367
|
-
}
|
|
368
|
-
async dispatch(event, payload) {
|
|
369
|
-
const handlers = this.listeners.get(event);
|
|
370
|
-
if (!handlers || handlers.size === 0) {
|
|
371
|
-
return;
|
|
372
|
-
}
|
|
373
|
-
for (const handler of handlers) {
|
|
374
|
-
await handler(payload);
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
var eventBus = new EventBus;
|
|
379
|
-
|
|
380
|
-
// ../../src/core/events/index.ts
|
|
381
|
-
function modelEventName(tableName, action) {
|
|
382
|
-
return `${tableName}.${action}`;
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
// ../../src/core/pagination/index.ts
|
|
386
|
-
function buildPaginationMeta(input) {
|
|
387
|
-
const lastPage = Math.max(1, Math.ceil(input.total / input.perPage));
|
|
388
|
-
return {
|
|
389
|
-
page: input.page,
|
|
390
|
-
per_page: input.perPage,
|
|
391
|
-
total: input.total,
|
|
392
|
-
last_page: lastPage
|
|
393
|
-
};
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
// ../../src/core/database/query.ts
|
|
397
|
-
function quoteIdentifier(identifier) {
|
|
398
|
-
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(identifier)) {
|
|
399
|
-
throw new Error(`Invalid SQL identifier: ${identifier}`);
|
|
400
|
-
}
|
|
401
|
-
return `"${identifier}"`;
|
|
402
|
-
}
|
|
403
|
-
function qualifyColumn(tableName, column) {
|
|
404
|
-
return `${quoteIdentifier(tableName)}.${quoteIdentifier(column)}`;
|
|
405
|
-
}
|
|
406
|
-
function resolveQualifiedColumn(defaultTable, columnName) {
|
|
407
|
-
if (columnName.includes(".")) {
|
|
408
|
-
const [table, column] = columnName.split(".", 2);
|
|
409
|
-
if (!table || !column) {
|
|
410
|
-
throw new Error(`Invalid qualified column: ${columnName}`);
|
|
411
|
-
}
|
|
412
|
-
return qualifyColumn(table, column);
|
|
413
|
-
}
|
|
414
|
-
return qualifyColumn(defaultTable, columnName);
|
|
415
|
-
}
|
|
416
|
-
function parseQualifiedColumn(reference) {
|
|
417
|
-
const [table, column] = reference.split(".", 2);
|
|
418
|
-
if (!table || !column) {
|
|
419
|
-
throw new Error(`Join columns must be qualified as table.column: ${reference}`);
|
|
420
|
-
}
|
|
421
|
-
return { table, column };
|
|
422
|
-
}
|
|
423
|
-
function normalizeDirection(direction = "ASC") {
|
|
424
|
-
return direction.toUpperCase() === "DESC" ? "DESC" : "ASC";
|
|
425
|
-
}
|
|
426
|
-
function isQueryOperator(value) {
|
|
427
|
-
return value !== null && !Array.isArray(value) && !(value instanceof Date) && typeof value === "object";
|
|
428
|
-
}
|
|
429
|
-
function pushParam(values, value) {
|
|
430
|
-
values.push(value);
|
|
431
|
-
return `$${values.length}`;
|
|
432
|
-
}
|
|
433
|
-
function buildInClause(column, values, params) {
|
|
434
|
-
if (values.length === 0) {
|
|
435
|
-
return "1 = 0";
|
|
436
|
-
}
|
|
437
|
-
const placeholders = values.map((value) => pushParam(params, value)).join(", ");
|
|
438
|
-
return `${column} IN (${placeholders})`;
|
|
439
|
-
}
|
|
440
|
-
function buildOperatorClauses(column, operator, params) {
|
|
441
|
-
const clauses = [];
|
|
442
|
-
if (operator.isNull === true) {
|
|
443
|
-
clauses.push(`${column} IS NULL`);
|
|
444
|
-
}
|
|
445
|
-
if (operator.isNull === false) {
|
|
446
|
-
clauses.push(`${column} IS NOT NULL`);
|
|
447
|
-
}
|
|
448
|
-
if (operator.eq !== undefined) {
|
|
449
|
-
if (operator.eq === null) {
|
|
450
|
-
clauses.push(`${column} IS NULL`);
|
|
451
|
-
} else {
|
|
452
|
-
clauses.push(`${column} = ${pushParam(params, operator.eq)}`);
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
if (operator.in !== undefined) {
|
|
456
|
-
clauses.push(buildInClause(column, operator.in, params));
|
|
457
|
-
}
|
|
458
|
-
if (operator.gt !== undefined) {
|
|
459
|
-
clauses.push(`${column} > ${pushParam(params, operator.gt)}`);
|
|
460
|
-
}
|
|
461
|
-
if (operator.gte !== undefined) {
|
|
462
|
-
clauses.push(`${column} >= ${pushParam(params, operator.gte)}`);
|
|
463
|
-
}
|
|
464
|
-
if (operator.lt !== undefined) {
|
|
465
|
-
clauses.push(`${column} < ${pushParam(params, operator.lt)}`);
|
|
466
|
-
}
|
|
467
|
-
if (operator.lte !== undefined) {
|
|
468
|
-
clauses.push(`${column} <= ${pushParam(params, operator.lte)}`);
|
|
469
|
-
}
|
|
470
|
-
if (operator.ilike !== undefined) {
|
|
471
|
-
clauses.push(`${column} ILIKE ${pushParam(params, `%${operator.ilike}%`)}`);
|
|
472
|
-
}
|
|
473
|
-
if (operator.tsMatch !== undefined) {
|
|
474
|
-
clauses.push(`${column} @@ plainto_tsquery('english', ${pushParam(params, operator.tsMatch)})`);
|
|
475
|
-
}
|
|
476
|
-
return clauses;
|
|
477
|
-
}
|
|
478
|
-
function appendWhereParts(tableName, where, params) {
|
|
479
|
-
const clauses = [];
|
|
480
|
-
for (const [columnName, filterValue] of Object.entries(where)) {
|
|
481
|
-
if (filterValue === undefined) {
|
|
482
|
-
continue;
|
|
483
|
-
}
|
|
484
|
-
const column = resolveQualifiedColumn(tableName, columnName);
|
|
485
|
-
if (Array.isArray(filterValue)) {
|
|
486
|
-
clauses.push(buildInClause(column, filterValue, params));
|
|
487
|
-
continue;
|
|
488
|
-
}
|
|
489
|
-
if (isQueryOperator(filterValue)) {
|
|
490
|
-
clauses.push(...buildOperatorClauses(column, filterValue, params));
|
|
491
|
-
continue;
|
|
492
|
-
}
|
|
493
|
-
if (filterValue === null) {
|
|
494
|
-
clauses.push(`${column} IS NULL`);
|
|
495
|
-
continue;
|
|
496
|
-
}
|
|
497
|
-
clauses.push(`${column} = ${pushParam(params, filterValue)}`);
|
|
498
|
-
}
|
|
499
|
-
return clauses.join(" AND ");
|
|
500
|
-
}
|
|
501
|
-
function buildWhereClause(tableName, where = {}) {
|
|
502
|
-
const params = [];
|
|
503
|
-
const body = appendWhereParts(tableName, where, params);
|
|
504
|
-
return {
|
|
505
|
-
clause: body.length > 0 ? ` WHERE ${body}` : "",
|
|
506
|
-
params
|
|
507
|
-
};
|
|
508
|
-
}
|
|
509
|
-
function buildWhereNodeClause(tableName, node, params) {
|
|
510
|
-
if ("where" in node) {
|
|
511
|
-
return appendWhereParts(tableName, node.where, params);
|
|
512
|
-
}
|
|
513
|
-
const grouped = buildWhereGroupClause(tableName, node.group, params);
|
|
514
|
-
if (!grouped) {
|
|
515
|
-
return "";
|
|
516
|
-
}
|
|
517
|
-
return grouped.includes(" OR ") ? `(${grouped})` : grouped;
|
|
518
|
-
}
|
|
519
|
-
function buildWhereGroupClause(tableName, nodes, params) {
|
|
520
|
-
let result = "";
|
|
521
|
-
for (const node of nodes) {
|
|
522
|
-
const part = buildWhereNodeClause(tableName, node, params);
|
|
523
|
-
if (!part) {
|
|
524
|
-
continue;
|
|
525
|
-
}
|
|
526
|
-
if (!result) {
|
|
527
|
-
result = part;
|
|
528
|
-
continue;
|
|
529
|
-
}
|
|
530
|
-
result = node.kind === "or" ? `${result} OR ${part}` : `${result} AND ${part}`;
|
|
531
|
-
}
|
|
532
|
-
if (!result) {
|
|
533
|
-
return "";
|
|
534
|
-
}
|
|
535
|
-
return result;
|
|
536
|
-
}
|
|
537
|
-
function buildAdvancedWhereClause(tableName, where = {}, whereNodes = [], params = []) {
|
|
538
|
-
const nodes = [];
|
|
539
|
-
if (Object.keys(where).length > 0) {
|
|
540
|
-
nodes.push({ kind: "and", where });
|
|
541
|
-
}
|
|
542
|
-
nodes.push(...whereNodes);
|
|
543
|
-
const combined = buildWhereGroupClause(tableName, nodes, params);
|
|
544
|
-
return {
|
|
545
|
-
clause: combined ? ` WHERE ${combined}` : "",
|
|
546
|
-
params
|
|
547
|
-
};
|
|
548
|
-
}
|
|
549
|
-
function resolveSoftDeleteColumn(table) {
|
|
550
|
-
if (!table.softDeletes) {
|
|
551
|
-
return null;
|
|
552
|
-
}
|
|
553
|
-
if (table.softDeletes === true) {
|
|
554
|
-
return "deleted_at";
|
|
555
|
-
}
|
|
556
|
-
return table.softDeletes.column ?? "deleted_at";
|
|
557
|
-
}
|
|
558
|
-
function appendSoftDeleteScope(table, options, clauses) {
|
|
559
|
-
const column = resolveSoftDeleteColumn(table);
|
|
560
|
-
if (!column) {
|
|
561
|
-
return;
|
|
562
|
-
}
|
|
563
|
-
const qualifiedColumn = qualifyColumn(table.name, column);
|
|
564
|
-
if (options.onlyTrashed) {
|
|
565
|
-
clauses.push(`${qualifiedColumn} IS NOT NULL`);
|
|
566
|
-
return;
|
|
567
|
-
}
|
|
568
|
-
if (!options.withTrashed) {
|
|
569
|
-
clauses.push(`${qualifiedColumn} IS NULL`);
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
function buildQueryWhereClause(table, options = {}, whereNodes = [], params = []) {
|
|
573
|
-
const { clause, params: whereParams } = buildAdvancedWhereClause(table.name, options.where ?? {}, whereNodes, params);
|
|
574
|
-
const softDeleteClauses = [];
|
|
575
|
-
appendSoftDeleteScope(table, options, softDeleteClauses);
|
|
576
|
-
if (softDeleteClauses.length === 0) {
|
|
577
|
-
return { clause, params: whereParams };
|
|
578
|
-
}
|
|
579
|
-
const base = clause.replace(/^ WHERE /, "");
|
|
580
|
-
const scope = softDeleteClauses.join(" AND ");
|
|
581
|
-
return {
|
|
582
|
-
clause: base ? ` WHERE (${base}) AND ${scope}` : ` WHERE ${scope}`,
|
|
583
|
-
params: whereParams
|
|
584
|
-
};
|
|
585
|
-
}
|
|
586
|
-
function isQueryOrder(value) {
|
|
587
|
-
return "column" in value;
|
|
588
|
-
}
|
|
589
|
-
function normalizeOrderBy(orderBy) {
|
|
590
|
-
if (!orderBy) {
|
|
591
|
-
return [];
|
|
592
|
-
}
|
|
593
|
-
if (Array.isArray(orderBy)) {
|
|
594
|
-
return orderBy;
|
|
595
|
-
}
|
|
596
|
-
if (isQueryOrder(orderBy)) {
|
|
597
|
-
return [orderBy];
|
|
598
|
-
}
|
|
599
|
-
return Object.entries(orderBy).map(([column, direction]) => ({
|
|
600
|
-
column,
|
|
601
|
-
direction
|
|
602
|
-
}));
|
|
603
|
-
}
|
|
604
|
-
function buildOrderByClause(tableName, orderBy) {
|
|
605
|
-
const parts = normalizeOrderBy(orderBy).map(({ column, direction }) => {
|
|
606
|
-
return `${resolveQualifiedColumn(tableName, column)} ${normalizeDirection(direction)}`;
|
|
607
|
-
});
|
|
608
|
-
return parts.length > 0 ? ` ORDER BY ${parts.join(", ")}` : "";
|
|
609
|
-
}
|
|
610
|
-
function buildGroupByClause(tableName, groupBy) {
|
|
611
|
-
if (!groupBy) {
|
|
612
|
-
return "";
|
|
613
|
-
}
|
|
614
|
-
const columns = (Array.isArray(groupBy) ? groupBy : [groupBy]).map((column) => String(column));
|
|
615
|
-
const parts = columns.map((column) => resolveQualifiedColumn(tableName, column));
|
|
616
|
-
return parts.length > 0 ? ` GROUP BY ${parts.join(", ")}` : "";
|
|
617
|
-
}
|
|
618
|
-
function buildHavingClause(tableName, having, params) {
|
|
619
|
-
if (!having) {
|
|
620
|
-
return "";
|
|
621
|
-
}
|
|
622
|
-
const body = appendWhereParts(tableName, having, params);
|
|
623
|
-
return body.length > 0 ? ` HAVING ${body}` : "";
|
|
624
|
-
}
|
|
625
|
-
function buildJoinClause(joins = []) {
|
|
626
|
-
return joins.map((join2) => {
|
|
627
|
-
const joinType = join2.type === "left" ? "LEFT JOIN" : "INNER JOIN";
|
|
628
|
-
const onClause = join2.on.map(({ left, right }) => `${qualifyColumn(left.table, left.column)} = ${qualifyColumn(right.table, right.column)}`).join(" AND ");
|
|
629
|
-
return ` ${joinType} ${quoteIdentifier(join2.table)} ON ${onClause}`;
|
|
630
|
-
}).join("");
|
|
631
|
-
}
|
|
632
|
-
function buildLimitClause(limit) {
|
|
633
|
-
if (limit === undefined) {
|
|
634
|
-
return "";
|
|
635
|
-
}
|
|
636
|
-
if (!Number.isInteger(limit) || limit <= 0) {
|
|
637
|
-
throw new Error("Query limit must be a positive integer.");
|
|
638
|
-
}
|
|
639
|
-
return ` LIMIT ${limit}`;
|
|
640
|
-
}
|
|
641
|
-
function buildOffsetClause(offset) {
|
|
642
|
-
if (offset === undefined) {
|
|
643
|
-
return "";
|
|
644
|
-
}
|
|
645
|
-
if (!Number.isInteger(offset) || offset < 0) {
|
|
646
|
-
throw new Error("Query offset must be a non-negative integer.");
|
|
647
|
-
}
|
|
648
|
-
return ` OFFSET ${offset}`;
|
|
649
|
-
}
|
|
650
|
-
function buildReturningColumns(table) {
|
|
651
|
-
return table.columns.map((column) => qualifyColumn(table.name, column)).join(", ");
|
|
652
|
-
}
|
|
653
|
-
function buildSelectList(table, select, params = []) {
|
|
654
|
-
if (!select || select.length === 0) {
|
|
655
|
-
return buildReturningColumns(table);
|
|
656
|
-
}
|
|
657
|
-
return select.map((item) => {
|
|
658
|
-
if (item.kind === "column") {
|
|
659
|
-
const column2 = qualifyColumn(item.table, item.column);
|
|
660
|
-
return item.as ? `${column2} AS ${quoteIdentifier(item.as)}` : column2;
|
|
661
|
-
}
|
|
662
|
-
if (item.kind === "literalText") {
|
|
663
|
-
return `${pushParam(params, item.value)}::text AS ${quoteIdentifier(item.as)}`;
|
|
664
|
-
}
|
|
665
|
-
const column = qualifyColumn(item.table, item.column);
|
|
666
|
-
const placeholder = pushParam(params, item.query);
|
|
667
|
-
return `ts_rank(${column}, plainto_tsquery('english', ${placeholder})) AS ${quoteIdentifier(item.as)}`;
|
|
668
|
-
}).join(", ");
|
|
669
|
-
}
|
|
670
|
-
function getDefinedColumnEntries(table, values, options = {}) {
|
|
671
|
-
const record = values;
|
|
672
|
-
const excluded = new Set(options.exclude ?? []);
|
|
673
|
-
return table.columns.flatMap((column) => {
|
|
674
|
-
if (excluded.has(column) || !Object.hasOwn(record, column)) {
|
|
675
|
-
return [];
|
|
676
|
-
}
|
|
677
|
-
const value = record[column];
|
|
678
|
-
if (value === undefined) {
|
|
679
|
-
return [];
|
|
680
|
-
}
|
|
681
|
-
return [[column, value]];
|
|
682
|
-
});
|
|
683
|
-
}
|
|
684
|
-
function buildSelectQuery(table, options = {}, whereNodes = []) {
|
|
685
|
-
const params = [];
|
|
686
|
-
const columns = buildSelectList(table, options.select, params);
|
|
687
|
-
const { clause } = buildQueryWhereClause(table, options, whereNodes, params);
|
|
688
|
-
const joins = buildJoinClause(options.joins);
|
|
689
|
-
const groupBy = buildGroupByClause(table.name, options.groupBy);
|
|
690
|
-
const havingClause = buildHavingClause(table.name, options.having, params);
|
|
691
|
-
const orderBy = buildOrderByClause(table.name, options.orderBy ?? table.defaultOrderBy);
|
|
692
|
-
const limit = buildLimitClause(options.limit);
|
|
693
|
-
const offset = buildOffsetClause(options.offset);
|
|
694
|
-
return {
|
|
695
|
-
text: `SELECT ${columns} FROM ${quoteIdentifier(table.name)}${joins}${clause}${groupBy}${havingClause}${orderBy}${limit}${offset}`,
|
|
696
|
-
params
|
|
697
|
-
};
|
|
698
|
-
}
|
|
699
|
-
function buildCountQuery(table, where = {}, options = {}, whereNodes = []) {
|
|
700
|
-
const params = [];
|
|
701
|
-
const { clause, params: whereParams } = buildQueryWhereClause(table, {
|
|
702
|
-
where,
|
|
703
|
-
withTrashed: options.withTrashed,
|
|
704
|
-
onlyTrashed: options.onlyTrashed
|
|
705
|
-
}, whereNodes);
|
|
706
|
-
params.push(...whereParams);
|
|
707
|
-
const joins = buildJoinClause(options.joins);
|
|
708
|
-
const groupBy = buildGroupByClause(table.name, options.groupBy);
|
|
709
|
-
return {
|
|
710
|
-
text: `SELECT COUNT(*) AS count FROM ${quoteIdentifier(table.name)}${joins}${clause}${groupBy}`,
|
|
711
|
-
params
|
|
712
|
-
};
|
|
713
|
-
}
|
|
714
|
-
function buildProjectionQuery(table, expression, alias, options = {}, whereNodes = []) {
|
|
715
|
-
assertSafeProjectionExpression(expression);
|
|
716
|
-
const params = [];
|
|
717
|
-
const { clause, params: whereParams } = buildQueryWhereClause(table, options, whereNodes);
|
|
718
|
-
params.push(...whereParams);
|
|
719
|
-
const joins = buildJoinClause(options.joins);
|
|
720
|
-
const groupBy = buildGroupByClause(table.name, options.groupBy);
|
|
721
|
-
const orderBy = buildOrderByClause(table.name, options.orderBy);
|
|
722
|
-
const limit = buildLimitClause(options.limit);
|
|
723
|
-
return {
|
|
724
|
-
text: `SELECT ${expression} AS ${quoteIdentifier(alias)} FROM ${quoteIdentifier(table.name)}${joins}${clause}${groupBy}${orderBy}${limit}`,
|
|
725
|
-
params
|
|
726
|
-
};
|
|
727
|
-
}
|
|
728
|
-
var SAFE_PROJECTION_EXPRESSION = /^(COUNT\(\*\)|(?:"[\w_]+"(?:\."[\w_]+")?)|(?:AVG|SUM|MIN|MAX)\((?:"[\w_]+"(?:\."[\w_]+")?)\))$/;
|
|
729
|
-
function assertSafeProjectionExpression(expression) {
|
|
730
|
-
if (!SAFE_PROJECTION_EXPRESSION.test(expression.trim())) {
|
|
731
|
-
throw new Error(`Unsafe projection expression: ${expression}`);
|
|
732
|
-
}
|
|
733
|
-
}
|
|
734
|
-
function buildGroupedCountQuery(table, column, where = {}, options = {}) {
|
|
735
|
-
const qualifiedColumn = qualifyColumn(table.name, column);
|
|
736
|
-
const { clause, params } = buildQueryWhereClause(table, {
|
|
737
|
-
where,
|
|
738
|
-
...options
|
|
739
|
-
});
|
|
740
|
-
return {
|
|
741
|
-
text: `SELECT ${qualifiedColumn} AS ${quoteIdentifier("value")}, COUNT(*) AS ${quoteIdentifier("count")} FROM ${quoteIdentifier(table.name)}${clause} GROUP BY ${qualifiedColumn} ORDER BY ${qualifiedColumn} ASC`,
|
|
742
|
-
params
|
|
743
|
-
};
|
|
744
|
-
}
|
|
745
|
-
function buildInsertQuery(table, values) {
|
|
746
|
-
const entries = getDefinedColumnEntries(table, values);
|
|
747
|
-
if (entries.length === 0) {
|
|
748
|
-
throw new Error(`Cannot insert into ${table.name} without any column values.`);
|
|
749
|
-
}
|
|
750
|
-
const params = [];
|
|
751
|
-
const columns = entries.map(([column]) => quoteIdentifier(column)).join(", ");
|
|
752
|
-
const placeholders = entries.map(([, value]) => pushParam(params, value)).join(", ");
|
|
753
|
-
const returningColumns = buildReturningColumns(table);
|
|
754
|
-
return {
|
|
755
|
-
text: `INSERT INTO ${quoteIdentifier(table.name)} (${columns}) VALUES (${placeholders}) RETURNING ${returningColumns}`,
|
|
756
|
-
params
|
|
757
|
-
};
|
|
758
|
-
}
|
|
759
|
-
function buildUpdateQuery(table, id, changes) {
|
|
760
|
-
const entries = getDefinedColumnEntries(table, changes, {
|
|
761
|
-
exclude: [table.primaryKey]
|
|
762
|
-
});
|
|
763
|
-
if (entries.length === 0) {
|
|
764
|
-
throw new Error(`Cannot update ${table.name} without any changed column values.`);
|
|
765
|
-
}
|
|
766
|
-
const params = [];
|
|
767
|
-
const setClause = entries.map(([column, value]) => `${quoteIdentifier(column)} = ${pushParam(params, value)}`).join(", ");
|
|
768
|
-
const primaryKeyPlaceholder = pushParam(params, id);
|
|
769
|
-
const returningColumns = buildReturningColumns(table);
|
|
770
|
-
const scopeClauses = [];
|
|
771
|
-
appendSoftDeleteScope(table, {}, scopeClauses);
|
|
772
|
-
const scopeSuffix = scopeClauses.length > 0 ? ` AND ${scopeClauses.join(" AND ")}` : "";
|
|
773
|
-
return {
|
|
774
|
-
text: `UPDATE ${quoteIdentifier(table.name)} SET ${setClause} WHERE ${quoteIdentifier(table.primaryKey)} = ${primaryKeyPlaceholder}${scopeSuffix} RETURNING ${returningColumns}`,
|
|
775
|
-
params
|
|
776
|
-
};
|
|
777
|
-
}
|
|
778
|
-
function buildSoftDeleteByIdQuery(table, id, deletedAt) {
|
|
779
|
-
const deletedAtColumn = resolveSoftDeleteColumn(table);
|
|
780
|
-
if (!deletedAtColumn) {
|
|
781
|
-
throw new Error(`Table ${table.name} does not support soft deletes.`);
|
|
782
|
-
}
|
|
783
|
-
const returningColumns = buildReturningColumns(table);
|
|
784
|
-
const scopeClauses = [];
|
|
785
|
-
appendSoftDeleteScope(table, {}, scopeClauses);
|
|
786
|
-
const scopeSuffix = scopeClauses.length > 0 ? ` AND ${scopeClauses.join(" AND ")}` : "";
|
|
787
|
-
return {
|
|
788
|
-
text: `UPDATE ${quoteIdentifier(table.name)} SET ${quoteIdentifier(deletedAtColumn)} = $1 WHERE ${quoteIdentifier(table.primaryKey)} = $2${scopeSuffix} RETURNING ${returningColumns}`,
|
|
789
|
-
params: [deletedAt, id]
|
|
790
|
-
};
|
|
791
|
-
}
|
|
792
|
-
function buildRestoreByIdQuery(table, id) {
|
|
793
|
-
const deletedAtColumn = resolveSoftDeleteColumn(table);
|
|
794
|
-
if (!deletedAtColumn) {
|
|
795
|
-
throw new Error(`Table ${table.name} does not support soft deletes.`);
|
|
796
|
-
}
|
|
797
|
-
const returningColumns = buildReturningColumns(table);
|
|
798
|
-
return {
|
|
799
|
-
text: `UPDATE ${quoteIdentifier(table.name)} SET ${quoteIdentifier(deletedAtColumn)} = $1 WHERE ${quoteIdentifier(table.primaryKey)} = $2 AND ${qualifyColumn(table.name, deletedAtColumn)} IS NOT NULL RETURNING ${returningColumns}`,
|
|
800
|
-
params: [null, id]
|
|
801
|
-
};
|
|
802
|
-
}
|
|
803
|
-
function buildDeleteByIdQuery(table, id) {
|
|
804
|
-
return {
|
|
805
|
-
text: `DELETE FROM ${quoteIdentifier(table.name)} WHERE ${quoteIdentifier(table.primaryKey)} = $1 RETURNING ${quoteIdentifier(table.primaryKey)} AS ${quoteIdentifier("deleted_id")}`,
|
|
806
|
-
params: [id]
|
|
807
|
-
};
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
// ../../src/core/database/relationships.ts
|
|
811
|
-
function hasMany(definition) {
|
|
812
|
-
return {
|
|
813
|
-
type: "hasMany",
|
|
814
|
-
...definition
|
|
815
|
-
};
|
|
816
|
-
}
|
|
817
|
-
function hasOne(definition) {
|
|
818
|
-
return {
|
|
819
|
-
type: "hasOne",
|
|
820
|
-
...definition
|
|
821
|
-
};
|
|
822
|
-
}
|
|
823
|
-
function belongsTo(definition) {
|
|
824
|
-
return {
|
|
825
|
-
type: "belongsTo",
|
|
826
|
-
...definition
|
|
827
|
-
};
|
|
828
|
-
}
|
|
829
|
-
function belongsToMany(definition) {
|
|
830
|
-
return {
|
|
831
|
-
type: "belongsToMany",
|
|
832
|
-
...definition
|
|
833
|
-
};
|
|
834
|
-
}
|
|
835
|
-
function indexHasManyRelation(parents, children, relation) {
|
|
836
|
-
const groups = new Map;
|
|
837
|
-
for (const parent of parents) {
|
|
838
|
-
groups.set(parent[relation.localKey], []);
|
|
839
|
-
}
|
|
840
|
-
for (const child of children) {
|
|
841
|
-
const key = child[relation.foreignKey];
|
|
842
|
-
const group = groups.get(key);
|
|
843
|
-
if (!group) {
|
|
844
|
-
continue;
|
|
845
|
-
}
|
|
846
|
-
group.push(child);
|
|
847
|
-
}
|
|
848
|
-
return groups;
|
|
849
|
-
}
|
|
850
|
-
function indexHasOneRelation(parents, children, relation) {
|
|
851
|
-
const grouped = indexHasManyRelation(parents, children, relation);
|
|
852
|
-
const result = new Map;
|
|
853
|
-
for (const parent of parents) {
|
|
854
|
-
const matches = grouped.get(parent[relation.localKey]) ?? [];
|
|
855
|
-
result.set(parent[relation.localKey], matches[0]);
|
|
856
|
-
}
|
|
857
|
-
return result;
|
|
858
|
-
}
|
|
859
|
-
function indexBelongsToRelation(children, parents, relation) {
|
|
860
|
-
const parentsById = new Map;
|
|
861
|
-
for (const parent of parents) {
|
|
862
|
-
parentsById.set(parent[relation.ownerKey], parent);
|
|
863
|
-
}
|
|
864
|
-
const result = new Map;
|
|
865
|
-
for (const child of children) {
|
|
866
|
-
const foreignKey = child[relation.foreignKey];
|
|
867
|
-
const parent = parentsById.get(foreignKey);
|
|
868
|
-
if (parent) {
|
|
869
|
-
result.set(foreignKey, parent);
|
|
870
|
-
}
|
|
871
|
-
}
|
|
872
|
-
return result;
|
|
873
|
-
}
|
|
874
|
-
function indexBelongsToManyRelation(parents, pivotRows, relatedRows, relation) {
|
|
875
|
-
const relatedById = new Map;
|
|
876
|
-
for (const related of relatedRows) {
|
|
877
|
-
relatedById.set(related[relation.relatedKey], related);
|
|
878
|
-
}
|
|
879
|
-
const groups = new Map;
|
|
880
|
-
for (const parent of parents) {
|
|
881
|
-
groups.set(parent[relation.parentKey], []);
|
|
882
|
-
}
|
|
883
|
-
for (const pivot of pivotRows) {
|
|
884
|
-
const parentId = pivot[relation.foreignPivotKey];
|
|
885
|
-
const relatedId = pivot[relation.relatedPivotKey];
|
|
886
|
-
const group = groups.get(parentId);
|
|
887
|
-
const related = relatedById.get(relatedId);
|
|
888
|
-
if (!group || !related) {
|
|
889
|
-
continue;
|
|
890
|
-
}
|
|
891
|
-
group.push(related);
|
|
892
|
-
}
|
|
893
|
-
return groups;
|
|
894
|
-
}
|
|
895
|
-
function morphMany(definition) {
|
|
896
|
-
return {
|
|
897
|
-
type: "morphMany",
|
|
898
|
-
...definition
|
|
899
|
-
};
|
|
900
|
-
}
|
|
901
|
-
function morphOne(definition) {
|
|
902
|
-
return {
|
|
903
|
-
type: "morphOne",
|
|
904
|
-
...definition
|
|
905
|
-
};
|
|
906
|
-
}
|
|
907
|
-
function morphTo(definition) {
|
|
908
|
-
return {
|
|
909
|
-
type: "morphTo",
|
|
910
|
-
...definition
|
|
911
|
-
};
|
|
912
|
-
}
|
|
913
|
-
function indexMorphManyRelation(parents, children, relation) {
|
|
914
|
-
const groups = new Map;
|
|
915
|
-
for (const parent of parents) {
|
|
916
|
-
groups.set(parent[relation.localKey], []);
|
|
917
|
-
}
|
|
918
|
-
for (const child of children) {
|
|
919
|
-
if (child[relation.morphTypeKey] !== relation.morphType) {
|
|
920
|
-
continue;
|
|
921
|
-
}
|
|
922
|
-
const key = child[relation.morphIdKey];
|
|
923
|
-
const group = groups.get(key);
|
|
924
|
-
if (!group) {
|
|
925
|
-
continue;
|
|
926
|
-
}
|
|
927
|
-
group.push(child);
|
|
928
|
-
}
|
|
929
|
-
return groups;
|
|
930
|
-
}
|
|
931
|
-
function indexMorphOneRelation(parents, children, relation) {
|
|
932
|
-
const grouped = indexMorphManyRelation(parents, children, relation);
|
|
933
|
-
const result = new Map;
|
|
934
|
-
for (const parent of parents) {
|
|
935
|
-
const matches = grouped.get(parent[relation.localKey]) ?? [];
|
|
936
|
-
result.set(parent[relation.localKey], matches[0]);
|
|
937
|
-
}
|
|
938
|
-
return result;
|
|
939
|
-
}
|
|
940
|
-
function indexMorphToRelation(children, parentsByType, relation) {
|
|
941
|
-
const result = new Map;
|
|
942
|
-
for (const child of children) {
|
|
943
|
-
const morphType = String(child[relation.morphTypeKey]);
|
|
944
|
-
const parents = parentsByType.get(morphType);
|
|
945
|
-
if (!parents) {
|
|
946
|
-
continue;
|
|
947
|
-
}
|
|
948
|
-
const parent = parents.get(child[relation.morphIdKey]);
|
|
949
|
-
if (parent) {
|
|
950
|
-
result.set(child[relation.morphIdKey], parent);
|
|
951
|
-
}
|
|
952
|
-
}
|
|
953
|
-
return result;
|
|
954
|
-
}
|
|
955
|
-
|
|
956
|
-
// ../../src/core/database/boundConnection.ts
|
|
957
|
-
var boundConnectionHolder = {
|
|
958
|
-
connection: null
|
|
959
|
-
};
|
|
960
|
-
function bindDatabaseConnection(connection) {
|
|
961
|
-
boundConnectionHolder.connection = connection;
|
|
962
|
-
}
|
|
963
|
-
function getBoundDatabaseConnection() {
|
|
964
|
-
return boundConnectionHolder.connection;
|
|
965
|
-
}
|
|
966
|
-
function resetBoundDatabaseConnection() {
|
|
967
|
-
boundConnectionHolder.connection = null;
|
|
968
|
-
}
|
|
969
|
-
|
|
970
|
-
// ../../src/core/runtime/asyncContextStore.ts
|
|
971
|
-
import { AsyncLocalStorage } from "async_hooks";
|
|
972
|
-
function createAsyncContextStore(key) {
|
|
973
|
-
const symbol = Symbol.for(key);
|
|
974
|
-
const globalRecord = globalThis;
|
|
975
|
-
const existing = globalRecord[symbol];
|
|
976
|
-
if (existing) {
|
|
977
|
-
return existing;
|
|
978
|
-
}
|
|
979
|
-
const store = new AsyncLocalStorage;
|
|
980
|
-
globalRecord[symbol] = store;
|
|
981
|
-
return store;
|
|
982
|
-
}
|
|
983
|
-
|
|
984
|
-
// ../../src/core/database/connectionContext.ts
|
|
985
|
-
var activeConnection = createAsyncContextStore("@getstrata/databaseConnectionContext");
|
|
986
|
-
function runWithDatabaseConnection(connection, callback) {
|
|
987
|
-
return activeConnection.run(connection, callback);
|
|
988
|
-
}
|
|
989
|
-
function getActiveDatabaseConnection(fallback) {
|
|
990
|
-
return activeConnection.getStore() ?? fallback;
|
|
991
|
-
}
|
|
992
|
-
function hasActiveDatabaseConnection() {
|
|
993
|
-
return activeConnection.getStore() !== undefined;
|
|
994
|
-
}
|
|
995
|
-
|
|
996
|
-
// ../../src/core/database/queryProxy.ts
|
|
997
|
-
var POOL_CONNECTION_METHODS = new Set(["begin", "close", "connect"]);
|
|
998
|
-
function createDatabaseQueryProxy(pool) {
|
|
999
|
-
function resolveDatabase() {
|
|
1000
|
-
return getActiveDatabaseConnection(pool);
|
|
1001
|
-
}
|
|
1002
|
-
function resolveDatabaseForProperty(property) {
|
|
1003
|
-
if (typeof property === "string" && POOL_CONNECTION_METHODS.has(property)) {
|
|
1004
|
-
return pool;
|
|
1005
|
-
}
|
|
1006
|
-
return resolveDatabase();
|
|
1007
|
-
}
|
|
1008
|
-
return new Proxy(function database() {}, {
|
|
1009
|
-
apply(_target, _thisArg, args) {
|
|
1010
|
-
return resolveDatabase()(...args);
|
|
1011
|
-
},
|
|
1012
|
-
get(_target, property) {
|
|
1013
|
-
const connection = resolveDatabaseForProperty(property);
|
|
1014
|
-
const value = connection[property];
|
|
1015
|
-
return typeof value === "function" ? value.bind(connection) : value;
|
|
1016
|
-
}
|
|
1017
|
-
});
|
|
1018
|
-
}
|
|
1019
|
-
|
|
1020
|
-
// ../../src/core/database/defaultConnection.ts
|
|
1021
|
-
var defaultPool = {
|
|
1022
|
-
connection: null
|
|
1023
|
-
};
|
|
1024
|
-
var defaultQuery = {
|
|
1025
|
-
connection: null
|
|
1026
|
-
};
|
|
1027
|
-
function registerDefaultDatabasePool(connection) {
|
|
1028
|
-
defaultPool.connection = connection;
|
|
1029
|
-
defaultQuery.connection = createDatabaseQueryProxy(connection);
|
|
1030
|
-
}
|
|
1031
|
-
function getDefaultDatabasePool() {
|
|
1032
|
-
if (!defaultPool.connection) {
|
|
1033
|
-
throw new Error("Default database pool is not registered. Call registerDefaultDatabasePool() during app bootstrap.");
|
|
1034
|
-
}
|
|
1035
|
-
return defaultPool.connection;
|
|
1036
|
-
}
|
|
1037
|
-
function getDefaultDatabaseQuery() {
|
|
1038
|
-
if (!defaultQuery.connection) {
|
|
1039
|
-
throw new Error("Default database query handle is not registered. Call registerDefaultDatabasePool() during app bootstrap.");
|
|
1040
|
-
}
|
|
1041
|
-
return defaultQuery.connection;
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
|
-
// ../../src/core/database/repositoryConnection.ts
|
|
1045
|
-
function resolveRepositoryConnection() {
|
|
1046
|
-
return getBoundDatabaseConnection() ?? getDefaultDatabaseQuery();
|
|
1047
|
-
}
|
|
1048
|
-
var repositoryConnection = new Proxy(function repositoryConnection2() {}, {
|
|
1049
|
-
apply(_target, _thisArg, args) {
|
|
1050
|
-
return resolveRepositoryConnection()(...args);
|
|
1051
|
-
},
|
|
1052
|
-
get(_target, property) {
|
|
1053
|
-
const connection = resolveRepositoryConnection();
|
|
1054
|
-
const value = connection[property];
|
|
1055
|
-
return typeof value === "function" ? value.bind(connection) : value;
|
|
1056
|
-
}
|
|
1057
|
-
});
|
|
1058
|
-
|
|
1059
|
-
// ../../src/core/database/whereBuilder.ts
|
|
1060
|
-
class WhereBuilder {
|
|
1061
|
-
nodes = [];
|
|
1062
|
-
where(where) {
|
|
1063
|
-
this.nodes.push({ kind: "and", where });
|
|
1064
|
-
return this;
|
|
1065
|
-
}
|
|
1066
|
-
orWhere(where) {
|
|
1067
|
-
this.nodes.push({ kind: "or", where });
|
|
1068
|
-
return this;
|
|
1069
|
-
}
|
|
1070
|
-
whereGroup(fn) {
|
|
1071
|
-
const nested = new WhereBuilder;
|
|
1072
|
-
fn(nested);
|
|
1073
|
-
if (nested.nodes.length > 0) {
|
|
1074
|
-
this.nodes.push({ kind: "and", group: nested.nodes });
|
|
1075
|
-
}
|
|
1076
|
-
return this;
|
|
1077
|
-
}
|
|
1078
|
-
orWhereGroup(fn) {
|
|
1079
|
-
const nested = new WhereBuilder;
|
|
1080
|
-
fn(nested);
|
|
1081
|
-
if (nested.nodes.length > 0) {
|
|
1082
|
-
this.nodes.push({ kind: "or", group: nested.nodes });
|
|
1083
|
-
}
|
|
1084
|
-
return this;
|
|
1085
|
-
}
|
|
1086
|
-
}
|
|
1087
|
-
|
|
1088
|
-
// ../../src/core/database/repositoryQuery.ts
|
|
1089
|
-
class RepositoryQuery {
|
|
1090
|
-
repository;
|
|
1091
|
-
whereClause;
|
|
1092
|
-
queryOptions;
|
|
1093
|
-
eagerLoads = [];
|
|
1094
|
-
whereNodes = [];
|
|
1095
|
-
constructor(repository, whereClause = {}, queryOptions = {}) {
|
|
1096
|
-
this.repository = repository;
|
|
1097
|
-
this.whereClause = whereClause;
|
|
1098
|
-
this.queryOptions = queryOptions;
|
|
1099
|
-
}
|
|
1100
|
-
where(input) {
|
|
1101
|
-
if (typeof input === "function") {
|
|
1102
|
-
const builder = new WhereBuilder;
|
|
1103
|
-
input(builder);
|
|
1104
|
-
this.whereNodes.push(...builder.nodes);
|
|
1105
|
-
return this;
|
|
1106
|
-
}
|
|
1107
|
-
this.whereClause = { ...this.whereClause, ...input };
|
|
1108
|
-
return this;
|
|
1109
|
-
}
|
|
1110
|
-
orWhere(input) {
|
|
1111
|
-
if (typeof input === "function") {
|
|
1112
|
-
const builder = new WhereBuilder;
|
|
1113
|
-
input(builder);
|
|
1114
|
-
if (builder.nodes.length > 0) {
|
|
1115
|
-
this.whereNodes.push({ kind: "or", group: builder.nodes });
|
|
1116
|
-
}
|
|
1117
|
-
return this;
|
|
1118
|
-
}
|
|
1119
|
-
this.whereNodes.push({ kind: "or", where: input });
|
|
1120
|
-
return this;
|
|
1121
|
-
}
|
|
1122
|
-
orderBy(orderBy) {
|
|
1123
|
-
this.queryOptions = { ...this.queryOptions, orderBy };
|
|
1124
|
-
return this;
|
|
1125
|
-
}
|
|
1126
|
-
limit(limit) {
|
|
1127
|
-
this.queryOptions = { ...this.queryOptions, limit };
|
|
1128
|
-
return this;
|
|
1129
|
-
}
|
|
1130
|
-
offset(offset) {
|
|
1131
|
-
this.queryOptions = { ...this.queryOptions, offset };
|
|
1132
|
-
return this;
|
|
1133
|
-
}
|
|
1134
|
-
join(left, right) {
|
|
1135
|
-
return this.addJoin("inner", left, right);
|
|
1136
|
-
}
|
|
1137
|
-
leftJoin(left, right) {
|
|
1138
|
-
return this.addJoin("left", left, right);
|
|
1139
|
-
}
|
|
1140
|
-
groupBy(groupBy) {
|
|
1141
|
-
this.queryOptions = { ...this.queryOptions, groupBy };
|
|
1142
|
-
return this;
|
|
1143
|
-
}
|
|
1144
|
-
having(having) {
|
|
1145
|
-
this.queryOptions = { ...this.queryOptions, having };
|
|
1146
|
-
return this;
|
|
1147
|
-
}
|
|
1148
|
-
withHasMany(as, relation, childRepository, options = {}) {
|
|
1149
|
-
this.eagerLoads.push({
|
|
1150
|
-
kind: "hasMany",
|
|
1151
|
-
as,
|
|
1152
|
-
relation,
|
|
1153
|
-
repository: childRepository,
|
|
1154
|
-
options
|
|
1155
|
-
});
|
|
1156
|
-
return this;
|
|
1157
|
-
}
|
|
1158
|
-
withBelongsTo(as, relation, parentRepository, options = {}) {
|
|
1159
|
-
this.eagerLoads.push({
|
|
1160
|
-
kind: "belongsTo",
|
|
1161
|
-
as,
|
|
1162
|
-
relation,
|
|
1163
|
-
repository: parentRepository,
|
|
1164
|
-
options
|
|
1165
|
-
});
|
|
1166
|
-
return this;
|
|
1167
|
-
}
|
|
1168
|
-
withMorphMany(as, relation, childRepository, options = {}) {
|
|
1169
|
-
this.eagerLoads.push({
|
|
1170
|
-
kind: "morphMany",
|
|
1171
|
-
as,
|
|
1172
|
-
relation,
|
|
1173
|
-
repository: childRepository,
|
|
1174
|
-
options
|
|
1175
|
-
});
|
|
1176
|
-
return this;
|
|
1177
|
-
}
|
|
1178
|
-
withMorphOne(as, relation, childRepository, options = {}) {
|
|
1179
|
-
this.eagerLoads.push({
|
|
1180
|
-
kind: "morphOne",
|
|
1181
|
-
as,
|
|
1182
|
-
relation,
|
|
1183
|
-
repository: childRepository,
|
|
1184
|
-
options
|
|
1185
|
-
});
|
|
1186
|
-
return this;
|
|
1187
|
-
}
|
|
1188
|
-
withMorphTo(as, relation, repositoriesByType, options = {}) {
|
|
1189
|
-
this.eagerLoads.push({
|
|
1190
|
-
kind: "morphTo",
|
|
1191
|
-
as,
|
|
1192
|
-
relation,
|
|
1193
|
-
repository: this.repository,
|
|
1194
|
-
morphRepositories: repositoriesByType,
|
|
1195
|
-
options
|
|
1196
|
-
});
|
|
1197
|
-
return this;
|
|
1198
|
-
}
|
|
1199
|
-
async get() {
|
|
1200
|
-
const rows = await this.repository.findAll(this.buildOptions());
|
|
1201
|
-
return await this.attach(rows);
|
|
1202
|
-
}
|
|
1203
|
-
async first() {
|
|
1204
|
-
const rows = await this.get();
|
|
1205
|
-
return rows[0] ?? null;
|
|
1206
|
-
}
|
|
1207
|
-
async paginate(options) {
|
|
1208
|
-
return await this.repository.paginate({
|
|
1209
|
-
...this.buildOptions(),
|
|
1210
|
-
page: options.page,
|
|
1211
|
-
perPage: options.perPage
|
|
1212
|
-
});
|
|
1213
|
-
}
|
|
1214
|
-
buildOptions() {
|
|
1215
|
-
return {
|
|
1216
|
-
...this.queryOptions,
|
|
1217
|
-
where: this.whereClause,
|
|
1218
|
-
whereNodes: this.whereNodes
|
|
1219
|
-
};
|
|
1220
|
-
}
|
|
1221
|
-
addJoin(type, left, right) {
|
|
1222
|
-
const leftRef = parseQualifiedColumn(left);
|
|
1223
|
-
const rightRef = parseQualifiedColumn(right);
|
|
1224
|
-
const table = type === "inner" ? rightRef.table : rightRef.table;
|
|
1225
|
-
const joins = this.queryOptions.joins ?? [];
|
|
1226
|
-
const existing = joins.find((join2) => join2.table === table && join2.type === type);
|
|
1227
|
-
if (existing) {
|
|
1228
|
-
existing.on.push({ left: leftRef, right: rightRef });
|
|
1229
|
-
return this;
|
|
1230
|
-
}
|
|
1231
|
-
this.queryOptions = {
|
|
1232
|
-
...this.queryOptions,
|
|
1233
|
-
joins: [
|
|
1234
|
-
...joins,
|
|
1235
|
-
{
|
|
1236
|
-
type,
|
|
1237
|
-
table,
|
|
1238
|
-
on: [{ left: leftRef, right: rightRef }]
|
|
1239
|
-
}
|
|
1240
|
-
]
|
|
1241
|
-
};
|
|
1242
|
-
return this;
|
|
1243
|
-
}
|
|
1244
|
-
async attach(rows) {
|
|
1245
|
-
if (rows.length === 0 || this.eagerLoads.length === 0) {
|
|
1246
|
-
return rows.map((row) => ({ ...row }));
|
|
1247
|
-
}
|
|
1248
|
-
let result = rows.map((row) => ({ ...row }));
|
|
1249
|
-
for (const load of this.eagerLoads) {
|
|
1250
|
-
if (load.kind === "hasMany") {
|
|
1251
|
-
const relation2 = load.relation;
|
|
1252
|
-
const grouped2 = await load.repository.withConnection(this.repository.getConnection()).loadHasManyForParents(rows, relation2, load.options);
|
|
1253
|
-
result = result.map((row) => ({
|
|
1254
|
-
...row,
|
|
1255
|
-
[load.as]: grouped2.get(row[relation2.localKey]) ?? []
|
|
1256
|
-
}));
|
|
1257
|
-
continue;
|
|
1258
|
-
}
|
|
1259
|
-
if (load.kind === "morphMany") {
|
|
1260
|
-
const relation2 = load.relation;
|
|
1261
|
-
const grouped2 = await load.repository.withConnection(this.repository.getConnection()).loadMorphManyForParents(rows, relation2, load.options);
|
|
1262
|
-
result = result.map((row) => ({
|
|
1263
|
-
...row,
|
|
1264
|
-
[load.as]: grouped2.get(row[relation2.localKey]) ?? []
|
|
1265
|
-
}));
|
|
1266
|
-
continue;
|
|
1267
|
-
}
|
|
1268
|
-
if (load.kind === "morphOne") {
|
|
1269
|
-
const relation2 = load.relation;
|
|
1270
|
-
const grouped2 = await load.repository.withConnection(this.repository.getConnection()).loadMorphOneForParents(rows, relation2, load.options);
|
|
1271
|
-
result = result.map((row) => ({
|
|
1272
|
-
...row,
|
|
1273
|
-
[load.as]: grouped2.get(row[relation2.localKey])
|
|
1274
|
-
}));
|
|
1275
|
-
continue;
|
|
1276
|
-
}
|
|
1277
|
-
if (load.kind === "morphTo") {
|
|
1278
|
-
const relation2 = load.relation;
|
|
1279
|
-
const grouped2 = await this.repository.loadMorphToForChildren(rows, relation2, load.morphRepositories ?? new Map, load.options);
|
|
1280
|
-
result = result.map((row) => ({
|
|
1281
|
-
...row,
|
|
1282
|
-
[load.as]: grouped2.get(row[relation2.morphIdKey])
|
|
1283
|
-
}));
|
|
1284
|
-
continue;
|
|
1285
|
-
}
|
|
1286
|
-
const relation = load.relation;
|
|
1287
|
-
const grouped = await this.repository.loadBelongsToForParents(rows, relation, load.repository, load.options);
|
|
1288
|
-
result = result.map((row) => ({
|
|
1289
|
-
...row,
|
|
1290
|
-
[load.as]: grouped.get(row[relation.foreignKey])
|
|
1291
|
-
}));
|
|
1292
|
-
}
|
|
1293
|
-
return result;
|
|
1294
|
-
}
|
|
1295
|
-
}
|
|
1296
|
-
|
|
1297
|
-
// ../../src/core/database/baseRepository.ts
|
|
1298
|
-
class BaseRepository {
|
|
1299
|
-
table;
|
|
1300
|
-
connection;
|
|
1301
|
-
constructor(table, connection = repositoryConnection) {
|
|
1302
|
-
this.table = table;
|
|
1303
|
-
this.connection = connection;
|
|
1304
|
-
}
|
|
1305
|
-
async findAll(options = {}) {
|
|
1306
|
-
return await withDatabaseErrorHandling(async () => {
|
|
1307
|
-
const { whereNodes, ...queryOptions } = options;
|
|
1308
|
-
const { text, params } = buildSelectQuery(this.table, queryOptions, whereNodes ?? []);
|
|
1309
|
-
return await this.connection.unsafe(text, params);
|
|
1310
|
-
});
|
|
1311
|
-
}
|
|
1312
|
-
async paginate(options) {
|
|
1313
|
-
const { whereNodes, where = {}, page, perPage, ...queryOptions } = options;
|
|
1314
|
-
const total = await this.countWhere(where, {
|
|
1315
|
-
withTrashed: options.withTrashed,
|
|
1316
|
-
onlyTrashed: options.onlyTrashed,
|
|
1317
|
-
joins: options.joins,
|
|
1318
|
-
groupBy: options.groupBy
|
|
1319
|
-
}, whereNodes);
|
|
1320
|
-
const offset = (page - 1) * perPage;
|
|
1321
|
-
const data = await this.findAll({
|
|
1322
|
-
...queryOptions,
|
|
1323
|
-
where,
|
|
1324
|
-
whereNodes,
|
|
1325
|
-
limit: perPage,
|
|
1326
|
-
offset
|
|
1327
|
-
});
|
|
1328
|
-
return {
|
|
1329
|
-
data,
|
|
1330
|
-
meta: buildPaginationMeta({ page, perPage, total })
|
|
1331
|
-
};
|
|
1332
|
-
}
|
|
1333
|
-
async chunk(count, callback, options = {}) {
|
|
1334
|
-
if (!Number.isInteger(count) || count <= 0) {
|
|
1335
|
-
throw new Error("Chunk size must be a positive integer.");
|
|
1336
|
-
}
|
|
1337
|
-
let offset = 0;
|
|
1338
|
-
while (true) {
|
|
1339
|
-
const rows = await this.findAll({
|
|
1340
|
-
...options,
|
|
1341
|
-
limit: count,
|
|
1342
|
-
offset
|
|
1343
|
-
});
|
|
1344
|
-
if (rows.length === 0) {
|
|
1345
|
-
return;
|
|
1346
|
-
}
|
|
1347
|
-
const shouldContinue = await callback(rows);
|
|
1348
|
-
if (shouldContinue === false || rows.length < count) {
|
|
1349
|
-
return;
|
|
1350
|
-
}
|
|
1351
|
-
offset += count;
|
|
1352
|
-
}
|
|
1353
|
-
}
|
|
1354
|
-
async cursorPaginate(options) {
|
|
1355
|
-
const {
|
|
1356
|
-
perPage,
|
|
1357
|
-
cursor,
|
|
1358
|
-
cursorColumn = this.table.primaryKey,
|
|
1359
|
-
direction = "asc",
|
|
1360
|
-
where = {},
|
|
1361
|
-
whereNodes,
|
|
1362
|
-
...queryOptions
|
|
1363
|
-
} = options;
|
|
1364
|
-
if (!Number.isInteger(perPage) || perPage <= 0) {
|
|
1365
|
-
throw new Error("Cursor page size must be a positive integer.");
|
|
1366
|
-
}
|
|
1367
|
-
const cursorWhere = { ...where };
|
|
1368
|
-
if (cursor !== undefined) {
|
|
1369
|
-
cursorWhere[cursorColumn] = direction === "asc" ? { gt: cursor } : { lt: cursor };
|
|
1370
|
-
}
|
|
1371
|
-
const rows = await this.findAll({
|
|
1372
|
-
...queryOptions,
|
|
1373
|
-
where: cursorWhere,
|
|
1374
|
-
whereNodes,
|
|
1375
|
-
orderBy: { [cursorColumn]: direction },
|
|
1376
|
-
limit: perPage + 1
|
|
1377
|
-
});
|
|
1378
|
-
const hasMore = rows.length > perPage;
|
|
1379
|
-
const data = hasMore ? rows.slice(0, perPage) : rows;
|
|
1380
|
-
const nextCursor = hasMore ? data[data.length - 1]?.[cursorColumn] ?? null : null;
|
|
1381
|
-
const prevCursor = cursor ?? null;
|
|
1382
|
-
return {
|
|
1383
|
-
data,
|
|
1384
|
-
meta: {
|
|
1385
|
-
per_page: perPage,
|
|
1386
|
-
next_cursor: nextCursor,
|
|
1387
|
-
prev_cursor: prevCursor,
|
|
1388
|
-
has_more: hasMore
|
|
1389
|
-
}
|
|
1390
|
-
};
|
|
1391
|
-
}
|
|
1392
|
-
async findById(id) {
|
|
1393
|
-
return await this.firstOrNull({
|
|
1394
|
-
[this.table.primaryKey]: id
|
|
1395
|
-
});
|
|
1396
|
-
}
|
|
1397
|
-
async findByIdOrThrow(id, errorFactory) {
|
|
1398
|
-
const record = await this.findById(id);
|
|
1399
|
-
if (record) {
|
|
1400
|
-
return record;
|
|
1401
|
-
}
|
|
1402
|
-
throw errorFactory?.(id) ?? new Error(`${this.table.name} ${String(id)} was not found.`);
|
|
1403
|
-
}
|
|
1404
|
-
async findByIds(ids) {
|
|
1405
|
-
const uniqueIds = [...new Set(ids)];
|
|
1406
|
-
if (uniqueIds.length === 0) {
|
|
1407
|
-
return [];
|
|
1408
|
-
}
|
|
1409
|
-
return await this.findWhere({
|
|
1410
|
-
[this.table.primaryKey]: uniqueIds
|
|
1411
|
-
});
|
|
1412
|
-
}
|
|
1413
|
-
async firstOrNull(where, options = {}) {
|
|
1414
|
-
const [record] = await this.findAll({ ...options, where, limit: 1 });
|
|
1415
|
-
return record ?? null;
|
|
1416
|
-
}
|
|
1417
|
-
async create(values) {
|
|
1418
|
-
return await withDatabaseErrorHandling(async () => {
|
|
1419
|
-
const { text, params } = buildInsertQuery(this.table, values);
|
|
1420
|
-
const [record] = await this.connection.unsafe(text, params);
|
|
1421
|
-
if (!record) {
|
|
1422
|
-
throw new Error(`Insert into ${this.table.name} did not return a record.`);
|
|
1423
|
-
}
|
|
1424
|
-
const entity = record;
|
|
1425
|
-
await eventBus.dispatch(modelEventName(this.table.name, "created"), entity);
|
|
1426
|
-
return entity;
|
|
1427
|
-
});
|
|
1428
|
-
}
|
|
1429
|
-
async updateById(id, changes) {
|
|
1430
|
-
return await withDatabaseErrorHandling(async () => {
|
|
1431
|
-
const { text, params } = buildUpdateQuery(this.table, id, changes);
|
|
1432
|
-
const [record] = await this.connection.unsafe(text, params);
|
|
1433
|
-
const entity = record ?? null;
|
|
1434
|
-
if (entity) {
|
|
1435
|
-
await eventBus.dispatch(modelEventName(this.table.name, "updated"), entity);
|
|
1436
|
-
}
|
|
1437
|
-
return entity;
|
|
1438
|
-
});
|
|
1439
|
-
}
|
|
1440
|
-
async updateByIdOrThrow(id, changes, errorFactory) {
|
|
1441
|
-
const record = await this.updateById(id, changes);
|
|
1442
|
-
if (record) {
|
|
1443
|
-
return record;
|
|
1444
|
-
}
|
|
1445
|
-
throw errorFactory?.(id) ?? new Error(`${this.table.name} ${String(id)} was not found.`);
|
|
1446
|
-
}
|
|
1447
|
-
async deleteById(id) {
|
|
1448
|
-
if (resolveSoftDeleteColumn(this.table)) {
|
|
1449
|
-
return await this.softDeleteById(id);
|
|
1450
|
-
}
|
|
1451
|
-
return await this.forceDeleteById(id);
|
|
1452
|
-
}
|
|
1453
|
-
async softDeleteById(id) {
|
|
1454
|
-
return await withDatabaseErrorHandling(async () => {
|
|
1455
|
-
const { text, params } = buildSoftDeleteByIdQuery(this.table, id, new Date);
|
|
1456
|
-
const [record] = await this.connection.unsafe(text, params);
|
|
1457
|
-
if (!record) {
|
|
1458
|
-
return false;
|
|
1459
|
-
}
|
|
1460
|
-
await eventBus.dispatch(modelEventName(this.table.name, "deleted"), record);
|
|
1461
|
-
return true;
|
|
1462
|
-
});
|
|
1463
|
-
}
|
|
1464
|
-
async forceDeleteById(id) {
|
|
1465
|
-
return await withDatabaseErrorHandling(async () => {
|
|
1466
|
-
const { text, params } = buildDeleteByIdQuery(this.table, id);
|
|
1467
|
-
const [row] = await this.connection.unsafe(text, params);
|
|
1468
|
-
if (!row) {
|
|
1469
|
-
return false;
|
|
1470
|
-
}
|
|
1471
|
-
await eventBus.dispatch(modelEventName(this.table.name, "force-deleted"), {
|
|
1472
|
-
id
|
|
1473
|
-
});
|
|
1474
|
-
return true;
|
|
1475
|
-
});
|
|
1476
|
-
}
|
|
1477
|
-
async restoreById(id) {
|
|
1478
|
-
return await withDatabaseErrorHandling(async () => {
|
|
1479
|
-
const { text, params } = buildRestoreByIdQuery(this.table, id);
|
|
1480
|
-
const [record] = await this.connection.unsafe(text, params);
|
|
1481
|
-
if (!record) {
|
|
1482
|
-
return null;
|
|
1483
|
-
}
|
|
1484
|
-
const entity = record;
|
|
1485
|
-
await eventBus.dispatch(modelEventName(this.table.name, "restored"), entity);
|
|
1486
|
-
return entity;
|
|
1487
|
-
});
|
|
1488
|
-
}
|
|
1489
|
-
withConnection(connection) {
|
|
1490
|
-
const clone = Object.create(Object.getPrototypeOf(this));
|
|
1491
|
-
Object.assign(clone, this);
|
|
1492
|
-
clone.connection = connection;
|
|
1493
|
-
return clone;
|
|
1494
|
-
}
|
|
1495
|
-
getConnection() {
|
|
1496
|
-
return this.connection;
|
|
1497
|
-
}
|
|
1498
|
-
getTable() {
|
|
1499
|
-
return this.table;
|
|
1500
|
-
}
|
|
1501
|
-
query(where = {}) {
|
|
1502
|
-
return new RepositoryQuery(this, where);
|
|
1503
|
-
}
|
|
1504
|
-
async findWhere(where, options = {}) {
|
|
1505
|
-
return await this.findAll({ ...options, where });
|
|
1506
|
-
}
|
|
1507
|
-
async countWhere(where = {}, options = {}, whereNodes = []) {
|
|
1508
|
-
const { text, params } = buildCountQuery(this.table, where, options, whereNodes);
|
|
1509
|
-
const [row] = await this.connection.unsafe(text, params);
|
|
1510
|
-
return Number(row?.count ?? 0);
|
|
1511
|
-
}
|
|
1512
|
-
async averageColumn(column, where = {}) {
|
|
1513
|
-
const qualifiedColumn = qualifyColumn(this.table.name, column);
|
|
1514
|
-
return await this.averageExpression(`AVG(${qualifiedColumn})`, "value", where);
|
|
1515
|
-
}
|
|
1516
|
-
async averageExpression(expression, alias, where = {}) {
|
|
1517
|
-
const { text, params } = buildProjectionQuery(this.table, expression, alias, { where });
|
|
1518
|
-
const [row] = await this.connection.unsafe(text, params);
|
|
1519
|
-
return Math.round(Number(row?.[alias] ?? 0));
|
|
1520
|
-
}
|
|
1521
|
-
async pluckNumberValues(expression, alias, options = {}) {
|
|
1522
|
-
const { text, params } = buildProjectionQuery(this.table, expression, alias, options);
|
|
1523
|
-
const rows = await this.connection.unsafe(text, params);
|
|
1524
|
-
return rows.flatMap((row) => {
|
|
1525
|
-
const value = row[alias];
|
|
1526
|
-
return value === null || value === undefined ? [] : [Number(value)];
|
|
1527
|
-
});
|
|
1528
|
-
}
|
|
1529
|
-
async countGroupedBy(column, where = {}) {
|
|
1530
|
-
const { text, params } = buildGroupedCountQuery(this.table, column, where);
|
|
1531
|
-
const rows = await this.connection.unsafe(text, params);
|
|
1532
|
-
return rows.map(({ value, count }) => ({
|
|
1533
|
-
value,
|
|
1534
|
-
count: Number(count)
|
|
1535
|
-
}));
|
|
1536
|
-
}
|
|
1537
|
-
async findByHasManyRelation(relation, parentId, options = {}) {
|
|
1538
|
-
return await this.findWhere({
|
|
1539
|
-
[relation.foreignKey]: parentId
|
|
1540
|
-
}, options);
|
|
1541
|
-
}
|
|
1542
|
-
async loadHasManyForParents(parents, relation, options = {}) {
|
|
1543
|
-
if (parents.length === 0) {
|
|
1544
|
-
return indexHasManyRelation(parents, [], relation);
|
|
1545
|
-
}
|
|
1546
|
-
const parentIds = [...new Set(parents.map((parent) => parent[relation.localKey]))];
|
|
1547
|
-
const children = await this.findWhere({
|
|
1548
|
-
[relation.foreignKey]: parentIds
|
|
1549
|
-
}, options);
|
|
1550
|
-
return indexHasManyRelation(parents, children, relation);
|
|
1551
|
-
}
|
|
1552
|
-
async loadBelongsToForParents(children, relation, parentRepository, options = {}) {
|
|
1553
|
-
if (children.length === 0) {
|
|
1554
|
-
return new Map;
|
|
1555
|
-
}
|
|
1556
|
-
const ownerIds = [...new Set(children.map((child) => child[relation.foreignKey]))];
|
|
1557
|
-
const parents = await parentRepository.withConnection(this.connection).findWhere({
|
|
1558
|
-
[relation.ownerKey]: ownerIds
|
|
1559
|
-
}, options);
|
|
1560
|
-
return indexBelongsToRelation(children, parents, relation);
|
|
1561
|
-
}
|
|
1562
|
-
async loadMorphManyForParents(parents, relation, options = {}) {
|
|
1563
|
-
if (parents.length === 0) {
|
|
1564
|
-
return indexMorphManyRelation(parents, [], relation);
|
|
1565
|
-
}
|
|
1566
|
-
const parentIds = [...new Set(parents.map((parent) => parent[relation.localKey]))];
|
|
1567
|
-
const children = await this.findWhere({
|
|
1568
|
-
[relation.morphTypeKey]: relation.morphType,
|
|
1569
|
-
[relation.morphIdKey]: parentIds
|
|
1570
|
-
}, options);
|
|
1571
|
-
return indexMorphManyRelation(parents, children, relation);
|
|
1572
|
-
}
|
|
1573
|
-
async loadMorphOneForParents(parents, relation, options = {}) {
|
|
1574
|
-
const grouped = await this.loadMorphManyForParents(parents, relation, options);
|
|
1575
|
-
const result = new Map;
|
|
1576
|
-
for (const parent of parents) {
|
|
1577
|
-
const matches = grouped.get(parent[relation.localKey]) ?? [];
|
|
1578
|
-
result.set(parent[relation.localKey], matches[0]);
|
|
1579
|
-
}
|
|
1580
|
-
return result;
|
|
1581
|
-
}
|
|
1582
|
-
async loadMorphToForChildren(children, relation, repositoriesByType, options = {}) {
|
|
1583
|
-
if (children.length === 0) {
|
|
1584
|
-
return new Map;
|
|
1585
|
-
}
|
|
1586
|
-
const idsByType = new Map;
|
|
1587
|
-
for (const child of children) {
|
|
1588
|
-
const morphType = String(child[relation.morphTypeKey]);
|
|
1589
|
-
const morphId = child[relation.morphIdKey];
|
|
1590
|
-
const ids = idsByType.get(morphType) ?? new Set;
|
|
1591
|
-
ids.add(morphId);
|
|
1592
|
-
idsByType.set(morphType, ids);
|
|
1593
|
-
}
|
|
1594
|
-
const parentsByType = new Map;
|
|
1595
|
-
for (const [morphType, ids] of idsByType) {
|
|
1596
|
-
const repository = repositoriesByType.get(morphType);
|
|
1597
|
-
if (!repository) {
|
|
1598
|
-
continue;
|
|
1599
|
-
}
|
|
1600
|
-
const ownerKey = repository.getTable().primaryKey;
|
|
1601
|
-
const parents = await repository.withConnection(this.connection).findWhere({
|
|
1602
|
-
[ownerKey]: [...ids]
|
|
1603
|
-
}, options);
|
|
1604
|
-
const indexed = new Map;
|
|
1605
|
-
for (const parent of parents) {
|
|
1606
|
-
indexed.set(parent[ownerKey], parent);
|
|
1607
|
-
}
|
|
1608
|
-
parentsByType.set(morphType, indexed);
|
|
1609
|
-
}
|
|
1610
|
-
return indexMorphToRelation(children, parentsByType, relation);
|
|
1611
|
-
}
|
|
1612
|
-
}
|
|
1613
|
-
var baseRepository_default = BaseRepository;
|
|
1614
|
-
// ../../src/core/database/connection.ts
|
|
1615
|
-
function createDatabaseConnection(source) {
|
|
1616
|
-
return {
|
|
1617
|
-
async unsafe(query, params = []) {
|
|
1618
|
-
return await source.unsafe(query, params);
|
|
1619
|
-
}
|
|
1620
|
-
};
|
|
1621
|
-
}
|
|
1622
|
-
// ../../src/core/database/model.ts
|
|
1623
|
-
var modelRepositories = new WeakMap;
|
|
1624
|
-
var modelGlobalScopes = new WeakMap;
|
|
1625
|
-
var modelBooted = new WeakSet;
|
|
1626
|
-
function resolveModelRepository(model) {
|
|
1627
|
-
const repository = modelRepositories.get(model);
|
|
1628
|
-
if (!repository) {
|
|
1629
|
-
throw new Error(`${model.name}.repository() is not implemented.`);
|
|
1630
|
-
}
|
|
1631
|
-
return repository;
|
|
1632
|
-
}
|
|
1633
|
-
function modelStatics(model) {
|
|
1634
|
-
return model;
|
|
1635
|
-
}
|
|
1636
|
-
function ensureBooted(model) {
|
|
1637
|
-
if (modelBooted.has(model)) {
|
|
1638
|
-
return;
|
|
1639
|
-
}
|
|
1640
|
-
modelBooted.add(model);
|
|
1641
|
-
const boot = model.boot;
|
|
1642
|
-
if (typeof boot === "function") {
|
|
1643
|
-
boot.call(model);
|
|
1644
|
-
}
|
|
1645
|
-
}
|
|
1646
|
-
function getGlobalScopes(model) {
|
|
1647
|
-
return modelGlobalScopes.get(model) ?? [];
|
|
1648
|
-
}
|
|
1649
|
-
function hydrateValue(value, cast) {
|
|
1650
|
-
if (value === null || value === undefined) {
|
|
1651
|
-
return value;
|
|
1652
|
-
}
|
|
1653
|
-
switch (cast) {
|
|
1654
|
-
case "date":
|
|
1655
|
-
case "datetime":
|
|
1656
|
-
return value instanceof Date ? value : new Date(String(value));
|
|
1657
|
-
case "json":
|
|
1658
|
-
return typeof value === "string" ? JSON.parse(value) : value;
|
|
1659
|
-
case "bool":
|
|
1660
|
-
case "boolean":
|
|
1661
|
-
return value === true || value === 1 || value === "1" || value === "true";
|
|
1662
|
-
default:
|
|
1663
|
-
return value;
|
|
1664
|
-
}
|
|
1665
|
-
}
|
|
1666
|
-
function dehydrateValue(value, cast) {
|
|
1667
|
-
if (value === null || value === undefined) {
|
|
1668
|
-
return value;
|
|
1669
|
-
}
|
|
1670
|
-
switch (cast) {
|
|
1671
|
-
case "date":
|
|
1672
|
-
case "datetime":
|
|
1673
|
-
return value instanceof Date ? value : new Date(String(value));
|
|
1674
|
-
case "json":
|
|
1675
|
-
return typeof value === "string" ? value : JSON.stringify(value);
|
|
1676
|
-
case "bool":
|
|
1677
|
-
case "boolean":
|
|
1678
|
-
return Boolean(value);
|
|
1679
|
-
default:
|
|
1680
|
-
return value;
|
|
1681
|
-
}
|
|
1682
|
-
}
|
|
1683
|
-
function filterMassAssignable(fillable, guarded, input) {
|
|
1684
|
-
const resolvedGuarded = guarded ?? true;
|
|
1685
|
-
if (fillable && fillable.length > 0) {
|
|
1686
|
-
const allowed = new Set(fillable);
|
|
1687
|
-
return Object.fromEntries(Object.entries(input).filter(([key]) => allowed.has(key)));
|
|
1688
|
-
}
|
|
1689
|
-
if (resolvedGuarded === true || resolvedGuarded.includes("*")) {
|
|
1690
|
-
return {};
|
|
1691
|
-
}
|
|
1692
|
-
const blocked = new Set(resolvedGuarded);
|
|
1693
|
-
return Object.fromEntries(Object.entries(input).filter(([key]) => !blocked.has(key)));
|
|
1694
|
-
}
|
|
1695
|
-
function applyCasts(values, casts, direction) {
|
|
1696
|
-
if (Object.keys(casts).length === 0) {
|
|
1697
|
-
return values;
|
|
1698
|
-
}
|
|
1699
|
-
const result = { ...values };
|
|
1700
|
-
const castFn = direction === "hydrate" ? hydrateValue : dehydrateValue;
|
|
1701
|
-
for (const [key, cast] of Object.entries(casts)) {
|
|
1702
|
-
if (key in result && cast) {
|
|
1703
|
-
result[key] = castFn(result[key], cast);
|
|
1704
|
-
}
|
|
1705
|
-
}
|
|
1706
|
-
return result;
|
|
1707
|
-
}
|
|
1708
|
-
function applyTimestampsOnCreate(columns, values, enabled) {
|
|
1709
|
-
if (!enabled) {
|
|
1710
|
-
return values;
|
|
1711
|
-
}
|
|
1712
|
-
const now = new Date;
|
|
1713
|
-
const result = { ...values };
|
|
1714
|
-
if (columns.includes("created_at")) {
|
|
1715
|
-
result.created_at = now;
|
|
1716
|
-
}
|
|
1717
|
-
if (columns.includes("updated_at")) {
|
|
1718
|
-
result.updated_at = now;
|
|
1719
|
-
}
|
|
1720
|
-
return result;
|
|
1721
|
-
}
|
|
1722
|
-
function applyTimestampsOnUpdate(columns, values, enabled) {
|
|
1723
|
-
if (!enabled) {
|
|
1724
|
-
return values;
|
|
1725
|
-
}
|
|
1726
|
-
const result = { ...values };
|
|
1727
|
-
if (columns.includes("updated_at")) {
|
|
1728
|
-
result.updated_at = new Date;
|
|
1729
|
-
}
|
|
1730
|
-
return result;
|
|
1731
|
-
}
|
|
1732
|
-
|
|
1733
|
-
class Model {
|
|
1734
|
-
attributes;
|
|
1735
|
-
repository;
|
|
1736
|
-
static $fillable;
|
|
1737
|
-
static $guarded;
|
|
1738
|
-
static $casts = {};
|
|
1739
|
-
static $timestamps = true;
|
|
1740
|
-
_exists;
|
|
1741
|
-
constructor(attributes, repository, exists = true) {
|
|
1742
|
-
this.attributes = attributes;
|
|
1743
|
-
this.repository = repository;
|
|
1744
|
-
this._exists = exists;
|
|
1745
|
-
}
|
|
1746
|
-
get $exists() {
|
|
1747
|
-
return this._exists;
|
|
1748
|
-
}
|
|
1749
|
-
get(key) {
|
|
1750
|
-
return this.attributes[key];
|
|
1751
|
-
}
|
|
1752
|
-
get id() {
|
|
1753
|
-
return this.attributes[this.primaryKey()];
|
|
1754
|
-
}
|
|
1755
|
-
toObject() {
|
|
1756
|
-
return { ...this.attributes };
|
|
1757
|
-
}
|
|
1758
|
-
primaryKey() {
|
|
1759
|
-
throw new Error(`${this.constructor.name}.primaryKey() is not implemented.`);
|
|
1760
|
-
}
|
|
1761
|
-
static primaryKeyField() {
|
|
1762
|
-
return resolveModelRepository(this).getTable().primaryKey;
|
|
1763
|
-
}
|
|
1764
|
-
static hydrateAttributes(attributes) {
|
|
1765
|
-
const casts = modelStatics(this).$casts ?? {};
|
|
1766
|
-
return applyCasts(attributes, casts, "hydrate");
|
|
1767
|
-
}
|
|
1768
|
-
static dehydrateAttributes(attributes) {
|
|
1769
|
-
const casts = modelStatics(this).$casts ?? {};
|
|
1770
|
-
return applyCasts(attributes, casts, "dehydrate");
|
|
1771
|
-
}
|
|
1772
|
-
static fromRecord(record, repository, exists = true) {
|
|
1773
|
-
const statics = modelStatics(this);
|
|
1774
|
-
const hydrated = statics.hydrateAttributes(record);
|
|
1775
|
-
return new statics(hydrated, repository, exists);
|
|
1776
|
-
}
|
|
1777
|
-
static boot() {}
|
|
1778
|
-
static addGlobalScope(_name, scope) {
|
|
1779
|
-
ensureBooted(this);
|
|
1780
|
-
const existing = modelGlobalScopes.get(this) ?? [];
|
|
1781
|
-
modelGlobalScopes.set(this, [
|
|
1782
|
-
...existing,
|
|
1783
|
-
scope
|
|
1784
|
-
]);
|
|
1785
|
-
}
|
|
1786
|
-
static repository() {
|
|
1787
|
-
return resolveModelRepository(this);
|
|
1788
|
-
}
|
|
1789
|
-
static query() {
|
|
1790
|
-
ensureBooted(this);
|
|
1791
|
-
const repository = resolveModelRepository(this);
|
|
1792
|
-
let query = repository.query();
|
|
1793
|
-
for (const scope of getGlobalScopes(this)) {
|
|
1794
|
-
query = scope(query);
|
|
1795
|
-
}
|
|
1796
|
-
return query;
|
|
1797
|
-
}
|
|
1798
|
-
static async create(attributes) {
|
|
1799
|
-
const statics = modelStatics(this);
|
|
1800
|
-
ensureBooted(this);
|
|
1801
|
-
const repository = resolveModelRepository(this);
|
|
1802
|
-
const table = repository.getTable();
|
|
1803
|
-
const timestamps = statics.$timestamps ?? true;
|
|
1804
|
-
const assignable = filterMassAssignable(statics.$fillable, statics.$guarded, attributes);
|
|
1805
|
-
const withTimestamps = applyTimestampsOnCreate(table.columns, assignable, timestamps);
|
|
1806
|
-
const payload = statics.dehydrateAttributes(withTimestamps);
|
|
1807
|
-
const record = await repository.create(payload);
|
|
1808
|
-
return statics.fromRecord(record, repository, true);
|
|
1809
|
-
}
|
|
1810
|
-
static async find(id) {
|
|
1811
|
-
const statics = modelStatics(this);
|
|
1812
|
-
const repository = resolveModelRepository(this);
|
|
1813
|
-
const primaryKey = repository.getTable().primaryKey;
|
|
1814
|
-
const record = await Model.query.call(this).where({ [primaryKey]: id }).first();
|
|
1815
|
-
return record ? statics.fromRecord(record, repository, true) : null;
|
|
1816
|
-
}
|
|
1817
|
-
static async findOrFail(id, errorFactory) {
|
|
1818
|
-
const model = await Model.find.call(this, id);
|
|
1819
|
-
if (model) {
|
|
1820
|
-
return model;
|
|
1821
|
-
}
|
|
1822
|
-
throw errorFactory?.(id) ?? new NotFoundError(`${this.name} ${String(id)} not found.`);
|
|
1823
|
-
}
|
|
1824
|
-
static async all(options = {}) {
|
|
1825
|
-
const statics = modelStatics(this);
|
|
1826
|
-
const repository = resolveModelRepository(this);
|
|
1827
|
-
let query = Model.query.call(this);
|
|
1828
|
-
if (options.orderBy) {
|
|
1829
|
-
query = query.orderBy(options.orderBy);
|
|
1830
|
-
}
|
|
1831
|
-
if (options.limit !== undefined) {
|
|
1832
|
-
query = query.limit(options.limit);
|
|
1833
|
-
}
|
|
1834
|
-
const rows = await query.get();
|
|
1835
|
-
return rows.map((row) => statics.fromRecord(row, repository, true));
|
|
1836
|
-
}
|
|
1837
|
-
static async firstWhere(where, options = {}) {
|
|
1838
|
-
const statics = modelStatics(this);
|
|
1839
|
-
const repository = resolveModelRepository(this);
|
|
1840
|
-
let query = Model.query.call(this).where(where);
|
|
1841
|
-
if (options.orderBy) {
|
|
1842
|
-
query = query.orderBy(options.orderBy);
|
|
1843
|
-
}
|
|
1844
|
-
const record = await query.first();
|
|
1845
|
-
return record ? statics.fromRecord(record, repository, true) : null;
|
|
1846
|
-
}
|
|
1847
|
-
async save() {
|
|
1848
|
-
const ModelClass = modelStatics(this.constructor);
|
|
1849
|
-
const timestamps = ModelClass.$timestamps ?? true;
|
|
1850
|
-
const casts = ModelClass.$casts ?? {};
|
|
1851
|
-
const table = this.repository.getTable();
|
|
1852
|
-
if (this.$exists) {
|
|
1853
|
-
const changes = applyTimestampsOnUpdate(table.columns, applyCasts(this.attributes, casts, "dehydrate"), timestamps);
|
|
1854
|
-
const record2 = await this.repository.updateByIdOrThrow(this.id, changes);
|
|
1855
|
-
this.attributes = ModelClass.hydrateAttributes(record2);
|
|
1856
|
-
return this;
|
|
1857
|
-
}
|
|
1858
|
-
const assignable = filterMassAssignable(ModelClass.$fillable, ModelClass.$guarded, this.attributes);
|
|
1859
|
-
const withTimestamps = applyTimestampsOnCreate(table.columns, assignable, timestamps);
|
|
1860
|
-
const payload = ModelClass.dehydrateAttributes(withTimestamps);
|
|
1861
|
-
const record = await this.repository.create(payload);
|
|
1862
|
-
this.attributes = ModelClass.hydrateAttributes(record);
|
|
1863
|
-
this._exists = true;
|
|
1864
|
-
return this;
|
|
1865
|
-
}
|
|
1866
|
-
async update(changes) {
|
|
1867
|
-
const ModelClass = modelStatics(this.constructor);
|
|
1868
|
-
const assignable = filterMassAssignable(ModelClass.$fillable, ModelClass.$guarded, changes);
|
|
1869
|
-
Object.assign(this.attributes, assignable);
|
|
1870
|
-
return await this.save();
|
|
1871
|
-
}
|
|
1872
|
-
async delete() {
|
|
1873
|
-
if (resolveSoftDeleteColumn(this.repository.getTable())) {
|
|
1874
|
-
return await this.repository.deleteById(this.id);
|
|
1875
|
-
}
|
|
1876
|
-
return await this.repository.forceDeleteById(this.id);
|
|
1877
|
-
}
|
|
1878
|
-
async forceDelete() {
|
|
1879
|
-
return await this.repository.forceDeleteById(this.id);
|
|
1880
|
-
}
|
|
1881
|
-
async restore() {
|
|
1882
|
-
const ModelClass = modelStatics(this.constructor);
|
|
1883
|
-
const record = await this.repository.restoreById(this.id);
|
|
1884
|
-
if (!record) {
|
|
1885
|
-
return null;
|
|
1886
|
-
}
|
|
1887
|
-
this.attributes = ModelClass.hydrateAttributes(record);
|
|
1888
|
-
return this;
|
|
1889
|
-
}
|
|
1890
|
-
async loadHasMany(as, relation, childRepository, options = {}) {
|
|
1891
|
-
const grouped = await childRepository.withConnection(this.repository.getConnection()).loadHasManyForParents([this.attributes], relation, options);
|
|
1892
|
-
const loaded = grouped.get(this.attributes[relation.localKey]) ?? [];
|
|
1893
|
-
return Object.assign(this, { [as]: loaded });
|
|
1894
|
-
}
|
|
1895
|
-
async loadHasOne(as, relation, childRepository, options = {}) {
|
|
1896
|
-
const loaded = await this.loadHasMany(as, relation, childRepository, { ...options, limit: 1 });
|
|
1897
|
-
const value = loaded[as]?.[0];
|
|
1898
|
-
return Object.assign(this, { [as]: value });
|
|
1899
|
-
}
|
|
1900
|
-
async loadBelongsTo(as, relation, parentRepository, options = {}) {
|
|
1901
|
-
const grouped = await this.repository.loadBelongsToForParents([this.attributes], relation, parentRepository, options);
|
|
1902
|
-
const loaded = grouped.get(this.attributes[relation.foreignKey]);
|
|
1903
|
-
return Object.assign(this, { [as]: loaded });
|
|
1904
|
-
}
|
|
1905
|
-
async loadBelongsToMany(as, relation, relatedRepository, options = {}) {
|
|
1906
|
-
const connection = this.repository.getConnection();
|
|
1907
|
-
const parentId = this.attributes[relation.parentKey];
|
|
1908
|
-
const pivotRows = await connection.unsafe(`SELECT * FROM ${relation.pivotTable} WHERE ${String(relation.foreignPivotKey)} = $1`, [parentId]);
|
|
1909
|
-
if (pivotRows.length === 0) {
|
|
1910
|
-
return Object.assign(this, { [as]: [] });
|
|
1911
|
-
}
|
|
1912
|
-
const relatedIds = [
|
|
1913
|
-
...new Set(pivotRows.map((row) => row[relation.relatedPivotKey]))
|
|
1914
|
-
];
|
|
1915
|
-
const relatedRows = await relatedRepository.withConnection(connection).findAll({
|
|
1916
|
-
...options,
|
|
1917
|
-
where: {
|
|
1918
|
-
[relation.relatedKey]: relatedIds
|
|
1919
|
-
}
|
|
1920
|
-
});
|
|
1921
|
-
const grouped = indexBelongsToManyRelation([this.attributes], pivotRows, relatedRows, relation);
|
|
1922
|
-
const loaded = grouped.get(parentId) ?? [];
|
|
1923
|
-
return Object.assign(this, { [as]: loaded });
|
|
1924
|
-
}
|
|
1925
|
-
mergeAttributes(patch) {
|
|
1926
|
-
Object.assign(this.attributes, patch);
|
|
1927
|
-
return this;
|
|
1928
|
-
}
|
|
1929
|
-
}
|
|
1930
|
-
function registerModelRepository(model, repository) {
|
|
1931
|
-
modelRepositories.set(model, repository);
|
|
1932
|
-
ensureBooted(model);
|
|
1933
|
-
return model;
|
|
1934
|
-
}
|
|
1935
|
-
// ../../src/core/database/schema/columnDefinition.ts
|
|
1936
|
-
class ColumnDefinition {
|
|
1937
|
-
name;
|
|
1938
|
-
kind;
|
|
1939
|
-
length;
|
|
1940
|
-
isNullable = false;
|
|
1941
|
-
isPrimary = false;
|
|
1942
|
-
isUnique = false;
|
|
1943
|
-
autoIncrement = false;
|
|
1944
|
-
defaultValue;
|
|
1945
|
-
checkExpression;
|
|
1946
|
-
foreignKey;
|
|
1947
|
-
constructor(name, kind) {
|
|
1948
|
-
this.name = name;
|
|
1949
|
-
this.kind = kind;
|
|
1950
|
-
}
|
|
1951
|
-
nullable() {
|
|
1952
|
-
this.isNullable = true;
|
|
1953
|
-
return this;
|
|
1954
|
-
}
|
|
1955
|
-
notNullable() {
|
|
1956
|
-
this.isNullable = false;
|
|
1957
|
-
return this;
|
|
1958
|
-
}
|
|
1959
|
-
default(value) {
|
|
1960
|
-
if (typeof value === "boolean") {
|
|
1961
|
-
this.defaultValue = value ? "TRUE" : "FALSE";
|
|
1962
|
-
return this;
|
|
1963
|
-
}
|
|
1964
|
-
if (typeof value === "number") {
|
|
1965
|
-
this.defaultValue = String(value);
|
|
1966
|
-
return this;
|
|
1967
|
-
}
|
|
1968
|
-
this.defaultValue = `'${value.replace(/'/g, "''")}'`;
|
|
1969
|
-
return this;
|
|
1970
|
-
}
|
|
1971
|
-
defaultRaw(expression) {
|
|
1972
|
-
this.defaultValue = expression;
|
|
1973
|
-
return this;
|
|
1974
|
-
}
|
|
1975
|
-
unique() {
|
|
1976
|
-
this.isUnique = true;
|
|
1977
|
-
return this;
|
|
1978
|
-
}
|
|
1979
|
-
primary() {
|
|
1980
|
-
this.isPrimary = true;
|
|
1981
|
-
return this;
|
|
1982
|
-
}
|
|
1983
|
-
check(expression) {
|
|
1984
|
-
this.checkExpression = expression;
|
|
1985
|
-
return this;
|
|
1986
|
-
}
|
|
1987
|
-
}
|
|
1988
|
-
|
|
1989
|
-
class ForeignIdColumnDefinition extends ColumnDefinition {
|
|
1990
|
-
constructor(name) {
|
|
1991
|
-
super(name, "foreignId");
|
|
1992
|
-
this.notNullable();
|
|
1993
|
-
}
|
|
1994
|
-
references(table, column = "id") {
|
|
1995
|
-
this.foreignKey = {
|
|
1996
|
-
referencesTable: table,
|
|
1997
|
-
referencesColumn: column
|
|
1998
|
-
};
|
|
1999
|
-
return this;
|
|
2000
|
-
}
|
|
2001
|
-
constrained(table) {
|
|
2002
|
-
const referencesTable = table ?? inferReferencedTable(this.name);
|
|
2003
|
-
return this.references(referencesTable);
|
|
2004
|
-
}
|
|
2005
|
-
cascadeOnDelete() {
|
|
2006
|
-
if (!this.foreignKey) {
|
|
2007
|
-
throw new Error(`Foreign key is not defined for column ${this.name}`);
|
|
2008
|
-
}
|
|
2009
|
-
this.foreignKey.onDelete = "cascade";
|
|
2010
|
-
return this;
|
|
2011
|
-
}
|
|
2012
|
-
nullOnDelete() {
|
|
2013
|
-
if (!this.foreignKey) {
|
|
2014
|
-
throw new Error(`Foreign key is not defined for column ${this.name}`);
|
|
2015
|
-
}
|
|
2016
|
-
this.foreignKey.onDelete = "set null";
|
|
2017
|
-
return this;
|
|
2018
|
-
}
|
|
2019
|
-
}
|
|
2020
|
-
function inferReferencedTable(columnName) {
|
|
2021
|
-
if (!columnName.endsWith("_id")) {
|
|
2022
|
-
throw new Error(`Cannot infer referenced table from column ${columnName}`);
|
|
2023
|
-
}
|
|
2024
|
-
return columnName.slice(0, -3);
|
|
2025
|
-
}
|
|
2026
|
-
|
|
2027
|
-
// ../../src/core/database/schema/blueprint.ts
|
|
2028
|
-
class Blueprint {
|
|
2029
|
-
table;
|
|
2030
|
-
action;
|
|
2031
|
-
columns = [];
|
|
2032
|
-
indexes = [];
|
|
2033
|
-
droppedColumns = [];
|
|
2034
|
-
droppedIndexes = [];
|
|
2035
|
-
constructor(table, action) {
|
|
2036
|
-
this.table = table;
|
|
2037
|
-
this.action = action;
|
|
2038
|
-
}
|
|
2039
|
-
id(name = "id") {
|
|
2040
|
-
const column = new ColumnDefinition(name, "id");
|
|
2041
|
-
column.primary();
|
|
2042
|
-
column.autoIncrement = true;
|
|
2043
|
-
this.columns.push(column);
|
|
2044
|
-
return column;
|
|
2045
|
-
}
|
|
2046
|
-
string(name, length) {
|
|
2047
|
-
const column = new ColumnDefinition(name, "string");
|
|
2048
|
-
column.length = length;
|
|
2049
|
-
column.notNullable();
|
|
2050
|
-
this.columns.push(column);
|
|
2051
|
-
return column;
|
|
2052
|
-
}
|
|
2053
|
-
text(name) {
|
|
2054
|
-
const column = new ColumnDefinition(name, "text");
|
|
2055
|
-
column.notNullable();
|
|
2056
|
-
this.columns.push(column);
|
|
2057
|
-
return column;
|
|
2058
|
-
}
|
|
2059
|
-
boolean(name) {
|
|
2060
|
-
const column = new ColumnDefinition(name, "boolean");
|
|
2061
|
-
column.notNullable();
|
|
2062
|
-
this.columns.push(column);
|
|
2063
|
-
return column;
|
|
2064
|
-
}
|
|
2065
|
-
integer(name) {
|
|
2066
|
-
const column = new ColumnDefinition(name, "integer");
|
|
2067
|
-
column.notNullable();
|
|
2068
|
-
this.columns.push(column);
|
|
2069
|
-
return column;
|
|
2070
|
-
}
|
|
2071
|
-
bigInteger(name) {
|
|
2072
|
-
const column = new ColumnDefinition(name, "bigInteger");
|
|
2073
|
-
column.notNullable();
|
|
2074
|
-
this.columns.push(column);
|
|
2075
|
-
return column;
|
|
2076
|
-
}
|
|
2077
|
-
timestamp(name) {
|
|
2078
|
-
const column = new ColumnDefinition(name, "timestamp");
|
|
2079
|
-
column.notNullable();
|
|
2080
|
-
this.columns.push(column);
|
|
2081
|
-
return column;
|
|
2082
|
-
}
|
|
2083
|
-
json(name) {
|
|
2084
|
-
const column = new ColumnDefinition(name, "json");
|
|
2085
|
-
column.notNullable();
|
|
2086
|
-
this.columns.push(column);
|
|
2087
|
-
return column;
|
|
2088
|
-
}
|
|
2089
|
-
jsonb(name) {
|
|
2090
|
-
const column = new ColumnDefinition(name, "jsonb");
|
|
2091
|
-
column.notNullable();
|
|
2092
|
-
this.columns.push(column);
|
|
2093
|
-
return column;
|
|
2094
|
-
}
|
|
2095
|
-
foreignId(name) {
|
|
2096
|
-
const column = new ForeignIdColumnDefinition(name);
|
|
2097
|
-
this.columns.push(column);
|
|
2098
|
-
return column;
|
|
2099
|
-
}
|
|
2100
|
-
timestamps() {
|
|
2101
|
-
this.timestamp("created_at").defaultRaw("NOW()");
|
|
2102
|
-
this.timestamp("updated_at").defaultRaw("NOW()");
|
|
2103
|
-
}
|
|
2104
|
-
softDeletes() {
|
|
2105
|
-
this.timestamp("deleted_at").nullable();
|
|
2106
|
-
}
|
|
2107
|
-
dropColumn(name) {
|
|
2108
|
-
this.droppedColumns.push(name);
|
|
2109
|
-
}
|
|
2110
|
-
dropSoftDeletes() {
|
|
2111
|
-
this.dropColumn("deleted_at");
|
|
2112
|
-
this.dropIndex(`idx_${this.table}_deleted_at`);
|
|
2113
|
-
}
|
|
2114
|
-
dropIndex(name) {
|
|
2115
|
-
this.droppedIndexes.push(name);
|
|
2116
|
-
}
|
|
2117
|
-
unique(columns, name) {
|
|
2118
|
-
this.indexes.push({
|
|
2119
|
-
name,
|
|
2120
|
-
columns: Array.isArray(columns) ? columns : [columns],
|
|
2121
|
-
kind: "unique"
|
|
2122
|
-
});
|
|
2123
|
-
}
|
|
2124
|
-
index(columns, options = {}) {
|
|
2125
|
-
this.indexes.push({
|
|
2126
|
-
name: options.name,
|
|
2127
|
-
columns: Array.isArray(columns) ? columns : [columns],
|
|
2128
|
-
kind: "index",
|
|
2129
|
-
order: options.order
|
|
2130
|
-
});
|
|
2131
|
-
}
|
|
2132
|
-
partialIndex(columns, where, nameOrOptions) {
|
|
2133
|
-
const options = typeof nameOrOptions === "string" ? { name: nameOrOptions } : nameOrOptions ?? {};
|
|
2134
|
-
this.indexes.push({
|
|
2135
|
-
name: options.name,
|
|
2136
|
-
columns: Array.isArray(columns) ? columns : [columns],
|
|
2137
|
-
kind: options.unique ? "uniquePartial" : "partial",
|
|
2138
|
-
where
|
|
2139
|
-
});
|
|
2140
|
-
}
|
|
2141
|
-
fullText(columns, name) {
|
|
2142
|
-
this.indexes.push({
|
|
2143
|
-
name,
|
|
2144
|
-
columns: Array.isArray(columns) ? columns : [columns],
|
|
2145
|
-
kind: "fullText"
|
|
2146
|
-
});
|
|
2147
|
-
}
|
|
2148
|
-
ginIndex(column, name) {
|
|
2149
|
-
this.indexes.push({
|
|
2150
|
-
name,
|
|
2151
|
-
columns: [column],
|
|
2152
|
-
kind: "gin"
|
|
2153
|
-
});
|
|
2154
|
-
}
|
|
2155
|
-
}
|
|
2156
|
-
// ../../src/core/database/schema/driver.ts
|
|
2157
|
-
function normalizeConnectionName(connection) {
|
|
2158
|
-
const normalized = connection.trim().toLowerCase();
|
|
2159
|
-
if (normalized === "pgsql" || normalized === "postgres" || normalized === "postgresql") {
|
|
2160
|
-
return "pgsql";
|
|
2161
|
-
}
|
|
2162
|
-
if (normalized === "mysql" || normalized === "mariadb") {
|
|
2163
|
-
return "mysql";
|
|
2164
|
-
}
|
|
2165
|
-
if (normalized === "sqlite") {
|
|
2166
|
-
return "sqlite";
|
|
2167
|
-
}
|
|
2168
|
-
throw new Error(`Unsupported DB_CONNECTION: ${connection}`);
|
|
2169
|
-
}
|
|
2170
|
-
function resolveDriverFromUrl(url) {
|
|
2171
|
-
const normalized = url.trim().toLowerCase();
|
|
2172
|
-
if (normalized.startsWith("postgres://") || normalized.startsWith("postgresql://") || normalized.startsWith("postgres:")) {
|
|
2173
|
-
return "pgsql";
|
|
2174
|
-
}
|
|
2175
|
-
if (normalized.startsWith("mysql://") || normalized.startsWith("mysql:")) {
|
|
2176
|
-
return "mysql";
|
|
2177
|
-
}
|
|
2178
|
-
if (normalized.startsWith("sqlite:")) {
|
|
2179
|
-
return "sqlite";
|
|
2180
|
-
}
|
|
2181
|
-
return null;
|
|
2182
|
-
}
|
|
2183
|
-
function resolveDatabaseDriver(options = {}) {
|
|
2184
|
-
const connection = options.connection ?? process.env.DB_CONNECTION;
|
|
2185
|
-
if (connection) {
|
|
2186
|
-
return normalizeConnectionName(connection);
|
|
2187
|
-
}
|
|
2188
|
-
const url = options.url ?? process.env.DATABASE_URL ?? "";
|
|
2189
|
-
const fromUrl = resolveDriverFromUrl(url);
|
|
2190
|
-
if (fromUrl) {
|
|
2191
|
-
return fromUrl;
|
|
2192
|
-
}
|
|
2193
|
-
return "pgsql";
|
|
2194
|
-
}
|
|
2195
|
-
// ../../src/core/database/schema/errors.ts
|
|
2196
|
-
class UnsupportedSchemaFeatureError extends Error {
|
|
2197
|
-
constructor(feature, driver) {
|
|
2198
|
-
super(`${feature} is not supported for the ${driver} driver`);
|
|
2199
|
-
this.name = "UnsupportedSchemaFeatureError";
|
|
2200
|
-
}
|
|
2201
|
-
}
|
|
2202
|
-
// ../../src/core/database/schema/grammars/grammar.ts
|
|
2203
|
-
function compileColumnType(driver, column) {
|
|
2204
|
-
switch (column.kind) {
|
|
2205
|
-
case "id":
|
|
2206
|
-
return compileIdType(driver);
|
|
2207
|
-
case "string":
|
|
2208
|
-
return compileStringType(driver, column.length);
|
|
2209
|
-
case "text":
|
|
2210
|
-
return compileTextType(driver);
|
|
2211
|
-
case "boolean":
|
|
2212
|
-
return compileBooleanType(driver);
|
|
2213
|
-
case "integer":
|
|
2214
|
-
case "foreignId":
|
|
2215
|
-
return compileIntegerType(driver);
|
|
2216
|
-
case "bigInteger":
|
|
2217
|
-
return compileBigIntegerType(driver);
|
|
2218
|
-
case "timestamp":
|
|
2219
|
-
return compileTimestampType(driver);
|
|
2220
|
-
case "json":
|
|
2221
|
-
return compileJsonType(driver);
|
|
2222
|
-
case "jsonb":
|
|
2223
|
-
return compileJsonbType(driver);
|
|
2224
|
-
default:
|
|
2225
|
-
throw new Error(`Unsupported column kind: ${column.kind}`);
|
|
2226
|
-
}
|
|
2227
|
-
}
|
|
2228
|
-
function compileIdType(driver) {
|
|
2229
|
-
switch (driver) {
|
|
2230
|
-
case "pgsql":
|
|
2231
|
-
return "SERIAL";
|
|
2232
|
-
case "mysql":
|
|
2233
|
-
return "BIGINT UNSIGNED";
|
|
2234
|
-
case "sqlite":
|
|
2235
|
-
return "INTEGER";
|
|
2236
|
-
}
|
|
2237
|
-
}
|
|
2238
|
-
function compileStringType(driver, length) {
|
|
2239
|
-
switch (driver) {
|
|
2240
|
-
case "pgsql":
|
|
2241
|
-
return "TEXT";
|
|
2242
|
-
case "mysql":
|
|
2243
|
-
return length ? `VARCHAR(${length})` : "VARCHAR(255)";
|
|
2244
|
-
case "sqlite":
|
|
2245
|
-
return "TEXT";
|
|
2246
|
-
}
|
|
2247
|
-
}
|
|
2248
|
-
function compileTextType(driver) {
|
|
2249
|
-
switch (driver) {
|
|
2250
|
-
case "pgsql":
|
|
2251
|
-
case "sqlite":
|
|
2252
|
-
return "TEXT";
|
|
2253
|
-
case "mysql":
|
|
2254
|
-
return "TEXT";
|
|
2255
|
-
}
|
|
2256
|
-
}
|
|
2257
|
-
function compileBooleanType(driver) {
|
|
2258
|
-
switch (driver) {
|
|
2259
|
-
case "pgsql":
|
|
2260
|
-
return "BOOLEAN";
|
|
2261
|
-
case "mysql":
|
|
2262
|
-
return "BOOLEAN";
|
|
2263
|
-
case "sqlite":
|
|
2264
|
-
return "INTEGER";
|
|
2265
|
-
}
|
|
2266
|
-
}
|
|
2267
|
-
function compileIntegerType(driver) {
|
|
2268
|
-
switch (driver) {
|
|
2269
|
-
case "pgsql":
|
|
2270
|
-
return "INTEGER";
|
|
2271
|
-
case "mysql":
|
|
2272
|
-
return "INT";
|
|
2273
|
-
case "sqlite":
|
|
2274
|
-
return "INTEGER";
|
|
2275
|
-
}
|
|
2276
|
-
}
|
|
2277
|
-
function compileBigIntegerType(driver) {
|
|
2278
|
-
switch (driver) {
|
|
2279
|
-
case "pgsql":
|
|
2280
|
-
return "BIGINT";
|
|
2281
|
-
case "mysql":
|
|
2282
|
-
return "BIGINT";
|
|
2283
|
-
case "sqlite":
|
|
2284
|
-
return "INTEGER";
|
|
2285
|
-
}
|
|
2286
|
-
}
|
|
2287
|
-
function compileTimestampType(driver) {
|
|
2288
|
-
switch (driver) {
|
|
2289
|
-
case "pgsql":
|
|
2290
|
-
return "TIMESTAMPTZ";
|
|
2291
|
-
case "mysql":
|
|
2292
|
-
return "TIMESTAMP";
|
|
2293
|
-
case "sqlite":
|
|
2294
|
-
return "TEXT";
|
|
2295
|
-
}
|
|
2296
|
-
}
|
|
2297
|
-
function compileJsonType(driver) {
|
|
2298
|
-
switch (driver) {
|
|
2299
|
-
case "pgsql":
|
|
2300
|
-
return "JSONB";
|
|
2301
|
-
case "mysql":
|
|
2302
|
-
return "JSON";
|
|
2303
|
-
case "sqlite":
|
|
2304
|
-
return "TEXT";
|
|
2305
|
-
}
|
|
2306
|
-
}
|
|
2307
|
-
function compileJsonbType(driver) {
|
|
2308
|
-
switch (driver) {
|
|
2309
|
-
case "pgsql":
|
|
2310
|
-
return "JSONB";
|
|
2311
|
-
case "mysql":
|
|
2312
|
-
return "JSON";
|
|
2313
|
-
case "sqlite":
|
|
2314
|
-
return "TEXT";
|
|
2315
|
-
}
|
|
2316
|
-
}
|
|
197
|
+
// ../../src/modules/user/provider.ts
|
|
198
|
+
import { OidcProvider } from "@getstrata/core/auth/oauth/oidcProvider";
|
|
199
|
+
import { GitHubOAuthProvider, MockOAuthProvider } from "@getstrata/core/auth/oauth/providers";
|
|
200
|
+
import { SamlProvider } from "@getstrata/core/auth/oauth/samlProvider";
|
|
2317
201
|
|
|
2318
|
-
// ../../src/
|
|
2319
|
-
function
|
|
2320
|
-
const table = quoteIdentifier(blueprint.table);
|
|
2321
|
-
const parts = blueprint.columns.map((column) => compileColumn(driver, column, "create"));
|
|
2322
|
-
for (const index of blueprint.indexes) {
|
|
2323
|
-
if (index.kind === "unique" && index.columns.length > 1) {
|
|
2324
|
-
const columns = index.columns.map((column) => quoteIdentifier(column)).join(", ");
|
|
2325
|
-
parts.push(`UNIQUE (${columns})`);
|
|
2326
|
-
}
|
|
2327
|
-
}
|
|
2328
|
-
const statements = [`CREATE TABLE IF NOT EXISTS ${table} (
|
|
2329
|
-
${parts.join(`,
|
|
2330
|
-
`)}
|
|
2331
|
-
)`];
|
|
2332
|
-
for (const index of blueprint.indexes) {
|
|
2333
|
-
if (index.kind === "unique" && index.columns.length === 1) {
|
|
2334
|
-
continue;
|
|
2335
|
-
}
|
|
2336
|
-
if (index.kind === "index") {
|
|
2337
|
-
statements.push(compileIndex(driver, blueprint.table, index));
|
|
2338
|
-
} else if (index.kind === "partial" || index.kind === "uniquePartial" || index.kind === "gin" || index.kind === "fullText") {
|
|
2339
|
-
statements.push(...compileSpecialIndex(driver, blueprint.table, index));
|
|
2340
|
-
}
|
|
2341
|
-
}
|
|
2342
|
-
return statements;
|
|
2343
|
-
}
|
|
2344
|
-
function compileAlterTable(driver, blueprint) {
|
|
2345
|
-
const statements = [];
|
|
2346
|
-
const table = quoteIdentifier(blueprint.table);
|
|
2347
|
-
for (const column of blueprint.columns) {
|
|
2348
|
-
const addPrefix = driver === "pgsql" ? "ADD COLUMN IF NOT EXISTS" : "ADD COLUMN";
|
|
2349
|
-
statements.push(`ALTER TABLE ${table}
|
|
2350
|
-
${addPrefix} ${compileColumn(driver, column, "alter")}`);
|
|
2351
|
-
}
|
|
2352
|
-
for (const columnName of blueprint.droppedColumns) {
|
|
2353
|
-
const dropPrefix = driver === "pgsql" ? "DROP COLUMN IF EXISTS" : "DROP COLUMN";
|
|
2354
|
-
statements.push(`ALTER TABLE ${table} ${dropPrefix} ${quoteIdentifier(columnName)}`);
|
|
2355
|
-
}
|
|
2356
|
-
for (const indexName of blueprint.droppedIndexes) {
|
|
2357
|
-
statements.push(`DROP INDEX IF EXISTS ${quoteIdentifier(indexName)}`);
|
|
2358
|
-
}
|
|
2359
|
-
for (const index of blueprint.indexes) {
|
|
2360
|
-
if (index.kind === "index" || index.kind === "unique") {
|
|
2361
|
-
statements.push(compileIndex(driver, blueprint.table, index));
|
|
2362
|
-
} else {
|
|
2363
|
-
statements.push(...compileSpecialIndex(driver, blueprint.table, index));
|
|
2364
|
-
}
|
|
2365
|
-
}
|
|
2366
|
-
return statements;
|
|
2367
|
-
}
|
|
2368
|
-
function compileDropTable(driver, tableName) {
|
|
2369
|
-
const cascade = driver === "pgsql" ? " CASCADE" : "";
|
|
2370
|
-
return [`DROP TABLE IF EXISTS ${quoteIdentifier(tableName)}${cascade}`];
|
|
2371
|
-
}
|
|
2372
|
-
function compileColumn(driver, column, mode) {
|
|
2373
|
-
const parts = [quoteIdentifier(column.name), compileColumnType(driver, column)];
|
|
2374
|
-
if (column.autoIncrement && driver === "mysql") {
|
|
2375
|
-
parts[1] = `${parts[1]} AUTO_INCREMENT`;
|
|
2376
|
-
}
|
|
2377
|
-
if (column.isPrimary && mode === "create") {
|
|
2378
|
-
if (driver === "sqlite") {
|
|
2379
|
-
parts.push("PRIMARY KEY AUTOINCREMENT");
|
|
2380
|
-
} else {
|
|
2381
|
-
parts.push("PRIMARY KEY");
|
|
2382
|
-
}
|
|
2383
|
-
} else if (!column.isNullable) {
|
|
2384
|
-
parts.push("NOT NULL");
|
|
2385
|
-
} else if (column.isNullable) {
|
|
2386
|
-
parts.push("NULL");
|
|
2387
|
-
}
|
|
2388
|
-
if (column.defaultValue !== undefined) {
|
|
2389
|
-
parts.push(`DEFAULT ${column.defaultValue}`);
|
|
2390
|
-
}
|
|
2391
|
-
if (column.isUnique) {
|
|
2392
|
-
parts.push("UNIQUE");
|
|
2393
|
-
}
|
|
2394
|
-
if (column.checkExpression) {
|
|
2395
|
-
parts.push(`CHECK (${column.checkExpression})`);
|
|
2396
|
-
}
|
|
2397
|
-
if (column.foreignKey) {
|
|
2398
|
-
const { referencesTable, referencesColumn, onDelete } = column.foreignKey;
|
|
2399
|
-
const reference = `${quoteIdentifier(referencesTable)}(${quoteIdentifier(referencesColumn)})`;
|
|
2400
|
-
let clause = `REFERENCES ${reference}`;
|
|
2401
|
-
if (onDelete === "cascade") {
|
|
2402
|
-
clause += " ON DELETE CASCADE";
|
|
2403
|
-
} else if (onDelete === "set null") {
|
|
2404
|
-
clause += " ON DELETE SET NULL";
|
|
2405
|
-
}
|
|
2406
|
-
parts.push(clause);
|
|
2407
|
-
}
|
|
2408
|
-
return parts.join(" ");
|
|
2409
|
-
}
|
|
2410
|
-
function compileIndex(_driver, tableName, index) {
|
|
2411
|
-
const indexName = index.name ?? defaultIndexName(tableName, index.columns, index.kind === "unique" ? "unique" : "index");
|
|
2412
|
-
const columns = index.columns.map((column) => {
|
|
2413
|
-
const quoted = quoteIdentifier(column);
|
|
2414
|
-
if (index.order === "desc") {
|
|
2415
|
-
return `${quoted} DESC`;
|
|
2416
|
-
}
|
|
2417
|
-
return quoted;
|
|
2418
|
-
}).join(", ");
|
|
2419
|
-
const unique = index.kind === "unique" ? "UNIQUE " : "";
|
|
2420
|
-
return `CREATE ${unique}INDEX IF NOT EXISTS ${quoteIdentifier(indexName)} ON ${quoteIdentifier(tableName)}(${columns})`;
|
|
2421
|
-
}
|
|
2422
|
-
function compileSpecialIndex(driver, tableName, index) {
|
|
2423
|
-
const indexName = index.name ?? defaultIndexName(tableName, index.columns, index.kind);
|
|
2424
|
-
const columns = index.columns.map((column) => quoteIdentifier(column)).join(", ");
|
|
2425
|
-
switch (index.kind) {
|
|
2426
|
-
case "partial":
|
|
2427
|
-
case "uniquePartial": {
|
|
2428
|
-
if (driver !== "pgsql") {
|
|
2429
|
-
throw new UnsupportedSchemaFeatureError("partialIndex()", driver);
|
|
2430
|
-
}
|
|
2431
|
-
const unique = index.kind === "uniquePartial" ? "UNIQUE " : "";
|
|
2432
|
-
return [
|
|
2433
|
-
`CREATE ${unique}INDEX IF NOT EXISTS ${quoteIdentifier(indexName)} ON ${quoteIdentifier(tableName)}(${columns}) WHERE ${index.where}`
|
|
2434
|
-
];
|
|
2435
|
-
}
|
|
2436
|
-
case "gin": {
|
|
2437
|
-
if (driver !== "pgsql") {
|
|
2438
|
-
throw new UnsupportedSchemaFeatureError("ginIndex()", driver);
|
|
2439
|
-
}
|
|
2440
|
-
return [
|
|
2441
|
-
`CREATE INDEX IF NOT EXISTS ${quoteIdentifier(indexName)} ON ${quoteIdentifier(tableName)} USING GIN(${columns})`
|
|
2442
|
-
];
|
|
2443
|
-
}
|
|
2444
|
-
case "fullText": {
|
|
2445
|
-
if (driver === "mysql") {
|
|
2446
|
-
return [
|
|
2447
|
-
`CREATE FULLTEXT INDEX ${quoteIdentifier(indexName)} ON ${quoteIdentifier(tableName)}(${columns})`
|
|
2448
|
-
];
|
|
2449
|
-
}
|
|
2450
|
-
if (driver === "pgsql") {
|
|
2451
|
-
throw new UnsupportedSchemaFeatureError("fullText(); use ginIndex() with a tsvector column on PostgreSQL", driver);
|
|
2452
|
-
}
|
|
2453
|
-
throw new UnsupportedSchemaFeatureError("fullText()", driver);
|
|
2454
|
-
}
|
|
2455
|
-
default:
|
|
2456
|
-
return [];
|
|
2457
|
-
}
|
|
2458
|
-
}
|
|
2459
|
-
function defaultIndexName(tableName, columns, kind) {
|
|
2460
|
-
return `idx_${tableName}_${columns.join("_")}_${kind}`;
|
|
2461
|
-
}
|
|
2462
|
-
function compileBlueprint(driver, blueprint) {
|
|
2463
|
-
switch (blueprint.action) {
|
|
2464
|
-
case "create":
|
|
2465
|
-
return compileCreateTable(driver, blueprint);
|
|
2466
|
-
case "alter":
|
|
2467
|
-
return compileAlterTable(driver, blueprint);
|
|
2468
|
-
case "drop":
|
|
2469
|
-
return compileDropTable(driver, blueprint.table);
|
|
2470
|
-
default:
|
|
2471
|
-
throw new Error(`Unsupported blueprint action: ${blueprint.action}`);
|
|
2472
|
-
}
|
|
2473
|
-
}
|
|
2474
|
-
// ../../src/core/database/schema/grammars/createGrammar.ts
|
|
2475
|
-
function createGrammar(driver) {
|
|
202
|
+
// ../../src/config/features.ts
|
|
203
|
+
function readFeatureFlags() {
|
|
2476
204
|
return {
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
205
|
+
webhooks: (process.env.FEATURE_WEBHOOKS ?? "true") !== "false",
|
|
206
|
+
fullTextSearch: (process.env.FEATURE_SEARCH ?? "true") !== "false",
|
|
207
|
+
auditLog: (process.env.FEATURE_AUDIT_LOG ?? "true") !== "false",
|
|
208
|
+
oauthLogin: (process.env.FEATURE_OAUTH ?? "true") !== "false",
|
|
209
|
+
samlLogin: (process.env.FEATURE_SAML ?? "false") === "true",
|
|
210
|
+
scim: (process.env.FEATURE_SCIM ?? "true") !== "false",
|
|
211
|
+
billing: (process.env.FEATURE_BILLING ?? "true") !== "false",
|
|
212
|
+
siemExport: (process.env.FEATURE_SIEM_EXPORT ?? "true") !== "false",
|
|
213
|
+
publicReads: (process.env.FEATURE_PUBLIC_READS ?? "true") !== "false",
|
|
214
|
+
emailVerification: (process.env.FEATURE_EMAIL_VERIFICATION ?? "false") === "true",
|
|
215
|
+
mfa: (process.env.FEATURE_MFA ?? "false") === "true"
|
|
2481
216
|
};
|
|
2482
217
|
}
|
|
218
|
+
var featureFlags = readFeatureFlags();
|
|
219
|
+
function isFeatureEnabled(feature) {
|
|
220
|
+
return readFeatureFlags()[feature];
|
|
221
|
+
}
|
|
2483
222
|
|
|
2484
|
-
// ../../src/
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
// ../../src/core/database/schema/grammars/postgresGrammar.ts
|
|
2488
|
-
var PostgresGrammar = createGrammar("pgsql");
|
|
223
|
+
// ../../src/modules/user/apiTokenRepository.ts
|
|
224
|
+
import { BaseRepository } from "@getstrata/core/database";
|
|
2489
225
|
|
|
2490
|
-
// ../../src/
|
|
2491
|
-
|
|
226
|
+
// ../../src/config/database.ts
|
|
227
|
+
function readInteger(name, fallback) {
|
|
228
|
+
const parsed = Number.parseInt(process.env[name] ?? String(fallback), 10);
|
|
229
|
+
return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback;
|
|
230
|
+
}
|
|
231
|
+
var databaseConfig = {
|
|
232
|
+
url: process.env.DATABASE_URL ?? "",
|
|
233
|
+
poolMax: readInteger("DB_POOL_MAX", 10),
|
|
234
|
+
idleTimeoutSeconds: readInteger("DB_POOL_IDLE_TIMEOUT", 30),
|
|
235
|
+
maxLifetimeSeconds: readInteger("DB_POOL_MAX_LIFETIME", 3600),
|
|
236
|
+
connectionTimeoutSeconds: readInteger("DB_CONNECTION_TIMEOUT", 10)
|
|
237
|
+
};
|
|
2492
238
|
|
|
2493
|
-
// ../../src/core/
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
return SqliteGrammar;
|
|
2502
|
-
default:
|
|
2503
|
-
throw new Error(`Unsupported database driver: ${driver}`);
|
|
239
|
+
// ../../src/core/runtime/asyncContextStore.ts
|
|
240
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
241
|
+
function createAsyncContextStore(key) {
|
|
242
|
+
const symbol = Symbol.for(key);
|
|
243
|
+
const globalRecord = globalThis;
|
|
244
|
+
const existing = globalRecord[symbol];
|
|
245
|
+
if (existing) {
|
|
246
|
+
return existing;
|
|
2504
247
|
}
|
|
248
|
+
const store = new AsyncLocalStorage;
|
|
249
|
+
globalRecord[symbol] = store;
|
|
250
|
+
return store;
|
|
2505
251
|
}
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
return this;
|
|
2518
|
-
}
|
|
2519
|
-
table(table, callback) {
|
|
2520
|
-
const blueprint = new Blueprint(table, "alter");
|
|
2521
|
-
callback(blueprint);
|
|
2522
|
-
this.#statements.push(...grammarForDriver(this.#driver).compile(blueprint));
|
|
2523
|
-
return this;
|
|
2524
|
-
}
|
|
2525
|
-
drop(table) {
|
|
2526
|
-
const blueprint = new Blueprint(table, "drop");
|
|
2527
|
-
this.#statements.push(...grammarForDriver(this.#driver).compile(blueprint));
|
|
2528
|
-
return this;
|
|
2529
|
-
}
|
|
2530
|
-
toSql() {
|
|
2531
|
-
return [...this.#statements];
|
|
2532
|
-
}
|
|
2533
|
-
async execute(db) {
|
|
2534
|
-
for (const statement of this.#statements) {
|
|
2535
|
-
await db.unsafe(statement);
|
|
2536
|
-
}
|
|
2537
|
-
}
|
|
252
|
+
|
|
253
|
+
// ../../src/core/database/connectionContext.ts
|
|
254
|
+
var activeConnection = createAsyncContextStore("@getstrata/databaseConnectionContext");
|
|
255
|
+
function runWithDatabaseConnection(connection, callback) {
|
|
256
|
+
return activeConnection.run(connection, callback);
|
|
257
|
+
}
|
|
258
|
+
function getActiveDatabaseConnection(fallback) {
|
|
259
|
+
return activeConnection.getStore() ?? fallback;
|
|
260
|
+
}
|
|
261
|
+
function hasActiveDatabaseConnection() {
|
|
262
|
+
return activeConnection.getStore() !== undefined;
|
|
2538
263
|
}
|
|
2539
264
|
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
265
|
+
// ../../src/core/database/queryProxy.ts
|
|
266
|
+
var POOL_CONNECTION_METHODS = new Set(["begin", "close", "connect"]);
|
|
267
|
+
function createDatabaseQueryProxy(pool) {
|
|
268
|
+
function resolveDatabase() {
|
|
269
|
+
return getActiveDatabaseConnection(pool);
|
|
2543
270
|
}
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
271
|
+
function resolveDatabaseForProperty(property) {
|
|
272
|
+
if (typeof property === "string" && POOL_CONNECTION_METHODS.has(property)) {
|
|
273
|
+
return pool;
|
|
274
|
+
}
|
|
275
|
+
return resolveDatabase();
|
|
2548
276
|
}
|
|
2549
|
-
}
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
277
|
+
return new Proxy(function database() {}, {
|
|
278
|
+
apply(_target, _thisArg, args) {
|
|
279
|
+
return resolveDatabase()(...args);
|
|
280
|
+
},
|
|
281
|
+
get(_target, property) {
|
|
282
|
+
const connection = resolveDatabaseForProperty(property);
|
|
283
|
+
const value = connection[property];
|
|
284
|
+
return typeof value === "function" ? value.bind(connection) : value;
|
|
2555
285
|
}
|
|
2556
286
|
});
|
|
2557
287
|
}
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
288
|
+
|
|
289
|
+
// ../../src/core/database/defaultConnection.ts
|
|
290
|
+
var defaultPool = {
|
|
291
|
+
connection: null
|
|
292
|
+
};
|
|
293
|
+
var defaultQuery = {
|
|
294
|
+
connection: null
|
|
295
|
+
};
|
|
296
|
+
function registerDefaultDatabasePool(connection) {
|
|
297
|
+
defaultPool.connection = connection;
|
|
298
|
+
defaultQuery.connection = createDatabaseQueryProxy(connection);
|
|
2565
299
|
}
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
throw new Error("Active database connection does not support transactions. Bind a client with begin() via bindDatabaseConnection.");
|
|
300
|
+
function getDefaultDatabasePool() {
|
|
301
|
+
if (!defaultPool.connection) {
|
|
302
|
+
throw new Error("Default database pool is not registered. Call registerDefaultDatabasePool() during app bootstrap.");
|
|
2570
303
|
}
|
|
2571
|
-
return
|
|
2572
|
-
return await operation(createDatabaseConnection(transaction));
|
|
2573
|
-
});
|
|
304
|
+
return defaultPool.connection;
|
|
2574
305
|
}
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
306
|
+
function getDefaultDatabaseQuery() {
|
|
307
|
+
if (!defaultQuery.connection) {
|
|
308
|
+
throw new Error("Default database query handle is not registered. Call registerDefaultDatabasePool() during app bootstrap.");
|
|
309
|
+
}
|
|
310
|
+
return defaultQuery.connection;
|
|
2579
311
|
}
|
|
2580
|
-
var databaseConfig = {
|
|
2581
|
-
url: process.env.DATABASE_URL ?? "",
|
|
2582
|
-
poolMax: readInteger("DB_POOL_MAX", 10),
|
|
2583
|
-
idleTimeoutSeconds: readInteger("DB_POOL_IDLE_TIMEOUT", 30),
|
|
2584
|
-
maxLifetimeSeconds: readInteger("DB_POOL_MAX_LIFETIME", 3600),
|
|
2585
|
-
connectionTimeoutSeconds: readInteger("DB_CONNECTION_TIMEOUT", 10)
|
|
2586
|
-
};
|
|
2587
312
|
|
|
2588
313
|
// ../../src/db/connection/createConnection.ts
|
|
2589
314
|
var {SQL } = globalThis.Bun;
|
|
2590
|
-
function
|
|
315
|
+
function createDatabaseConnection(config) {
|
|
2591
316
|
if (!config.url) {
|
|
2592
317
|
throw new Error("DATABASE_URL is not configured. Set DATABASE_URL before starting the app or running integration tests.");
|
|
2593
318
|
}
|
|
@@ -2606,7 +331,7 @@ var connectionHolder = {
|
|
|
2606
331
|
};
|
|
2607
332
|
function getDatabase() {
|
|
2608
333
|
if (!connectionHolder.connection) {
|
|
2609
|
-
connectionHolder.connection =
|
|
334
|
+
connectionHolder.connection = createDatabaseConnection(databaseConfig);
|
|
2610
335
|
registerDefaultDatabasePool(connectionHolder.connection);
|
|
2611
336
|
}
|
|
2612
337
|
return connectionHolder.connection;
|
|
@@ -2628,6 +353,7 @@ var db = new Proxy(function database() {}, {
|
|
|
2628
353
|
var connection_default = db;
|
|
2629
354
|
|
|
2630
355
|
// ../../src/modules/user/apiTokenTable.ts
|
|
356
|
+
import { defineTable } from "@getstrata/core/database";
|
|
2631
357
|
var apiTokenTable = defineTable({
|
|
2632
358
|
name: "api_token",
|
|
2633
359
|
primaryKey: "id",
|
|
@@ -2644,235 +370,14 @@ var apiTokenTable = defineTable({
|
|
|
2644
370
|
defaultOrderBy: { column: "id", direction: "ASC" }
|
|
2645
371
|
});
|
|
2646
372
|
|
|
2647
|
-
// ../../src/
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
}
|
|
2654
|
-
|
|
2655
|
-
return await Bun.password.verify(password, passwordHash);
|
|
2656
|
-
}
|
|
2657
|
-
|
|
2658
|
-
// ../../src/core/crypto/fieldEncryption.ts
|
|
2659
|
-
import { createCipheriv, createDecipheriv, createHmac, randomBytes } from "crypto";
|
|
2660
|
-
var ENCRYPTION_PREFIX = "enc:v1:";
|
|
2661
|
-
var IV_LENGTH = 12;
|
|
2662
|
-
var TAG_LENGTH = 16;
|
|
2663
|
-
function resolveEncryptionKey() {
|
|
2664
|
-
const raw = process.env.KMS_ENCRYPTION_KEY?.trim();
|
|
2665
|
-
if (!raw) {
|
|
2666
|
-
return null;
|
|
2667
|
-
}
|
|
2668
|
-
if (/^[0-9a-f]{64}$/i.test(raw)) {
|
|
2669
|
-
return Buffer.from(raw, "hex");
|
|
2670
|
-
}
|
|
2671
|
-
const decoded = Buffer.from(raw, "base64");
|
|
2672
|
-
if (decoded.length === 32) {
|
|
2673
|
-
return decoded;
|
|
2674
|
-
}
|
|
2675
|
-
throw new Error("KMS_ENCRYPTION_KEY must be 32 bytes (hex or base64).");
|
|
2676
|
-
}
|
|
2677
|
-
function isFieldEncryptionEnabled() {
|
|
2678
|
-
const featureFlag = process.env.FEATURE_FIELD_ENCRYPTION;
|
|
2679
|
-
if (featureFlag === "false") {
|
|
2680
|
-
return false;
|
|
2681
|
-
}
|
|
2682
|
-
if (featureFlag === "true") {
|
|
2683
|
-
return true;
|
|
2684
|
-
}
|
|
2685
|
-
return (process.env.APP_ENV ?? "local") === "production";
|
|
2686
|
-
}
|
|
2687
|
-
function encryptField(plaintext, key) {
|
|
2688
|
-
const iv = randomBytes(IV_LENGTH);
|
|
2689
|
-
const cipher = createCipheriv("aes-256-gcm", key, iv);
|
|
2690
|
-
const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
|
2691
|
-
const tag = cipher.getAuthTag();
|
|
2692
|
-
const payload = Buffer.concat([iv, encrypted, tag]).toString("base64");
|
|
2693
|
-
return `${ENCRYPTION_PREFIX}${payload}`;
|
|
2694
|
-
}
|
|
2695
|
-
function decryptField(value, key) {
|
|
2696
|
-
if (!value.startsWith(ENCRYPTION_PREFIX)) {
|
|
2697
|
-
return value;
|
|
2698
|
-
}
|
|
2699
|
-
const payload = Buffer.from(value.slice(ENCRYPTION_PREFIX.length), "base64");
|
|
2700
|
-
const iv = payload.subarray(0, IV_LENGTH);
|
|
2701
|
-
const tag = payload.subarray(payload.length - TAG_LENGTH);
|
|
2702
|
-
const ciphertext = payload.subarray(IV_LENGTH, payload.length - TAG_LENGTH);
|
|
2703
|
-
const decipher = createDecipheriv("aes-256-gcm", key, iv);
|
|
2704
|
-
decipher.setAuthTag(tag);
|
|
2705
|
-
return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf8");
|
|
2706
|
-
}
|
|
2707
|
-
function hashLookupValue(normalizedValue, key) {
|
|
2708
|
-
return createHmac("sha256", key).update(normalizedValue).digest("hex");
|
|
2709
|
-
}
|
|
2710
|
-
function normalizeEmail(email) {
|
|
2711
|
-
return email.trim().toLowerCase();
|
|
2712
|
-
}
|
|
2713
|
-
function protectEmail(email) {
|
|
2714
|
-
const normalized = normalizeEmail(email);
|
|
2715
|
-
const key = resolveEncryptionKey();
|
|
2716
|
-
if (!key || !isFieldEncryptionEnabled()) {
|
|
2717
|
-
return { storedEmail: normalized, emailLookup: normalized };
|
|
2718
|
-
}
|
|
2719
|
-
return {
|
|
2720
|
-
storedEmail: encryptField(normalized, key),
|
|
2721
|
-
emailLookup: hashLookupValue(normalized, key)
|
|
2722
|
-
};
|
|
2723
|
-
}
|
|
2724
|
-
function revealEmail(storedEmail) {
|
|
2725
|
-
const key = resolveEncryptionKey();
|
|
2726
|
-
if (!key || !storedEmail.startsWith(ENCRYPTION_PREFIX)) {
|
|
2727
|
-
return storedEmail;
|
|
2728
|
-
}
|
|
2729
|
-
return decryptField(storedEmail, key);
|
|
2730
|
-
}
|
|
2731
|
-
function emailLookupForQuery(email) {
|
|
2732
|
-
const normalized = normalizeEmail(email);
|
|
2733
|
-
const key = resolveEncryptionKey();
|
|
2734
|
-
if (!key || !isFieldEncryptionEnabled()) {
|
|
2735
|
-
return normalized;
|
|
2736
|
-
}
|
|
2737
|
-
return hashLookupValue(normalized, key);
|
|
2738
|
-
}
|
|
2739
|
-
|
|
2740
|
-
// ../../src/core/crypto/mfaSecret.ts
|
|
2741
|
-
function protectMfaSecret(secret) {
|
|
2742
|
-
const key = resolveEncryptionKey();
|
|
2743
|
-
if (!isFieldEncryptionEnabled() || !key) {
|
|
2744
|
-
return secret;
|
|
2745
|
-
}
|
|
2746
|
-
return encryptField(secret, key);
|
|
2747
|
-
}
|
|
2748
|
-
function revealMfaSecret(stored) {
|
|
2749
|
-
if (!stored) {
|
|
2750
|
-
return null;
|
|
2751
|
-
}
|
|
2752
|
-
const key = resolveEncryptionKey();
|
|
2753
|
-
if (!isFieldEncryptionEnabled() || !key || !stored.startsWith("enc:v1:")) {
|
|
2754
|
-
return stored;
|
|
2755
|
-
}
|
|
2756
|
-
return decryptField(stored, key);
|
|
2757
|
-
}
|
|
2758
|
-
|
|
2759
|
-
// ../../src/core/auth/authContext.ts
|
|
2760
|
-
var authContext = createAsyncContextStore("@getstrata/authContext");
|
|
2761
|
-
function runWithAuthUser(user, callback) {
|
|
2762
|
-
return authContext.run(user, callback);
|
|
2763
|
-
}
|
|
2764
|
-
function currentAuthUser() {
|
|
2765
|
-
return authContext.getStore() ?? null;
|
|
2766
|
-
}
|
|
2767
|
-
|
|
2768
|
-
// ../../src/core/http/requestMetaContext.ts
|
|
2769
|
-
var requestMetaContext = createAsyncContextStore("@getstrata/requestMetaContext");
|
|
2770
|
-
function runWithRequestMeta(meta, callback) {
|
|
2771
|
-
return requestMetaContext.run(meta, callback);
|
|
2772
|
-
}
|
|
2773
|
-
function currentRequestMeta() {
|
|
2774
|
-
return requestMetaContext.getStore() ?? {
|
|
2775
|
-
ipAddress: null,
|
|
2776
|
-
userAgent: null
|
|
2777
|
-
};
|
|
2778
|
-
}
|
|
2779
|
-
|
|
2780
|
-
// ../../src/core/security/securityEvents.ts
|
|
2781
|
-
function logSecurityEvent(event, details = {}) {
|
|
2782
|
-
const meta = currentRequestMeta();
|
|
2783
|
-
const user = currentAuthUser();
|
|
2784
|
-
console.log(JSON.stringify({
|
|
2785
|
-
level: "security",
|
|
2786
|
-
event,
|
|
2787
|
-
timestamp: new Date().toISOString(),
|
|
2788
|
-
ip_address: meta.ipAddress ?? null,
|
|
2789
|
-
user_agent: meta.userAgent ?? null,
|
|
2790
|
-
user_id: user?.id ?? null,
|
|
2791
|
-
...details
|
|
2792
|
-
}));
|
|
2793
|
-
}
|
|
2794
|
-
|
|
2795
|
-
// ../../src/core/security/tokenExpiry.ts
|
|
2796
|
-
function resolveDefaultTokenExpiryDays() {
|
|
2797
|
-
const raw = process.env.API_TOKEN_DEFAULT_EXPIRY_DAYS?.trim();
|
|
2798
|
-
if (!raw) {
|
|
2799
|
-
return null;
|
|
2800
|
-
}
|
|
2801
|
-
const parsed = Number.parseInt(raw, 10);
|
|
2802
|
-
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
2803
|
-
return null;
|
|
2804
|
-
}
|
|
2805
|
-
return parsed;
|
|
2806
|
-
}
|
|
2807
|
-
|
|
2808
|
-
// ../../src/core/security/totp.ts
|
|
2809
|
-
import { createHmac as createHmac2 } from "crypto";
|
|
2810
|
-
function decodeBase32(input) {
|
|
2811
|
-
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
|
2812
|
-
const normalized = input.replace(/=+$/u, "").toUpperCase();
|
|
2813
|
-
let bits = "";
|
|
2814
|
-
for (const char of normalized) {
|
|
2815
|
-
const value = alphabet.indexOf(char);
|
|
2816
|
-
if (value === -1) {
|
|
2817
|
-
throw new Error("Invalid base32 character in MFA secret.");
|
|
2818
|
-
}
|
|
2819
|
-
bits += value.toString(2).padStart(5, "0");
|
|
2820
|
-
}
|
|
2821
|
-
const bytes = [];
|
|
2822
|
-
for (let index = 0;index + 8 <= bits.length; index += 8) {
|
|
2823
|
-
bytes.push(Number.parseInt(bits.slice(index, index + 8), 2));
|
|
2824
|
-
}
|
|
2825
|
-
return Buffer.from(bytes);
|
|
2826
|
-
}
|
|
2827
|
-
function generateTotp(secret, counter, digits = 6) {
|
|
2828
|
-
const key = decodeBase32(secret);
|
|
2829
|
-
const buffer = Buffer.alloc(8);
|
|
2830
|
-
buffer.writeBigUInt64BE(BigInt(counter));
|
|
2831
|
-
const digest = createHmac2("sha1", key).update(buffer).digest();
|
|
2832
|
-
const lastByte = digest[digest.length - 1] ?? 0;
|
|
2833
|
-
const offset = lastByte & 15;
|
|
2834
|
-
const b0 = digest[offset] ?? 0;
|
|
2835
|
-
const b1 = digest[offset + 1] ?? 0;
|
|
2836
|
-
const b2 = digest[offset + 2] ?? 0;
|
|
2837
|
-
const b3 = digest[offset + 3] ?? 0;
|
|
2838
|
-
const code = (b0 & 127) << 24 | (b1 & 255) << 16 | (b2 & 255) << 8 | b3 & 255;
|
|
2839
|
-
return String(code % 10 ** digits).padStart(digits, "0");
|
|
2840
|
-
}
|
|
2841
|
-
function verifyTotp(secret, token, window = 1) {
|
|
2842
|
-
const normalized = token.trim();
|
|
2843
|
-
if (!/^\d{6}$/u.test(normalized)) {
|
|
2844
|
-
return false;
|
|
2845
|
-
}
|
|
2846
|
-
const timestep = Math.floor(Date.now() / 30000);
|
|
2847
|
-
for (let offset = -window;offset <= window; offset += 1) {
|
|
2848
|
-
if (generateTotp(secret, timestep + offset) === normalized) {
|
|
2849
|
-
return true;
|
|
2850
|
-
}
|
|
2851
|
-
}
|
|
2852
|
-
return false;
|
|
2853
|
-
}
|
|
2854
|
-
|
|
2855
|
-
// ../../src/core/tenant/tenantContext.ts
|
|
2856
|
-
var tenantContext = createAsyncContextStore("@getstrata/tenantContext");
|
|
2857
|
-
function runWithTenant(tenant, callback) {
|
|
2858
|
-
return tenantContext.run(tenant, callback);
|
|
2859
|
-
}
|
|
2860
|
-
function currentTenant() {
|
|
2861
|
-
return tenantContext.getStore() ?? null;
|
|
2862
|
-
}
|
|
2863
|
-
function currentTenantId() {
|
|
2864
|
-
return currentTenant()?.id ?? 1;
|
|
2865
|
-
}
|
|
2866
|
-
function rateLimitMultiplierForPlan(plan) {
|
|
2867
|
-
switch (plan) {
|
|
2868
|
-
case "enterprise":
|
|
2869
|
-
return 4;
|
|
2870
|
-
case "pro":
|
|
2871
|
-
return 2;
|
|
2872
|
-
default:
|
|
2873
|
-
return 1;
|
|
2874
|
-
}
|
|
2875
|
-
}
|
|
373
|
+
// ../../src/modules/user/authService.ts
|
|
374
|
+
import { verifyPassword } from "@getstrata/core/auth/password";
|
|
375
|
+
import { revealMfaSecret } from "@getstrata/core/crypto/mfaSecret";
|
|
376
|
+
import { UnauthorizedError as UnauthorizedError2 } from "@getstrata/core/errors/http";
|
|
377
|
+
import { logSecurityEvent } from "@getstrata/core/security/securityEvents";
|
|
378
|
+
import { resolveDefaultTokenExpiryDays } from "@getstrata/core/security/tokenExpiry";
|
|
379
|
+
import { verifyTotp } from "@getstrata/core/security/totp";
|
|
380
|
+
import { currentTenantId } from "@getstrata/core/tenant/tenantContext";
|
|
2876
381
|
|
|
2877
382
|
// ../../src/domain/abilities.ts
|
|
2878
383
|
var MEMBER_ABILITIES = [
|
|
@@ -2933,22 +438,22 @@ class AuthService {
|
|
|
2933
438
|
const user = await this.users.findByEmail(email);
|
|
2934
439
|
if (!user?.password_hash) {
|
|
2935
440
|
logSecurityEvent("auth_login_failed", { reason: "unknown_user", email });
|
|
2936
|
-
throw new
|
|
441
|
+
throw new UnauthorizedError2("Invalid credentials.");
|
|
2937
442
|
}
|
|
2938
443
|
const valid = await verifyPassword(password, user.password_hash);
|
|
2939
444
|
if (!valid) {
|
|
2940
445
|
logSecurityEvent("auth_login_failed", { reason: "invalid_password", user_id: user.id });
|
|
2941
|
-
throw new
|
|
446
|
+
throw new UnauthorizedError2("Invalid credentials.");
|
|
2942
447
|
}
|
|
2943
448
|
if (isFeatureEnabled("emailVerification") && !user.email_verified_at) {
|
|
2944
449
|
logSecurityEvent("auth_login_blocked", { reason: "email_unverified", user_id: user.id });
|
|
2945
|
-
throw new
|
|
450
|
+
throw new UnauthorizedError2("Email address is not verified.");
|
|
2946
451
|
}
|
|
2947
452
|
if (isFeatureEnabled("mfa") && user.mfa_enabled) {
|
|
2948
453
|
const mfaSecret = revealMfaSecret(user.mfa_secret);
|
|
2949
454
|
if (!mfaSecret || !options.mfaCode || !verifyTotp(mfaSecret, options.mfaCode)) {
|
|
2950
455
|
logSecurityEvent("auth_login_failed", { reason: "invalid_mfa", user_id: user.id });
|
|
2951
|
-
throw new
|
|
456
|
+
throw new UnauthorizedError2("Invalid MFA code.");
|
|
2952
457
|
}
|
|
2953
458
|
}
|
|
2954
459
|
logSecurityEvent("auth_login_success", { user_id: user.id, method: "password" });
|
|
@@ -2961,7 +466,7 @@ class AuthService {
|
|
|
2961
466
|
async loginWithOAuth(providerName, code) {
|
|
2962
467
|
const provider = this.oauthProviders.get(providerName);
|
|
2963
468
|
if (!provider) {
|
|
2964
|
-
throw new
|
|
469
|
+
throw new UnauthorizedError2("Unsupported OAuth provider.");
|
|
2965
470
|
}
|
|
2966
471
|
const profile = await provider.exchangeCode(code);
|
|
2967
472
|
const user = await this.findOrCreateOAuthUser(providerName, profile);
|
|
@@ -2975,7 +480,7 @@ class AuthService {
|
|
|
2975
480
|
buildOAuthAuthorizationUrl(providerName, state) {
|
|
2976
481
|
const provider = this.oauthProviders.get(providerName);
|
|
2977
482
|
if (!provider) {
|
|
2978
|
-
throw new
|
|
483
|
+
throw new UnauthorizedError2("Unsupported OAuth provider.");
|
|
2979
484
|
}
|
|
2980
485
|
return provider.getAuthorizationUrl(state);
|
|
2981
486
|
}
|
|
@@ -3005,23 +510,42 @@ class AuthService {
|
|
|
3005
510
|
}
|
|
3006
511
|
}
|
|
3007
512
|
|
|
513
|
+
// ../../src/modules/user/notificationRepository.ts
|
|
514
|
+
import { BaseRepository as BaseRepository2 } from "@getstrata/core/database";
|
|
515
|
+
|
|
3008
516
|
// ../../src/modules/user/notificationTable.ts
|
|
3009
|
-
|
|
517
|
+
import { defineTable as defineTable2 } from "@getstrata/core/database";
|
|
518
|
+
var notificationTable = defineTable2({
|
|
3010
519
|
name: "notification",
|
|
3011
520
|
primaryKey: "id",
|
|
3012
521
|
columns: ["id", "user_id", "tenant_id", "type", "title", "body", "data", "read_at", "created_at"],
|
|
3013
522
|
defaultOrderBy: { column: "created_at", direction: "DESC" }
|
|
3014
523
|
});
|
|
3015
524
|
|
|
525
|
+
// ../../src/modules/user/notificationService.ts
|
|
526
|
+
import { NotFoundError as NotFoundError2 } from "@getstrata/core/errors/http";
|
|
527
|
+
|
|
3016
528
|
// ../../src/modules/user/oauthIdentityRepository.ts
|
|
3017
|
-
|
|
529
|
+
import { BaseRepository as BaseRepository3, defineTable as defineTable3 } from "@getstrata/core/database";
|
|
530
|
+
var oauthIdentityTable = defineTable3({
|
|
3018
531
|
name: "oauth_identity",
|
|
3019
532
|
primaryKey: "id",
|
|
3020
533
|
columns: ["id", "user_id", "provider", "provider_user_id", "email", "created_at"]
|
|
3021
534
|
});
|
|
3022
535
|
|
|
536
|
+
// ../../src/modules/user/repository.ts
|
|
537
|
+
import {
|
|
538
|
+
emailLookupForQuery,
|
|
539
|
+
protectEmail,
|
|
540
|
+
revealEmail
|
|
541
|
+
} from "@getstrata/core/crypto/fieldEncryption";
|
|
542
|
+
import { revealMfaSecret as revealMfaSecret2 } from "@getstrata/core/crypto/mfaSecret";
|
|
543
|
+
import { BaseRepository as BaseRepository4 } from "@getstrata/core/database";
|
|
544
|
+
import { currentTenantId as currentTenantId2 } from "@getstrata/core/tenant/tenantContext";
|
|
545
|
+
|
|
3023
546
|
// ../../src/modules/user/table.ts
|
|
3024
|
-
|
|
547
|
+
import { defineTable as defineTable4 } from "@getstrata/core/database";
|
|
548
|
+
var userTable = defineTable4({
|
|
3025
549
|
name: "users",
|
|
3026
550
|
primaryKey: "id",
|
|
3027
551
|
columns: [
|
|
@@ -3041,22 +565,23 @@ var userTable = defineTable({
|
|
|
3041
565
|
defaultOrderBy: { column: "id", direction: "ASC" }
|
|
3042
566
|
});
|
|
3043
567
|
|
|
3044
|
-
// ../../src/
|
|
3045
|
-
import {
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
}
|
|
3049
|
-
function hashApiToken(token) {
|
|
3050
|
-
const pepper = resolveTokenPepper();
|
|
3051
|
-
if (pepper && pepper !== "workhub-dev-token-pepper") {
|
|
3052
|
-
return createHmac3("sha256", pepper).update(token).digest("hex");
|
|
3053
|
-
}
|
|
3054
|
-
return createHash("sha256").update(token).digest("hex");
|
|
3055
|
-
}
|
|
568
|
+
// ../../src/modules/user/tokenService.ts
|
|
569
|
+
import { hashApiToken } from "@getstrata/core/auth/tokenHash";
|
|
570
|
+
import { ForbiddenError as ForbiddenError2, NotFoundError as NotFoundError3 } from "@getstrata/core/errors/http";
|
|
571
|
+
import { resolveDefaultTokenExpiryDays as resolveDefaultTokenExpiryDays2 } from "@getstrata/core/security/tokenExpiry";
|
|
3056
572
|
|
|
3057
573
|
// ../../src/modules/user/provider.ts
|
|
3058
574
|
var tokenServiceToken = CORE_TOKEN_SERVICE_TOKEN;
|
|
3059
575
|
|
|
576
|
+
// ../../src/core/auth/authContext.ts
|
|
577
|
+
var authContext = createAsyncContextStore("@getstrata/authContext");
|
|
578
|
+
function runWithAuthUser(user, callback) {
|
|
579
|
+
return authContext.run(user, callback);
|
|
580
|
+
}
|
|
581
|
+
function currentAuthUser() {
|
|
582
|
+
return authContext.getStore() ?? null;
|
|
583
|
+
}
|
|
584
|
+
|
|
3060
585
|
// ../../src/core/http/csrfToken.ts
|
|
3061
586
|
import { timingSafeEqual } from "crypto";
|
|
3062
587
|
|
|
@@ -3088,6 +613,18 @@ function readBunRequestCookie(request, name) {
|
|
|
3088
613
|
return request.cookies.get(name) ?? readRequestCookie(request, name);
|
|
3089
614
|
}
|
|
3090
615
|
|
|
616
|
+
// ../../src/core/http/requestMetaContext.ts
|
|
617
|
+
var requestMetaContext = createAsyncContextStore("@getstrata/requestMetaContext");
|
|
618
|
+
function runWithRequestMeta(meta, callback) {
|
|
619
|
+
return requestMetaContext.run(meta, callback);
|
|
620
|
+
}
|
|
621
|
+
function currentRequestMeta() {
|
|
622
|
+
return requestMetaContext.getStore() ?? {
|
|
623
|
+
ipAddress: null,
|
|
624
|
+
userAgent: null
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
|
|
3091
628
|
// ../../src/core/http/csrfToken.ts
|
|
3092
629
|
var CSRF_COOKIE = "workhub_csrf";
|
|
3093
630
|
var CSRF_TTL_MS = 60 * 60 * 1000;
|
|
@@ -3167,14 +704,14 @@ function resolveCsrfTokenForRequest(request) {
|
|
|
3167
704
|
}
|
|
3168
705
|
|
|
3169
706
|
// ../../src/core/http/flashSession.ts
|
|
3170
|
-
import { createHmac
|
|
707
|
+
import { createHmac, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
3171
708
|
var FLASH_COOKIE = "workhub_flash";
|
|
3172
709
|
var FLASH_TTL_MS = 60 * 1000;
|
|
3173
710
|
function resolveFlashSecret() {
|
|
3174
711
|
return process.env.SESSION_SECRET?.trim() || process.env.OAUTH_STATE_SECRET?.trim() || "workhub-dev-flash-secret";
|
|
3175
712
|
}
|
|
3176
713
|
function signFlashPayload(payload, issuedAt) {
|
|
3177
|
-
const signature =
|
|
714
|
+
const signature = createHmac("sha256", resolveFlashSecret()).update(`${payload}.${issuedAt}`).digest("hex");
|
|
3178
715
|
return `${payload}.${issuedAt}.${signature}`;
|
|
3179
716
|
}
|
|
3180
717
|
function readFlashCookie(request) {
|