@getstrata/core 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bootstrap/httpKernel.d.ts +2 -0
- package/dist/config/queue.d.ts +8 -0
- package/dist/config/rateLimit.d.ts +2 -1
- package/dist/core/database/baseRepository.d.ts +16 -4
- package/dist/core/database/index.d.ts +10 -4
- package/dist/core/database/migrations/advisoryLock.d.ts +6 -0
- package/dist/core/database/migrations/runner.d.ts +8 -2
- package/dist/core/database/migrations/types.d.ts +1 -0
- package/dist/core/database/model.d.ts +46 -17
- package/dist/core/database/query.d.ts +25 -14
- package/dist/core/database/relationships.d.ts +32 -2
- package/dist/core/database/repositoryQuery.d.ts +16 -1
- package/dist/core/database/schema/blueprint.d.ts +47 -0
- package/dist/core/database/schema/columnDefinition.d.ts +36 -0
- package/dist/core/database/schema/driver.d.ts +8 -0
- package/dist/core/database/schema/errors.d.ts +4 -0
- package/dist/core/database/schema/grammars/compileStatements.d.ts +8 -0
- package/dist/core/database/schema/grammars/createGrammar.d.ts +4 -0
- package/dist/core/database/schema/grammars/grammar.d.ts +10 -0
- package/dist/core/database/schema/grammars/index.d.ts +7 -0
- package/dist/core/database/schema/grammars/mysqlGrammar.d.ts +2 -0
- package/dist/core/database/schema/grammars/postgresGrammar.d.ts +2 -0
- package/dist/core/database/schema/grammars/sqliteGrammar.d.ts +2 -0
- package/dist/core/database/schema/index.d.ts +12 -0
- package/dist/core/database/schema/schema.d.ts +21 -0
- package/dist/core/database/seeders/runner.d.ts +6 -0
- package/dist/core/database/seeders/types.d.ts +8 -0
- package/dist/core/database/types.d.ts +39 -2
- package/dist/core/database/whereBuilder.d.ts +17 -0
- package/dist/core/http/index.d.ts +1 -1
- package/dist/core/http/securedRouteModelBinding.d.ts +2 -1
- package/dist/core/lifecycle/gracefulShutdown.d.ts +6 -0
- package/dist/core/pagination/index.d.ts +11 -1
- package/dist/core/queue/failedJobRepository.d.ts +6 -0
- package/dist/core/queue/failedJobService.d.ts +15 -0
- package/dist/core/queue/failedJobTable.d.ts +3 -0
- package/dist/core/queue/jobRegistry.d.ts +14 -0
- package/dist/core/queue/jobRunner.d.ts +9 -0
- package/dist/core/queue/publicQueue.d.ts +15 -0
- package/dist/core/queue/redisQueue.d.ts +29 -0
- package/dist/core/queue/resilientQueue.d.ts +9 -0
- package/dist/core/queue/types.d.ts +8 -0
- package/dist/core/scheduler/schedule.d.ts +15 -0
- package/dist/entries/auth/accessControl.js +113 -0
- package/dist/entries/auth/authContext.js +15 -0
- package/dist/entries/auth/guard.js +2870 -0
- package/dist/entries/auth/membershipContext.js +276 -0
- package/dist/entries/auth/membershipScope.js +390 -0
- package/dist/entries/auth/membershipService.js +477 -0
- package/dist/entries/auth/oauth/oidcProvider.js +49 -0
- package/dist/entries/auth/oauth/providers.js +71 -0
- package/dist/entries/auth/oauth/samlProvider.js +26 -0
- package/dist/entries/auth/oauth/types.js +1 -0
- package/dist/entries/auth/password.js +15 -0
- package/dist/entries/auth/policy.js +134 -0
- package/dist/entries/auth/sessionCookie.js +75 -0
- package/dist/entries/auth/tokenHash.js +17 -0
- package/dist/entries/cache/tags.js +13 -0
- package/dist/entries/crypto/fieldEncryption.js +93 -0
- package/dist/entries/crypto/mfaSecret.js +105 -0
- package/dist/entries/database/factory.js +17 -0
- package/dist/entries/database/seeders.js +30 -0
- package/dist/entries/database/types.js +1 -0
- package/dist/entries/database.js +2218 -0
- package/dist/entries/errors/http.js +78 -0
- package/dist/entries/http/contentNegotiation.js +26 -0
- package/dist/entries/http/csrfToken.js +147 -0
- package/dist/entries/http/etag.js +170 -0
- package/dist/entries/http/flashSession.js +106 -0
- package/dist/entries/http/middleware.js +68 -0
- package/dist/entries/http/parseFormBody.js +88 -0
- package/dist/entries/http/requestMetaContext.js +18 -0
- package/dist/entries/http/resources.js +19 -0
- package/dist/entries/http/webErrorResponse.js +3143 -0
- package/dist/entries/http/webFormRequest.js +293 -0
- package/dist/entries/http.js +3924 -0
- package/dist/entries/jobs/dispatchWebhookJob.js +342 -0
- package/dist/entries/lifecycle/gracefulShutdown.js +50 -0
- package/dist/entries/metrics/prometheus.js +70 -0
- package/dist/entries/pagination.js +14 -0
- package/dist/entries/queue/createAppQueue.js +2848 -0
- package/dist/entries/queue/failedJobService.js +38 -0
- package/dist/entries/queue/jobRegistry.js +32 -0
- package/dist/entries/queue/jobRunner.js +75 -0
- package/dist/entries/queue/publicQueue.js +2476 -0
- package/dist/entries/queue/queueMetrics.js +2898 -0
- package/dist/entries/queue/types.js +1 -0
- package/dist/entries/security/oauthState.js +75 -0
- package/dist/entries/security/publicReads.js +33 -0
- package/dist/entries/security/safeUrl.js +143 -0
- package/dist/entries/security/securityEvents.js +41 -0
- package/dist/entries/security/stripeWebhook.js +115 -0
- package/dist/entries/security/tokenExpiry.js +16 -0
- package/dist/entries/security/totp.js +51 -0
- package/dist/entries/storage/storage.js +123 -0
- package/dist/entries/tenant/tenantContext.js +30 -0
- package/dist/entries/tenant/tenantMiddleware.js +312 -0
- package/dist/entries/tracing/traceContext.js +15 -0
- package/dist/entries/validation/rules.js +232 -0
- package/dist/entries/view.js +3072 -0
- package/dist/framework/public-api.d.ts +65 -38
- package/dist/index.js +3198 -308
- package/dist/modules/user/apiTokenRepository.d.ts +1 -1
- package/dist/modules/user/apiTokenTable.d.ts +1 -1
- package/dist/modules/user/authService.d.ts +1 -1
- package/dist/modules/user/notificationRepository.d.ts +1 -1
- package/dist/modules/user/notificationService.d.ts +1 -1
- package/dist/modules/user/notificationTable.d.ts +1 -1
- package/dist/modules/user/oauthIdentityRepository.d.ts +2 -2
- package/dist/modules/user/provider.d.ts +1 -1
- package/dist/modules/user/repository.d.ts +1 -1
- package/dist/modules/user/table.d.ts +1 -1
- package/dist/modules/user/tokenService.d.ts +1 -1
- package/package.json +289 -3
package/dist/index.js
CHANGED
|
@@ -135,6 +135,13 @@ class UnauthorizedError extends HttpError {
|
|
|
135
135
|
super(401, message, details);
|
|
136
136
|
}
|
|
137
137
|
}
|
|
138
|
+
|
|
139
|
+
class PayloadTooLargeError extends HttpError {
|
|
140
|
+
constructor(message = "Payload Too Large", details) {
|
|
141
|
+
super(413, message, details);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
138
145
|
class PreconditionFailedError extends HttpError {
|
|
139
146
|
constructor(message = "Precondition Failed", details) {
|
|
140
147
|
super(412, message, details);
|
|
@@ -370,6 +377,23 @@ function quoteIdentifier(identifier) {
|
|
|
370
377
|
function qualifyColumn(tableName, column) {
|
|
371
378
|
return `${quoteIdentifier(tableName)}.${quoteIdentifier(column)}`;
|
|
372
379
|
}
|
|
380
|
+
function resolveQualifiedColumn(defaultTable, columnName) {
|
|
381
|
+
if (columnName.includes(".")) {
|
|
382
|
+
const [table, column] = columnName.split(".", 2);
|
|
383
|
+
if (!table || !column) {
|
|
384
|
+
throw new Error(`Invalid qualified column: ${columnName}`);
|
|
385
|
+
}
|
|
386
|
+
return qualifyColumn(table, column);
|
|
387
|
+
}
|
|
388
|
+
return qualifyColumn(defaultTable, columnName);
|
|
389
|
+
}
|
|
390
|
+
function parseQualifiedColumn(reference) {
|
|
391
|
+
const [table, column] = reference.split(".", 2);
|
|
392
|
+
if (!table || !column) {
|
|
393
|
+
throw new Error(`Join columns must be qualified as table.column: ${reference}`);
|
|
394
|
+
}
|
|
395
|
+
return { table, column };
|
|
396
|
+
}
|
|
373
397
|
function normalizeDirection(direction = "ASC") {
|
|
374
398
|
return direction.toUpperCase() === "DESC" ? "DESC" : "ASC";
|
|
375
399
|
}
|
|
@@ -417,16 +441,21 @@ function buildOperatorClauses(column, operator, params) {
|
|
|
417
441
|
if (operator.lte !== undefined) {
|
|
418
442
|
clauses.push(`${column} <= ${pushParam(params, operator.lte)}`);
|
|
419
443
|
}
|
|
444
|
+
if (operator.ilike !== undefined) {
|
|
445
|
+
clauses.push(`${column} ILIKE ${pushParam(params, `%${operator.ilike}%`)}`);
|
|
446
|
+
}
|
|
447
|
+
if (operator.tsMatch !== undefined) {
|
|
448
|
+
clauses.push(`${column} @@ plainto_tsquery('english', ${pushParam(params, operator.tsMatch)})`);
|
|
449
|
+
}
|
|
420
450
|
return clauses;
|
|
421
451
|
}
|
|
422
|
-
function
|
|
452
|
+
function appendWhereParts(tableName, where, params) {
|
|
423
453
|
const clauses = [];
|
|
424
|
-
const params = [];
|
|
425
454
|
for (const [columnName, filterValue] of Object.entries(where)) {
|
|
426
455
|
if (filterValue === undefined) {
|
|
427
456
|
continue;
|
|
428
457
|
}
|
|
429
|
-
const column =
|
|
458
|
+
const column = resolveQualifiedColumn(tableName, columnName);
|
|
430
459
|
if (Array.isArray(filterValue)) {
|
|
431
460
|
clauses.push(buildInClause(column, filterValue, params));
|
|
432
461
|
continue;
|
|
@@ -441,8 +470,54 @@ function buildWhereClause(tableName, where = {}) {
|
|
|
441
470
|
}
|
|
442
471
|
clauses.push(`${column} = ${pushParam(params, filterValue)}`);
|
|
443
472
|
}
|
|
473
|
+
return clauses.join(" AND ");
|
|
474
|
+
}
|
|
475
|
+
function buildWhereClause(tableName, where = {}) {
|
|
476
|
+
const params = [];
|
|
477
|
+
const body = appendWhereParts(tableName, where, params);
|
|
478
|
+
return {
|
|
479
|
+
clause: body.length > 0 ? ` WHERE ${body}` : "",
|
|
480
|
+
params
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
function buildWhereNodeClause(tableName, node, params) {
|
|
484
|
+
if ("where" in node) {
|
|
485
|
+
return appendWhereParts(tableName, node.where, params);
|
|
486
|
+
}
|
|
487
|
+
const grouped = buildWhereGroupClause(tableName, node.group, params);
|
|
488
|
+
if (!grouped) {
|
|
489
|
+
return "";
|
|
490
|
+
}
|
|
491
|
+
return grouped.includes(" OR ") ? `(${grouped})` : grouped;
|
|
492
|
+
}
|
|
493
|
+
function buildWhereGroupClause(tableName, nodes, params) {
|
|
494
|
+
let result = "";
|
|
495
|
+
for (const node of nodes) {
|
|
496
|
+
const part = buildWhereNodeClause(tableName, node, params);
|
|
497
|
+
if (!part) {
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
500
|
+
if (!result) {
|
|
501
|
+
result = part;
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
result = node.kind === "or" ? `${result} OR ${part}` : `${result} AND ${part}`;
|
|
505
|
+
}
|
|
506
|
+
if (!result) {
|
|
507
|
+
return "";
|
|
508
|
+
}
|
|
509
|
+
return result;
|
|
510
|
+
}
|
|
511
|
+
function buildAdvancedWhereClause(tableName, where = {}, whereNodes = []) {
|
|
512
|
+
const params = [];
|
|
513
|
+
const nodes = [];
|
|
514
|
+
if (Object.keys(where).length > 0) {
|
|
515
|
+
nodes.push({ kind: "and", where });
|
|
516
|
+
}
|
|
517
|
+
nodes.push(...whereNodes);
|
|
518
|
+
const combined = buildWhereGroupClause(tableName, nodes, params);
|
|
444
519
|
return {
|
|
445
|
-
clause:
|
|
520
|
+
clause: combined ? ` WHERE ${combined}` : "",
|
|
446
521
|
params
|
|
447
522
|
};
|
|
448
523
|
}
|
|
@@ -469,12 +544,17 @@ function appendSoftDeleteScope(table, options, clauses) {
|
|
|
469
544
|
clauses.push(`${qualifiedColumn} IS NULL`);
|
|
470
545
|
}
|
|
471
546
|
}
|
|
472
|
-
function buildQueryWhereClause(table, options = {}) {
|
|
473
|
-
const { clause, params } =
|
|
474
|
-
const
|
|
475
|
-
appendSoftDeleteScope(table, options,
|
|
547
|
+
function buildQueryWhereClause(table, options = {}, whereNodes = []) {
|
|
548
|
+
const { clause, params } = buildAdvancedWhereClause(table.name, options.where ?? {}, whereNodes);
|
|
549
|
+
const softDeleteClauses = [];
|
|
550
|
+
appendSoftDeleteScope(table, options, softDeleteClauses);
|
|
551
|
+
if (softDeleteClauses.length === 0) {
|
|
552
|
+
return { clause, params };
|
|
553
|
+
}
|
|
554
|
+
const base = clause.replace(/^ WHERE /, "");
|
|
555
|
+
const scope = softDeleteClauses.join(" AND ");
|
|
476
556
|
return {
|
|
477
|
-
clause:
|
|
557
|
+
clause: base ? ` WHERE (${base}) AND ${scope}` : ` WHERE ${scope}`,
|
|
478
558
|
params
|
|
479
559
|
};
|
|
480
560
|
}
|
|
@@ -498,10 +578,32 @@ function normalizeOrderBy(orderBy) {
|
|
|
498
578
|
}
|
|
499
579
|
function buildOrderByClause(tableName, orderBy) {
|
|
500
580
|
const parts = normalizeOrderBy(orderBy).map(({ column, direction }) => {
|
|
501
|
-
return `${
|
|
581
|
+
return `${resolveQualifiedColumn(tableName, column)} ${normalizeDirection(direction)}`;
|
|
502
582
|
});
|
|
503
583
|
return parts.length > 0 ? ` ORDER BY ${parts.join(", ")}` : "";
|
|
504
584
|
}
|
|
585
|
+
function buildGroupByClause(tableName, groupBy) {
|
|
586
|
+
if (!groupBy) {
|
|
587
|
+
return "";
|
|
588
|
+
}
|
|
589
|
+
const columns = (Array.isArray(groupBy) ? groupBy : [groupBy]).map((column) => String(column));
|
|
590
|
+
const parts = columns.map((column) => resolveQualifiedColumn(tableName, column));
|
|
591
|
+
return parts.length > 0 ? ` GROUP BY ${parts.join(", ")}` : "";
|
|
592
|
+
}
|
|
593
|
+
function buildHavingClause(tableName, having, params) {
|
|
594
|
+
if (!having) {
|
|
595
|
+
return "";
|
|
596
|
+
}
|
|
597
|
+
const body = appendWhereParts(tableName, having, params);
|
|
598
|
+
return body.length > 0 ? ` HAVING ${body}` : "";
|
|
599
|
+
}
|
|
600
|
+
function buildJoinClause(joins = []) {
|
|
601
|
+
return joins.map((join) => {
|
|
602
|
+
const joinType = join.type === "left" ? "LEFT JOIN" : "INNER JOIN";
|
|
603
|
+
const onClause = join.on.map(({ left, right }) => `${qualifyColumn(left.table, left.column)} = ${qualifyColumn(right.table, right.column)}`).join(" AND ");
|
|
604
|
+
return ` ${joinType} ${quoteIdentifier(join.table)} ON ${onClause}`;
|
|
605
|
+
}).join("");
|
|
606
|
+
}
|
|
505
607
|
function buildLimitClause(limit) {
|
|
506
608
|
if (limit === undefined) {
|
|
507
609
|
return "";
|
|
@@ -523,6 +625,23 @@ function buildOffsetClause(offset) {
|
|
|
523
625
|
function buildReturningColumns(table) {
|
|
524
626
|
return table.columns.map((column) => qualifyColumn(table.name, column)).join(", ");
|
|
525
627
|
}
|
|
628
|
+
function buildSelectList(table, select, params = []) {
|
|
629
|
+
if (!select || select.length === 0) {
|
|
630
|
+
return buildReturningColumns(table);
|
|
631
|
+
}
|
|
632
|
+
return select.map((item) => {
|
|
633
|
+
if (item.kind === "column") {
|
|
634
|
+
const column2 = qualifyColumn(item.table, item.column);
|
|
635
|
+
return item.as ? `${column2} AS ${quoteIdentifier(item.as)}` : column2;
|
|
636
|
+
}
|
|
637
|
+
if (item.kind === "literalText") {
|
|
638
|
+
return `${pushParam(params, item.value)}::text AS ${quoteIdentifier(item.as)}`;
|
|
639
|
+
}
|
|
640
|
+
const column = qualifyColumn(item.table, item.column);
|
|
641
|
+
const placeholder = pushParam(params, item.query);
|
|
642
|
+
return `ts_rank(${column}, plainto_tsquery('english', ${placeholder})) AS ${quoteIdentifier(item.as)}`;
|
|
643
|
+
}).join(", ");
|
|
644
|
+
}
|
|
526
645
|
function getDefinedColumnEntries(table, values, options = {}) {
|
|
527
646
|
const record = values;
|
|
528
647
|
const excluded = new Set(options.exclude ?? []);
|
|
@@ -537,34 +656,48 @@ function getDefinedColumnEntries(table, values, options = {}) {
|
|
|
537
656
|
return [[column, value]];
|
|
538
657
|
});
|
|
539
658
|
}
|
|
540
|
-
function buildSelectQuery(table, options = {}) {
|
|
541
|
-
const
|
|
542
|
-
const
|
|
659
|
+
function buildSelectQuery(table, options = {}, whereNodes = []) {
|
|
660
|
+
const params = [];
|
|
661
|
+
const columns = buildSelectList(table, options.select, params);
|
|
662
|
+
const { clause, params: whereParams } = buildQueryWhereClause(table, options, whereNodes);
|
|
663
|
+
params.push(...whereParams);
|
|
664
|
+
const joins = buildJoinClause(options.joins);
|
|
665
|
+
const groupBy = buildGroupByClause(table.name, options.groupBy);
|
|
666
|
+
const havingClause = buildHavingClause(table.name, options.having, params);
|
|
543
667
|
const orderBy = buildOrderByClause(table.name, options.orderBy ?? table.defaultOrderBy);
|
|
544
668
|
const limit = buildLimitClause(options.limit);
|
|
545
669
|
const offset = buildOffsetClause(options.offset);
|
|
546
670
|
return {
|
|
547
|
-
text: `SELECT ${columns} FROM ${quoteIdentifier(table.name)}${clause}${orderBy}${limit}${offset}`,
|
|
671
|
+
text: `SELECT ${columns} FROM ${quoteIdentifier(table.name)}${joins}${clause}${groupBy}${havingClause}${orderBy}${limit}${offset}`,
|
|
548
672
|
params
|
|
549
673
|
};
|
|
550
674
|
}
|
|
551
|
-
function buildCountQuery(table, where = {}, options = {}) {
|
|
552
|
-
const
|
|
675
|
+
function buildCountQuery(table, where = {}, options = {}, whereNodes = []) {
|
|
676
|
+
const params = [];
|
|
677
|
+
const { clause, params: whereParams } = buildQueryWhereClause(table, {
|
|
553
678
|
where,
|
|
554
|
-
|
|
555
|
-
|
|
679
|
+
withTrashed: options.withTrashed,
|
|
680
|
+
onlyTrashed: options.onlyTrashed
|
|
681
|
+
}, whereNodes);
|
|
682
|
+
params.push(...whereParams);
|
|
683
|
+
const joins = buildJoinClause(options.joins);
|
|
684
|
+
const groupBy = buildGroupByClause(table.name, options.groupBy);
|
|
556
685
|
return {
|
|
557
|
-
text: `SELECT COUNT(*) AS count FROM ${quoteIdentifier(table.name)}${clause}`,
|
|
686
|
+
text: `SELECT COUNT(*) AS count FROM ${quoteIdentifier(table.name)}${joins}${clause}${groupBy}`,
|
|
558
687
|
params
|
|
559
688
|
};
|
|
560
689
|
}
|
|
561
|
-
function buildProjectionQuery(table, expression, alias, options = {}) {
|
|
690
|
+
function buildProjectionQuery(table, expression, alias, options = {}, whereNodes = []) {
|
|
562
691
|
assertSafeProjectionExpression(expression);
|
|
563
|
-
const
|
|
692
|
+
const params = [];
|
|
693
|
+
const { clause, params: whereParams } = buildQueryWhereClause(table, options, whereNodes);
|
|
694
|
+
params.push(...whereParams);
|
|
695
|
+
const joins = buildJoinClause(options.joins);
|
|
696
|
+
const groupBy = buildGroupByClause(table.name, options.groupBy);
|
|
564
697
|
const orderBy = buildOrderByClause(table.name, options.orderBy);
|
|
565
698
|
const limit = buildLimitClause(options.limit);
|
|
566
699
|
return {
|
|
567
|
-
text: `SELECT ${expression} AS ${quoteIdentifier(alias)} FROM ${quoteIdentifier(table.name)}${clause}${orderBy}${limit}`,
|
|
700
|
+
text: `SELECT ${expression} AS ${quoteIdentifier(alias)} FROM ${quoteIdentifier(table.name)}${joins}${clause}${groupBy}${orderBy}${limit}`,
|
|
568
701
|
params
|
|
569
702
|
};
|
|
570
703
|
}
|
|
@@ -657,12 +790,24 @@ function hasMany(definition) {
|
|
|
657
790
|
...definition
|
|
658
791
|
};
|
|
659
792
|
}
|
|
793
|
+
function hasOne(definition) {
|
|
794
|
+
return {
|
|
795
|
+
type: "hasOne",
|
|
796
|
+
...definition
|
|
797
|
+
};
|
|
798
|
+
}
|
|
660
799
|
function belongsTo(definition) {
|
|
661
800
|
return {
|
|
662
801
|
type: "belongsTo",
|
|
663
802
|
...definition
|
|
664
803
|
};
|
|
665
804
|
}
|
|
805
|
+
function belongsToMany(definition) {
|
|
806
|
+
return {
|
|
807
|
+
type: "belongsToMany",
|
|
808
|
+
...definition
|
|
809
|
+
};
|
|
810
|
+
}
|
|
666
811
|
function indexHasManyRelation(parents, children, relation) {
|
|
667
812
|
const groups = new Map;
|
|
668
813
|
for (const parent of parents) {
|
|
@@ -678,6 +823,15 @@ function indexHasManyRelation(parents, children, relation) {
|
|
|
678
823
|
}
|
|
679
824
|
return groups;
|
|
680
825
|
}
|
|
826
|
+
function indexHasOneRelation(parents, children, relation) {
|
|
827
|
+
const grouped = indexHasManyRelation(parents, children, relation);
|
|
828
|
+
const result = new Map;
|
|
829
|
+
for (const parent of parents) {
|
|
830
|
+
const matches = grouped.get(parent[relation.localKey]) ?? [];
|
|
831
|
+
result.set(parent[relation.localKey], matches[0]);
|
|
832
|
+
}
|
|
833
|
+
return result;
|
|
834
|
+
}
|
|
681
835
|
function indexBelongsToRelation(children, parents, relation) {
|
|
682
836
|
const parentsById = new Map;
|
|
683
837
|
for (const parent of parents) {
|
|
@@ -693,6 +847,27 @@ function indexBelongsToRelation(children, parents, relation) {
|
|
|
693
847
|
}
|
|
694
848
|
return result;
|
|
695
849
|
}
|
|
850
|
+
function indexBelongsToManyRelation(parents, pivotRows, relatedRows, relation) {
|
|
851
|
+
const relatedById = new Map;
|
|
852
|
+
for (const related of relatedRows) {
|
|
853
|
+
relatedById.set(related[relation.relatedKey], related);
|
|
854
|
+
}
|
|
855
|
+
const groups = new Map;
|
|
856
|
+
for (const parent of parents) {
|
|
857
|
+
groups.set(parent[relation.parentKey], []);
|
|
858
|
+
}
|
|
859
|
+
for (const pivot of pivotRows) {
|
|
860
|
+
const parentId = pivot[relation.foreignPivotKey];
|
|
861
|
+
const relatedId = pivot[relation.relatedPivotKey];
|
|
862
|
+
const group = groups.get(parentId);
|
|
863
|
+
const related = relatedById.get(relatedId);
|
|
864
|
+
if (!group || !related) {
|
|
865
|
+
continue;
|
|
866
|
+
}
|
|
867
|
+
group.push(related);
|
|
868
|
+
}
|
|
869
|
+
return groups;
|
|
870
|
+
}
|
|
696
871
|
|
|
697
872
|
// ../../src/config/database.ts
|
|
698
873
|
function readInteger(name, fallback) {
|
|
@@ -710,6 +885,9 @@ var databaseConfig = {
|
|
|
710
885
|
// ../../src/core/database/connectionContext.ts
|
|
711
886
|
import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
|
|
712
887
|
var activeConnection = new AsyncLocalStorage2;
|
|
888
|
+
function runWithDatabaseConnection(connection, callback) {
|
|
889
|
+
return activeConnection.run(connection, callback);
|
|
890
|
+
}
|
|
713
891
|
function getActiveDatabaseConnection(fallback) {
|
|
714
892
|
return activeConnection.getStore() ?? fallback;
|
|
715
893
|
}
|
|
@@ -784,19 +962,67 @@ var repositoryConnection = new Proxy({}, {
|
|
|
784
962
|
}
|
|
785
963
|
});
|
|
786
964
|
|
|
965
|
+
// ../../src/core/database/whereBuilder.ts
|
|
966
|
+
class WhereBuilder {
|
|
967
|
+
nodes = [];
|
|
968
|
+
where(where) {
|
|
969
|
+
this.nodes.push({ kind: "and", where });
|
|
970
|
+
return this;
|
|
971
|
+
}
|
|
972
|
+
orWhere(where) {
|
|
973
|
+
this.nodes.push({ kind: "or", where });
|
|
974
|
+
return this;
|
|
975
|
+
}
|
|
976
|
+
whereGroup(fn) {
|
|
977
|
+
const nested = new WhereBuilder;
|
|
978
|
+
fn(nested);
|
|
979
|
+
if (nested.nodes.length > 0) {
|
|
980
|
+
this.nodes.push({ kind: "and", group: nested.nodes });
|
|
981
|
+
}
|
|
982
|
+
return this;
|
|
983
|
+
}
|
|
984
|
+
orWhereGroup(fn) {
|
|
985
|
+
const nested = new WhereBuilder;
|
|
986
|
+
fn(nested);
|
|
987
|
+
if (nested.nodes.length > 0) {
|
|
988
|
+
this.nodes.push({ kind: "or", group: nested.nodes });
|
|
989
|
+
}
|
|
990
|
+
return this;
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
|
|
787
994
|
// ../../src/core/database/repositoryQuery.ts
|
|
788
995
|
class RepositoryQuery {
|
|
789
996
|
repository;
|
|
790
997
|
whereClause;
|
|
791
998
|
queryOptions;
|
|
792
999
|
eagerLoads = [];
|
|
1000
|
+
whereNodes = [];
|
|
793
1001
|
constructor(repository, whereClause = {}, queryOptions = {}) {
|
|
794
1002
|
this.repository = repository;
|
|
795
1003
|
this.whereClause = whereClause;
|
|
796
1004
|
this.queryOptions = queryOptions;
|
|
797
1005
|
}
|
|
798
|
-
where(
|
|
799
|
-
|
|
1006
|
+
where(input) {
|
|
1007
|
+
if (typeof input === "function") {
|
|
1008
|
+
const builder = new WhereBuilder;
|
|
1009
|
+
input(builder);
|
|
1010
|
+
this.whereNodes.push(...builder.nodes);
|
|
1011
|
+
return this;
|
|
1012
|
+
}
|
|
1013
|
+
this.whereClause = { ...this.whereClause, ...input };
|
|
1014
|
+
return this;
|
|
1015
|
+
}
|
|
1016
|
+
orWhere(input) {
|
|
1017
|
+
if (typeof input === "function") {
|
|
1018
|
+
const builder = new WhereBuilder;
|
|
1019
|
+
input(builder);
|
|
1020
|
+
if (builder.nodes.length > 0) {
|
|
1021
|
+
this.whereNodes.push({ kind: "or", group: builder.nodes });
|
|
1022
|
+
}
|
|
1023
|
+
return this;
|
|
1024
|
+
}
|
|
1025
|
+
this.whereNodes.push({ kind: "or", where: input });
|
|
800
1026
|
return this;
|
|
801
1027
|
}
|
|
802
1028
|
orderBy(orderBy) {
|
|
@@ -807,6 +1033,24 @@ class RepositoryQuery {
|
|
|
807
1033
|
this.queryOptions = { ...this.queryOptions, limit };
|
|
808
1034
|
return this;
|
|
809
1035
|
}
|
|
1036
|
+
offset(offset) {
|
|
1037
|
+
this.queryOptions = { ...this.queryOptions, offset };
|
|
1038
|
+
return this;
|
|
1039
|
+
}
|
|
1040
|
+
join(left, right) {
|
|
1041
|
+
return this.addJoin("inner", left, right);
|
|
1042
|
+
}
|
|
1043
|
+
leftJoin(left, right) {
|
|
1044
|
+
return this.addJoin("left", left, right);
|
|
1045
|
+
}
|
|
1046
|
+
groupBy(groupBy) {
|
|
1047
|
+
this.queryOptions = { ...this.queryOptions, groupBy };
|
|
1048
|
+
return this;
|
|
1049
|
+
}
|
|
1050
|
+
having(having) {
|
|
1051
|
+
this.queryOptions = { ...this.queryOptions, having };
|
|
1052
|
+
return this;
|
|
1053
|
+
}
|
|
810
1054
|
withHasMany(as, relation, childRepository, options = {}) {
|
|
811
1055
|
this.eagerLoads.push({
|
|
812
1056
|
kind: "hasMany",
|
|
@@ -828,16 +1072,50 @@ class RepositoryQuery {
|
|
|
828
1072
|
return this;
|
|
829
1073
|
}
|
|
830
1074
|
async get() {
|
|
831
|
-
const rows = await this.repository.findAll(
|
|
832
|
-
...this.queryOptions,
|
|
833
|
-
where: this.whereClause
|
|
834
|
-
});
|
|
1075
|
+
const rows = await this.repository.findAll(this.buildOptions());
|
|
835
1076
|
return await this.attach(rows);
|
|
836
1077
|
}
|
|
837
1078
|
async first() {
|
|
838
1079
|
const rows = await this.get();
|
|
839
1080
|
return rows[0] ?? null;
|
|
840
1081
|
}
|
|
1082
|
+
async paginate(options) {
|
|
1083
|
+
return await this.repository.paginate({
|
|
1084
|
+
...this.buildOptions(),
|
|
1085
|
+
page: options.page,
|
|
1086
|
+
perPage: options.perPage
|
|
1087
|
+
});
|
|
1088
|
+
}
|
|
1089
|
+
buildOptions() {
|
|
1090
|
+
return {
|
|
1091
|
+
...this.queryOptions,
|
|
1092
|
+
where: this.whereClause,
|
|
1093
|
+
whereNodes: this.whereNodes
|
|
1094
|
+
};
|
|
1095
|
+
}
|
|
1096
|
+
addJoin(type, left, right) {
|
|
1097
|
+
const leftRef = parseQualifiedColumn(left);
|
|
1098
|
+
const rightRef = parseQualifiedColumn(right);
|
|
1099
|
+
const table = type === "inner" ? rightRef.table : rightRef.table;
|
|
1100
|
+
const joins = this.queryOptions.joins ?? [];
|
|
1101
|
+
const existing = joins.find((join) => join.table === table && join.type === type);
|
|
1102
|
+
if (existing) {
|
|
1103
|
+
existing.on.push({ left: leftRef, right: rightRef });
|
|
1104
|
+
return this;
|
|
1105
|
+
}
|
|
1106
|
+
this.queryOptions = {
|
|
1107
|
+
...this.queryOptions,
|
|
1108
|
+
joins: [
|
|
1109
|
+
...joins,
|
|
1110
|
+
{
|
|
1111
|
+
type,
|
|
1112
|
+
table,
|
|
1113
|
+
on: [{ left: leftRef, right: rightRef }]
|
|
1114
|
+
}
|
|
1115
|
+
]
|
|
1116
|
+
};
|
|
1117
|
+
return this;
|
|
1118
|
+
}
|
|
841
1119
|
async attach(rows) {
|
|
842
1120
|
if (rows.length === 0 || this.eagerLoads.length === 0) {
|
|
843
1121
|
return rows.map((row) => ({ ...row }));
|
|
@@ -874,21 +1152,24 @@ class BaseRepository {
|
|
|
874
1152
|
}
|
|
875
1153
|
async findAll(options = {}) {
|
|
876
1154
|
return await withDatabaseErrorHandling(async () => {
|
|
877
|
-
const {
|
|
1155
|
+
const { whereNodes, ...queryOptions } = options;
|
|
1156
|
+
const { text, params } = buildSelectQuery(this.table, queryOptions, whereNodes ?? []);
|
|
878
1157
|
return await this.connection.unsafe(text, params);
|
|
879
1158
|
});
|
|
880
1159
|
}
|
|
881
1160
|
async paginate(options) {
|
|
882
|
-
const
|
|
1161
|
+
const { whereNodes, where = {}, page, perPage, ...queryOptions } = options;
|
|
883
1162
|
const total = await this.countWhere(where, {
|
|
884
1163
|
withTrashed: options.withTrashed,
|
|
885
|
-
onlyTrashed: options.onlyTrashed
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
1164
|
+
onlyTrashed: options.onlyTrashed,
|
|
1165
|
+
joins: options.joins,
|
|
1166
|
+
groupBy: options.groupBy
|
|
1167
|
+
}, whereNodes);
|
|
1168
|
+
const offset = (page - 1) * perPage;
|
|
889
1169
|
const data = await this.findAll({
|
|
890
1170
|
...queryOptions,
|
|
891
1171
|
where,
|
|
1172
|
+
whereNodes,
|
|
892
1173
|
limit: perPage,
|
|
893
1174
|
offset
|
|
894
1175
|
});
|
|
@@ -897,6 +1178,65 @@ class BaseRepository {
|
|
|
897
1178
|
meta: buildPaginationMeta({ page, perPage, total })
|
|
898
1179
|
};
|
|
899
1180
|
}
|
|
1181
|
+
async chunk(count, callback, options = {}) {
|
|
1182
|
+
if (!Number.isInteger(count) || count <= 0) {
|
|
1183
|
+
throw new Error("Chunk size must be a positive integer.");
|
|
1184
|
+
}
|
|
1185
|
+
let offset = 0;
|
|
1186
|
+
while (true) {
|
|
1187
|
+
const rows = await this.findAll({
|
|
1188
|
+
...options,
|
|
1189
|
+
limit: count,
|
|
1190
|
+
offset
|
|
1191
|
+
});
|
|
1192
|
+
if (rows.length === 0) {
|
|
1193
|
+
return;
|
|
1194
|
+
}
|
|
1195
|
+
const shouldContinue = await callback(rows);
|
|
1196
|
+
if (shouldContinue === false || rows.length < count) {
|
|
1197
|
+
return;
|
|
1198
|
+
}
|
|
1199
|
+
offset += count;
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
async cursorPaginate(options) {
|
|
1203
|
+
const {
|
|
1204
|
+
perPage,
|
|
1205
|
+
cursor,
|
|
1206
|
+
cursorColumn = this.table.primaryKey,
|
|
1207
|
+
direction = "asc",
|
|
1208
|
+
where = {},
|
|
1209
|
+
whereNodes,
|
|
1210
|
+
...queryOptions
|
|
1211
|
+
} = options;
|
|
1212
|
+
if (!Number.isInteger(perPage) || perPage <= 0) {
|
|
1213
|
+
throw new Error("Cursor page size must be a positive integer.");
|
|
1214
|
+
}
|
|
1215
|
+
const cursorWhere = { ...where };
|
|
1216
|
+
if (cursor !== undefined) {
|
|
1217
|
+
cursorWhere[cursorColumn] = direction === "asc" ? { gt: cursor } : { lt: cursor };
|
|
1218
|
+
}
|
|
1219
|
+
const rows = await this.findAll({
|
|
1220
|
+
...queryOptions,
|
|
1221
|
+
where: cursorWhere,
|
|
1222
|
+
whereNodes,
|
|
1223
|
+
orderBy: { [cursorColumn]: direction },
|
|
1224
|
+
limit: perPage + 1
|
|
1225
|
+
});
|
|
1226
|
+
const hasMore = rows.length > perPage;
|
|
1227
|
+
const data = hasMore ? rows.slice(0, perPage) : rows;
|
|
1228
|
+
const nextCursor = hasMore ? data[data.length - 1]?.[cursorColumn] ?? null : null;
|
|
1229
|
+
const prevCursor = cursor ?? null;
|
|
1230
|
+
return {
|
|
1231
|
+
data,
|
|
1232
|
+
meta: {
|
|
1233
|
+
per_page: perPage,
|
|
1234
|
+
next_cursor: nextCursor,
|
|
1235
|
+
prev_cursor: prevCursor,
|
|
1236
|
+
has_more: hasMore
|
|
1237
|
+
}
|
|
1238
|
+
};
|
|
1239
|
+
}
|
|
900
1240
|
async findById(id) {
|
|
901
1241
|
return await this.firstOrNull({
|
|
902
1242
|
[this.table.primaryKey]: id
|
|
@@ -1003,14 +1343,17 @@ class BaseRepository {
|
|
|
1003
1343
|
getConnection() {
|
|
1004
1344
|
return this.connection;
|
|
1005
1345
|
}
|
|
1346
|
+
getTable() {
|
|
1347
|
+
return this.table;
|
|
1348
|
+
}
|
|
1006
1349
|
query(where = {}) {
|
|
1007
1350
|
return new RepositoryQuery(this, where);
|
|
1008
1351
|
}
|
|
1009
1352
|
async findWhere(where, options = {}) {
|
|
1010
1353
|
return await this.findAll({ ...options, where });
|
|
1011
1354
|
}
|
|
1012
|
-
async countWhere(where = {}, options = {}) {
|
|
1013
|
-
const { text, params } = buildCountQuery(this.table, where, options);
|
|
1355
|
+
async countWhere(where = {}, options = {}, whereNodes = []) {
|
|
1356
|
+
const { text, params } = buildCountQuery(this.table, where, options, whereNodes);
|
|
1014
1357
|
const [row] = await this.connection.unsafe(text, params);
|
|
1015
1358
|
return Number(row?.count ?? 0);
|
|
1016
1359
|
}
|
|
@@ -1070,6 +1413,16 @@ var baseRepository_default = BaseRepository;
|
|
|
1070
1413
|
function bindDatabaseConnection2(connection) {
|
|
1071
1414
|
bindDatabaseConnection(connection);
|
|
1072
1415
|
}
|
|
1416
|
+
// ../../src/core/database/migrations/advisoryLock.ts
|
|
1417
|
+
var MIGRATION_LOCK_KEY = 42424242;
|
|
1418
|
+
async function withMigrationLock(db2, callback, lockKey = MIGRATION_LOCK_KEY) {
|
|
1419
|
+
await db2.unsafe("SELECT pg_advisory_lock($1)", [lockKey]);
|
|
1420
|
+
try {
|
|
1421
|
+
return await callback();
|
|
1422
|
+
} finally {
|
|
1423
|
+
await db2.unsafe("SELECT pg_advisory_unlock($1)", [lockKey]);
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1073
1426
|
// ../../src/core/database/migrations/runner.ts
|
|
1074
1427
|
import { readdir } from "fs/promises";
|
|
1075
1428
|
import { join } from "path";
|
|
@@ -1111,7 +1464,7 @@ async function getMigrationStatus(db2, migrations) {
|
|
|
1111
1464
|
batch: appliedByName.get(name) ?? null
|
|
1112
1465
|
}));
|
|
1113
1466
|
}
|
|
1114
|
-
async function
|
|
1467
|
+
async function runPendingMigrations(db2, migrations, options = {}) {
|
|
1115
1468
|
const applied = await getAppliedMigrations(db2);
|
|
1116
1469
|
const appliedNames = new Set(applied.map(({ name }) => name));
|
|
1117
1470
|
const nextBatch = applied.reduce((currentMax, { batch }) => Math.max(currentMax, Number(batch)), 0) + 1;
|
|
@@ -1126,6 +1479,13 @@ async function migrateDatabase(db2, migrations, options = {}) {
|
|
|
1126
1479
|
}
|
|
1127
1480
|
return pendingMigrations.length;
|
|
1128
1481
|
}
|
|
1482
|
+
async function migrateDatabase(db2, migrations, options = {}) {
|
|
1483
|
+
const { advisoryLock = false, onMigration } = options;
|
|
1484
|
+
if (advisoryLock) {
|
|
1485
|
+
return withMigrationLock(db2, () => runPendingMigrations(db2, migrations, { onMigration }));
|
|
1486
|
+
}
|
|
1487
|
+
return runPendingMigrations(db2, migrations, { onMigration });
|
|
1488
|
+
}
|
|
1129
1489
|
async function rollbackDatabase(db2, migrations, options = {}) {
|
|
1130
1490
|
const applied = await getAppliedMigrations(db2);
|
|
1131
1491
|
if (applied.length === 0) {
|
|
@@ -1146,20 +1506,29 @@ async function rollbackDatabase(db2, migrations, options = {}) {
|
|
|
1146
1506
|
return rolledBack;
|
|
1147
1507
|
}
|
|
1148
1508
|
async function freshDatabase(db2, migrations, options = {}) {
|
|
1149
|
-
const
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1509
|
+
const runFresh = async () => {
|
|
1510
|
+
const applied = await getAppliedMigrations(db2);
|
|
1511
|
+
const appliedNames = new Set(applied.map(({ name }) => name));
|
|
1512
|
+
const appliedMigrations = migrations.filter(({ name }) => appliedNames.has(name));
|
|
1513
|
+
for (const migration of [...appliedMigrations].reverse()) {
|
|
1514
|
+
options.onMigration?.(migration.name);
|
|
1515
|
+
await migration.down(db2);
|
|
1516
|
+
}
|
|
1517
|
+
if (appliedMigrations.length > 0) {
|
|
1518
|
+
await db2.unsafe(`DELETE FROM ${MIGRATIONS_TABLE}`);
|
|
1519
|
+
}
|
|
1520
|
+
await runPendingMigrations(db2, migrations, options);
|
|
1521
|
+
};
|
|
1522
|
+
if (options.advisoryLock) {
|
|
1523
|
+
await withMigrationLock(db2, runFresh);
|
|
1524
|
+
return;
|
|
1158
1525
|
}
|
|
1159
|
-
await
|
|
1526
|
+
await runFresh();
|
|
1160
1527
|
}
|
|
1161
1528
|
// ../../src/core/database/model.ts
|
|
1162
1529
|
var modelRepositories = new WeakMap;
|
|
1530
|
+
var modelGlobalScopes = new WeakMap;
|
|
1531
|
+
var modelBooted = new WeakSet;
|
|
1163
1532
|
function resolveModelRepository(model) {
|
|
1164
1533
|
const repository = modelRepositories.get(model);
|
|
1165
1534
|
if (!repository) {
|
|
@@ -1167,13 +1536,121 @@ function resolveModelRepository(model) {
|
|
|
1167
1536
|
}
|
|
1168
1537
|
return repository;
|
|
1169
1538
|
}
|
|
1539
|
+
function modelStatics(model) {
|
|
1540
|
+
return model;
|
|
1541
|
+
}
|
|
1542
|
+
function ensureBooted(model) {
|
|
1543
|
+
if (modelBooted.has(model)) {
|
|
1544
|
+
return;
|
|
1545
|
+
}
|
|
1546
|
+
modelBooted.add(model);
|
|
1547
|
+
const boot = model.boot;
|
|
1548
|
+
if (typeof boot === "function") {
|
|
1549
|
+
boot.call(model);
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
function getGlobalScopes(model) {
|
|
1553
|
+
return modelGlobalScopes.get(model) ?? [];
|
|
1554
|
+
}
|
|
1555
|
+
function hydrateValue(value, cast) {
|
|
1556
|
+
if (value === null || value === undefined) {
|
|
1557
|
+
return value;
|
|
1558
|
+
}
|
|
1559
|
+
switch (cast) {
|
|
1560
|
+
case "date":
|
|
1561
|
+
case "datetime":
|
|
1562
|
+
return value instanceof Date ? value : new Date(String(value));
|
|
1563
|
+
case "json":
|
|
1564
|
+
return typeof value === "string" ? JSON.parse(value) : value;
|
|
1565
|
+
case "bool":
|
|
1566
|
+
case "boolean":
|
|
1567
|
+
return value === true || value === 1 || value === "1" || value === "true";
|
|
1568
|
+
default:
|
|
1569
|
+
return value;
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
function dehydrateValue(value, cast) {
|
|
1573
|
+
if (value === null || value === undefined) {
|
|
1574
|
+
return value;
|
|
1575
|
+
}
|
|
1576
|
+
switch (cast) {
|
|
1577
|
+
case "date":
|
|
1578
|
+
case "datetime":
|
|
1579
|
+
return value instanceof Date ? value : new Date(String(value));
|
|
1580
|
+
case "json":
|
|
1581
|
+
return typeof value === "string" ? value : JSON.stringify(value);
|
|
1582
|
+
case "bool":
|
|
1583
|
+
case "boolean":
|
|
1584
|
+
return Boolean(value);
|
|
1585
|
+
default:
|
|
1586
|
+
return value;
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
function filterMassAssignable(fillable, guarded, input) {
|
|
1590
|
+
const resolvedGuarded = guarded ?? true;
|
|
1591
|
+
if (fillable && fillable.length > 0) {
|
|
1592
|
+
const allowed = new Set(fillable);
|
|
1593
|
+
return Object.fromEntries(Object.entries(input).filter(([key]) => allowed.has(key)));
|
|
1594
|
+
}
|
|
1595
|
+
if (resolvedGuarded === true || resolvedGuarded.includes("*")) {
|
|
1596
|
+
return {};
|
|
1597
|
+
}
|
|
1598
|
+
const blocked = new Set(resolvedGuarded);
|
|
1599
|
+
return Object.fromEntries(Object.entries(input).filter(([key]) => !blocked.has(key)));
|
|
1600
|
+
}
|
|
1601
|
+
function applyCasts(values, casts, direction) {
|
|
1602
|
+
if (Object.keys(casts).length === 0) {
|
|
1603
|
+
return values;
|
|
1604
|
+
}
|
|
1605
|
+
const result = { ...values };
|
|
1606
|
+
const castFn = direction === "hydrate" ? hydrateValue : dehydrateValue;
|
|
1607
|
+
for (const [key, cast] of Object.entries(casts)) {
|
|
1608
|
+
if (key in result && cast) {
|
|
1609
|
+
result[key] = castFn(result[key], cast);
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
return result;
|
|
1613
|
+
}
|
|
1614
|
+
function applyTimestampsOnCreate(columns, values, enabled) {
|
|
1615
|
+
if (!enabled) {
|
|
1616
|
+
return values;
|
|
1617
|
+
}
|
|
1618
|
+
const now = new Date;
|
|
1619
|
+
const result = { ...values };
|
|
1620
|
+
if (columns.includes("created_at")) {
|
|
1621
|
+
result.created_at = now;
|
|
1622
|
+
}
|
|
1623
|
+
if (columns.includes("updated_at")) {
|
|
1624
|
+
result.updated_at = now;
|
|
1625
|
+
}
|
|
1626
|
+
return result;
|
|
1627
|
+
}
|
|
1628
|
+
function applyTimestampsOnUpdate(columns, values, enabled) {
|
|
1629
|
+
if (!enabled) {
|
|
1630
|
+
return values;
|
|
1631
|
+
}
|
|
1632
|
+
const result = { ...values };
|
|
1633
|
+
if (columns.includes("updated_at")) {
|
|
1634
|
+
result.updated_at = new Date;
|
|
1635
|
+
}
|
|
1636
|
+
return result;
|
|
1637
|
+
}
|
|
1170
1638
|
|
|
1171
1639
|
class Model {
|
|
1172
1640
|
attributes;
|
|
1173
1641
|
repository;
|
|
1174
|
-
|
|
1642
|
+
static $fillable;
|
|
1643
|
+
static $guarded;
|
|
1644
|
+
static $casts = {};
|
|
1645
|
+
static $timestamps = true;
|
|
1646
|
+
_exists;
|
|
1647
|
+
constructor(attributes, repository, exists = true) {
|
|
1175
1648
|
this.attributes = attributes;
|
|
1176
1649
|
this.repository = repository;
|
|
1650
|
+
this._exists = exists;
|
|
1651
|
+
}
|
|
1652
|
+
get $exists() {
|
|
1653
|
+
return this._exists;
|
|
1177
1654
|
}
|
|
1178
1655
|
get(key) {
|
|
1179
1656
|
return this.attributes[key];
|
|
@@ -1187,43 +1664,169 @@ class Model {
|
|
|
1187
1664
|
primaryKey() {
|
|
1188
1665
|
throw new Error(`${this.constructor.name}.primaryKey() is not implemented.`);
|
|
1189
1666
|
}
|
|
1667
|
+
static primaryKeyField() {
|
|
1668
|
+
return resolveModelRepository(Model).getTable().primaryKey;
|
|
1669
|
+
}
|
|
1670
|
+
static hydrateAttributes(attributes) {
|
|
1671
|
+
const casts = Model.$casts ?? {};
|
|
1672
|
+
return applyCasts(attributes, casts, "hydrate");
|
|
1673
|
+
}
|
|
1674
|
+
static dehydrateAttributes(attributes) {
|
|
1675
|
+
const casts = Model.$casts ?? {};
|
|
1676
|
+
return applyCasts(attributes, casts, "dehydrate");
|
|
1677
|
+
}
|
|
1678
|
+
static fromRecord(record, repository, exists = true) {
|
|
1679
|
+
const hydrated = Model.hydrateAttributes(record);
|
|
1680
|
+
return new Model(hydrated, repository, exists);
|
|
1681
|
+
}
|
|
1682
|
+
static boot() {}
|
|
1683
|
+
static addGlobalScope(_name, scope) {
|
|
1684
|
+
ensureBooted(Model);
|
|
1685
|
+
const existing = modelGlobalScopes.get(Model) ?? [];
|
|
1686
|
+
modelGlobalScopes.set(Model, [
|
|
1687
|
+
...existing,
|
|
1688
|
+
scope
|
|
1689
|
+
]);
|
|
1690
|
+
}
|
|
1190
1691
|
static repository() {
|
|
1191
1692
|
return resolveModelRepository(this);
|
|
1192
1693
|
}
|
|
1193
1694
|
static query() {
|
|
1194
|
-
|
|
1695
|
+
ensureBooted(this);
|
|
1696
|
+
const repository = resolveModelRepository(Model);
|
|
1697
|
+
let query = repository.query();
|
|
1698
|
+
for (const scope of getGlobalScopes(Model)) {
|
|
1699
|
+
query = scope(query);
|
|
1700
|
+
}
|
|
1701
|
+
return query;
|
|
1702
|
+
}
|
|
1703
|
+
static async create(attributes) {
|
|
1704
|
+
const statics = modelStatics(this);
|
|
1705
|
+
ensureBooted(Model);
|
|
1706
|
+
const repository = resolveModelRepository(Model);
|
|
1707
|
+
const table = repository.getTable();
|
|
1708
|
+
const timestamps = statics.$timestamps ?? true;
|
|
1709
|
+
const assignable = filterMassAssignable(statics.$fillable, statics.$guarded, attributes);
|
|
1710
|
+
const withTimestamps = applyTimestampsOnCreate(table.columns, assignable, timestamps);
|
|
1711
|
+
const payload = statics.dehydrateAttributes(withTimestamps);
|
|
1712
|
+
const record = await repository.create(payload);
|
|
1713
|
+
return statics.fromRecord(record, repository, true);
|
|
1195
1714
|
}
|
|
1196
1715
|
static async find(id) {
|
|
1197
|
-
const
|
|
1198
|
-
const
|
|
1199
|
-
|
|
1716
|
+
const statics = modelStatics(this);
|
|
1717
|
+
const repository = resolveModelRepository(Model);
|
|
1718
|
+
const primaryKey = statics.primaryKeyField();
|
|
1719
|
+
const record = await Model.query.call(Model).where({ [primaryKey]: id }).first();
|
|
1720
|
+
return record ? statics.fromRecord(record, repository, true) : null;
|
|
1200
1721
|
}
|
|
1201
1722
|
static async findOrFail(id, errorFactory) {
|
|
1202
|
-
const
|
|
1203
|
-
|
|
1204
|
-
|
|
1723
|
+
const model = await Model.find.call(Model, id);
|
|
1724
|
+
if (model) {
|
|
1725
|
+
return model;
|
|
1726
|
+
}
|
|
1727
|
+
throw errorFactory?.(id) ?? new NotFoundError(`${Model.name} ${String(id)} not found.`);
|
|
1205
1728
|
}
|
|
1206
1729
|
static async all(options = {}) {
|
|
1207
|
-
const
|
|
1208
|
-
const
|
|
1209
|
-
|
|
1730
|
+
const statics = modelStatics(this);
|
|
1731
|
+
const repository = resolveModelRepository(Model);
|
|
1732
|
+
let query = Model.query.call(Model);
|
|
1733
|
+
if (options.orderBy) {
|
|
1734
|
+
query = query.orderBy(options.orderBy);
|
|
1735
|
+
}
|
|
1736
|
+
if (options.limit !== undefined) {
|
|
1737
|
+
query = query.limit(options.limit);
|
|
1738
|
+
}
|
|
1739
|
+
const rows = await query.get();
|
|
1740
|
+
return rows.map((row) => statics.fromRecord(row, repository, true));
|
|
1210
1741
|
}
|
|
1211
1742
|
static async firstWhere(where, options = {}) {
|
|
1212
|
-
const
|
|
1213
|
-
const
|
|
1214
|
-
|
|
1215
|
-
|
|
1743
|
+
const statics = modelStatics(this);
|
|
1744
|
+
const repository = resolveModelRepository(Model);
|
|
1745
|
+
let query = Model.query.call(Model).where(where);
|
|
1746
|
+
if (options.orderBy) {
|
|
1747
|
+
query = query.orderBy(options.orderBy);
|
|
1748
|
+
}
|
|
1749
|
+
const record = await query.first();
|
|
1750
|
+
return record ? statics.fromRecord(record, repository, true) : null;
|
|
1751
|
+
}
|
|
1752
|
+
async save() {
|
|
1753
|
+
const ModelClass = modelStatics(this.constructor);
|
|
1754
|
+
const timestamps = ModelClass.$timestamps ?? true;
|
|
1755
|
+
const casts = ModelClass.$casts ?? {};
|
|
1756
|
+
const table = this.repository.getTable();
|
|
1757
|
+
if (this.$exists) {
|
|
1758
|
+
const changes = applyTimestampsOnUpdate(table.columns, applyCasts(this.attributes, casts, "dehydrate"), timestamps);
|
|
1759
|
+
const record2 = await this.repository.updateByIdOrThrow(this.id, changes);
|
|
1760
|
+
this.attributes = ModelClass.hydrateAttributes(record2);
|
|
1761
|
+
return this;
|
|
1762
|
+
}
|
|
1763
|
+
const assignable = filterMassAssignable(ModelClass.$fillable, ModelClass.$guarded, this.attributes);
|
|
1764
|
+
const withTimestamps = applyTimestampsOnCreate(table.columns, assignable, timestamps);
|
|
1765
|
+
const payload = ModelClass.dehydrateAttributes(withTimestamps);
|
|
1766
|
+
const record = await this.repository.create(payload);
|
|
1767
|
+
this.attributes = ModelClass.hydrateAttributes(record);
|
|
1768
|
+
this._exists = true;
|
|
1769
|
+
return this;
|
|
1770
|
+
}
|
|
1771
|
+
async update(changes) {
|
|
1772
|
+
const ModelClass = modelStatics(this.constructor);
|
|
1773
|
+
const assignable = filterMassAssignable(ModelClass.$fillable, ModelClass.$guarded, changes);
|
|
1774
|
+
Object.assign(this.attributes, assignable);
|
|
1775
|
+
return await this.save();
|
|
1776
|
+
}
|
|
1777
|
+
async delete() {
|
|
1778
|
+
if (resolveSoftDeleteColumn(this.repository.getTable())) {
|
|
1779
|
+
return await this.repository.deleteById(this.id);
|
|
1780
|
+
}
|
|
1781
|
+
return await this.repository.forceDeleteById(this.id);
|
|
1782
|
+
}
|
|
1783
|
+
async forceDelete() {
|
|
1784
|
+
return await this.repository.forceDeleteById(this.id);
|
|
1785
|
+
}
|
|
1786
|
+
async restore() {
|
|
1787
|
+
const ModelClass = modelStatics(this.constructor);
|
|
1788
|
+
const record = await this.repository.restoreById(this.id);
|
|
1789
|
+
if (!record) {
|
|
1790
|
+
return null;
|
|
1791
|
+
}
|
|
1792
|
+
this.attributes = ModelClass.hydrateAttributes(record);
|
|
1793
|
+
return this;
|
|
1216
1794
|
}
|
|
1217
1795
|
async loadHasMany(as, relation, childRepository, options = {}) {
|
|
1218
1796
|
const grouped = await childRepository.withConnection(this.repository.getConnection()).loadHasManyForParents([this.attributes], relation, options);
|
|
1219
1797
|
const loaded = grouped.get(this.attributes[relation.localKey]) ?? [];
|
|
1220
1798
|
return Object.assign(this, { [as]: loaded });
|
|
1221
1799
|
}
|
|
1800
|
+
async loadHasOne(as, relation, childRepository, options = {}) {
|
|
1801
|
+
const loaded = await this.loadHasMany(as, relation, childRepository, { ...options, limit: 1 });
|
|
1802
|
+
const value = loaded[as]?.[0];
|
|
1803
|
+
return Object.assign(this, { [as]: value });
|
|
1804
|
+
}
|
|
1222
1805
|
async loadBelongsTo(as, relation, parentRepository, options = {}) {
|
|
1223
1806
|
const grouped = await this.repository.loadBelongsToForParents([this.attributes], relation, parentRepository, options);
|
|
1224
1807
|
const loaded = grouped.get(this.attributes[relation.foreignKey]);
|
|
1225
1808
|
return Object.assign(this, { [as]: loaded });
|
|
1226
1809
|
}
|
|
1810
|
+
async loadBelongsToMany(as, relation, relatedRepository, options = {}) {
|
|
1811
|
+
const connection = this.repository.getConnection();
|
|
1812
|
+
const parentId = this.attributes[relation.parentKey];
|
|
1813
|
+
const pivotRows = await connection.unsafe(`SELECT * FROM ${relation.pivotTable} WHERE ${String(relation.foreignPivotKey)} = $1`, [parentId]);
|
|
1814
|
+
if (pivotRows.length === 0) {
|
|
1815
|
+
return Object.assign(this, { [as]: [] });
|
|
1816
|
+
}
|
|
1817
|
+
const relatedIds = [
|
|
1818
|
+
...new Set(pivotRows.map((row) => row[relation.relatedPivotKey]))
|
|
1819
|
+
];
|
|
1820
|
+
const relatedRows = await relatedRepository.withConnection(connection).findAll({
|
|
1821
|
+
...options,
|
|
1822
|
+
where: {
|
|
1823
|
+
[relation.relatedKey]: relatedIds
|
|
1824
|
+
}
|
|
1825
|
+
});
|
|
1826
|
+
const grouped = indexBelongsToManyRelation([this.attributes], pivotRows, relatedRows, relation);
|
|
1827
|
+
const loaded = grouped.get(parentId) ?? [];
|
|
1828
|
+
return Object.assign(this, { [as]: loaded });
|
|
1829
|
+
}
|
|
1227
1830
|
mergeAttributes(patch) {
|
|
1228
1831
|
Object.assign(this.attributes, patch);
|
|
1229
1832
|
return this;
|
|
@@ -1231,52 +1834,718 @@ class Model {
|
|
|
1231
1834
|
}
|
|
1232
1835
|
function registerModelRepository(model, repository) {
|
|
1233
1836
|
modelRepositories.set(model, repository);
|
|
1837
|
+
ensureBooted(model);
|
|
1234
1838
|
return model;
|
|
1235
1839
|
}
|
|
1236
|
-
// ../../src/core/database/
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1840
|
+
// ../../src/core/database/schema/columnDefinition.ts
|
|
1841
|
+
class ColumnDefinition {
|
|
1842
|
+
name;
|
|
1843
|
+
kind;
|
|
1844
|
+
length;
|
|
1845
|
+
isNullable = false;
|
|
1846
|
+
isPrimary = false;
|
|
1847
|
+
isUnique = false;
|
|
1848
|
+
autoIncrement = false;
|
|
1849
|
+
defaultValue;
|
|
1850
|
+
checkExpression;
|
|
1851
|
+
foreignKey;
|
|
1852
|
+
constructor(name, kind) {
|
|
1853
|
+
this.name = name;
|
|
1854
|
+
this.kind = kind;
|
|
1855
|
+
}
|
|
1856
|
+
nullable() {
|
|
1857
|
+
this.isNullable = true;
|
|
1858
|
+
return this;
|
|
1245
1859
|
}
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
const line = JSON.stringify(entry);
|
|
1255
|
-
if (level === "error") {
|
|
1256
|
-
console.error(line);
|
|
1257
|
-
return;
|
|
1860
|
+
notNullable() {
|
|
1861
|
+
this.isNullable = false;
|
|
1862
|
+
return this;
|
|
1863
|
+
}
|
|
1864
|
+
default(value) {
|
|
1865
|
+
if (typeof value === "boolean") {
|
|
1866
|
+
this.defaultValue = value ? "TRUE" : "FALSE";
|
|
1867
|
+
return this;
|
|
1258
1868
|
}
|
|
1259
|
-
|
|
1869
|
+
if (typeof value === "number") {
|
|
1870
|
+
this.defaultValue = String(value);
|
|
1871
|
+
return this;
|
|
1872
|
+
}
|
|
1873
|
+
this.defaultValue = `'${value.replace(/'/g, "''")}'`;
|
|
1874
|
+
return this;
|
|
1260
1875
|
}
|
|
1261
|
-
|
|
1262
|
-
this.
|
|
1876
|
+
defaultRaw(expression) {
|
|
1877
|
+
this.defaultValue = expression;
|
|
1878
|
+
return this;
|
|
1263
1879
|
}
|
|
1264
|
-
|
|
1265
|
-
this.
|
|
1880
|
+
unique() {
|
|
1881
|
+
this.isUnique = true;
|
|
1882
|
+
return this;
|
|
1266
1883
|
}
|
|
1267
|
-
|
|
1268
|
-
this.
|
|
1884
|
+
primary() {
|
|
1885
|
+
this.isPrimary = true;
|
|
1886
|
+
return this;
|
|
1269
1887
|
}
|
|
1270
|
-
|
|
1271
|
-
this.
|
|
1888
|
+
check(expression) {
|
|
1889
|
+
this.checkExpression = expression;
|
|
1890
|
+
return this;
|
|
1272
1891
|
}
|
|
1273
1892
|
}
|
|
1274
|
-
|
|
1893
|
+
|
|
1894
|
+
class ForeignIdColumnDefinition extends ColumnDefinition {
|
|
1895
|
+
constructor(name) {
|
|
1896
|
+
super(name, "foreignId");
|
|
1897
|
+
this.notNullable();
|
|
1898
|
+
}
|
|
1899
|
+
references(table, column = "id") {
|
|
1900
|
+
this.foreignKey = {
|
|
1901
|
+
referencesTable: table,
|
|
1902
|
+
referencesColumn: column
|
|
1903
|
+
};
|
|
1904
|
+
return this;
|
|
1905
|
+
}
|
|
1906
|
+
constrained(table) {
|
|
1907
|
+
const referencesTable = table ?? inferReferencedTable(this.name);
|
|
1908
|
+
return this.references(referencesTable);
|
|
1909
|
+
}
|
|
1910
|
+
cascadeOnDelete() {
|
|
1911
|
+
if (!this.foreignKey) {
|
|
1912
|
+
throw new Error(`Foreign key is not defined for column ${this.name}`);
|
|
1913
|
+
}
|
|
1914
|
+
this.foreignKey.onDelete = "cascade";
|
|
1915
|
+
return this;
|
|
1916
|
+
}
|
|
1917
|
+
nullOnDelete() {
|
|
1918
|
+
if (!this.foreignKey) {
|
|
1919
|
+
throw new Error(`Foreign key is not defined for column ${this.name}`);
|
|
1920
|
+
}
|
|
1921
|
+
this.foreignKey.onDelete = "set null";
|
|
1922
|
+
return this;
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
function inferReferencedTable(columnName) {
|
|
1926
|
+
if (!columnName.endsWith("_id")) {
|
|
1927
|
+
throw new Error(`Cannot infer referenced table from column ${columnName}`);
|
|
1928
|
+
}
|
|
1929
|
+
return columnName.slice(0, -3);
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
// ../../src/core/database/schema/blueprint.ts
|
|
1933
|
+
class Blueprint {
|
|
1934
|
+
table;
|
|
1935
|
+
action;
|
|
1936
|
+
columns = [];
|
|
1937
|
+
indexes = [];
|
|
1938
|
+
droppedColumns = [];
|
|
1939
|
+
droppedIndexes = [];
|
|
1940
|
+
constructor(table, action) {
|
|
1941
|
+
this.table = table;
|
|
1942
|
+
this.action = action;
|
|
1943
|
+
}
|
|
1944
|
+
id(name = "id") {
|
|
1945
|
+
const column = new ColumnDefinition(name, "id");
|
|
1946
|
+
column.primary();
|
|
1947
|
+
column.autoIncrement = true;
|
|
1948
|
+
this.columns.push(column);
|
|
1949
|
+
return column;
|
|
1950
|
+
}
|
|
1951
|
+
string(name, length) {
|
|
1952
|
+
const column = new ColumnDefinition(name, "string");
|
|
1953
|
+
column.length = length;
|
|
1954
|
+
column.notNullable();
|
|
1955
|
+
this.columns.push(column);
|
|
1956
|
+
return column;
|
|
1957
|
+
}
|
|
1958
|
+
text(name) {
|
|
1959
|
+
const column = new ColumnDefinition(name, "text");
|
|
1960
|
+
column.notNullable();
|
|
1961
|
+
this.columns.push(column);
|
|
1962
|
+
return column;
|
|
1963
|
+
}
|
|
1964
|
+
boolean(name) {
|
|
1965
|
+
const column = new ColumnDefinition(name, "boolean");
|
|
1966
|
+
column.notNullable();
|
|
1967
|
+
this.columns.push(column);
|
|
1968
|
+
return column;
|
|
1969
|
+
}
|
|
1970
|
+
integer(name) {
|
|
1971
|
+
const column = new ColumnDefinition(name, "integer");
|
|
1972
|
+
column.notNullable();
|
|
1973
|
+
this.columns.push(column);
|
|
1974
|
+
return column;
|
|
1975
|
+
}
|
|
1976
|
+
bigInteger(name) {
|
|
1977
|
+
const column = new ColumnDefinition(name, "bigInteger");
|
|
1978
|
+
column.notNullable();
|
|
1979
|
+
this.columns.push(column);
|
|
1980
|
+
return column;
|
|
1981
|
+
}
|
|
1982
|
+
timestamp(name) {
|
|
1983
|
+
const column = new ColumnDefinition(name, "timestamp");
|
|
1984
|
+
column.notNullable();
|
|
1985
|
+
this.columns.push(column);
|
|
1986
|
+
return column;
|
|
1987
|
+
}
|
|
1988
|
+
json(name) {
|
|
1989
|
+
const column = new ColumnDefinition(name, "json");
|
|
1990
|
+
column.notNullable();
|
|
1991
|
+
this.columns.push(column);
|
|
1992
|
+
return column;
|
|
1993
|
+
}
|
|
1994
|
+
jsonb(name) {
|
|
1995
|
+
const column = new ColumnDefinition(name, "jsonb");
|
|
1996
|
+
column.notNullable();
|
|
1997
|
+
this.columns.push(column);
|
|
1998
|
+
return column;
|
|
1999
|
+
}
|
|
2000
|
+
foreignId(name) {
|
|
2001
|
+
const column = new ForeignIdColumnDefinition(name);
|
|
2002
|
+
this.columns.push(column);
|
|
2003
|
+
return column;
|
|
2004
|
+
}
|
|
2005
|
+
timestamps() {
|
|
2006
|
+
this.timestamp("created_at").defaultRaw("NOW()");
|
|
2007
|
+
this.timestamp("updated_at").defaultRaw("NOW()");
|
|
2008
|
+
}
|
|
2009
|
+
softDeletes() {
|
|
2010
|
+
this.timestamp("deleted_at").nullable();
|
|
2011
|
+
}
|
|
2012
|
+
dropColumn(name) {
|
|
2013
|
+
this.droppedColumns.push(name);
|
|
2014
|
+
}
|
|
2015
|
+
dropSoftDeletes() {
|
|
2016
|
+
this.dropColumn("deleted_at");
|
|
2017
|
+
this.dropIndex(`idx_${this.table}_deleted_at`);
|
|
2018
|
+
}
|
|
2019
|
+
dropIndex(name) {
|
|
2020
|
+
this.droppedIndexes.push(name);
|
|
2021
|
+
}
|
|
2022
|
+
unique(columns, name) {
|
|
2023
|
+
this.indexes.push({
|
|
2024
|
+
name,
|
|
2025
|
+
columns: Array.isArray(columns) ? columns : [columns],
|
|
2026
|
+
kind: "unique"
|
|
2027
|
+
});
|
|
2028
|
+
}
|
|
2029
|
+
index(columns, options = {}) {
|
|
2030
|
+
this.indexes.push({
|
|
2031
|
+
name: options.name,
|
|
2032
|
+
columns: Array.isArray(columns) ? columns : [columns],
|
|
2033
|
+
kind: "index",
|
|
2034
|
+
order: options.order
|
|
2035
|
+
});
|
|
2036
|
+
}
|
|
2037
|
+
partialIndex(columns, where, nameOrOptions) {
|
|
2038
|
+
const options = typeof nameOrOptions === "string" ? { name: nameOrOptions } : nameOrOptions ?? {};
|
|
2039
|
+
this.indexes.push({
|
|
2040
|
+
name: options.name,
|
|
2041
|
+
columns: Array.isArray(columns) ? columns : [columns],
|
|
2042
|
+
kind: options.unique ? "uniquePartial" : "partial",
|
|
2043
|
+
where
|
|
2044
|
+
});
|
|
2045
|
+
}
|
|
2046
|
+
fullText(columns, name) {
|
|
2047
|
+
this.indexes.push({
|
|
2048
|
+
name,
|
|
2049
|
+
columns: Array.isArray(columns) ? columns : [columns],
|
|
2050
|
+
kind: "fullText"
|
|
2051
|
+
});
|
|
2052
|
+
}
|
|
2053
|
+
ginIndex(column, name) {
|
|
2054
|
+
this.indexes.push({
|
|
2055
|
+
name,
|
|
2056
|
+
columns: [column],
|
|
2057
|
+
kind: "gin"
|
|
2058
|
+
});
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
// ../../src/core/database/schema/driver.ts
|
|
2062
|
+
function normalizeConnectionName(connection) {
|
|
2063
|
+
const normalized = connection.trim().toLowerCase();
|
|
2064
|
+
if (normalized === "pgsql" || normalized === "postgres" || normalized === "postgresql") {
|
|
2065
|
+
return "pgsql";
|
|
2066
|
+
}
|
|
2067
|
+
if (normalized === "mysql" || normalized === "mariadb") {
|
|
2068
|
+
return "mysql";
|
|
2069
|
+
}
|
|
2070
|
+
if (normalized === "sqlite") {
|
|
2071
|
+
return "sqlite";
|
|
2072
|
+
}
|
|
2073
|
+
throw new Error(`Unsupported DB_CONNECTION: ${connection}`);
|
|
2074
|
+
}
|
|
2075
|
+
function resolveDriverFromUrl(url) {
|
|
2076
|
+
const normalized = url.trim().toLowerCase();
|
|
2077
|
+
if (normalized.startsWith("postgres://") || normalized.startsWith("postgresql://") || normalized.startsWith("postgres:")) {
|
|
2078
|
+
return "pgsql";
|
|
2079
|
+
}
|
|
2080
|
+
if (normalized.startsWith("mysql://") || normalized.startsWith("mysql:")) {
|
|
2081
|
+
return "mysql";
|
|
2082
|
+
}
|
|
2083
|
+
if (normalized.startsWith("sqlite:")) {
|
|
2084
|
+
return "sqlite";
|
|
2085
|
+
}
|
|
2086
|
+
return null;
|
|
2087
|
+
}
|
|
2088
|
+
function resolveDatabaseDriver(options = {}) {
|
|
2089
|
+
const connection = options.connection ?? process.env.DB_CONNECTION;
|
|
2090
|
+
if (connection) {
|
|
2091
|
+
return normalizeConnectionName(connection);
|
|
2092
|
+
}
|
|
2093
|
+
const url = options.url ?? process.env.DATABASE_URL ?? "";
|
|
2094
|
+
const fromUrl = resolveDriverFromUrl(url);
|
|
2095
|
+
if (fromUrl) {
|
|
2096
|
+
return fromUrl;
|
|
2097
|
+
}
|
|
2098
|
+
return "pgsql";
|
|
2099
|
+
}
|
|
2100
|
+
// ../../src/core/database/schema/errors.ts
|
|
2101
|
+
class UnsupportedSchemaFeatureError extends Error {
|
|
2102
|
+
constructor(feature, driver) {
|
|
2103
|
+
super(`${feature} is not supported for the ${driver} driver`);
|
|
2104
|
+
this.name = "UnsupportedSchemaFeatureError";
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
// ../../src/core/database/schema/grammars/grammar.ts
|
|
2108
|
+
function compileColumnType(driver, column) {
|
|
2109
|
+
switch (column.kind) {
|
|
2110
|
+
case "id":
|
|
2111
|
+
return compileIdType(driver);
|
|
2112
|
+
case "string":
|
|
2113
|
+
return compileStringType(driver, column.length);
|
|
2114
|
+
case "text":
|
|
2115
|
+
return compileTextType(driver);
|
|
2116
|
+
case "boolean":
|
|
2117
|
+
return compileBooleanType(driver);
|
|
2118
|
+
case "integer":
|
|
2119
|
+
case "foreignId":
|
|
2120
|
+
return compileIntegerType(driver);
|
|
2121
|
+
case "bigInteger":
|
|
2122
|
+
return compileBigIntegerType(driver);
|
|
2123
|
+
case "timestamp":
|
|
2124
|
+
return compileTimestampType(driver);
|
|
2125
|
+
case "json":
|
|
2126
|
+
return compileJsonType(driver);
|
|
2127
|
+
case "jsonb":
|
|
2128
|
+
return compileJsonbType(driver);
|
|
2129
|
+
default:
|
|
2130
|
+
throw new Error(`Unsupported column kind: ${column.kind}`);
|
|
2131
|
+
}
|
|
2132
|
+
}
|
|
2133
|
+
function compileIdType(driver) {
|
|
2134
|
+
switch (driver) {
|
|
2135
|
+
case "pgsql":
|
|
2136
|
+
return "SERIAL";
|
|
2137
|
+
case "mysql":
|
|
2138
|
+
return "BIGINT UNSIGNED";
|
|
2139
|
+
case "sqlite":
|
|
2140
|
+
return "INTEGER";
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
function compileStringType(driver, length) {
|
|
2144
|
+
switch (driver) {
|
|
2145
|
+
case "pgsql":
|
|
2146
|
+
return "TEXT";
|
|
2147
|
+
case "mysql":
|
|
2148
|
+
return length ? `VARCHAR(${length})` : "VARCHAR(255)";
|
|
2149
|
+
case "sqlite":
|
|
2150
|
+
return "TEXT";
|
|
2151
|
+
}
|
|
2152
|
+
}
|
|
2153
|
+
function compileTextType(driver) {
|
|
2154
|
+
switch (driver) {
|
|
2155
|
+
case "pgsql":
|
|
2156
|
+
case "sqlite":
|
|
2157
|
+
return "TEXT";
|
|
2158
|
+
case "mysql":
|
|
2159
|
+
return "TEXT";
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
function compileBooleanType(driver) {
|
|
2163
|
+
switch (driver) {
|
|
2164
|
+
case "pgsql":
|
|
2165
|
+
return "BOOLEAN";
|
|
2166
|
+
case "mysql":
|
|
2167
|
+
return "BOOLEAN";
|
|
2168
|
+
case "sqlite":
|
|
2169
|
+
return "INTEGER";
|
|
2170
|
+
}
|
|
2171
|
+
}
|
|
2172
|
+
function compileIntegerType(driver) {
|
|
2173
|
+
switch (driver) {
|
|
2174
|
+
case "pgsql":
|
|
2175
|
+
return "INTEGER";
|
|
2176
|
+
case "mysql":
|
|
2177
|
+
return "INT";
|
|
2178
|
+
case "sqlite":
|
|
2179
|
+
return "INTEGER";
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
2182
|
+
function compileBigIntegerType(driver) {
|
|
2183
|
+
switch (driver) {
|
|
2184
|
+
case "pgsql":
|
|
2185
|
+
return "BIGINT";
|
|
2186
|
+
case "mysql":
|
|
2187
|
+
return "BIGINT";
|
|
2188
|
+
case "sqlite":
|
|
2189
|
+
return "INTEGER";
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
function compileTimestampType(driver) {
|
|
2193
|
+
switch (driver) {
|
|
2194
|
+
case "pgsql":
|
|
2195
|
+
return "TIMESTAMPTZ";
|
|
2196
|
+
case "mysql":
|
|
2197
|
+
return "TIMESTAMP";
|
|
2198
|
+
case "sqlite":
|
|
2199
|
+
return "TEXT";
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
function compileJsonType(driver) {
|
|
2203
|
+
switch (driver) {
|
|
2204
|
+
case "pgsql":
|
|
2205
|
+
return "JSONB";
|
|
2206
|
+
case "mysql":
|
|
2207
|
+
return "JSON";
|
|
2208
|
+
case "sqlite":
|
|
2209
|
+
return "TEXT";
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
2212
|
+
function compileJsonbType(driver) {
|
|
2213
|
+
switch (driver) {
|
|
2214
|
+
case "pgsql":
|
|
2215
|
+
return "JSONB";
|
|
2216
|
+
case "mysql":
|
|
2217
|
+
return "JSON";
|
|
2218
|
+
case "sqlite":
|
|
2219
|
+
return "TEXT";
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
|
|
2223
|
+
// ../../src/core/database/schema/grammars/compileStatements.ts
|
|
2224
|
+
function compileCreateTable(driver, blueprint) {
|
|
2225
|
+
const table = quoteIdentifier(blueprint.table);
|
|
2226
|
+
const parts = blueprint.columns.map((column) => compileColumn(driver, column, "create"));
|
|
2227
|
+
for (const index of blueprint.indexes) {
|
|
2228
|
+
if (index.kind === "unique" && index.columns.length > 1) {
|
|
2229
|
+
const columns = index.columns.map((column) => quoteIdentifier(column)).join(", ");
|
|
2230
|
+
parts.push(`UNIQUE (${columns})`);
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
2233
|
+
const statements = [`CREATE TABLE IF NOT EXISTS ${table} (
|
|
2234
|
+
${parts.join(`,
|
|
2235
|
+
`)}
|
|
2236
|
+
)`];
|
|
2237
|
+
for (const index of blueprint.indexes) {
|
|
2238
|
+
if (index.kind === "unique" && index.columns.length === 1) {
|
|
2239
|
+
continue;
|
|
2240
|
+
}
|
|
2241
|
+
if (index.kind === "index") {
|
|
2242
|
+
statements.push(compileIndex(driver, blueprint.table, index));
|
|
2243
|
+
} else if (index.kind === "partial" || index.kind === "uniquePartial" || index.kind === "gin" || index.kind === "fullText") {
|
|
2244
|
+
statements.push(...compileSpecialIndex(driver, blueprint.table, index));
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
2247
|
+
return statements;
|
|
2248
|
+
}
|
|
2249
|
+
function compileAlterTable(driver, blueprint) {
|
|
2250
|
+
const statements = [];
|
|
2251
|
+
const table = quoteIdentifier(blueprint.table);
|
|
2252
|
+
for (const column of blueprint.columns) {
|
|
2253
|
+
const addPrefix = driver === "pgsql" ? "ADD COLUMN IF NOT EXISTS" : "ADD COLUMN";
|
|
2254
|
+
statements.push(`ALTER TABLE ${table}
|
|
2255
|
+
${addPrefix} ${compileColumn(driver, column, "alter")}`);
|
|
2256
|
+
}
|
|
2257
|
+
for (const columnName of blueprint.droppedColumns) {
|
|
2258
|
+
const dropPrefix = driver === "pgsql" ? "DROP COLUMN IF EXISTS" : "DROP COLUMN";
|
|
2259
|
+
statements.push(`ALTER TABLE ${table} ${dropPrefix} ${quoteIdentifier(columnName)}`);
|
|
2260
|
+
}
|
|
2261
|
+
for (const indexName of blueprint.droppedIndexes) {
|
|
2262
|
+
statements.push(`DROP INDEX IF EXISTS ${quoteIdentifier(indexName)}`);
|
|
2263
|
+
}
|
|
2264
|
+
for (const index of blueprint.indexes) {
|
|
2265
|
+
if (index.kind === "index" || index.kind === "unique") {
|
|
2266
|
+
statements.push(compileIndex(driver, blueprint.table, index));
|
|
2267
|
+
} else {
|
|
2268
|
+
statements.push(...compileSpecialIndex(driver, blueprint.table, index));
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
2271
|
+
return statements;
|
|
2272
|
+
}
|
|
2273
|
+
function compileDropTable(driver, tableName) {
|
|
2274
|
+
const cascade = driver === "pgsql" ? " CASCADE" : "";
|
|
2275
|
+
return [`DROP TABLE IF EXISTS ${quoteIdentifier(tableName)}${cascade}`];
|
|
2276
|
+
}
|
|
2277
|
+
function compileColumn(driver, column, mode) {
|
|
2278
|
+
const parts = [quoteIdentifier(column.name), compileColumnType(driver, column)];
|
|
2279
|
+
if (column.autoIncrement && driver === "mysql") {
|
|
2280
|
+
parts[1] = `${parts[1]} AUTO_INCREMENT`;
|
|
2281
|
+
}
|
|
2282
|
+
if (column.isPrimary && mode === "create") {
|
|
2283
|
+
if (driver === "sqlite") {
|
|
2284
|
+
parts.push("PRIMARY KEY AUTOINCREMENT");
|
|
2285
|
+
} else {
|
|
2286
|
+
parts.push("PRIMARY KEY");
|
|
2287
|
+
}
|
|
2288
|
+
} else if (!column.isNullable) {
|
|
2289
|
+
parts.push("NOT NULL");
|
|
2290
|
+
} else if (column.isNullable) {
|
|
2291
|
+
parts.push("NULL");
|
|
2292
|
+
}
|
|
2293
|
+
if (column.defaultValue !== undefined) {
|
|
2294
|
+
parts.push(`DEFAULT ${column.defaultValue}`);
|
|
2295
|
+
}
|
|
2296
|
+
if (column.isUnique) {
|
|
2297
|
+
parts.push("UNIQUE");
|
|
2298
|
+
}
|
|
2299
|
+
if (column.checkExpression) {
|
|
2300
|
+
parts.push(`CHECK (${column.checkExpression})`);
|
|
2301
|
+
}
|
|
2302
|
+
if (column.foreignKey) {
|
|
2303
|
+
const { referencesTable, referencesColumn, onDelete } = column.foreignKey;
|
|
2304
|
+
const reference = `${quoteIdentifier(referencesTable)}(${quoteIdentifier(referencesColumn)})`;
|
|
2305
|
+
let clause = `REFERENCES ${reference}`;
|
|
2306
|
+
if (onDelete === "cascade") {
|
|
2307
|
+
clause += " ON DELETE CASCADE";
|
|
2308
|
+
} else if (onDelete === "set null") {
|
|
2309
|
+
clause += " ON DELETE SET NULL";
|
|
2310
|
+
}
|
|
2311
|
+
parts.push(clause);
|
|
2312
|
+
}
|
|
2313
|
+
return parts.join(" ");
|
|
2314
|
+
}
|
|
2315
|
+
function compileIndex(_driver, tableName, index) {
|
|
2316
|
+
const indexName = index.name ?? defaultIndexName(tableName, index.columns, index.kind === "unique" ? "unique" : "index");
|
|
2317
|
+
const columns = index.columns.map((column) => {
|
|
2318
|
+
const quoted = quoteIdentifier(column);
|
|
2319
|
+
if (index.order === "desc") {
|
|
2320
|
+
return `${quoted} DESC`;
|
|
2321
|
+
}
|
|
2322
|
+
return quoted;
|
|
2323
|
+
}).join(", ");
|
|
2324
|
+
const unique = index.kind === "unique" ? "UNIQUE " : "";
|
|
2325
|
+
return `CREATE ${unique}INDEX IF NOT EXISTS ${quoteIdentifier(indexName)} ON ${quoteIdentifier(tableName)}(${columns})`;
|
|
2326
|
+
}
|
|
2327
|
+
function compileSpecialIndex(driver, tableName, index) {
|
|
2328
|
+
const indexName = index.name ?? defaultIndexName(tableName, index.columns, index.kind);
|
|
2329
|
+
const columns = index.columns.map((column) => quoteIdentifier(column)).join(", ");
|
|
2330
|
+
switch (index.kind) {
|
|
2331
|
+
case "partial":
|
|
2332
|
+
case "uniquePartial": {
|
|
2333
|
+
if (driver !== "pgsql") {
|
|
2334
|
+
throw new UnsupportedSchemaFeatureError("partialIndex()", driver);
|
|
2335
|
+
}
|
|
2336
|
+
const unique = index.kind === "uniquePartial" ? "UNIQUE " : "";
|
|
2337
|
+
return [
|
|
2338
|
+
`CREATE ${unique}INDEX IF NOT EXISTS ${quoteIdentifier(indexName)} ON ${quoteIdentifier(tableName)}(${columns}) WHERE ${index.where}`
|
|
2339
|
+
];
|
|
2340
|
+
}
|
|
2341
|
+
case "gin": {
|
|
2342
|
+
if (driver !== "pgsql") {
|
|
2343
|
+
throw new UnsupportedSchemaFeatureError("ginIndex()", driver);
|
|
2344
|
+
}
|
|
2345
|
+
return [
|
|
2346
|
+
`CREATE INDEX IF NOT EXISTS ${quoteIdentifier(indexName)} ON ${quoteIdentifier(tableName)} USING GIN(${columns})`
|
|
2347
|
+
];
|
|
2348
|
+
}
|
|
2349
|
+
case "fullText": {
|
|
2350
|
+
if (driver === "mysql") {
|
|
2351
|
+
return [
|
|
2352
|
+
`CREATE FULLTEXT INDEX ${quoteIdentifier(indexName)} ON ${quoteIdentifier(tableName)}(${columns})`
|
|
2353
|
+
];
|
|
2354
|
+
}
|
|
2355
|
+
if (driver === "pgsql") {
|
|
2356
|
+
throw new UnsupportedSchemaFeatureError("fullText(); use ginIndex() with a tsvector column on PostgreSQL", driver);
|
|
2357
|
+
}
|
|
2358
|
+
throw new UnsupportedSchemaFeatureError("fullText()", driver);
|
|
2359
|
+
}
|
|
2360
|
+
default:
|
|
2361
|
+
return [];
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
2364
|
+
function defaultIndexName(tableName, columns, kind) {
|
|
2365
|
+
return `idx_${tableName}_${columns.join("_")}_${kind}`;
|
|
2366
|
+
}
|
|
2367
|
+
function compileBlueprint(driver, blueprint) {
|
|
2368
|
+
switch (blueprint.action) {
|
|
2369
|
+
case "create":
|
|
2370
|
+
return compileCreateTable(driver, blueprint);
|
|
2371
|
+
case "alter":
|
|
2372
|
+
return compileAlterTable(driver, blueprint);
|
|
2373
|
+
case "drop":
|
|
2374
|
+
return compileDropTable(driver, blueprint.table);
|
|
2375
|
+
default:
|
|
2376
|
+
throw new Error(`Unsupported blueprint action: ${blueprint.action}`);
|
|
2377
|
+
}
|
|
2378
|
+
}
|
|
2379
|
+
// ../../src/core/database/schema/grammars/createGrammar.ts
|
|
2380
|
+
function createGrammar(driver) {
|
|
2381
|
+
return {
|
|
2382
|
+
driver,
|
|
2383
|
+
compile(blueprint) {
|
|
2384
|
+
return compileBlueprint(driver, blueprint);
|
|
2385
|
+
}
|
|
2386
|
+
};
|
|
2387
|
+
}
|
|
2388
|
+
|
|
2389
|
+
// ../../src/core/database/schema/grammars/mysqlGrammar.ts
|
|
2390
|
+
var MySqlGrammar = createGrammar("mysql");
|
|
2391
|
+
|
|
2392
|
+
// ../../src/core/database/schema/grammars/postgresGrammar.ts
|
|
2393
|
+
var PostgresGrammar = createGrammar("pgsql");
|
|
2394
|
+
|
|
2395
|
+
// ../../src/core/database/schema/grammars/sqliteGrammar.ts
|
|
2396
|
+
var SqliteGrammar = createGrammar("sqlite");
|
|
2397
|
+
|
|
2398
|
+
// ../../src/core/database/schema/grammars/index.ts
|
|
2399
|
+
function grammarForDriver(driver) {
|
|
2400
|
+
switch (driver) {
|
|
2401
|
+
case "pgsql":
|
|
2402
|
+
return PostgresGrammar;
|
|
2403
|
+
case "mysql":
|
|
2404
|
+
return MySqlGrammar;
|
|
2405
|
+
case "sqlite":
|
|
2406
|
+
return SqliteGrammar;
|
|
2407
|
+
default:
|
|
2408
|
+
throw new Error(`Unsupported database driver: ${driver}`);
|
|
2409
|
+
}
|
|
2410
|
+
}
|
|
2411
|
+
// ../../src/core/database/schema/schema.ts
|
|
2412
|
+
class SchemaBuilder {
|
|
2413
|
+
#driver;
|
|
2414
|
+
#statements = [];
|
|
2415
|
+
constructor(driver) {
|
|
2416
|
+
this.#driver = driver;
|
|
2417
|
+
}
|
|
2418
|
+
create(table, callback) {
|
|
2419
|
+
const blueprint = new Blueprint(table, "create");
|
|
2420
|
+
callback(blueprint);
|
|
2421
|
+
this.#statements.push(...grammarForDriver(this.#driver).compile(blueprint));
|
|
2422
|
+
return this;
|
|
2423
|
+
}
|
|
2424
|
+
table(table, callback) {
|
|
2425
|
+
const blueprint = new Blueprint(table, "alter");
|
|
2426
|
+
callback(blueprint);
|
|
2427
|
+
this.#statements.push(...grammarForDriver(this.#driver).compile(blueprint));
|
|
2428
|
+
return this;
|
|
2429
|
+
}
|
|
2430
|
+
drop(table) {
|
|
2431
|
+
const blueprint = new Blueprint(table, "drop");
|
|
2432
|
+
this.#statements.push(...grammarForDriver(this.#driver).compile(blueprint));
|
|
2433
|
+
return this;
|
|
2434
|
+
}
|
|
2435
|
+
toSql() {
|
|
2436
|
+
return [...this.#statements];
|
|
2437
|
+
}
|
|
2438
|
+
async execute(db2) {
|
|
2439
|
+
for (const statement of this.#statements) {
|
|
2440
|
+
await db2.unsafe(statement);
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2443
|
+
}
|
|
2444
|
+
|
|
2445
|
+
class Schema {
|
|
2446
|
+
static builder(driver) {
|
|
2447
|
+
return new SchemaBuilder(driver ?? resolveDatabaseDriver());
|
|
2448
|
+
}
|
|
2449
|
+
static async run(db2, driver, callback) {
|
|
2450
|
+
const schema = Schema.builder(driver);
|
|
2451
|
+
await callback(schema);
|
|
2452
|
+
await schema.execute(db2);
|
|
2453
|
+
}
|
|
2454
|
+
}
|
|
2455
|
+
function createSchemaBuilder(db2, driver) {
|
|
2456
|
+
const builder = Schema.builder(driver);
|
|
2457
|
+
return Object.assign(builder, {
|
|
2458
|
+
async commit() {
|
|
2459
|
+
await builder.execute(db2);
|
|
2460
|
+
}
|
|
2461
|
+
});
|
|
2462
|
+
}
|
|
2463
|
+
// ../../src/core/database/seeders/runner.ts
|
|
2464
|
+
import { readdir as readdir2 } from "fs/promises";
|
|
2465
|
+
import { join as join2 } from "path";
|
|
2466
|
+
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
2467
|
+
async function loadSeedersFromDirectory(directory) {
|
|
2468
|
+
const entries = await readdir2(directory);
|
|
2469
|
+
const seederFiles = entries.filter((entry) => (entry.endsWith(".ts") || entry.endsWith(".js")) && entry !== "types.ts" && entry !== "index.ts" && entry !== "runner.ts").sort();
|
|
2470
|
+
const loadedSeeders = await Promise.all(seederFiles.map(async (fileName) => {
|
|
2471
|
+
const moduleUrl = pathToFileURL2(join2(directory, fileName)).href;
|
|
2472
|
+
const module = await import(moduleUrl);
|
|
2473
|
+
return module.default;
|
|
2474
|
+
}));
|
|
2475
|
+
return loadedSeeders.filter((seeder) => seeder?.name !== undefined);
|
|
2476
|
+
}
|
|
2477
|
+
async function runSeedersFromDirectory(directory, db2, options) {
|
|
2478
|
+
const seeders = await loadSeedersFromDirectory(directory);
|
|
2479
|
+
if (seeders.length === 0) {
|
|
2480
|
+
return 0;
|
|
2481
|
+
}
|
|
2482
|
+
for (const seeder of seeders) {
|
|
2483
|
+
options?.onSeeder?.(seeder.name);
|
|
2484
|
+
await seeder.run(db2);
|
|
2485
|
+
}
|
|
2486
|
+
return seeders.length;
|
|
2487
|
+
}
|
|
2488
|
+
// ../../src/core/database/table.ts
|
|
2489
|
+
function defineTable(definition) {
|
|
2490
|
+
return definition;
|
|
2491
|
+
}
|
|
2492
|
+
// ../../src/core/database/connection.ts
|
|
2493
|
+
function createDatabaseConnection2(source) {
|
|
2494
|
+
return {
|
|
2495
|
+
async unsafe(query, params = []) {
|
|
2496
|
+
return await source.unsafe(query, params);
|
|
2497
|
+
}
|
|
2498
|
+
};
|
|
2499
|
+
}
|
|
2500
|
+
|
|
2501
|
+
// ../../src/core/database/transaction.ts
|
|
2502
|
+
async function runInTransaction(operation) {
|
|
2503
|
+
return await connection_default.begin(async (transaction) => {
|
|
2504
|
+
return await operation(createDatabaseConnection2(transaction));
|
|
2505
|
+
});
|
|
2506
|
+
}
|
|
2507
|
+
// ../../src/core/logging/logger.ts
|
|
2508
|
+
class Logger {
|
|
2509
|
+
channel;
|
|
2510
|
+
constructor(channel = "app") {
|
|
2511
|
+
this.channel = channel;
|
|
2512
|
+
}
|
|
2513
|
+
write(level, message, context = {}) {
|
|
2514
|
+
const entry = {
|
|
2515
|
+
level,
|
|
2516
|
+
channel: this.channel,
|
|
2517
|
+
message,
|
|
2518
|
+
timestamp: new Date().toISOString(),
|
|
2519
|
+
...context
|
|
2520
|
+
};
|
|
2521
|
+
const line = JSON.stringify(entry);
|
|
2522
|
+
if (level === "error") {
|
|
2523
|
+
console.error(line);
|
|
2524
|
+
return;
|
|
2525
|
+
}
|
|
2526
|
+
console.log(line);
|
|
2527
|
+
}
|
|
2528
|
+
debug(message, context) {
|
|
2529
|
+
this.write("debug", message, context);
|
|
2530
|
+
}
|
|
2531
|
+
info(message, context) {
|
|
2532
|
+
this.write("info", message, context);
|
|
2533
|
+
}
|
|
2534
|
+
warn(message, context) {
|
|
2535
|
+
this.write("warn", message, context);
|
|
2536
|
+
}
|
|
2537
|
+
error(message, context) {
|
|
2538
|
+
this.write("error", message, context);
|
|
2539
|
+
}
|
|
2540
|
+
}
|
|
2541
|
+
var appLogger = new Logger("app");
|
|
1275
2542
|
|
|
1276
2543
|
// ../../src/bootstrap/config.ts
|
|
1277
2544
|
var CORE_QUEUE_TOKEN = "core.queue";
|
|
1278
2545
|
var CORE_POLICY_GATE_TOKEN = "core.policyGate";
|
|
1279
2546
|
var CORE_AUTH_TOKEN = "core.auth";
|
|
2547
|
+
var CORE_TOKEN_SERVICE_TOKEN = "core.tokenService";
|
|
2548
|
+
var DEFAULT_QUEUE_DRIVER = "sync";
|
|
1280
2549
|
|
|
1281
2550
|
// ../../src/bootstrap/applicationRegistry.ts
|
|
1282
2551
|
var activeContext;
|
|
@@ -1304,6 +2573,9 @@ function resolveApplicationConfig() {
|
|
|
1304
2573
|
function resolveApplicationLogger() {
|
|
1305
2574
|
return appLogger;
|
|
1306
2575
|
}
|
|
2576
|
+
function resolveApplicationDependencies() {
|
|
2577
|
+
return requireActiveApplicationContext().dependencies;
|
|
2578
|
+
}
|
|
1307
2579
|
|
|
1308
2580
|
// ../../src/core/mail/mailer.ts
|
|
1309
2581
|
function resolveSmtpConfig() {
|
|
@@ -1488,7 +2760,7 @@ function mailer() {
|
|
|
1488
2760
|
// ../../src/core/storage/storage.ts
|
|
1489
2761
|
var {S3Client } = globalThis.Bun;
|
|
1490
2762
|
import { mkdir, readFile, unlink, writeFile } from "fs/promises";
|
|
1491
|
-
import { dirname, join as
|
|
2763
|
+
import { dirname, join as join3 } from "path";
|
|
1492
2764
|
|
|
1493
2765
|
class LocalStorageDriver {
|
|
1494
2766
|
rootDirectory;
|
|
@@ -1496,7 +2768,7 @@ class LocalStorageDriver {
|
|
|
1496
2768
|
this.rootDirectory = rootDirectory;
|
|
1497
2769
|
}
|
|
1498
2770
|
resolvePath(path) {
|
|
1499
|
-
return
|
|
2771
|
+
return join3(this.rootDirectory, path.replace(/^\/+/, ""));
|
|
1500
2772
|
}
|
|
1501
2773
|
async put(path, contents) {
|
|
1502
2774
|
const absolutePath = this.resolvePath(path);
|
|
@@ -1627,105 +2899,738 @@ function mail() {
|
|
|
1627
2899
|
function storageFacade() {
|
|
1628
2900
|
return storage();
|
|
1629
2901
|
}
|
|
1630
|
-
// ../../src/
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
2902
|
+
// ../../src/core/http/bodySizeLimitMiddleware.ts
|
|
2903
|
+
var DEFAULT_MAX_BODY_BYTES = 1048576;
|
|
2904
|
+
function resolveMaxBodyBytes() {
|
|
2905
|
+
const raw = process.env.MAX_REQUEST_BODY_BYTES?.trim();
|
|
2906
|
+
if (!raw) {
|
|
2907
|
+
return DEFAULT_MAX_BODY_BYTES;
|
|
1635
2908
|
}
|
|
1636
|
-
|
|
1637
|
-
|
|
2909
|
+
const parsed = Number.parseInt(raw, 10);
|
|
2910
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
2911
|
+
return DEFAULT_MAX_BODY_BYTES;
|
|
1638
2912
|
}
|
|
1639
|
-
return
|
|
2913
|
+
return parsed;
|
|
1640
2914
|
}
|
|
1641
|
-
function
|
|
1642
|
-
return
|
|
2915
|
+
function createBodySizeLimitMiddleware(maxBytes = resolveMaxBodyBytes()) {
|
|
2916
|
+
return async (request, next) => {
|
|
2917
|
+
const contentLength = request.headers.get("content-length");
|
|
2918
|
+
if (contentLength) {
|
|
2919
|
+
const bytes = Number.parseInt(contentLength, 10);
|
|
2920
|
+
if (Number.isInteger(bytes) && bytes > maxBytes) {
|
|
2921
|
+
const error = new PayloadTooLargeError(`Request body exceeds the ${maxBytes} byte limit.`);
|
|
2922
|
+
return Response.json({ error: error.message }, { status: error.status });
|
|
2923
|
+
}
|
|
2924
|
+
}
|
|
2925
|
+
return await next();
|
|
2926
|
+
};
|
|
1643
2927
|
}
|
|
2928
|
+
// ../../src/core/http/csrfToken.ts
|
|
2929
|
+
import { createHmac, randomBytes, timingSafeEqual } from "crypto";
|
|
1644
2930
|
|
|
1645
|
-
// ../../src/core/
|
|
1646
|
-
import {
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
function htmlResponse(html, init = {}) {
|
|
1651
|
-
return new Response(html, {
|
|
1652
|
-
status: init.status ?? 200,
|
|
1653
|
-
statusText: init.statusText,
|
|
1654
|
-
headers: {
|
|
1655
|
-
"Content-Type": "text/html; charset=utf-8"
|
|
1656
|
-
}
|
|
1657
|
-
});
|
|
2931
|
+
// ../../src/core/http/requestMetaContext.ts
|
|
2932
|
+
import { AsyncLocalStorage as AsyncLocalStorage3 } from "async_hooks";
|
|
2933
|
+
var requestMetaContext = new AsyncLocalStorage3;
|
|
2934
|
+
function runWithRequestMeta(meta, callback) {
|
|
2935
|
+
return requestMetaContext.run(meta, callback);
|
|
1658
2936
|
}
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
fullTextSearch: (process.env.FEATURE_SEARCH ?? "true") !== "false",
|
|
1664
|
-
auditLog: (process.env.FEATURE_AUDIT_LOG ?? "true") !== "false",
|
|
1665
|
-
oauthLogin: (process.env.FEATURE_OAUTH ?? "true") !== "false",
|
|
1666
|
-
samlLogin: (process.env.FEATURE_SAML ?? "false") === "true",
|
|
1667
|
-
scim: (process.env.FEATURE_SCIM ?? "true") !== "false",
|
|
1668
|
-
billing: (process.env.FEATURE_BILLING ?? "true") !== "false",
|
|
1669
|
-
siemExport: (process.env.FEATURE_SIEM_EXPORT ?? "true") !== "false",
|
|
1670
|
-
publicReads: (process.env.FEATURE_PUBLIC_READS ?? "true") !== "false",
|
|
1671
|
-
emailVerification: (process.env.FEATURE_EMAIL_VERIFICATION ?? "false") === "true",
|
|
1672
|
-
mfa: (process.env.FEATURE_MFA ?? "false") === "true"
|
|
2937
|
+
function currentRequestMeta() {
|
|
2938
|
+
return requestMetaContext.getStore() ?? {
|
|
2939
|
+
ipAddress: null,
|
|
2940
|
+
userAgent: null
|
|
1673
2941
|
};
|
|
1674
2942
|
}
|
|
1675
|
-
var featureFlags = readFeatureFlags();
|
|
1676
|
-
function isFeatureEnabled(feature) {
|
|
1677
|
-
return readFeatureFlags()[feature];
|
|
1678
|
-
}
|
|
1679
|
-
// ../../src/modules/user/apiTokenTable.ts
|
|
1680
|
-
var apiTokenTable = defineTable({
|
|
1681
|
-
name: "api_token",
|
|
1682
|
-
primaryKey: "id",
|
|
1683
|
-
columns: [
|
|
1684
|
-
"id",
|
|
1685
|
-
"user_id",
|
|
1686
|
-
"name",
|
|
1687
|
-
"token_hash",
|
|
1688
|
-
"abilities",
|
|
1689
|
-
"last_used_at",
|
|
1690
|
-
"expires_at",
|
|
1691
|
-
"created_at"
|
|
1692
|
-
],
|
|
1693
|
-
defaultOrderBy: { column: "id", direction: "ASC" }
|
|
1694
|
-
});
|
|
1695
2943
|
|
|
1696
|
-
// ../../src/core/
|
|
1697
|
-
|
|
1698
|
-
|
|
2944
|
+
// ../../src/core/http/csrfToken.ts
|
|
2945
|
+
var CSRF_COOKIE = "workhub_csrf";
|
|
2946
|
+
var CSRF_TTL_MS = 60 * 60 * 1000;
|
|
2947
|
+
function resolveCsrfSecret() {
|
|
2948
|
+
return process.env.SESSION_SECRET?.trim() || process.env.OAUTH_STATE_SECRET?.trim() || process.env.ADMIN_API_TOKEN?.trim() || "workhub-dev-csrf-secret";
|
|
1699
2949
|
}
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
if (!raw) {
|
|
2950
|
+
function signCsrfToken(token, issuedAt) {
|
|
2951
|
+
const payload = `${token}.${issuedAt}`;
|
|
2952
|
+
const signature = createHmac("sha256", resolveCsrfSecret()).update(payload).digest("hex");
|
|
2953
|
+
return `${payload}.${signature}`;
|
|
2954
|
+
}
|
|
2955
|
+
function readCsrfCookie(request) {
|
|
2956
|
+
const cookieHeader = request.headers.get("cookie");
|
|
2957
|
+
if (!cookieHeader) {
|
|
1709
2958
|
return null;
|
|
1710
2959
|
}
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
return decoded;
|
|
2960
|
+
for (const part of cookieHeader.split(";")) {
|
|
2961
|
+
const [name, ...rest] = part.trim().split("=");
|
|
2962
|
+
if (name === CSRF_COOKIE) {
|
|
2963
|
+
return decodeURIComponent(rest.join("="));
|
|
2964
|
+
}
|
|
1717
2965
|
}
|
|
1718
|
-
|
|
2966
|
+
return null;
|
|
1719
2967
|
}
|
|
1720
|
-
function
|
|
1721
|
-
const
|
|
1722
|
-
if (
|
|
1723
|
-
return
|
|
2968
|
+
function parseSignedCsrfValue(cookieValue) {
|
|
2969
|
+
const parts = cookieValue.split(".");
|
|
2970
|
+
if (parts.length !== 3) {
|
|
2971
|
+
return null;
|
|
1724
2972
|
}
|
|
1725
|
-
|
|
1726
|
-
|
|
2973
|
+
const [token, issuedAtRaw, cookieSignature] = parts;
|
|
2974
|
+
if (!token || !issuedAtRaw || !cookieSignature) {
|
|
2975
|
+
return null;
|
|
1727
2976
|
}
|
|
1728
|
-
|
|
2977
|
+
const issuedAt = Number.parseInt(issuedAtRaw, 10);
|
|
2978
|
+
if (!Number.isFinite(issuedAt)) {
|
|
2979
|
+
return null;
|
|
2980
|
+
}
|
|
2981
|
+
if (Date.now() - issuedAt > CSRF_TTL_MS) {
|
|
2982
|
+
return null;
|
|
2983
|
+
}
|
|
2984
|
+
const expectedSignature = signCsrfToken(token, issuedAt).split(".").pop();
|
|
2985
|
+
if (!expectedSignature) {
|
|
2986
|
+
return null;
|
|
2987
|
+
}
|
|
2988
|
+
const expectedBuffer = Buffer.from(expectedSignature);
|
|
2989
|
+
const actualBuffer = Buffer.from(cookieSignature);
|
|
2990
|
+
if (expectedBuffer.length !== actualBuffer.length) {
|
|
2991
|
+
return null;
|
|
2992
|
+
}
|
|
2993
|
+
if (!timingSafeEqual(expectedBuffer, actualBuffer)) {
|
|
2994
|
+
return null;
|
|
2995
|
+
}
|
|
2996
|
+
return { token, issuedAt };
|
|
2997
|
+
}
|
|
2998
|
+
function createCsrfTokenCookie() {
|
|
2999
|
+
const token = randomBytes(24).toString("hex");
|
|
3000
|
+
const issuedAt = Date.now();
|
|
3001
|
+
const value = signCsrfToken(token, issuedAt);
|
|
3002
|
+
return {
|
|
3003
|
+
token,
|
|
3004
|
+
cookie: `${CSRF_COOKIE}=${encodeURIComponent(value)}; Path=/; SameSite=Lax; Max-Age=3600`
|
|
3005
|
+
};
|
|
3006
|
+
}
|
|
3007
|
+
function resolveCsrfToken(request) {
|
|
3008
|
+
const cookieValue = readCsrfCookie(request);
|
|
3009
|
+
if (cookieValue) {
|
|
3010
|
+
const parsed = parseSignedCsrfValue(cookieValue);
|
|
3011
|
+
if (parsed) {
|
|
3012
|
+
return { token: parsed.token };
|
|
3013
|
+
}
|
|
3014
|
+
}
|
|
3015
|
+
return createCsrfTokenCookie();
|
|
3016
|
+
}
|
|
3017
|
+
function readSubmittedCsrfToken(request) {
|
|
3018
|
+
const headerToken = request.headers.get("x-csrf-token")?.trim();
|
|
3019
|
+
if (headerToken) {
|
|
3020
|
+
return headerToken;
|
|
3021
|
+
}
|
|
3022
|
+
return null;
|
|
3023
|
+
}
|
|
3024
|
+
async function readSubmittedCsrfTokenFromBody(request) {
|
|
3025
|
+
const headerToken = readSubmittedCsrfToken(request);
|
|
3026
|
+
if (headerToken) {
|
|
3027
|
+
return headerToken;
|
|
3028
|
+
}
|
|
3029
|
+
const contentType = request.headers.get("content-type")?.toLowerCase() ?? "";
|
|
3030
|
+
if (contentType.includes("application/x-www-form-urlencoded") || contentType.includes("multipart/form-data")) {
|
|
3031
|
+
const formData = await request.clone().formData();
|
|
3032
|
+
const field = formData.get("_token");
|
|
3033
|
+
if (typeof field === "string" && field.trim().length > 0) {
|
|
3034
|
+
return field.trim();
|
|
3035
|
+
}
|
|
3036
|
+
}
|
|
3037
|
+
return null;
|
|
3038
|
+
}
|
|
3039
|
+
function verifyCsrfToken(request, submittedToken) {
|
|
3040
|
+
if (!submittedToken) {
|
|
3041
|
+
return false;
|
|
3042
|
+
}
|
|
3043
|
+
const cookieValue = readCsrfCookie(request);
|
|
3044
|
+
if (!cookieValue) {
|
|
3045
|
+
return false;
|
|
3046
|
+
}
|
|
3047
|
+
const parsed = parseSignedCsrfValue(cookieValue);
|
|
3048
|
+
if (!parsed) {
|
|
3049
|
+
return false;
|
|
3050
|
+
}
|
|
3051
|
+
const submittedBuffer = Buffer.from(submittedToken);
|
|
3052
|
+
const expectedBuffer = Buffer.from(parsed.token);
|
|
3053
|
+
if (submittedBuffer.length !== expectedBuffer.length) {
|
|
3054
|
+
return false;
|
|
3055
|
+
}
|
|
3056
|
+
return timingSafeEqual(submittedBuffer, expectedBuffer);
|
|
3057
|
+
}
|
|
3058
|
+
function resolveCsrfTokenForRequest(request) {
|
|
3059
|
+
const metaToken = currentRequestMeta().csrfToken;
|
|
3060
|
+
if (metaToken) {
|
|
3061
|
+
return metaToken;
|
|
3062
|
+
}
|
|
3063
|
+
return resolveCsrfToken(request).token;
|
|
3064
|
+
}
|
|
3065
|
+
|
|
3066
|
+
// ../../src/core/http/csrfMiddleware.ts
|
|
3067
|
+
var MUTATING_METHODS = new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
3068
|
+
function appendSetCookie(response, cookie) {
|
|
3069
|
+
const headers = new Headers(response.headers);
|
|
3070
|
+
headers.append("set-cookie", cookie);
|
|
3071
|
+
return new Response(response.body, {
|
|
3072
|
+
status: response.status,
|
|
3073
|
+
statusText: response.statusText,
|
|
3074
|
+
headers
|
|
3075
|
+
});
|
|
3076
|
+
}
|
|
3077
|
+
function createCsrfMiddleware() {
|
|
3078
|
+
return async (request, next) => {
|
|
3079
|
+
const method = request.method.toUpperCase();
|
|
3080
|
+
if (!MUTATING_METHODS.has(method)) {
|
|
3081
|
+
const csrf = resolveCsrfToken(request);
|
|
3082
|
+
const meta = currentRequestMeta();
|
|
3083
|
+
meta.csrfToken = csrf.token;
|
|
3084
|
+
const response = await next();
|
|
3085
|
+
if (!csrf.cookie) {
|
|
3086
|
+
return response;
|
|
3087
|
+
}
|
|
3088
|
+
return appendSetCookie(response, csrf.cookie);
|
|
3089
|
+
}
|
|
3090
|
+
const submitted = await readSubmittedCsrfTokenFromBody(request);
|
|
3091
|
+
if (!verifyCsrfToken(request, submitted)) {
|
|
3092
|
+
throw new ForbiddenError("Invalid or missing CSRF token.");
|
|
3093
|
+
}
|
|
3094
|
+
return await next();
|
|
3095
|
+
};
|
|
3096
|
+
}
|
|
3097
|
+
// ../../src/core/http/etag.ts
|
|
3098
|
+
import { createHash } from "crypto";
|
|
3099
|
+
function isEtagEnabled() {
|
|
3100
|
+
return (process.env.FEATURE_ETAG ?? "true") !== "false";
|
|
3101
|
+
}
|
|
3102
|
+
function formatWeakEtag(digest) {
|
|
3103
|
+
return `W/"${digest}"`;
|
|
3104
|
+
}
|
|
3105
|
+
function computeEtagFromJson(data) {
|
|
3106
|
+
const digest = createHash("sha256").update(JSON.stringify(data)).digest("hex").slice(0, 32);
|
|
3107
|
+
return formatWeakEtag(digest);
|
|
3108
|
+
}
|
|
3109
|
+
function etagFromResource(resource) {
|
|
3110
|
+
const version = resource.updated_at ?? resource.created_at ?? "";
|
|
3111
|
+
const versionText = version instanceof Date ? version.toISOString() : version ? String(version) : "0";
|
|
3112
|
+
const digest = createHash("sha256").update(`${String(resource.id ?? "0")}:${versionText}`).digest("hex").slice(0, 32);
|
|
3113
|
+
return formatWeakEtag(digest);
|
|
3114
|
+
}
|
|
3115
|
+
function normalizeEtag(value) {
|
|
3116
|
+
return value.trim();
|
|
3117
|
+
}
|
|
3118
|
+
function etagValuesMatch(left, right) {
|
|
3119
|
+
return normalizeEtag(left) === normalizeEtag(right);
|
|
3120
|
+
}
|
|
3121
|
+
function parseEtagList(header) {
|
|
3122
|
+
if (!header) {
|
|
3123
|
+
return [];
|
|
3124
|
+
}
|
|
3125
|
+
return header.split(",").map((value) => normalizeEtag(value)).filter(Boolean);
|
|
3126
|
+
}
|
|
3127
|
+
function ifNoneMatchSatisfied(request, etag) {
|
|
3128
|
+
const header = request.headers.get("if-none-match");
|
|
3129
|
+
if (!header) {
|
|
3130
|
+
return false;
|
|
3131
|
+
}
|
|
3132
|
+
if (header.trim() === "*") {
|
|
3133
|
+
return true;
|
|
3134
|
+
}
|
|
3135
|
+
return parseEtagList(header).some((candidate) => etagValuesMatch(candidate, etag));
|
|
3136
|
+
}
|
|
3137
|
+
function ifMatchSatisfied(request, etag) {
|
|
3138
|
+
const header = request.headers.get("if-match");
|
|
3139
|
+
if (!header) {
|
|
3140
|
+
return false;
|
|
3141
|
+
}
|
|
3142
|
+
if (header.trim() === "*") {
|
|
3143
|
+
return true;
|
|
3144
|
+
}
|
|
3145
|
+
return parseEtagList(header).some((candidate) => etagValuesMatch(candidate, etag));
|
|
3146
|
+
}
|
|
3147
|
+
function assertIfMatch(request, etag, options = {}) {
|
|
3148
|
+
const header = request.headers.get("if-match");
|
|
3149
|
+
if (!header) {
|
|
3150
|
+
if (options.required) {
|
|
3151
|
+
throw new PreconditionFailedError("If-Match header is required.");
|
|
3152
|
+
}
|
|
3153
|
+
return;
|
|
3154
|
+
}
|
|
3155
|
+
if (!ifMatchSatisfied(request, etag)) {
|
|
3156
|
+
throw new PreconditionFailedError("Resource ETag does not match If-Match.");
|
|
3157
|
+
}
|
|
3158
|
+
}
|
|
3159
|
+
function applyEtagHeaders(headers, etag) {
|
|
3160
|
+
const next = new Headers(headers);
|
|
3161
|
+
next.set("ETag", etag);
|
|
3162
|
+
next.set("Cache-Control", "private, must-revalidate");
|
|
3163
|
+
next.append("Vary", "Authorization");
|
|
3164
|
+
next.append("Vary", "X-Tenant-Id");
|
|
3165
|
+
return next;
|
|
3166
|
+
}
|
|
3167
|
+
function notModifiedResponse(etag) {
|
|
3168
|
+
return new Response(null, {
|
|
3169
|
+
status: 304,
|
|
3170
|
+
headers: applyEtagHeaders(new Headers, etag)
|
|
3171
|
+
});
|
|
3172
|
+
}
|
|
3173
|
+
function applyConditionalGet(request, response, etag) {
|
|
3174
|
+
if (!isEtagEnabled()) {
|
|
3175
|
+
return response;
|
|
3176
|
+
}
|
|
3177
|
+
if (ifNoneMatchSatisfied(request, etag)) {
|
|
3178
|
+
return notModifiedResponse(etag);
|
|
3179
|
+
}
|
|
3180
|
+
const headers = applyEtagHeaders(new Headers(response.headers), etag);
|
|
3181
|
+
return new Response(response.body, {
|
|
3182
|
+
status: response.status,
|
|
3183
|
+
statusText: response.statusText,
|
|
3184
|
+
headers
|
|
3185
|
+
});
|
|
3186
|
+
}
|
|
3187
|
+
// ../../src/core/tenant/tenantContext.ts
|
|
3188
|
+
import { AsyncLocalStorage as AsyncLocalStorage4 } from "async_hooks";
|
|
3189
|
+
var tenantContext = new AsyncLocalStorage4;
|
|
3190
|
+
function runWithTenant(tenant, callback) {
|
|
3191
|
+
return tenantContext.run(tenant, callback);
|
|
3192
|
+
}
|
|
3193
|
+
function currentTenant() {
|
|
3194
|
+
return tenantContext.getStore() ?? null;
|
|
3195
|
+
}
|
|
3196
|
+
function currentTenantId() {
|
|
3197
|
+
return currentTenant()?.id ?? 1;
|
|
3198
|
+
}
|
|
3199
|
+
function rateLimitMultiplierForPlan(plan) {
|
|
3200
|
+
switch (plan) {
|
|
3201
|
+
case "enterprise":
|
|
3202
|
+
return 4;
|
|
3203
|
+
case "pro":
|
|
3204
|
+
return 2;
|
|
3205
|
+
default:
|
|
3206
|
+
return 1;
|
|
3207
|
+
}
|
|
3208
|
+
}
|
|
3209
|
+
|
|
3210
|
+
// ../../src/core/http/validation.ts
|
|
3211
|
+
function buildRequestCacheKey(fallbackPath, request) {
|
|
3212
|
+
if (!request) {
|
|
3213
|
+
return fallbackPath;
|
|
3214
|
+
}
|
|
3215
|
+
const url = new URL(request.url);
|
|
3216
|
+
const user = currentAuthUser();
|
|
3217
|
+
const authScope = user ? `u:${user.id}` : "guest";
|
|
3218
|
+
const tenantScope = `t:${currentTenantId()}`;
|
|
3219
|
+
return `${authScope}|${tenantScope}|${url.pathname}${url.search}`;
|
|
3220
|
+
}
|
|
3221
|
+
function getQueryParams(request) {
|
|
3222
|
+
if (!request) {
|
|
3223
|
+
return new URLSearchParams;
|
|
3224
|
+
}
|
|
3225
|
+
return new URL(request.url).searchParams;
|
|
3226
|
+
}
|
|
3227
|
+
function parseOptionalPositiveIntQueryParam(params, name) {
|
|
3228
|
+
const value = params.get(name);
|
|
3229
|
+
if (value === null || value.trim() === "") {
|
|
3230
|
+
return;
|
|
3231
|
+
}
|
|
3232
|
+
const parsed = Number.parseInt(value, 10);
|
|
3233
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
3234
|
+
throw new BadRequestError(`Invalid query parameter "${name}". Expected a positive integer.`);
|
|
3235
|
+
}
|
|
3236
|
+
return parsed;
|
|
3237
|
+
}
|
|
3238
|
+
function parseOptionalBooleanQueryParam(params, name) {
|
|
3239
|
+
const value = params.get(name);
|
|
3240
|
+
if (value === null || value.trim() === "") {
|
|
3241
|
+
return;
|
|
3242
|
+
}
|
|
3243
|
+
switch (value.toLowerCase()) {
|
|
3244
|
+
case "true":
|
|
3245
|
+
case "1":
|
|
3246
|
+
return true;
|
|
3247
|
+
case "false":
|
|
3248
|
+
case "0":
|
|
3249
|
+
return false;
|
|
3250
|
+
default:
|
|
3251
|
+
throw new BadRequestError(`Invalid query parameter "${name}". Expected a boolean.`);
|
|
3252
|
+
}
|
|
3253
|
+
}
|
|
3254
|
+
function parseOptionalEnumQueryParam(params, name, allowedValues) {
|
|
3255
|
+
const value = params.get(name);
|
|
3256
|
+
if (value === null || value.trim() === "") {
|
|
3257
|
+
return;
|
|
3258
|
+
}
|
|
3259
|
+
if (!allowedValues.includes(value)) {
|
|
3260
|
+
throw new BadRequestError(`Invalid query parameter "${name}". Expected one of: ${allowedValues.join(", ")}.`);
|
|
3261
|
+
}
|
|
3262
|
+
return value;
|
|
3263
|
+
}
|
|
3264
|
+
function expectObject(value, label = "request body") {
|
|
3265
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
3266
|
+
throw new BadRequestError(`${label} must be a JSON object.`);
|
|
3267
|
+
}
|
|
3268
|
+
return value;
|
|
3269
|
+
}
|
|
3270
|
+
async function parseJsonBody(request, validator) {
|
|
3271
|
+
let payload;
|
|
3272
|
+
try {
|
|
3273
|
+
payload = await request.json();
|
|
3274
|
+
} catch {
|
|
3275
|
+
throw new BadRequestError("Request body must be valid JSON.");
|
|
3276
|
+
}
|
|
3277
|
+
return validator(payload);
|
|
3278
|
+
}
|
|
3279
|
+
function readRequiredString(payload, field, options = {}) {
|
|
3280
|
+
const value = payload[field];
|
|
3281
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
3282
|
+
throw new BadRequestError(`"${field}" is required and must be a string.`);
|
|
3283
|
+
}
|
|
3284
|
+
const trimmed = value.trim();
|
|
3285
|
+
if (options.minLength !== undefined && trimmed.length < options.minLength) {
|
|
3286
|
+
throw new BadRequestError(`"${field}" must be at least ${options.minLength} characters.`);
|
|
3287
|
+
}
|
|
3288
|
+
if (options.maxLength !== undefined && trimmed.length > options.maxLength) {
|
|
3289
|
+
throw new BadRequestError(`"${field}" must be at most ${options.maxLength} characters.`);
|
|
3290
|
+
}
|
|
3291
|
+
if (options.pattern && !options.pattern.test(trimmed)) {
|
|
3292
|
+
throw new BadRequestError(`"${field}" has an invalid format.`);
|
|
3293
|
+
}
|
|
3294
|
+
return trimmed;
|
|
3295
|
+
}
|
|
3296
|
+
function readOptionalString(payload, field, options = {}) {
|
|
3297
|
+
if (!(field in payload) || payload[field] === undefined) {
|
|
3298
|
+
return;
|
|
3299
|
+
}
|
|
3300
|
+
return readRequiredString(payload, field, options);
|
|
3301
|
+
}
|
|
3302
|
+
function readRequiredEnum(payload, field, allowedValues) {
|
|
3303
|
+
const value = readRequiredString(payload, field);
|
|
3304
|
+
if (!allowedValues.includes(value)) {
|
|
3305
|
+
throw new BadRequestError(`"${field}" must be one of: ${allowedValues.join(", ")}.`);
|
|
3306
|
+
}
|
|
3307
|
+
return value;
|
|
3308
|
+
}
|
|
3309
|
+
function readOptionalEnum(payload, field, allowedValues) {
|
|
3310
|
+
if (!(field in payload) || payload[field] === undefined) {
|
|
3311
|
+
return;
|
|
3312
|
+
}
|
|
3313
|
+
return readRequiredEnum(payload, field, allowedValues);
|
|
3314
|
+
}
|
|
3315
|
+
function readRequiredPositiveInt(payload, field) {
|
|
3316
|
+
const value = payload[field];
|
|
3317
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
|
|
3318
|
+
throw new BadRequestError(`"${field}" is required and must be a positive integer.`);
|
|
3319
|
+
}
|
|
3320
|
+
return value;
|
|
3321
|
+
}
|
|
3322
|
+
function readOptionalPositiveInt(payload, field) {
|
|
3323
|
+
if (!(field in payload) || payload[field] === undefined) {
|
|
3324
|
+
return;
|
|
3325
|
+
}
|
|
3326
|
+
return readRequiredPositiveInt(payload, field);
|
|
3327
|
+
}
|
|
3328
|
+
function parsePositiveIntParam(value, name = "id") {
|
|
3329
|
+
const parsed = Number.parseInt(value, 10);
|
|
3330
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
3331
|
+
throw new BadRequestError(`Invalid ${name}. Expected a positive integer.`);
|
|
3332
|
+
}
|
|
3333
|
+
return parsed;
|
|
3334
|
+
}
|
|
3335
|
+
|
|
3336
|
+
// ../../src/core/http/formRequest.ts
|
|
3337
|
+
class FormRequest {
|
|
3338
|
+
authorize(_request) {
|
|
3339
|
+
return true;
|
|
3340
|
+
}
|
|
3341
|
+
async validate(request) {
|
|
3342
|
+
if (!await this.authorize(request)) {
|
|
3343
|
+
throw new ForbiddenError;
|
|
3344
|
+
}
|
|
3345
|
+
return await parseJsonBody(request, (payload) => this.parse(payload));
|
|
3346
|
+
}
|
|
3347
|
+
}
|
|
3348
|
+
|
|
3349
|
+
class QueryFormRequest {
|
|
3350
|
+
validate(request) {
|
|
3351
|
+
return this.parseQuery(request);
|
|
3352
|
+
}
|
|
3353
|
+
}
|
|
3354
|
+
// ../../src/config/frontend.ts
|
|
3355
|
+
function readFrontendMode() {
|
|
3356
|
+
const mode = (process.env.FRONTEND_MODE ?? "api").trim();
|
|
3357
|
+
if (mode === "server-htmx") {
|
|
3358
|
+
return "server-htmx";
|
|
3359
|
+
}
|
|
3360
|
+
if (mode === "spa-react") {
|
|
3361
|
+
return "spa-react";
|
|
3362
|
+
}
|
|
3363
|
+
return "api";
|
|
3364
|
+
}
|
|
3365
|
+
function isViewsEnabled() {
|
|
3366
|
+
return readFrontendMode() === "server-htmx";
|
|
3367
|
+
}
|
|
3368
|
+
|
|
3369
|
+
// ../../src/core/view/etaViewEngine.ts
|
|
3370
|
+
import { join as join4 } from "path";
|
|
3371
|
+
import { Eta } from "eta";
|
|
3372
|
+
var DEFAULT_VIEWS_DIRECTORY = join4(process.cwd(), "resources/views");
|
|
3373
|
+
var DEFAULT_LAYOUT = "layouts/app.eta";
|
|
3374
|
+
|
|
3375
|
+
class EtaViewEngine {
|
|
3376
|
+
eta;
|
|
3377
|
+
resolveLayoutData;
|
|
3378
|
+
constructor(viewsDirectory = DEFAULT_VIEWS_DIRECTORY, resolveLayoutData) {
|
|
3379
|
+
this.eta = new Eta({
|
|
3380
|
+
views: viewsDirectory,
|
|
3381
|
+
autoTrim: false
|
|
3382
|
+
});
|
|
3383
|
+
this.resolveLayoutData = resolveLayoutData;
|
|
3384
|
+
}
|
|
3385
|
+
async render(name, data = {}, options = {}) {
|
|
3386
|
+
const template = name.endsWith(".eta") ? name : `${name}.eta`;
|
|
3387
|
+
const layoutData = this.resolveLayoutData ? await this.resolveLayoutData() : {};
|
|
3388
|
+
const mergedData = { ...layoutData, ...data };
|
|
3389
|
+
const body = await this.eta.renderAsync(template, mergedData);
|
|
3390
|
+
const layout = options.layout ?? DEFAULT_LAYOUT;
|
|
3391
|
+
if (layout === false) {
|
|
3392
|
+
return body;
|
|
3393
|
+
}
|
|
3394
|
+
const layoutTemplate = layout.endsWith(".eta") ? layout : `${layout}.eta`;
|
|
3395
|
+
return await this.eta.renderAsync(layoutTemplate, {
|
|
3396
|
+
...mergedData,
|
|
3397
|
+
body
|
|
3398
|
+
});
|
|
3399
|
+
}
|
|
3400
|
+
}
|
|
3401
|
+
// ../../src/core/view/htmlResponse.ts
|
|
3402
|
+
function htmlResponse(html, init = {}) {
|
|
3403
|
+
return new Response(html, {
|
|
3404
|
+
status: init.status ?? 200,
|
|
3405
|
+
statusText: init.statusText,
|
|
3406
|
+
headers: {
|
|
3407
|
+
"Content-Type": "text/html; charset=utf-8"
|
|
3408
|
+
}
|
|
3409
|
+
});
|
|
3410
|
+
}
|
|
3411
|
+
function isHtmxRequest(request) {
|
|
3412
|
+
return request.headers.get("HX-Request") === "true";
|
|
3413
|
+
}
|
|
3414
|
+
// ../../src/core/auth/oauth/oidcProvider.ts
|
|
3415
|
+
class OidcProvider {
|
|
3416
|
+
options;
|
|
3417
|
+
name;
|
|
3418
|
+
constructor(options) {
|
|
3419
|
+
this.options = options;
|
|
3420
|
+
this.name = options.name;
|
|
3421
|
+
}
|
|
3422
|
+
getAuthorizationUrl(state) {
|
|
3423
|
+
const params = new URLSearchParams({
|
|
3424
|
+
client_id: this.options.clientId,
|
|
3425
|
+
redirect_uri: this.options.redirectUri,
|
|
3426
|
+
response_type: "code",
|
|
3427
|
+
scope: (this.options.scopes ?? ["openid", "email", "profile"]).join(" "),
|
|
3428
|
+
state
|
|
3429
|
+
});
|
|
3430
|
+
return `${this.options.issuer.replace(/\/$/, "")}/authorize?${params.toString()}`;
|
|
3431
|
+
}
|
|
3432
|
+
async exchangeCode(code) {
|
|
3433
|
+
const tokenResponse = await fetch(`${this.options.issuer.replace(/\/$/, "")}/token`, {
|
|
3434
|
+
method: "POST",
|
|
3435
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
3436
|
+
body: new URLSearchParams({
|
|
3437
|
+
grant_type: "authorization_code",
|
|
3438
|
+
code,
|
|
3439
|
+
redirect_uri: this.options.redirectUri,
|
|
3440
|
+
client_id: this.options.clientId,
|
|
3441
|
+
client_secret: this.options.clientSecret
|
|
3442
|
+
})
|
|
3443
|
+
});
|
|
3444
|
+
const tokenBody = await tokenResponse.json();
|
|
3445
|
+
if (!tokenBody.access_token) {
|
|
3446
|
+
throw new Error("OIDC token exchange failed.");
|
|
3447
|
+
}
|
|
3448
|
+
const profileResponse = await fetch(`${this.options.issuer.replace(/\/$/, "")}/userinfo`, {
|
|
3449
|
+
headers: { authorization: `Bearer ${tokenBody.access_token}` }
|
|
3450
|
+
});
|
|
3451
|
+
const profile = await profileResponse.json();
|
|
3452
|
+
return {
|
|
3453
|
+
providerUserId: profile.sub,
|
|
3454
|
+
email: profile.email ?? `${profile.sub}@oidc.local`,
|
|
3455
|
+
name: profile.name ?? profile.sub
|
|
3456
|
+
};
|
|
3457
|
+
}
|
|
3458
|
+
}
|
|
3459
|
+
|
|
3460
|
+
// ../../src/core/auth/oauth/providers.ts
|
|
3461
|
+
class GitHubOAuthProvider {
|
|
3462
|
+
options;
|
|
3463
|
+
name = "github";
|
|
3464
|
+
constructor(options) {
|
|
3465
|
+
this.options = options;
|
|
3466
|
+
}
|
|
3467
|
+
getAuthorizationUrl(state) {
|
|
3468
|
+
const params = new URLSearchParams({
|
|
3469
|
+
client_id: this.options.clientId,
|
|
3470
|
+
redirect_uri: this.options.redirectUri,
|
|
3471
|
+
scope: "read:user user:email",
|
|
3472
|
+
state
|
|
3473
|
+
});
|
|
3474
|
+
return `https://github.com/login/oauth/authorize?${params.toString()}`;
|
|
3475
|
+
}
|
|
3476
|
+
async exchangeCode(code) {
|
|
3477
|
+
const tokenResponse = await fetch("https://github.com/login/oauth/access_token", {
|
|
3478
|
+
method: "POST",
|
|
3479
|
+
headers: {
|
|
3480
|
+
accept: "application/json",
|
|
3481
|
+
"content-type": "application/json"
|
|
3482
|
+
},
|
|
3483
|
+
body: JSON.stringify({
|
|
3484
|
+
client_id: this.options.clientId,
|
|
3485
|
+
client_secret: this.options.clientSecret,
|
|
3486
|
+
code,
|
|
3487
|
+
redirect_uri: this.options.redirectUri
|
|
3488
|
+
})
|
|
3489
|
+
});
|
|
3490
|
+
const tokenBody = await tokenResponse.json();
|
|
3491
|
+
if (!tokenBody.access_token) {
|
|
3492
|
+
throw new Error("GitHub OAuth token exchange failed.");
|
|
3493
|
+
}
|
|
3494
|
+
const profileResponse = await fetch("https://api.github.com/user", {
|
|
3495
|
+
headers: {
|
|
3496
|
+
authorization: `Bearer ${tokenBody.access_token}`,
|
|
3497
|
+
accept: "application/json",
|
|
3498
|
+
"user-agent": "workhub"
|
|
3499
|
+
}
|
|
3500
|
+
});
|
|
3501
|
+
const profile = await profileResponse.json();
|
|
3502
|
+
return {
|
|
3503
|
+
providerUserId: String(profile.id),
|
|
3504
|
+
email: profile.email ?? `${profile.login}@users.noreply.github.com`,
|
|
3505
|
+
name: profile.name ?? profile.login
|
|
3506
|
+
};
|
|
3507
|
+
}
|
|
3508
|
+
}
|
|
3509
|
+
|
|
3510
|
+
class MockOAuthProvider {
|
|
3511
|
+
profile;
|
|
3512
|
+
name = "mock";
|
|
3513
|
+
constructor(profile) {
|
|
3514
|
+
this.profile = profile;
|
|
3515
|
+
}
|
|
3516
|
+
getAuthorizationUrl(state) {
|
|
3517
|
+
return `https://mock.oauth/authorize?state=${encodeURIComponent(state)}`;
|
|
3518
|
+
}
|
|
3519
|
+
async exchangeCode(code) {
|
|
3520
|
+
if (code !== "valid-code") {
|
|
3521
|
+
throw new Error("Invalid OAuth code.");
|
|
3522
|
+
}
|
|
3523
|
+
return this.profile;
|
|
3524
|
+
}
|
|
3525
|
+
}
|
|
3526
|
+
|
|
3527
|
+
// ../../src/core/auth/oauth/samlProvider.ts
|
|
3528
|
+
class SamlProvider {
|
|
3529
|
+
loginUrl;
|
|
3530
|
+
name = "saml";
|
|
3531
|
+
constructor(loginUrl) {
|
|
3532
|
+
this.loginUrl = loginUrl;
|
|
3533
|
+
}
|
|
3534
|
+
getAuthorizationUrl(state) {
|
|
3535
|
+
return `${this.loginUrl}?state=${encodeURIComponent(state)}`;
|
|
3536
|
+
}
|
|
3537
|
+
async exchangeCode(code) {
|
|
3538
|
+
if (!code.startsWith("saml:")) {
|
|
3539
|
+
throw new Error("Invalid SAML assertion reference.");
|
|
3540
|
+
}
|
|
3541
|
+
const [, email, name] = code.split(":");
|
|
3542
|
+
return {
|
|
3543
|
+
providerUserId: email ?? "saml-user",
|
|
3544
|
+
email: email ?? "saml-user@workhub.test",
|
|
3545
|
+
name: name ?? "SAML User"
|
|
3546
|
+
};
|
|
3547
|
+
}
|
|
3548
|
+
}
|
|
3549
|
+
|
|
3550
|
+
// ../../src/config/features.ts
|
|
3551
|
+
function readFeatureFlags() {
|
|
3552
|
+
return {
|
|
3553
|
+
webhooks: (process.env.FEATURE_WEBHOOKS ?? "true") !== "false",
|
|
3554
|
+
fullTextSearch: (process.env.FEATURE_SEARCH ?? "true") !== "false",
|
|
3555
|
+
auditLog: (process.env.FEATURE_AUDIT_LOG ?? "true") !== "false",
|
|
3556
|
+
oauthLogin: (process.env.FEATURE_OAUTH ?? "true") !== "false",
|
|
3557
|
+
samlLogin: (process.env.FEATURE_SAML ?? "false") === "true",
|
|
3558
|
+
scim: (process.env.FEATURE_SCIM ?? "true") !== "false",
|
|
3559
|
+
billing: (process.env.FEATURE_BILLING ?? "true") !== "false",
|
|
3560
|
+
siemExport: (process.env.FEATURE_SIEM_EXPORT ?? "true") !== "false",
|
|
3561
|
+
publicReads: (process.env.FEATURE_PUBLIC_READS ?? "true") !== "false",
|
|
3562
|
+
emailVerification: (process.env.FEATURE_EMAIL_VERIFICATION ?? "false") === "true",
|
|
3563
|
+
mfa: (process.env.FEATURE_MFA ?? "false") === "true"
|
|
3564
|
+
};
|
|
3565
|
+
}
|
|
3566
|
+
var featureFlags = readFeatureFlags();
|
|
3567
|
+
function isFeatureEnabled(feature) {
|
|
3568
|
+
return readFeatureFlags()[feature];
|
|
3569
|
+
}
|
|
3570
|
+
// ../../src/modules/user/apiTokenTable.ts
|
|
3571
|
+
var apiTokenTable = defineTable({
|
|
3572
|
+
name: "api_token",
|
|
3573
|
+
primaryKey: "id",
|
|
3574
|
+
columns: [
|
|
3575
|
+
"id",
|
|
3576
|
+
"user_id",
|
|
3577
|
+
"name",
|
|
3578
|
+
"token_hash",
|
|
3579
|
+
"abilities",
|
|
3580
|
+
"last_used_at",
|
|
3581
|
+
"expires_at",
|
|
3582
|
+
"created_at"
|
|
3583
|
+
],
|
|
3584
|
+
defaultOrderBy: { column: "id", direction: "ASC" }
|
|
3585
|
+
});
|
|
3586
|
+
|
|
3587
|
+
// ../../src/core/auth/password.ts
|
|
3588
|
+
async function hashPassword(password) {
|
|
3589
|
+
return await Bun.password.hash(password, {
|
|
3590
|
+
algorithm: "bcrypt",
|
|
3591
|
+
cost: 10
|
|
3592
|
+
});
|
|
3593
|
+
}
|
|
3594
|
+
async function verifyPassword(password, passwordHash) {
|
|
3595
|
+
return await Bun.password.verify(password, passwordHash);
|
|
3596
|
+
}
|
|
3597
|
+
|
|
3598
|
+
// ../../src/core/crypto/fieldEncryption.ts
|
|
3599
|
+
import { createCipheriv, createDecipheriv, createHmac as createHmac2, randomBytes as randomBytes2 } from "crypto";
|
|
3600
|
+
var ENCRYPTION_PREFIX = "enc:v1:";
|
|
3601
|
+
var IV_LENGTH = 12;
|
|
3602
|
+
var TAG_LENGTH = 16;
|
|
3603
|
+
function resolveEncryptionKey() {
|
|
3604
|
+
const raw = process.env.KMS_ENCRYPTION_KEY?.trim();
|
|
3605
|
+
if (!raw) {
|
|
3606
|
+
return null;
|
|
3607
|
+
}
|
|
3608
|
+
if (/^[0-9a-f]{64}$/i.test(raw)) {
|
|
3609
|
+
return Buffer.from(raw, "hex");
|
|
3610
|
+
}
|
|
3611
|
+
const decoded = Buffer.from(raw, "base64");
|
|
3612
|
+
if (decoded.length === 32) {
|
|
3613
|
+
return decoded;
|
|
3614
|
+
}
|
|
3615
|
+
throw new Error("KMS_ENCRYPTION_KEY must be 32 bytes (hex or base64).");
|
|
3616
|
+
}
|
|
3617
|
+
function isFieldEncryptionEnabled() {
|
|
3618
|
+
const featureFlag = process.env.FEATURE_FIELD_ENCRYPTION;
|
|
3619
|
+
if (featureFlag === "false") {
|
|
3620
|
+
return false;
|
|
3621
|
+
}
|
|
3622
|
+
if (featureFlag === "true") {
|
|
3623
|
+
return true;
|
|
3624
|
+
}
|
|
3625
|
+
return (process.env.APP_ENV ?? "local") === "production";
|
|
3626
|
+
}
|
|
3627
|
+
function encryptField(plaintext, key) {
|
|
3628
|
+
const iv = randomBytes2(IV_LENGTH);
|
|
3629
|
+
const cipher = createCipheriv("aes-256-gcm", key, iv);
|
|
3630
|
+
const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
|
3631
|
+
const tag = cipher.getAuthTag();
|
|
3632
|
+
const payload = Buffer.concat([iv, encrypted, tag]).toString("base64");
|
|
3633
|
+
return `${ENCRYPTION_PREFIX}${payload}`;
|
|
1729
3634
|
}
|
|
1730
3635
|
function decryptField(value, key) {
|
|
1731
3636
|
if (!value.startsWith(ENCRYPTION_PREFIX)) {
|
|
@@ -1739,8 +3644,47 @@ function decryptField(value, key) {
|
|
|
1739
3644
|
decipher.setAuthTag(tag);
|
|
1740
3645
|
return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf8");
|
|
1741
3646
|
}
|
|
3647
|
+
function hashLookupValue(normalizedValue, key) {
|
|
3648
|
+
return createHmac2("sha256", key).update(normalizedValue).digest("hex");
|
|
3649
|
+
}
|
|
3650
|
+
function normalizeEmail(email) {
|
|
3651
|
+
return email.trim().toLowerCase();
|
|
3652
|
+
}
|
|
3653
|
+
function protectEmail(email) {
|
|
3654
|
+
const normalized = normalizeEmail(email);
|
|
3655
|
+
const key = resolveEncryptionKey();
|
|
3656
|
+
if (!key || !isFieldEncryptionEnabled()) {
|
|
3657
|
+
return { storedEmail: normalized, emailLookup: normalized };
|
|
3658
|
+
}
|
|
3659
|
+
return {
|
|
3660
|
+
storedEmail: encryptField(normalized, key),
|
|
3661
|
+
emailLookup: hashLookupValue(normalized, key)
|
|
3662
|
+
};
|
|
3663
|
+
}
|
|
3664
|
+
function revealEmail(storedEmail) {
|
|
3665
|
+
const key = resolveEncryptionKey();
|
|
3666
|
+
if (!key || !storedEmail.startsWith(ENCRYPTION_PREFIX)) {
|
|
3667
|
+
return storedEmail;
|
|
3668
|
+
}
|
|
3669
|
+
return decryptField(storedEmail, key);
|
|
3670
|
+
}
|
|
3671
|
+
function emailLookupForQuery(email) {
|
|
3672
|
+
const normalized = normalizeEmail(email);
|
|
3673
|
+
const key = resolveEncryptionKey();
|
|
3674
|
+
if (!key || !isFieldEncryptionEnabled()) {
|
|
3675
|
+
return normalized;
|
|
3676
|
+
}
|
|
3677
|
+
return hashLookupValue(normalized, key);
|
|
3678
|
+
}
|
|
1742
3679
|
|
|
1743
3680
|
// ../../src/core/crypto/mfaSecret.ts
|
|
3681
|
+
function protectMfaSecret(secret) {
|
|
3682
|
+
const key = resolveEncryptionKey();
|
|
3683
|
+
if (!isFieldEncryptionEnabled() || !key) {
|
|
3684
|
+
return secret;
|
|
3685
|
+
}
|
|
3686
|
+
return encryptField(secret, key);
|
|
3687
|
+
}
|
|
1744
3688
|
function revealMfaSecret(stored) {
|
|
1745
3689
|
if (!stored) {
|
|
1746
3690
|
return null;
|
|
@@ -1752,16 +3696,6 @@ function revealMfaSecret(stored) {
|
|
|
1752
3696
|
return decryptField(stored, key);
|
|
1753
3697
|
}
|
|
1754
3698
|
|
|
1755
|
-
// ../../src/core/http/requestMetaContext.ts
|
|
1756
|
-
import { AsyncLocalStorage as AsyncLocalStorage3 } from "async_hooks";
|
|
1757
|
-
var requestMetaContext = new AsyncLocalStorage3;
|
|
1758
|
-
function currentRequestMeta() {
|
|
1759
|
-
return requestMetaContext.getStore() ?? {
|
|
1760
|
-
ipAddress: null,
|
|
1761
|
-
userAgent: null
|
|
1762
|
-
};
|
|
1763
|
-
}
|
|
1764
|
-
|
|
1765
3699
|
// ../../src/core/security/securityEvents.ts
|
|
1766
3700
|
function logSecurityEvent(event, details = {}) {
|
|
1767
3701
|
const meta = currentRequestMeta();
|
|
@@ -1791,7 +3725,7 @@ function resolveDefaultTokenExpiryDays() {
|
|
|
1791
3725
|
}
|
|
1792
3726
|
|
|
1793
3727
|
// ../../src/core/security/totp.ts
|
|
1794
|
-
import { createHmac as
|
|
3728
|
+
import { createHmac as createHmac3 } from "crypto";
|
|
1795
3729
|
function decodeBase32(input) {
|
|
1796
3730
|
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
|
1797
3731
|
const normalized = input.replace(/=+$/u, "").toUpperCase();
|
|
@@ -1813,7 +3747,7 @@ function generateTotp(secret, counter, digits = 6) {
|
|
|
1813
3747
|
const key = decodeBase32(secret);
|
|
1814
3748
|
const buffer = Buffer.alloc(8);
|
|
1815
3749
|
buffer.writeBigUInt64BE(BigInt(counter));
|
|
1816
|
-
const digest =
|
|
3750
|
+
const digest = createHmac3("sha1", key).update(buffer).digest();
|
|
1817
3751
|
const lastByte = digest[digest.length - 1] ?? 0;
|
|
1818
3752
|
const offset = lastByte & 15;
|
|
1819
3753
|
const b0 = digest[offset] ?? 0;
|
|
@@ -1837,16 +3771,6 @@ function verifyTotp(secret, token, window = 1) {
|
|
|
1837
3771
|
return false;
|
|
1838
3772
|
}
|
|
1839
3773
|
|
|
1840
|
-
// ../../src/core/tenant/tenantContext.ts
|
|
1841
|
-
import { AsyncLocalStorage as AsyncLocalStorage4 } from "async_hooks";
|
|
1842
|
-
var tenantContext = new AsyncLocalStorage4;
|
|
1843
|
-
function currentTenant() {
|
|
1844
|
-
return tenantContext.getStore() ?? null;
|
|
1845
|
-
}
|
|
1846
|
-
function currentTenantId() {
|
|
1847
|
-
return currentTenant()?.id ?? 1;
|
|
1848
|
-
}
|
|
1849
|
-
|
|
1850
3774
|
// ../../src/domain/abilities.ts
|
|
1851
3775
|
var MEMBER_ABILITIES = [
|
|
1852
3776
|
"organizations:read",
|
|
@@ -2014,11 +3938,160 @@ var userTable = defineTable({
|
|
|
2014
3938
|
defaultOrderBy: { column: "id", direction: "ASC" }
|
|
2015
3939
|
});
|
|
2016
3940
|
|
|
2017
|
-
// ../../src/core/
|
|
2018
|
-
|
|
3941
|
+
// ../../src/core/auth/tokenHash.ts
|
|
3942
|
+
import { createHash as createHash2, createHmac as createHmac4 } from "crypto";
|
|
3943
|
+
function resolveTokenPepper() {
|
|
3944
|
+
return process.env.TOKEN_HASH_PEPPER?.trim() ?? "workhub-dev-token-pepper";
|
|
3945
|
+
}
|
|
3946
|
+
function hashApiToken(token) {
|
|
3947
|
+
const pepper = resolveTokenPepper();
|
|
3948
|
+
if (pepper && pepper !== "workhub-dev-token-pepper") {
|
|
3949
|
+
return createHmac4("sha256", pepper).update(token).digest("hex");
|
|
3950
|
+
}
|
|
3951
|
+
return createHash2("sha256").update(token).digest("hex");
|
|
3952
|
+
}
|
|
3953
|
+
|
|
3954
|
+
// ../../src/modules/user/provider.ts
|
|
3955
|
+
var tokenServiceToken = CORE_TOKEN_SERVICE_TOKEN;
|
|
2019
3956
|
|
|
2020
3957
|
// ../../src/core/http/flashSession.ts
|
|
3958
|
+
import { createHmac as createHmac5, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
3959
|
+
var FLASH_COOKIE = "workhub_flash";
|
|
2021
3960
|
var FLASH_TTL_MS = 60 * 1000;
|
|
3961
|
+
function resolveFlashSecret() {
|
|
3962
|
+
return process.env.SESSION_SECRET?.trim() || process.env.OAUTH_STATE_SECRET?.trim() || "workhub-dev-flash-secret";
|
|
3963
|
+
}
|
|
3964
|
+
function signFlashPayload(payload, issuedAt) {
|
|
3965
|
+
const signature = createHmac5("sha256", resolveFlashSecret()).update(`${payload}.${issuedAt}`).digest("hex");
|
|
3966
|
+
return `${payload}.${issuedAt}.${signature}`;
|
|
3967
|
+
}
|
|
3968
|
+
function readFlashCookie(request) {
|
|
3969
|
+
const cookieHeader = request.headers.get("cookie");
|
|
3970
|
+
if (!cookieHeader) {
|
|
3971
|
+
return null;
|
|
3972
|
+
}
|
|
3973
|
+
for (const part of cookieHeader.split(";")) {
|
|
3974
|
+
const [name, ...rest] = part.trim().split("=");
|
|
3975
|
+
if (name === FLASH_COOKIE) {
|
|
3976
|
+
return decodeURIComponent(rest.join("="));
|
|
3977
|
+
}
|
|
3978
|
+
}
|
|
3979
|
+
return null;
|
|
3980
|
+
}
|
|
3981
|
+
function parseFlashCookie(cookieValue) {
|
|
3982
|
+
const parts = cookieValue.split(".");
|
|
3983
|
+
if (parts.length < 3) {
|
|
3984
|
+
return null;
|
|
3985
|
+
}
|
|
3986
|
+
const signature = parts.pop();
|
|
3987
|
+
const issuedAtRaw = parts.pop();
|
|
3988
|
+
const payload = parts.join(".");
|
|
3989
|
+
if (!signature || !issuedAtRaw || !payload) {
|
|
3990
|
+
return null;
|
|
3991
|
+
}
|
|
3992
|
+
const issuedAt = Number.parseInt(issuedAtRaw, 10);
|
|
3993
|
+
if (!Number.isFinite(issuedAt) || Date.now() - issuedAt > FLASH_TTL_MS) {
|
|
3994
|
+
return null;
|
|
3995
|
+
}
|
|
3996
|
+
const expectedSignature = signFlashPayload(payload, issuedAt).split(".").pop();
|
|
3997
|
+
if (!expectedSignature) {
|
|
3998
|
+
return null;
|
|
3999
|
+
}
|
|
4000
|
+
const expectedBuffer = Buffer.from(expectedSignature);
|
|
4001
|
+
const actualBuffer = Buffer.from(signature);
|
|
4002
|
+
if (expectedBuffer.length !== actualBuffer.length) {
|
|
4003
|
+
return null;
|
|
4004
|
+
}
|
|
4005
|
+
if (!timingSafeEqual2(expectedBuffer, actualBuffer)) {
|
|
4006
|
+
return null;
|
|
4007
|
+
}
|
|
4008
|
+
try {
|
|
4009
|
+
const parsed = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
|
4010
|
+
if (!parsed?.message || typeof parsed.message !== "string") {
|
|
4011
|
+
return null;
|
|
4012
|
+
}
|
|
4013
|
+
if (parsed.level !== "success" && parsed.level !== "error" && parsed.level !== "info") {
|
|
4014
|
+
return null;
|
|
4015
|
+
}
|
|
4016
|
+
return parsed;
|
|
4017
|
+
} catch {
|
|
4018
|
+
return null;
|
|
4019
|
+
}
|
|
4020
|
+
}
|
|
4021
|
+
function createFlashCookie(message) {
|
|
4022
|
+
const payload = Buffer.from(JSON.stringify(message), "utf8").toString("base64url");
|
|
4023
|
+
const issuedAt = Date.now();
|
|
4024
|
+
const value = signFlashPayload(payload, issuedAt);
|
|
4025
|
+
return `${FLASH_COOKIE}=${encodeURIComponent(value)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=60`;
|
|
4026
|
+
}
|
|
4027
|
+
function clearFlashCookie() {
|
|
4028
|
+
return `${FLASH_COOKIE}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`;
|
|
4029
|
+
}
|
|
4030
|
+
function pullFlash(request) {
|
|
4031
|
+
const cookieValue = readFlashCookie(request);
|
|
4032
|
+
if (!cookieValue) {
|
|
4033
|
+
return null;
|
|
4034
|
+
}
|
|
4035
|
+
return parseFlashCookie(cookieValue);
|
|
4036
|
+
}
|
|
4037
|
+
function flashResponse(response, message) {
|
|
4038
|
+
const headers = new Headers(response.headers);
|
|
4039
|
+
headers.append("set-cookie", createFlashCookie(message));
|
|
4040
|
+
return new Response(response.body, {
|
|
4041
|
+
status: response.status,
|
|
4042
|
+
statusText: response.statusText,
|
|
4043
|
+
headers
|
|
4044
|
+
});
|
|
4045
|
+
}
|
|
4046
|
+
function withFlashClear(response) {
|
|
4047
|
+
const headers = new Headers(response.headers);
|
|
4048
|
+
headers.append("set-cookie", clearFlashCookie());
|
|
4049
|
+
return new Response(response.body, {
|
|
4050
|
+
status: response.status,
|
|
4051
|
+
statusText: response.statusText,
|
|
4052
|
+
headers
|
|
4053
|
+
});
|
|
4054
|
+
}
|
|
4055
|
+
|
|
4056
|
+
// ../../src/core/view/webLayoutData.ts
|
|
4057
|
+
async function resolveWebLayoutData(container, request) {
|
|
4058
|
+
const csrfToken = request ? resolveCsrfTokenForRequest(request) : "";
|
|
4059
|
+
const flash = request ? currentRequestMeta().flash ?? pullFlash(request) : null;
|
|
4060
|
+
const authUser = currentAuthUser();
|
|
4061
|
+
if (!authUser) {
|
|
4062
|
+
return { authUser: null, csrfToken, flash };
|
|
4063
|
+
}
|
|
4064
|
+
const userId = Number(authUser.id);
|
|
4065
|
+
if (!Number.isInteger(userId) || userId <= 0) {
|
|
4066
|
+
return { authUser: null, csrfToken, flash };
|
|
4067
|
+
}
|
|
4068
|
+
if (!container.has(tokenServiceToken)) {
|
|
4069
|
+
return {
|
|
4070
|
+
authUser: {
|
|
4071
|
+
id: userId,
|
|
4072
|
+
email: "",
|
|
4073
|
+
role: authUser.role ?? "member"
|
|
4074
|
+
},
|
|
4075
|
+
csrfToken,
|
|
4076
|
+
flash
|
|
4077
|
+
};
|
|
4078
|
+
}
|
|
4079
|
+
const tokenService = container.resolve(tokenServiceToken);
|
|
4080
|
+
try {
|
|
4081
|
+
const user = await tokenService.findByIdOrThrow(userId);
|
|
4082
|
+
return {
|
|
4083
|
+
authUser: {
|
|
4084
|
+
id: userId,
|
|
4085
|
+
email: user.email ?? "",
|
|
4086
|
+
role: authUser.role ?? user.role ?? "member"
|
|
4087
|
+
},
|
|
4088
|
+
csrfToken,
|
|
4089
|
+
flash
|
|
4090
|
+
};
|
|
4091
|
+
} catch {
|
|
4092
|
+
return { authUser: null, csrfToken, flash };
|
|
4093
|
+
}
|
|
4094
|
+
}
|
|
2022
4095
|
// ../../src/core/http/contentNegotiation.ts
|
|
2023
4096
|
function requestPrefersJson(request) {
|
|
2024
4097
|
if (!request) {
|
|
@@ -2078,127 +4151,123 @@ function webErrorResponse(error, request) {
|
|
|
2078
4151
|
status: mappedError.status
|
|
2079
4152
|
});
|
|
2080
4153
|
}
|
|
2081
|
-
// ../../src/core/http/
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
}
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
}
|
|
2098
|
-
|
|
2099
|
-
return normalizeEtag(left) === normalizeEtag(right);
|
|
2100
|
-
}
|
|
2101
|
-
function parseEtagList(header) {
|
|
2102
|
-
if (!header) {
|
|
2103
|
-
return [];
|
|
2104
|
-
}
|
|
2105
|
-
return header.split(",").map((value) => normalizeEtag(value)).filter(Boolean);
|
|
2106
|
-
}
|
|
2107
|
-
function ifNoneMatchSatisfied(request, etag) {
|
|
2108
|
-
const header = request.headers.get("if-none-match");
|
|
2109
|
-
if (!header) {
|
|
2110
|
-
return false;
|
|
2111
|
-
}
|
|
2112
|
-
if (header.trim() === "*") {
|
|
2113
|
-
return true;
|
|
2114
|
-
}
|
|
2115
|
-
return parseEtagList(header).some((candidate) => etagValuesMatch(candidate, etag));
|
|
2116
|
-
}
|
|
2117
|
-
function ifMatchSatisfied(request, etag) {
|
|
2118
|
-
const header = request.headers.get("if-match");
|
|
2119
|
-
if (!header) {
|
|
2120
|
-
return false;
|
|
2121
|
-
}
|
|
2122
|
-
if (header.trim() === "*") {
|
|
2123
|
-
return true;
|
|
2124
|
-
}
|
|
2125
|
-
return parseEtagList(header).some((candidate) => etagValuesMatch(candidate, etag));
|
|
4154
|
+
// ../../src/core/http/authMiddleware.ts
|
|
4155
|
+
function createAuthMiddleware(auth2) {
|
|
4156
|
+
return async (request, next) => {
|
|
4157
|
+
const user = await auth2.resolve(request);
|
|
4158
|
+
return await runWithAuthUser(user, async () => {
|
|
4159
|
+
const response = await next();
|
|
4160
|
+
if (user) {
|
|
4161
|
+
const headers = new Headers(response.headers);
|
|
4162
|
+
headers.set("x-authenticated-user-id", String(user.id));
|
|
4163
|
+
return new Response(response.body, {
|
|
4164
|
+
status: response.status,
|
|
4165
|
+
statusText: response.statusText,
|
|
4166
|
+
headers
|
|
4167
|
+
});
|
|
4168
|
+
}
|
|
4169
|
+
return response;
|
|
4170
|
+
});
|
|
4171
|
+
};
|
|
2126
4172
|
}
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
4173
|
+
// ../../src/core/http/authorizeMiddleware.ts
|
|
4174
|
+
function createAuthorizeMiddleware(gate, auth2, resource, action) {
|
|
4175
|
+
return async (request, next) => {
|
|
4176
|
+
const user = await auth2.resolve(request);
|
|
4177
|
+
if (!gate.allows(resource, action, user)) {
|
|
4178
|
+
const error = new ForbiddenError;
|
|
4179
|
+
return Response.json({ error: error.message }, { status: error.status });
|
|
2132
4180
|
}
|
|
2133
|
-
return;
|
|
2134
|
-
}
|
|
2135
|
-
if (!ifMatchSatisfied(request, etag)) {
|
|
2136
|
-
throw new PreconditionFailedError("Resource ETag does not match If-Match.");
|
|
2137
|
-
}
|
|
2138
|
-
}
|
|
2139
|
-
function applyEtagHeaders(headers, etag) {
|
|
2140
|
-
const next = new Headers(headers);
|
|
2141
|
-
next.set("ETag", etag);
|
|
2142
|
-
next.set("Cache-Control", "private, must-revalidate");
|
|
2143
|
-
next.append("Vary", "Authorization");
|
|
2144
|
-
next.append("Vary", "X-Tenant-Id");
|
|
2145
|
-
return next;
|
|
4181
|
+
return await next();
|
|
4182
|
+
};
|
|
2146
4183
|
}
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
4184
|
+
// ../../src/core/http/conditionalResponse.ts
|
|
4185
|
+
function jsonResponse(data, init = {}) {
|
|
4186
|
+
return Response.json(data, {
|
|
4187
|
+
status: init.status ?? 200,
|
|
4188
|
+
headers: init.headers
|
|
2151
4189
|
});
|
|
2152
4190
|
}
|
|
2153
|
-
function
|
|
2154
|
-
if (!isEtagEnabled()) {
|
|
2155
|
-
return
|
|
4191
|
+
function conditionalJsonResponse(request, data, init = {}) {
|
|
4192
|
+
if (!request || !isEtagEnabled()) {
|
|
4193
|
+
return jsonResponse(data, init);
|
|
2156
4194
|
}
|
|
4195
|
+
const etag = computeEtagFromJson(data);
|
|
2157
4196
|
if (ifNoneMatchSatisfied(request, etag)) {
|
|
2158
4197
|
return notModifiedResponse(etag);
|
|
2159
4198
|
}
|
|
2160
|
-
const
|
|
4199
|
+
const response = jsonResponse(data, init);
|
|
4200
|
+
const headers = new Headers(response.headers);
|
|
4201
|
+
headers.set("ETag", etag);
|
|
4202
|
+
headers.set("Cache-Control", "private, must-revalidate");
|
|
4203
|
+
headers.append("Vary", "Authorization");
|
|
4204
|
+
headers.append("Vary", "X-Tenant-Id");
|
|
2161
4205
|
return new Response(response.body, {
|
|
2162
4206
|
status: response.status,
|
|
2163
4207
|
statusText: response.statusText,
|
|
2164
4208
|
headers
|
|
2165
4209
|
});
|
|
2166
4210
|
}
|
|
2167
|
-
// ../../src/core/http/
|
|
2168
|
-
function
|
|
2169
|
-
|
|
2170
|
-
return new URLSearchParams;
|
|
2171
|
-
}
|
|
2172
|
-
return new URL(request.url).searchParams;
|
|
4211
|
+
// ../../src/core/http/middleware.ts
|
|
4212
|
+
function isRouteHandler(value) {
|
|
4213
|
+
return typeof value === "function";
|
|
2173
4214
|
}
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
payload = await request.json();
|
|
2178
|
-
} catch {
|
|
2179
|
-
throw new BadRequestError("Request body must be valid JSON.");
|
|
4215
|
+
function isMethodRouteMap(value) {
|
|
4216
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
4217
|
+
return false;
|
|
2180
4218
|
}
|
|
2181
|
-
|
|
4219
|
+
const entries = Object.entries(value);
|
|
4220
|
+
return entries.length > 0 && entries.every(([, handler]) => isRouteHandler(handler));
|
|
2182
4221
|
}
|
|
2183
|
-
function
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
4222
|
+
function composeMiddleware(...middleware) {
|
|
4223
|
+
return (handler) => {
|
|
4224
|
+
return async (request) => {
|
|
4225
|
+
let index = 0;
|
|
4226
|
+
const dispatch = async () => {
|
|
4227
|
+
if (index >= middleware.length) {
|
|
4228
|
+
return await handler(request);
|
|
4229
|
+
}
|
|
4230
|
+
const current = middleware[index];
|
|
4231
|
+
index += 1;
|
|
4232
|
+
if (!current) {
|
|
4233
|
+
return await handler(request);
|
|
4234
|
+
}
|
|
4235
|
+
return await current(request, dispatch);
|
|
4236
|
+
};
|
|
4237
|
+
return await dispatch();
|
|
4238
|
+
};
|
|
4239
|
+
};
|
|
2189
4240
|
}
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
4241
|
+
async function requestIdMiddleware(request, next) {
|
|
4242
|
+
const requestId = request.headers.get("x-request-id") ?? crypto.randomUUID();
|
|
4243
|
+
const response = await next();
|
|
4244
|
+
const headers = new Headers(response.headers);
|
|
4245
|
+
headers.set("x-request-id", requestId);
|
|
4246
|
+
return new Response(response.body, {
|
|
4247
|
+
status: response.status,
|
|
4248
|
+
statusText: response.statusText,
|
|
4249
|
+
headers
|
|
4250
|
+
});
|
|
4251
|
+
}
|
|
4252
|
+
function wrapRouteHandler(handler, middleware) {
|
|
4253
|
+
if (isMethodRouteMap(handler)) {
|
|
4254
|
+
const wrapped = {};
|
|
4255
|
+
for (const [method, routeHandler] of Object.entries(handler)) {
|
|
4256
|
+
wrapped[method] = composeMiddleware(...middleware)(routeHandler);
|
|
2199
4257
|
}
|
|
2200
|
-
return
|
|
4258
|
+
return wrapped;
|
|
4259
|
+
}
|
|
4260
|
+
if (isRouteHandler(handler)) {
|
|
4261
|
+
return composeMiddleware(...middleware)(handler);
|
|
4262
|
+
}
|
|
4263
|
+
return handler;
|
|
4264
|
+
}
|
|
4265
|
+
function applyMiddlewareToRoutes(routes, middleware) {
|
|
4266
|
+
const wrapped = {};
|
|
4267
|
+
for (const [path, routeHandler] of Object.entries(routes)) {
|
|
4268
|
+
wrapped[path] = wrapRouteHandler(routeHandler, middleware);
|
|
2201
4269
|
}
|
|
4270
|
+
return wrapped;
|
|
2202
4271
|
}
|
|
2203
4272
|
// ../../src/core/http/pagination.ts
|
|
2204
4273
|
var DEFAULT_PER_PAGE = 15;
|
|
@@ -2231,14 +4300,74 @@ function parsePaginationQuery(request) {
|
|
|
2231
4300
|
function paginatedResponse(data, meta, init = {}) {
|
|
2232
4301
|
return Response.json({ data, meta }, init);
|
|
2233
4302
|
}
|
|
4303
|
+
// ../../src/core/http/requireAuthMiddleware.ts
|
|
4304
|
+
function createRequireAuthMiddleware(auth2) {
|
|
4305
|
+
return async (request, next) => {
|
|
4306
|
+
if (!await auth2.check(request)) {
|
|
4307
|
+
const error = new UnauthorizedError;
|
|
4308
|
+
return Response.json({ error: error.message }, { status: error.status });
|
|
4309
|
+
}
|
|
4310
|
+
return await next();
|
|
4311
|
+
};
|
|
4312
|
+
}
|
|
4313
|
+
// ../../src/core/http/resources.ts
|
|
4314
|
+
function serializeDate(value) {
|
|
4315
|
+
return value instanceof Date ? value.toISOString() : value;
|
|
4316
|
+
}
|
|
4317
|
+
function toResourceCollection(items, transformer) {
|
|
4318
|
+
return items.map(transformer);
|
|
4319
|
+
}
|
|
4320
|
+
function toPaginatedResourceCollection(items, meta, transformer) {
|
|
4321
|
+
return {
|
|
4322
|
+
data: toResourceCollection(items, transformer),
|
|
4323
|
+
meta
|
|
4324
|
+
};
|
|
4325
|
+
}
|
|
4326
|
+
// ../../src/core/http/routeMiddleware.ts
|
|
4327
|
+
function withMiddleware(...middleware) {
|
|
4328
|
+
const wrap = composeMiddleware(...middleware);
|
|
4329
|
+
return (handler) => {
|
|
4330
|
+
return wrap(handler);
|
|
4331
|
+
};
|
|
4332
|
+
}
|
|
4333
|
+
// ../../src/core/http/routeModelBinding.ts
|
|
4334
|
+
function bindRouteModel(param, resolver, handler) {
|
|
4335
|
+
return async (request) => {
|
|
4336
|
+
const id = parsePositiveIntParam(String(request.params[param]), String(param));
|
|
4337
|
+
const model = await resolver(id, request);
|
|
4338
|
+
return await handler(request, model);
|
|
4339
|
+
};
|
|
4340
|
+
}
|
|
2234
4341
|
// ../../src/core/http/securedRouteModelBinding.ts
|
|
2235
4342
|
function isMutatingPolicyAction(action) {
|
|
2236
4343
|
return action === "update" || action === "delete";
|
|
2237
4344
|
}
|
|
2238
|
-
function securedBindRouteModel(param, resolver, authorization, handler) {
|
|
4345
|
+
function securedBindRouteModel(param, resolver, authorization, handler) {
|
|
4346
|
+
return async (request) => {
|
|
4347
|
+
const id = parsePositiveIntParam(String(request.params[param]), String(param));
|
|
4348
|
+
const model = await resolver(id, request);
|
|
4349
|
+
const gate = resolveApplicationPolicyGate();
|
|
4350
|
+
const auth2 = resolveApplicationAuth();
|
|
4351
|
+
gate.authorize(authorization.resource, authorization.action, await auth2.resolve(request), model);
|
|
4352
|
+
if (isEtagEnabled() && isMutatingPolicyAction(authorization.action)) {
|
|
4353
|
+
assertIfMatch(request, etagFromResource(model), {
|
|
4354
|
+
required: authorization.requireIfMatch ?? true
|
|
4355
|
+
});
|
|
4356
|
+
}
|
|
4357
|
+
const response = await handler(request, model);
|
|
4358
|
+
if (isEtagEnabled() && authorization.action === "view") {
|
|
4359
|
+
return applyConditionalGet(request, response, etagFromResource(model));
|
|
4360
|
+
}
|
|
4361
|
+
return response;
|
|
4362
|
+
};
|
|
4363
|
+
}
|
|
4364
|
+
function securedBindRouteModelByKey(param, resolver, authorization, handler) {
|
|
2239
4365
|
return async (request) => {
|
|
2240
|
-
const
|
|
2241
|
-
|
|
4366
|
+
const key = String(request.params[param] ?? "").trim();
|
|
4367
|
+
if (!key) {
|
|
4368
|
+
throw new BadRequestError(`Missing route parameter "${String(param)}".`);
|
|
4369
|
+
}
|
|
4370
|
+
const model = await resolver(key, request);
|
|
2242
4371
|
const gate = resolveApplicationPolicyGate();
|
|
2243
4372
|
const auth2 = resolveApplicationAuth();
|
|
2244
4373
|
gate.authorize(authorization.resource, authorization.action, await auth2.resolve(request), model);
|
|
@@ -2268,16 +4397,79 @@ var ALLOWED_UPLOAD_MIME_TYPES = new Set([
|
|
|
2268
4397
|
"text/plain",
|
|
2269
4398
|
"text/csv"
|
|
2270
4399
|
]);
|
|
4400
|
+
function resolveMaxUploadBytes() {
|
|
4401
|
+
const raw = process.env.MAX_UPLOAD_BYTES?.trim() ?? process.env.MAX_REQUEST_BODY_BYTES?.trim();
|
|
4402
|
+
if (!raw) {
|
|
4403
|
+
return DEFAULT_MAX_UPLOAD_BYTES;
|
|
4404
|
+
}
|
|
4405
|
+
const parsed = Number.parseInt(raw, 10);
|
|
4406
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
4407
|
+
return DEFAULT_MAX_UPLOAD_BYTES;
|
|
4408
|
+
}
|
|
4409
|
+
return parsed;
|
|
4410
|
+
}
|
|
4411
|
+
function normalizeMimeType(mimeType) {
|
|
4412
|
+
return mimeType.split(";")[0]?.trim().toLowerCase() ?? "";
|
|
4413
|
+
}
|
|
4414
|
+
function isAllowedMimeType(mimeType) {
|
|
4415
|
+
const normalized = normalizeMimeType(mimeType);
|
|
4416
|
+
if (!normalized || normalized === "application/octet-stream") {
|
|
4417
|
+
return true;
|
|
4418
|
+
}
|
|
4419
|
+
return ALLOWED_UPLOAD_MIME_TYPES.has(normalized);
|
|
4420
|
+
}
|
|
4421
|
+
|
|
4422
|
+
// ../../src/core/http/parseMultipartUpload.ts
|
|
4423
|
+
function normalizeMimeType2(mimeType) {
|
|
4424
|
+
return mimeType.split(";")[0]?.trim().toLowerCase() || "application/octet-stream";
|
|
4425
|
+
}
|
|
4426
|
+
function sanitizeUploadFileName(name) {
|
|
4427
|
+
const base = name.split(/[/\\]/).pop()?.trim() ?? "upload";
|
|
4428
|
+
const sanitized = base.replace(/[^\w.\-()+ ]+/g, "_").slice(0, 200);
|
|
4429
|
+
return sanitized.length > 0 ? sanitized : "upload";
|
|
4430
|
+
}
|
|
4431
|
+
async function parseMultipartUpload(request, fieldName = "file") {
|
|
4432
|
+
const contentType = request.headers.get("content-type")?.toLowerCase() ?? "";
|
|
4433
|
+
if (!contentType.includes("multipart/form-data")) {
|
|
4434
|
+
throw new BadRequestError("Expected multipart form data.");
|
|
4435
|
+
}
|
|
4436
|
+
const formData = await request.formData();
|
|
4437
|
+
const value = formData.get(fieldName);
|
|
4438
|
+
if (!(value instanceof File)) {
|
|
4439
|
+
throw new BadRequestError(`Missing upload field "${fieldName}".`);
|
|
4440
|
+
}
|
|
4441
|
+
if (value.size <= 0) {
|
|
4442
|
+
throw new BadRequestError("Uploaded file is empty.");
|
|
4443
|
+
}
|
|
4444
|
+
const maxBytes = resolveMaxUploadBytes();
|
|
4445
|
+
if (value.size > maxBytes) {
|
|
4446
|
+
throw new PayloadTooLargeError(`Upload exceeds the ${maxBytes} byte limit.`);
|
|
4447
|
+
}
|
|
4448
|
+
const mimeType = normalizeMimeType2(value.type.trim() || "application/octet-stream");
|
|
4449
|
+
if (!isAllowedMimeType(mimeType)) {
|
|
4450
|
+
throw new BadRequestError(`File type "${mimeType}" is not allowed.`);
|
|
4451
|
+
}
|
|
4452
|
+
return {
|
|
4453
|
+
fileName: sanitizeUploadFileName(value.name),
|
|
4454
|
+
mimeType,
|
|
4455
|
+
size: value.size,
|
|
4456
|
+
contents: new Uint8Array(await value.arrayBuffer())
|
|
4457
|
+
};
|
|
4458
|
+
}
|
|
4459
|
+
// ../../src/core/http/route.ts
|
|
4460
|
+
function getRouteParams(request) {
|
|
4461
|
+
return request.params;
|
|
4462
|
+
}
|
|
2271
4463
|
|
|
2272
4464
|
// ../../src/core/http/index.ts
|
|
2273
|
-
function
|
|
4465
|
+
function jsonResponse2(data, init = {}) {
|
|
2274
4466
|
return Response.json(data, {
|
|
2275
4467
|
status: init.status ?? 200,
|
|
2276
4468
|
headers: init.headers
|
|
2277
4469
|
});
|
|
2278
4470
|
}
|
|
2279
4471
|
function createdResponse(data, init = {}) {
|
|
2280
|
-
return
|
|
4472
|
+
return jsonResponse2(data, { ...init, status: init.status ?? 201 });
|
|
2281
4473
|
}
|
|
2282
4474
|
function noContentResponse() {
|
|
2283
4475
|
return new Response(null, { status: 204 });
|
|
@@ -2303,6 +4495,73 @@ function withErrorHandling(handler) {
|
|
|
2303
4495
|
}
|
|
2304
4496
|
};
|
|
2305
4497
|
}
|
|
4498
|
+
// ../../src/core/http/loginThrottleMiddleware.ts
|
|
4499
|
+
var {RedisClient } = globalThis.Bun;
|
|
4500
|
+
function resolveLoginIdentity(request) {
|
|
4501
|
+
return request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? "unknown";
|
|
4502
|
+
}
|
|
4503
|
+
async function resolveLoginEmail(request) {
|
|
4504
|
+
const contentType = request.headers.get("content-type")?.toLowerCase() ?? "";
|
|
4505
|
+
try {
|
|
4506
|
+
if (contentType.includes("application/x-www-form-urlencoded") || contentType.includes("multipart/form-data")) {
|
|
4507
|
+
const formData = await request.clone().formData();
|
|
4508
|
+
const email = formData.get("email");
|
|
4509
|
+
return typeof email === "string" ? email.trim().toLowerCase() : "unknown";
|
|
4510
|
+
}
|
|
4511
|
+
const payload = await request.clone().json();
|
|
4512
|
+
return typeof payload.email === "string" ? payload.email.trim().toLowerCase() : "unknown";
|
|
4513
|
+
} catch {
|
|
4514
|
+
return "unknown";
|
|
4515
|
+
}
|
|
4516
|
+
}
|
|
4517
|
+
function createLoginThrottleMiddleware(options) {
|
|
4518
|
+
const client = new RedisClient(options.redisUrl);
|
|
4519
|
+
const prefix = options.keyPrefix ?? "workhub:login-throttle:";
|
|
4520
|
+
return async (request, next) => {
|
|
4521
|
+
const identity = resolveLoginIdentity(request);
|
|
4522
|
+
const email = await resolveLoginEmail(request);
|
|
4523
|
+
const throttleKey = `${prefix}${identity}:${email}`;
|
|
4524
|
+
const attempts = Number(await client.incr(throttleKey));
|
|
4525
|
+
if (attempts === 1) {
|
|
4526
|
+
await client.expire(throttleKey, options.decaySeconds);
|
|
4527
|
+
}
|
|
4528
|
+
if (attempts > options.maxAttempts) {
|
|
4529
|
+
return Response.json({ error: "Too many login attempts. Try again later." }, {
|
|
4530
|
+
status: 429,
|
|
4531
|
+
headers: {
|
|
4532
|
+
"retry-after": String(options.decaySeconds)
|
|
4533
|
+
}
|
|
4534
|
+
});
|
|
4535
|
+
}
|
|
4536
|
+
return await next();
|
|
4537
|
+
};
|
|
4538
|
+
}
|
|
4539
|
+
// ../../src/core/http/memoryThrottleMiddleware.ts
|
|
4540
|
+
var buckets = new Map;
|
|
4541
|
+
function createMemoryThrottleMiddleware(options) {
|
|
4542
|
+
const prefix = options.keyPrefix ?? "workhub:memory-throttle:";
|
|
4543
|
+
return async (request, next) => {
|
|
4544
|
+
const path = new URL(request.url).pathname;
|
|
4545
|
+
const identity = request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? request.headers.get("authorization")?.slice(0, 32) ?? "unknown";
|
|
4546
|
+
const key = `${prefix}${identity}:${path}`;
|
|
4547
|
+
const now = Date.now();
|
|
4548
|
+
const existing = buckets.get(key);
|
|
4549
|
+
if (!existing || existing.resetAt <= now) {
|
|
4550
|
+
buckets.set(key, { count: 1, resetAt: now + options.decaySeconds * 1000 });
|
|
4551
|
+
return await next();
|
|
4552
|
+
}
|
|
4553
|
+
existing.count += 1;
|
|
4554
|
+
if (existing.count > options.maxAttempts) {
|
|
4555
|
+
return Response.json({ error: "Too many requests." }, {
|
|
4556
|
+
status: 429,
|
|
4557
|
+
headers: {
|
|
4558
|
+
"retry-after": String(options.decaySeconds)
|
|
4559
|
+
}
|
|
4560
|
+
});
|
|
4561
|
+
}
|
|
4562
|
+
return await next();
|
|
4563
|
+
};
|
|
4564
|
+
}
|
|
2306
4565
|
// ../../src/core/metrics/prometheus.ts
|
|
2307
4566
|
class PrometheusRegistry {
|
|
2308
4567
|
httpRequestsTotal = new Map;
|
|
@@ -2389,6 +4648,128 @@ function createMetricsMiddleware() {
|
|
|
2389
4648
|
return response;
|
|
2390
4649
|
};
|
|
2391
4650
|
}
|
|
4651
|
+
// ../../src/core/http/requireWebAuthMiddleware.ts
|
|
4652
|
+
function createRequireWebAuthMiddleware(auth2) {
|
|
4653
|
+
return async (request, next) => {
|
|
4654
|
+
const user = await auth2.resolve(request);
|
|
4655
|
+
if (user) {
|
|
4656
|
+
return await next();
|
|
4657
|
+
}
|
|
4658
|
+
if (requestPrefersJson(request)) {
|
|
4659
|
+
throw new UnauthorizedError;
|
|
4660
|
+
}
|
|
4661
|
+
const redirectTarget = encodeURIComponent(new URL(request.url).pathname);
|
|
4662
|
+
return Response.redirect(`/login?redirect=${redirectTarget}`, 302);
|
|
4663
|
+
};
|
|
4664
|
+
}
|
|
4665
|
+
// ../../src/config/app.ts
|
|
4666
|
+
var appConfig = {
|
|
4667
|
+
name: "WorkHub",
|
|
4668
|
+
env: process.env.APP_ENV ?? "local",
|
|
4669
|
+
debug: (process.env.APP_DEBUG ?? "true") !== "false",
|
|
4670
|
+
url: process.env.APP_URL ?? "http://localhost:3000",
|
|
4671
|
+
apiPrefix: process.env.API_PREFIX ?? "/api/v1"
|
|
4672
|
+
};
|
|
4673
|
+
|
|
4674
|
+
// ../../src/config/contentSecurityPolicy.ts
|
|
4675
|
+
function strictApiContentSecurityPolicy() {
|
|
4676
|
+
return "default-src 'none'; frame-ancestors 'none'; base-uri 'none'";
|
|
4677
|
+
}
|
|
4678
|
+
function serverHtmxContentSecurityPolicy() {
|
|
4679
|
+
return [
|
|
4680
|
+
"default-src 'self'",
|
|
4681
|
+
"script-src 'self' https://unpkg.com",
|
|
4682
|
+
"style-src 'self'",
|
|
4683
|
+
"connect-src 'self'",
|
|
4684
|
+
"img-src 'self'",
|
|
4685
|
+
"font-src 'self'",
|
|
4686
|
+
"form-action 'self'",
|
|
4687
|
+
"frame-ancestors 'none'",
|
|
4688
|
+
"base-uri 'self'"
|
|
4689
|
+
].join("; ");
|
|
4690
|
+
}
|
|
4691
|
+
function spaContentSecurityPolicy() {
|
|
4692
|
+
return [
|
|
4693
|
+
"default-src 'self'",
|
|
4694
|
+
"script-src 'self'",
|
|
4695
|
+
"style-src 'self'",
|
|
4696
|
+
"connect-src 'self'",
|
|
4697
|
+
"img-src 'self'",
|
|
4698
|
+
"font-src 'self'",
|
|
4699
|
+
"frame-ancestors 'none'",
|
|
4700
|
+
"base-uri 'self'"
|
|
4701
|
+
].join("; ");
|
|
4702
|
+
}
|
|
4703
|
+
function resolveContentSecurityPolicy(response) {
|
|
4704
|
+
const contentType = response.headers.get("content-type")?.toLowerCase() ?? "";
|
|
4705
|
+
if (!contentType.includes("text/html")) {
|
|
4706
|
+
return strictApiContentSecurityPolicy();
|
|
4707
|
+
}
|
|
4708
|
+
const frontendMode = (process.env.FRONTEND_MODE ?? "api").trim();
|
|
4709
|
+
if (frontendMode === "server-htmx") {
|
|
4710
|
+
return serverHtmxContentSecurityPolicy();
|
|
4711
|
+
}
|
|
4712
|
+
if (frontendMode === "spa-react") {
|
|
4713
|
+
return spaContentSecurityPolicy();
|
|
4714
|
+
}
|
|
4715
|
+
return strictApiContentSecurityPolicy();
|
|
4716
|
+
}
|
|
4717
|
+
|
|
4718
|
+
// ../../src/core/http/securityHeadersMiddleware.ts
|
|
4719
|
+
function createSecurityHeadersMiddleware() {
|
|
4720
|
+
return async (_request, next) => {
|
|
4721
|
+
const response = await next();
|
|
4722
|
+
const headers = new Headers(response.headers);
|
|
4723
|
+
headers.set("X-Content-Type-Options", "nosniff");
|
|
4724
|
+
headers.set("X-Frame-Options", "DENY");
|
|
4725
|
+
headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
|
|
4726
|
+
headers.set("X-XSS-Protection", "0");
|
|
4727
|
+
headers.set("Content-Security-Policy", resolveContentSecurityPolicy(response));
|
|
4728
|
+
if (appConfig.env === "production") {
|
|
4729
|
+
headers.set("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
|
|
4730
|
+
}
|
|
4731
|
+
return new Response(response.body, {
|
|
4732
|
+
status: response.status,
|
|
4733
|
+
statusText: response.statusText,
|
|
4734
|
+
headers
|
|
4735
|
+
});
|
|
4736
|
+
};
|
|
4737
|
+
}
|
|
4738
|
+
// ../../src/core/http/throttleMiddleware.ts
|
|
4739
|
+
var {RedisClient: RedisClient2 } = globalThis.Bun;
|
|
4740
|
+
function resolveThrottleIdentity(request) {
|
|
4741
|
+
const user = currentAuthUser();
|
|
4742
|
+
if (user?.tokenId !== undefined) {
|
|
4743
|
+
return `token:${user.tokenId}`;
|
|
4744
|
+
}
|
|
4745
|
+
if (user) {
|
|
4746
|
+
return `user:${user.id}`;
|
|
4747
|
+
}
|
|
4748
|
+
return request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? "unknown";
|
|
4749
|
+
}
|
|
4750
|
+
function createThrottleMiddleware(options) {
|
|
4751
|
+
const client = new RedisClient2(options.redisUrl);
|
|
4752
|
+
const prefix = options.keyPrefix ?? "workhub:throttle:";
|
|
4753
|
+
return async (request, next) => {
|
|
4754
|
+
const identity = resolveThrottleIdentity(request);
|
|
4755
|
+
const path = new URL(request.url).pathname;
|
|
4756
|
+
const throttleKey = `${prefix}${identity}:${path}`;
|
|
4757
|
+
const attempts = Number(await client.incr(throttleKey));
|
|
4758
|
+
if (attempts === 1) {
|
|
4759
|
+
await client.expire(throttleKey, options.decaySeconds);
|
|
4760
|
+
}
|
|
4761
|
+
const maxAttempts = options.maxAttempts * rateLimitMultiplierForPlan(currentTenant()?.plan ?? "free");
|
|
4762
|
+
if (attempts > maxAttempts) {
|
|
4763
|
+
return Response.json({ error: "Too many requests." }, {
|
|
4764
|
+
status: 429,
|
|
4765
|
+
headers: {
|
|
4766
|
+
"retry-after": String(options.decaySeconds)
|
|
4767
|
+
}
|
|
4768
|
+
});
|
|
4769
|
+
}
|
|
4770
|
+
return await next();
|
|
4771
|
+
};
|
|
4772
|
+
}
|
|
2392
4773
|
// ../../src/core/http/parseFormBody.ts
|
|
2393
4774
|
async function parseFormBody(request) {
|
|
2394
4775
|
const contentType = request.headers.get("content-type")?.toLowerCase() ?? "";
|
|
@@ -2430,6 +4811,49 @@ class WebFormRequest {
|
|
|
2430
4811
|
}
|
|
2431
4812
|
}
|
|
2432
4813
|
}
|
|
4814
|
+
// ../../src/core/lifecycle/gracefulShutdown.ts
|
|
4815
|
+
var shutdownHandlers = new Map;
|
|
4816
|
+
var shutdownInstalled = false;
|
|
4817
|
+
var shuttingDown = false;
|
|
4818
|
+
function registerShutdownHandler(name, handler) {
|
|
4819
|
+
shutdownHandlers.set(name, handler);
|
|
4820
|
+
return () => {
|
|
4821
|
+
shutdownHandlers.delete(name);
|
|
4822
|
+
};
|
|
4823
|
+
}
|
|
4824
|
+
async function runGracefulShutdown(signal) {
|
|
4825
|
+
if (shuttingDown) {
|
|
4826
|
+
return;
|
|
4827
|
+
}
|
|
4828
|
+
shuttingDown = true;
|
|
4829
|
+
console.log(`[shutdown] Received ${signal}, draining ${shutdownHandlers.size} handler(s)...`);
|
|
4830
|
+
for (const [name, handler] of shutdownHandlers) {
|
|
4831
|
+
try {
|
|
4832
|
+
await handler();
|
|
4833
|
+
console.log(`[shutdown] Completed ${name}`);
|
|
4834
|
+
} catch (error) {
|
|
4835
|
+
console.error(`[shutdown] Failed ${name}:`, error);
|
|
4836
|
+
}
|
|
4837
|
+
}
|
|
4838
|
+
}
|
|
4839
|
+
function installGracefulShutdownSignals(signals = ["SIGINT", "SIGTERM"]) {
|
|
4840
|
+
if (shutdownInstalled) {
|
|
4841
|
+
return;
|
|
4842
|
+
}
|
|
4843
|
+
shutdownInstalled = true;
|
|
4844
|
+
for (const signal of signals) {
|
|
4845
|
+
process.on(signal, () => {
|
|
4846
|
+
runGracefulShutdown(signal).finally(() => {
|
|
4847
|
+
process.exit(0);
|
|
4848
|
+
});
|
|
4849
|
+
});
|
|
4850
|
+
}
|
|
4851
|
+
}
|
|
4852
|
+
function resetGracefulShutdownForTests() {
|
|
4853
|
+
shutdownHandlers.clear();
|
|
4854
|
+
shutdownInstalled = false;
|
|
4855
|
+
shuttingDown = false;
|
|
4856
|
+
}
|
|
2433
4857
|
// ../../src/core/queue/index.ts
|
|
2434
4858
|
class Job {
|
|
2435
4859
|
maxAttempts;
|
|
@@ -2455,6 +4879,333 @@ class AsyncQueue {
|
|
|
2455
4879
|
function createQueue(driver) {
|
|
2456
4880
|
return driver === "async" ? new AsyncQueue : new SyncQueue;
|
|
2457
4881
|
}
|
|
4882
|
+
// ../../src/core/queue/failedJobTable.ts
|
|
4883
|
+
var failedJobTable = defineTable({
|
|
4884
|
+
name: "failed_job",
|
|
4885
|
+
primaryKey: "id",
|
|
4886
|
+
columns: ["id", "job_name", "payload", "exception", "failed_at"],
|
|
4887
|
+
defaultOrderBy: { column: "failed_at", direction: "DESC" }
|
|
4888
|
+
});
|
|
4889
|
+
|
|
4890
|
+
// ../../src/core/queue/failedJobRepository.ts
|
|
4891
|
+
class FailedJobRepository extends baseRepository_default {
|
|
4892
|
+
constructor() {
|
|
4893
|
+
super(failedJobTable);
|
|
4894
|
+
}
|
|
4895
|
+
}
|
|
4896
|
+
var failedJobRepository_default = FailedJobRepository;
|
|
4897
|
+
|
|
4898
|
+
// ../../src/core/queue/failedJobService.ts
|
|
4899
|
+
class FailedJobService {
|
|
4900
|
+
repository;
|
|
4901
|
+
constructor(repository) {
|
|
4902
|
+
this.repository = repository;
|
|
4903
|
+
}
|
|
4904
|
+
async recordFailure(input) {
|
|
4905
|
+
return await this.repository.create({
|
|
4906
|
+
job_name: input.jobName,
|
|
4907
|
+
payload: input.payload,
|
|
4908
|
+
exception: input.exception,
|
|
4909
|
+
failed_at: new Date
|
|
4910
|
+
});
|
|
4911
|
+
}
|
|
4912
|
+
listRecent(limit = 50) {
|
|
4913
|
+
return this.repository.findAll({
|
|
4914
|
+
limit,
|
|
4915
|
+
orderBy: { column: "failed_at", direction: "DESC" }
|
|
4916
|
+
});
|
|
4917
|
+
}
|
|
4918
|
+
async retry(id) {
|
|
4919
|
+
const failedJob = await this.repository.findByIdOrThrow(id, (jobId) => new Error(`Failed job ${jobId} not found.`));
|
|
4920
|
+
await this.repository.deleteById(id);
|
|
4921
|
+
return failedJob;
|
|
4922
|
+
}
|
|
4923
|
+
async flush() {
|
|
4924
|
+
const jobs = await this.repository.findAll();
|
|
4925
|
+
let deleted = 0;
|
|
4926
|
+
for (const job of jobs) {
|
|
4927
|
+
if (await this.repository.deleteById(job.id)) {
|
|
4928
|
+
deleted += 1;
|
|
4929
|
+
}
|
|
4930
|
+
}
|
|
4931
|
+
return deleted;
|
|
4932
|
+
}
|
|
4933
|
+
}
|
|
4934
|
+
var failedJobService_default = FailedJobService;
|
|
4935
|
+
|
|
4936
|
+
// ../../src/core/queue/jobRegistry.ts
|
|
4937
|
+
class JobRegistry {
|
|
4938
|
+
constructor() {}
|
|
4939
|
+
factories = new Map;
|
|
4940
|
+
instances = new WeakMap;
|
|
4941
|
+
register(name, factory) {
|
|
4942
|
+
this.factories.set(name, factory);
|
|
4943
|
+
}
|
|
4944
|
+
resolveName(job) {
|
|
4945
|
+
return this.instances.get(job);
|
|
4946
|
+
}
|
|
4947
|
+
track(name, job) {
|
|
4948
|
+
this.instances.set(job, name);
|
|
4949
|
+
return job;
|
|
4950
|
+
}
|
|
4951
|
+
create(name) {
|
|
4952
|
+
const factory = this.factories.get(name);
|
|
4953
|
+
if (!factory) {
|
|
4954
|
+
return;
|
|
4955
|
+
}
|
|
4956
|
+
return factory();
|
|
4957
|
+
}
|
|
4958
|
+
names() {
|
|
4959
|
+
return [...this.factories.keys()];
|
|
4960
|
+
}
|
|
4961
|
+
}
|
|
4962
|
+
var jobRegistry = new JobRegistry;
|
|
4963
|
+
|
|
4964
|
+
// ../../src/core/queue/redisQueue.ts
|
|
4965
|
+
var {RedisClient: RedisClient3 } = globalThis.Bun;
|
|
4966
|
+
|
|
4967
|
+
// ../../src/config/queue.ts
|
|
4968
|
+
var queueConfig = {
|
|
4969
|
+
driver: process.env.QUEUE_DRIVER === "redis" || process.env.QUEUE_DRIVER === "async" || process.env.QUEUE_DRIVER === "sync" ? process.env.QUEUE_DRIVER : DEFAULT_QUEUE_DRIVER,
|
|
4970
|
+
maxAttempts: Number(process.env.QUEUE_MAX_ATTEMPTS ?? "3"),
|
|
4971
|
+
backoffMs: Number(process.env.QUEUE_BACKOFF_MS ?? "1000")
|
|
4972
|
+
};
|
|
4973
|
+
|
|
4974
|
+
// ../../src/core/queue/jobRunner.ts
|
|
4975
|
+
async function runQueueJob(envelope, failedJobs) {
|
|
4976
|
+
const job = jobRegistry.create(envelope.name);
|
|
4977
|
+
if (!job) {
|
|
4978
|
+
throw new Error(`Unknown job "${envelope.name}".`);
|
|
4979
|
+
}
|
|
4980
|
+
const attempts = envelope.attempts ?? 0;
|
|
4981
|
+
try {
|
|
4982
|
+
await job.handle(envelope.payload);
|
|
4983
|
+
} catch (error) {
|
|
4984
|
+
const nextAttempt = attempts + 1;
|
|
4985
|
+
const maxAttempts = job.maxAttempts ?? queueConfig.maxAttempts;
|
|
4986
|
+
if (nextAttempt < maxAttempts) {
|
|
4987
|
+
const backoffMs = job.backoffMs ?? queueConfig.backoffMs;
|
|
4988
|
+
await new Promise((resolve) => setTimeout(resolve, backoffMs * nextAttempt));
|
|
4989
|
+
await runQueueJob({
|
|
4990
|
+
...envelope,
|
|
4991
|
+
attempts: nextAttempt
|
|
4992
|
+
}, failedJobs);
|
|
4993
|
+
return;
|
|
4994
|
+
}
|
|
4995
|
+
await failedJobs.recordFailure({
|
|
4996
|
+
jobName: envelope.name,
|
|
4997
|
+
payload: envelope.payload,
|
|
4998
|
+
exception: error instanceof Error ? error.stack ?? error.message : String(error)
|
|
4999
|
+
});
|
|
5000
|
+
throw error;
|
|
5001
|
+
}
|
|
5002
|
+
}
|
|
5003
|
+
|
|
5004
|
+
// ../../src/core/queue/redisQueue.ts
|
|
5005
|
+
var QUEUE_LIST_KEY = "workhub:queue:default";
|
|
5006
|
+
var QUEUE_HIGH_KEY = "workhub:queue:high";
|
|
5007
|
+
var QUEUE_LOW_KEY = "workhub:queue:low";
|
|
5008
|
+
var QUEUE_KEYS = [QUEUE_HIGH_KEY, QUEUE_LIST_KEY, QUEUE_LOW_KEY];
|
|
5009
|
+
function queueKeyForPriority(priority = "default") {
|
|
5010
|
+
switch (priority) {
|
|
5011
|
+
case "high":
|
|
5012
|
+
return QUEUE_HIGH_KEY;
|
|
5013
|
+
case "low":
|
|
5014
|
+
return QUEUE_LOW_KEY;
|
|
5015
|
+
default:
|
|
5016
|
+
return QUEUE_LIST_KEY;
|
|
5017
|
+
}
|
|
5018
|
+
}
|
|
5019
|
+
function parseQueueJobEnvelope(rawPayload) {
|
|
5020
|
+
let parsed;
|
|
5021
|
+
try {
|
|
5022
|
+
parsed = JSON.parse(rawPayload);
|
|
5023
|
+
} catch {
|
|
5024
|
+
console.error("[QueueWorker] Ignoring malformed queue payload");
|
|
5025
|
+
return null;
|
|
5026
|
+
}
|
|
5027
|
+
if (!parsed || typeof parsed !== "object") {
|
|
5028
|
+
console.error("[QueueWorker] Ignoring non-object queue payload");
|
|
5029
|
+
return null;
|
|
5030
|
+
}
|
|
5031
|
+
const envelope = parsed;
|
|
5032
|
+
if (typeof envelope.name !== "string" || envelope.name.length === 0) {
|
|
5033
|
+
console.error("[QueueWorker] Ignoring queue payload without job name");
|
|
5034
|
+
return null;
|
|
5035
|
+
}
|
|
5036
|
+
if (!jobRegistry.create(envelope.name)) {
|
|
5037
|
+
console.error(`[QueueWorker] Ignoring unknown job name: ${envelope.name}`);
|
|
5038
|
+
return null;
|
|
5039
|
+
}
|
|
5040
|
+
if (envelope.payload !== undefined && (typeof envelope.payload !== "object" || envelope.payload === null)) {
|
|
5041
|
+
console.error("[QueueWorker] Ignoring queue payload with invalid payload object");
|
|
5042
|
+
return null;
|
|
5043
|
+
}
|
|
5044
|
+
return {
|
|
5045
|
+
name: envelope.name,
|
|
5046
|
+
payload: envelope.payload ?? {},
|
|
5047
|
+
attempts: typeof envelope.attempts === "number" ? envelope.attempts : 0
|
|
5048
|
+
};
|
|
5049
|
+
}
|
|
5050
|
+
|
|
5051
|
+
class RedisQueue {
|
|
5052
|
+
client;
|
|
5053
|
+
constructor(redisUrl) {
|
|
5054
|
+
this.client = new RedisClient3(redisUrl);
|
|
5055
|
+
}
|
|
5056
|
+
async dispatch(job, payload) {
|
|
5057
|
+
const name = jobRegistry.resolveName(job);
|
|
5058
|
+
if (!name) {
|
|
5059
|
+
throw new Error("Job is not registered with the queue worker registry.");
|
|
5060
|
+
}
|
|
5061
|
+
const envelope = {
|
|
5062
|
+
name,
|
|
5063
|
+
payload,
|
|
5064
|
+
attempts: 0
|
|
5065
|
+
};
|
|
5066
|
+
const queueKey = queueKeyForPriority(job.priority);
|
|
5067
|
+
await this.client.lpush(queueKey, JSON.stringify(envelope));
|
|
5068
|
+
}
|
|
5069
|
+
}
|
|
5070
|
+
|
|
5071
|
+
class QueueWorker {
|
|
5072
|
+
failedJobs;
|
|
5073
|
+
timeoutSeconds;
|
|
5074
|
+
running = false;
|
|
5075
|
+
stopping = false;
|
|
5076
|
+
client;
|
|
5077
|
+
constructor(redisUrl, failedJobs, timeoutSeconds = 5) {
|
|
5078
|
+
this.failedJobs = failedJobs;
|
|
5079
|
+
this.timeoutSeconds = timeoutSeconds;
|
|
5080
|
+
this.client = new RedisClient3(redisUrl);
|
|
5081
|
+
}
|
|
5082
|
+
requestStop() {
|
|
5083
|
+
this.stopping = true;
|
|
5084
|
+
}
|
|
5085
|
+
isRunning() {
|
|
5086
|
+
return this.running;
|
|
5087
|
+
}
|
|
5088
|
+
async processNext() {
|
|
5089
|
+
let result = null;
|
|
5090
|
+
for (const queueKey of QUEUE_KEYS) {
|
|
5091
|
+
result = await this.client.brpop(queueKey, 1);
|
|
5092
|
+
if (result) {
|
|
5093
|
+
break;
|
|
5094
|
+
}
|
|
5095
|
+
}
|
|
5096
|
+
if (!result) {
|
|
5097
|
+
result = await this.client.brpop(QUEUE_LIST_KEY, this.timeoutSeconds);
|
|
5098
|
+
}
|
|
5099
|
+
if (!result) {
|
|
5100
|
+
return false;
|
|
5101
|
+
}
|
|
5102
|
+
const [, rawPayload] = result;
|
|
5103
|
+
const envelope = parseQueueJobEnvelope(rawPayload);
|
|
5104
|
+
if (!envelope) {
|
|
5105
|
+
return true;
|
|
5106
|
+
}
|
|
5107
|
+
try {
|
|
5108
|
+
await runQueueJob(envelope, this.failedJobs);
|
|
5109
|
+
} catch (error) {
|
|
5110
|
+
console.error("[QueueWorker] Job failed:", error);
|
|
5111
|
+
}
|
|
5112
|
+
return true;
|
|
5113
|
+
}
|
|
5114
|
+
async run() {
|
|
5115
|
+
this.running = true;
|
|
5116
|
+
while (!this.stopping) {
|
|
5117
|
+
await this.processNext();
|
|
5118
|
+
}
|
|
5119
|
+
this.running = false;
|
|
5120
|
+
}
|
|
5121
|
+
close() {
|
|
5122
|
+
this.client.close();
|
|
5123
|
+
}
|
|
5124
|
+
}
|
|
5125
|
+
|
|
5126
|
+
// ../../src/core/queue/resilientQueue.ts
|
|
5127
|
+
class ResilientQueue {
|
|
5128
|
+
failedJobs;
|
|
5129
|
+
asyncDispatch;
|
|
5130
|
+
constructor(failedJobs, asyncDispatch = false) {
|
|
5131
|
+
this.failedJobs = failedJobs;
|
|
5132
|
+
this.asyncDispatch = asyncDispatch;
|
|
5133
|
+
}
|
|
5134
|
+
async dispatch(job, payload) {
|
|
5135
|
+
const name = jobRegistry.resolveName(job);
|
|
5136
|
+
if (!name) {
|
|
5137
|
+
throw new Error("Job is not registered with the queue worker registry.");
|
|
5138
|
+
}
|
|
5139
|
+
const envelope = {
|
|
5140
|
+
name,
|
|
5141
|
+
payload,
|
|
5142
|
+
attempts: 0
|
|
5143
|
+
};
|
|
5144
|
+
if (this.asyncDispatch) {
|
|
5145
|
+
setTimeout(() => {
|
|
5146
|
+
runQueueJob(envelope, this.failedJobs).catch((error) => {
|
|
5147
|
+
console.error("[ResilientQueue] Job failed:", error);
|
|
5148
|
+
});
|
|
5149
|
+
}, 0);
|
|
5150
|
+
return;
|
|
5151
|
+
}
|
|
5152
|
+
await runQueueJob(envelope, this.failedJobs);
|
|
5153
|
+
}
|
|
5154
|
+
}
|
|
5155
|
+
|
|
5156
|
+
// ../../src/core/queue/publicQueue.ts
|
|
5157
|
+
function createFailedJobService() {
|
|
5158
|
+
return new failedJobService_default(new failedJobRepository_default);
|
|
5159
|
+
}
|
|
5160
|
+
function createTrackedJob(name, job) {
|
|
5161
|
+
return jobRegistry.track(name, job);
|
|
5162
|
+
}
|
|
5163
|
+
function createProductionQueue(driver, options = {}) {
|
|
5164
|
+
options.registerJobs?.();
|
|
5165
|
+
const failedJobs = options.failedJobs ?? createFailedJobService();
|
|
5166
|
+
if (driver === "redis") {
|
|
5167
|
+
if (!options.redisUrl) {
|
|
5168
|
+
throw new Error('QUEUE_DRIVER="redis" requires REDIS_URL to be set.');
|
|
5169
|
+
}
|
|
5170
|
+
return new RedisQueue(options.redisUrl);
|
|
5171
|
+
}
|
|
5172
|
+
return new ResilientQueue(failedJobs, driver === "async");
|
|
5173
|
+
}
|
|
5174
|
+
function createQueueWorker(redisUrl, failedJobs = createFailedJobService()) {
|
|
5175
|
+
return new QueueWorker(redisUrl, failedJobs);
|
|
5176
|
+
}
|
|
5177
|
+
// ../../src/core/scheduler/schedule.ts
|
|
5178
|
+
class Schedule {
|
|
5179
|
+
tasks = [];
|
|
5180
|
+
command(expression, name, run) {
|
|
5181
|
+
this.tasks.push({ expression, name, run });
|
|
5182
|
+
return this;
|
|
5183
|
+
}
|
|
5184
|
+
dueTasks(now = new Date) {
|
|
5185
|
+
const minute = now.getMinutes();
|
|
5186
|
+
return this.tasks.filter((task) => {
|
|
5187
|
+
if (task.expression === "* * * * *") {
|
|
5188
|
+
return true;
|
|
5189
|
+
}
|
|
5190
|
+
if (task.expression.startsWith("*/")) {
|
|
5191
|
+
const interval = Number.parseInt(task.expression.slice(2), 10);
|
|
5192
|
+
return Number.isInteger(interval) && interval > 0 && minute % interval === 0;
|
|
5193
|
+
}
|
|
5194
|
+
return false;
|
|
5195
|
+
});
|
|
5196
|
+
}
|
|
5197
|
+
tasksList() {
|
|
5198
|
+
return [...this.tasks];
|
|
5199
|
+
}
|
|
5200
|
+
}
|
|
5201
|
+
var appSchedule = new Schedule;
|
|
5202
|
+
async function runDueScheduledTasks(schedule = appSchedule, now = new Date) {
|
|
5203
|
+
const due = schedule.dueTasks(now);
|
|
5204
|
+
for (const task of due) {
|
|
5205
|
+
await task.run();
|
|
5206
|
+
}
|
|
5207
|
+
return due.length;
|
|
5208
|
+
}
|
|
2458
5209
|
// ../../src/core/validation/rules.ts
|
|
2459
5210
|
function required() {
|
|
2460
5211
|
return (field, value) => {
|
|
@@ -2497,6 +5248,45 @@ function maxLength(maximum) {
|
|
|
2497
5248
|
return;
|
|
2498
5249
|
};
|
|
2499
5250
|
}
|
|
5251
|
+
function pattern(expression) {
|
|
5252
|
+
return (field, value) => {
|
|
5253
|
+
if (typeof value !== "string") {
|
|
5254
|
+
return;
|
|
5255
|
+
}
|
|
5256
|
+
if (!expression.test(value.trim())) {
|
|
5257
|
+
return `"${field}" has an invalid format.`;
|
|
5258
|
+
}
|
|
5259
|
+
return;
|
|
5260
|
+
};
|
|
5261
|
+
}
|
|
5262
|
+
function enumRule(allowedValues) {
|
|
5263
|
+
return (field, value) => {
|
|
5264
|
+
if (typeof value !== "string") {
|
|
5265
|
+
return;
|
|
5266
|
+
}
|
|
5267
|
+
if (!allowedValues.includes(value)) {
|
|
5268
|
+
return `"${field}" must be one of: ${allowedValues.join(", ")}.`;
|
|
5269
|
+
}
|
|
5270
|
+
return;
|
|
5271
|
+
};
|
|
5272
|
+
}
|
|
5273
|
+
function optional() {
|
|
5274
|
+
return () => {
|
|
5275
|
+
return;
|
|
5276
|
+
};
|
|
5277
|
+
}
|
|
5278
|
+
function integerRule() {
|
|
5279
|
+
return (field, value) => {
|
|
5280
|
+
if (value === undefined || value === null || value === "") {
|
|
5281
|
+
return;
|
|
5282
|
+
}
|
|
5283
|
+
const parsed = typeof value === "number" ? value : Number.parseInt(String(value), 10);
|
|
5284
|
+
if (!Number.isInteger(parsed)) {
|
|
5285
|
+
return `"${field}" must be an integer.`;
|
|
5286
|
+
}
|
|
5287
|
+
return;
|
|
5288
|
+
};
|
|
5289
|
+
}
|
|
2500
5290
|
function emailRule() {
|
|
2501
5291
|
return (field, value) => {
|
|
2502
5292
|
if (typeof value !== "string") {
|
|
@@ -2509,6 +5299,40 @@ function emailRule() {
|
|
|
2509
5299
|
return;
|
|
2510
5300
|
};
|
|
2511
5301
|
}
|
|
5302
|
+
function confirmed(fieldName) {
|
|
5303
|
+
return (field, value, payload) => {
|
|
5304
|
+
const confirmationKey = `${fieldName}_confirmation`;
|
|
5305
|
+
const confirmation = payload[confirmationKey];
|
|
5306
|
+
if (value !== confirmation) {
|
|
5307
|
+
return `"${field}" confirmation does not match.`;
|
|
5308
|
+
}
|
|
5309
|
+
return;
|
|
5310
|
+
};
|
|
5311
|
+
}
|
|
5312
|
+
function positiveIntegerRule() {
|
|
5313
|
+
return (field, value) => {
|
|
5314
|
+
if (value === undefined || value === null || value === "") {
|
|
5315
|
+
return;
|
|
5316
|
+
}
|
|
5317
|
+
const parsed = typeof value === "number" ? value : Number.parseInt(String(value), 10);
|
|
5318
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
5319
|
+
return `"${field}" must be a positive integer.`;
|
|
5320
|
+
}
|
|
5321
|
+
return;
|
|
5322
|
+
};
|
|
5323
|
+
}
|
|
5324
|
+
function integerRange(minimum, maximum) {
|
|
5325
|
+
return (field, value) => {
|
|
5326
|
+
if (value === undefined || value === null || value === "") {
|
|
5327
|
+
return;
|
|
5328
|
+
}
|
|
5329
|
+
const parsed = typeof value === "number" ? value : Number.parseInt(String(value), 10);
|
|
5330
|
+
if (!Number.isInteger(parsed) || parsed < minimum || parsed > maximum) {
|
|
5331
|
+
return `"${field}" must be an integer between ${minimum} and ${maximum}.`;
|
|
5332
|
+
}
|
|
5333
|
+
return;
|
|
5334
|
+
};
|
|
5335
|
+
}
|
|
2512
5336
|
function validateObject(payload, schema) {
|
|
2513
5337
|
if (payload === null || typeof payload !== "object" || Array.isArray(payload)) {
|
|
2514
5338
|
throw new ValidationError("Request body must be a JSON object.");
|
|
@@ -2533,15 +5357,29 @@ function validateObject(payload, schema) {
|
|
|
2533
5357
|
return output;
|
|
2534
5358
|
}
|
|
2535
5359
|
export {
|
|
5360
|
+
withMigrationLock,
|
|
5361
|
+
withMiddleware,
|
|
2536
5362
|
withErrorHandling,
|
|
2537
5363
|
validateObject,
|
|
5364
|
+
toResourceCollection,
|
|
5365
|
+
toPaginatedResourceCollection,
|
|
2538
5366
|
stringRule,
|
|
2539
5367
|
storageFacade as storage,
|
|
5368
|
+
serializeDate,
|
|
5369
|
+
securedBindRouteModelByKey,
|
|
2540
5370
|
securedBindRouteModel,
|
|
5371
|
+
runWithDatabaseConnection,
|
|
2541
5372
|
runWithAuthUser,
|
|
5373
|
+
runSeedersFromDirectory,
|
|
5374
|
+
runInTransaction,
|
|
5375
|
+
runGracefulShutdown,
|
|
5376
|
+
runDueScheduledTasks,
|
|
2542
5377
|
rollbackDatabase,
|
|
5378
|
+
resolveWebLayoutData,
|
|
2543
5379
|
resolveService,
|
|
5380
|
+
resolveDatabaseDriver,
|
|
2544
5381
|
required,
|
|
5382
|
+
registerShutdownHandler,
|
|
2545
5383
|
registerModelRepository,
|
|
2546
5384
|
queue,
|
|
2547
5385
|
prometheusRegistry,
|
|
@@ -2556,53 +5394,105 @@ export {
|
|
|
2556
5394
|
mailer,
|
|
2557
5395
|
mail,
|
|
2558
5396
|
log,
|
|
5397
|
+
loadSeedersFromDirectory,
|
|
2559
5398
|
loadMigrationsFromDirectory,
|
|
2560
|
-
jsonResponse,
|
|
5399
|
+
jsonResponse2 as jsonResponse,
|
|
5400
|
+
jobRegistry,
|
|
5401
|
+
isHtmxRequest,
|
|
2561
5402
|
isEtagEnabled,
|
|
5403
|
+
installGracefulShutdownSignals,
|
|
5404
|
+
inferReferencedTable,
|
|
5405
|
+
indexHasOneRelation,
|
|
2562
5406
|
indexHasManyRelation,
|
|
2563
5407
|
indexBelongsToRelation,
|
|
5408
|
+
indexBelongsToManyRelation,
|
|
5409
|
+
hydrateValue,
|
|
5410
|
+
htmlResponse,
|
|
5411
|
+
hasOne,
|
|
2564
5412
|
hasMany,
|
|
5413
|
+
grammarForDriver,
|
|
2565
5414
|
getMigrationStatus,
|
|
5415
|
+
getActiveDatabaseConnection,
|
|
2566
5416
|
freshDatabase,
|
|
5417
|
+
filterMassAssignable,
|
|
2567
5418
|
events,
|
|
2568
5419
|
etagFromResource,
|
|
2569
5420
|
emailRule,
|
|
5421
|
+
dehydrateValue,
|
|
2570
5422
|
defineTable,
|
|
2571
5423
|
currentAuthUser,
|
|
2572
5424
|
createdResponse,
|
|
5425
|
+
createTrackedJob,
|
|
5426
|
+
createThrottleMiddleware,
|
|
5427
|
+
createSecurityHeadersMiddleware,
|
|
5428
|
+
createSchemaBuilder,
|
|
5429
|
+
createRequireWebAuthMiddleware,
|
|
5430
|
+
createRequireAuthMiddleware,
|
|
5431
|
+
createQueueWorker,
|
|
2573
5432
|
createQueue,
|
|
5433
|
+
createProductionQueue,
|
|
2574
5434
|
createMetricsMiddleware,
|
|
5435
|
+
createMemoryThrottleMiddleware,
|
|
5436
|
+
createLoginThrottleMiddleware,
|
|
5437
|
+
createFailedJobService,
|
|
5438
|
+
createCsrfMiddleware,
|
|
5439
|
+
createBodySizeLimitMiddleware,
|
|
5440
|
+
createAuthorizeMiddleware,
|
|
5441
|
+
createAuthMiddleware,
|
|
2575
5442
|
config,
|
|
5443
|
+
composeMiddleware,
|
|
5444
|
+
compileBlueprint,
|
|
2576
5445
|
cache,
|
|
2577
5446
|
bindDatabaseConnection2 as bindDatabaseConnection,
|
|
5447
|
+
belongsToMany,
|
|
2578
5448
|
belongsTo,
|
|
2579
5449
|
auth,
|
|
2580
5450
|
assertIfMatch,
|
|
5451
|
+
applyMiddlewareToRoutes,
|
|
5452
|
+
applyCasts,
|
|
5453
|
+
appSchedule,
|
|
5454
|
+
WhereBuilder,
|
|
2581
5455
|
WebFormRequest,
|
|
2582
5456
|
ValidationError,
|
|
5457
|
+
UnsupportedSchemaFeatureError,
|
|
2583
5458
|
UnprocessableEntityError,
|
|
2584
5459
|
UnauthorizedError,
|
|
2585
5460
|
SyncQueue,
|
|
2586
5461
|
StorageManager,
|
|
5462
|
+
SqliteGrammar,
|
|
2587
5463
|
ServiceContainer,
|
|
5464
|
+
Schema,
|
|
5465
|
+
Schedule,
|
|
5466
|
+
ResilientQueue,
|
|
2588
5467
|
RepositoryQuery,
|
|
5468
|
+
RedisQueue,
|
|
5469
|
+
QueueWorker,
|
|
2589
5470
|
PrometheusRegistry,
|
|
2590
5471
|
PreconditionFailedError,
|
|
5472
|
+
PostgresGrammar,
|
|
2591
5473
|
PolicyGate,
|
|
2592
5474
|
Policy,
|
|
2593
5475
|
NotFoundError,
|
|
5476
|
+
MySqlGrammar,
|
|
2594
5477
|
Model,
|
|
2595
5478
|
Mailer,
|
|
2596
5479
|
LogMailDriver,
|
|
2597
5480
|
LocalStorageDriver,
|
|
2598
5481
|
Job,
|
|
2599
5482
|
FormRequest,
|
|
5483
|
+
ForeignIdColumnDefinition,
|
|
2600
5484
|
ForbiddenError,
|
|
5485
|
+
failedJobService_default as FailedJobService,
|
|
5486
|
+
failedJobRepository_default as FailedJobRepository,
|
|
2601
5487
|
EventBus,
|
|
5488
|
+
EtaViewEngine,
|
|
5489
|
+
DEFAULT_VIEWS_DIRECTORY,
|
|
2602
5490
|
ConflictError,
|
|
2603
5491
|
ConfigStore,
|
|
5492
|
+
ColumnDefinition,
|
|
2604
5493
|
repository_default as CacheRepository,
|
|
2605
5494
|
CACHE_TAGS,
|
|
5495
|
+
Blueprint,
|
|
2606
5496
|
baseRepository_default as BaseRepository,
|
|
2607
5497
|
BadRequestError,
|
|
2608
5498
|
AsyncQueue
|