@getstrata/core 0.5.90 → 0.5.97
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/CHANGELOG.md +13 -0
- package/README.md +1 -1
- package/dist/core/database/errors.d.ts +1 -1
- package/dist/core/database/factory.d.ts +3 -0
- package/dist/core/errors/http.d.ts +8 -1
- package/dist/core/scheduler/osCron.d.ts +3 -1
- package/dist/entries/database/errors.js +4 -3
- package/dist/entries/database/factory.js +13 -0
- package/dist/entries/http/response.js +10 -9
- package/dist/entries/http/webErrorResponse.js +8 -7
- package/dist/entries/scheduler/osCron.js +26 -0
- package/dist/entries/scheduler/schedule.js +43 -20
- package/dist/framework/public-api.d.ts +2 -2
- package/dist/index.js +59 -28
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# @getstrata/core changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.97
|
|
4
|
+
|
|
5
|
+
- `mapDatabaseError()` recognizes HTTP errors by `status` + `message`, not only `instanceof HttpError`. Subpath builds duplicate the class, so a thrown `ForbiddenError` was remapped to `400 Bad Request`.
|
|
6
|
+
|
|
7
|
+
## 0.5.92
|
|
8
|
+
|
|
9
|
+
- Re-export `resolveMembershipLookup` and `runWithMembershipContext` from the public barrel. Shared subpath shims re-export that barrel, so published `@getstrata/bootstrap` can import `@getstrata/core/auth/membershipContext` without a missing-export boot failure.
|
|
10
|
+
|
|
11
|
+
## 0.5.91
|
|
12
|
+
|
|
13
|
+
- `Schedule.command()` accepts any expression `Bun.cron.parse` understands. `dueTasks()` uses the next fire time in the current minute instead of a `*/N` whitelist.
|
|
14
|
+
- `Factory.create()` persists `make()` through subclass `persist()` and strips a placeholder `id` of `0`. No states, sequences, or relationships.
|
|
15
|
+
|
|
3
16
|
## 0.5.90
|
|
4
17
|
|
|
5
18
|
- **Breaking identity defaults:** `appKeyPrefix()` is `strata` and `appDisplayName()` is `Strata` when `APP_KEY_PREFIX` / `APP_NAME` are unset (were `workhub` / `WorkHub`). WorkHub pins those env vars.
|
package/README.md
CHANGED
|
@@ -82,7 +82,7 @@ import type { Migration } from "@getstrata/core/database/migrations/types";
|
|
|
82
82
|
Package name: **`@getstrata/core`** (npm org [`@getstrata`](https://www.npmjs.com/org/getstrata)).
|
|
83
83
|
|
|
84
84
|
1. Add `NPM_TOKEN` to GitHub repository secrets.
|
|
85
|
-
2. Tag a release: `git tag v0.5.
|
|
85
|
+
2. Tag a release: `git tag v0.5.96 && git push origin v0.5.96`
|
|
86
86
|
3. [Release workflow](../../.github/workflows/release.yml) builds and runs `npm publish --access public`.
|
|
87
87
|
|
|
88
88
|
Previously published as `@eyk-workhub/framework@0.1.0`, deprecated in favor of this package.
|
|
@@ -2,5 +2,8 @@ declare class Factory<TRecord extends object> {
|
|
|
2
2
|
constructor();
|
|
3
3
|
protected definition(): TRecord;
|
|
4
4
|
make(overrides?: Partial<TRecord>): TRecord;
|
|
5
|
+
create(overrides?: Partial<TRecord>): Promise<TRecord>;
|
|
6
|
+
protected insertable(record: TRecord): Partial<TRecord>;
|
|
7
|
+
protected persist(_values: Partial<TRecord>): Promise<TRecord>;
|
|
5
8
|
}
|
|
6
9
|
export { Factory };
|
|
@@ -30,4 +30,11 @@ declare class PayloadTooLargeError extends HttpError {
|
|
|
30
30
|
declare class PreconditionFailedError extends HttpError {
|
|
31
31
|
constructor(message?: string, details?: unknown);
|
|
32
32
|
}
|
|
33
|
-
|
|
33
|
+
interface HttpErrorLike {
|
|
34
|
+
status: number;
|
|
35
|
+
message: string;
|
|
36
|
+
details?: unknown;
|
|
37
|
+
}
|
|
38
|
+
declare function isHttpErrorLike(error: unknown): error is HttpErrorLike;
|
|
39
|
+
declare function toHttpError(error: unknown): HttpError | null;
|
|
40
|
+
export { BadRequestError, ConflictError, ForbiddenError, HttpError, isHttpErrorLike, NotFoundError, PayloadTooLargeError, PreconditionFailedError, toHttpError, UnauthorizedError, UnprocessableEntityError, ValidationError, };
|
|
@@ -7,8 +7,10 @@ interface InProcessCronJob {
|
|
|
7
7
|
unref(): InProcessCronJob;
|
|
8
8
|
}
|
|
9
9
|
declare function parseScheduleExpression(expression: string, from?: Date): Date | null;
|
|
10
|
+
declare function isScheduleExpressionDue(expression: string, now?: Date): boolean;
|
|
11
|
+
declare function assertScheduleExpression(expression: string): void;
|
|
10
12
|
declare function registerInProcessScheduleRunner(run: () => void | Promise<void>, expression?: string): InProcessCronJob;
|
|
11
13
|
declare function installOsScheduleRunner(workerScriptPath: string, expression?: string, title?: string): Promise<void>;
|
|
12
14
|
declare function uninstallOsScheduleRunner(title?: string): Promise<void>;
|
|
13
15
|
export type { InProcessCronJob };
|
|
14
|
-
export { DEFAULT_SCHEDULE_RUN_EXPRESSION, installOsScheduleRunner, OS_CRON_JOB_TITLE, parseScheduleExpression, registerInProcessScheduleRunner, uninstallOsScheduleRunner, };
|
|
16
|
+
export { assertScheduleExpression, DEFAULT_SCHEDULE_RUN_EXPRESSION, installOsScheduleRunner, isScheduleExpressionDue, OS_CRON_JOB_TITLE, parseScheduleExpression, registerInProcessScheduleRunner, uninstallOsScheduleRunner, };
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import {
|
|
4
4
|
BadRequestError,
|
|
5
5
|
ConflictError,
|
|
6
|
-
|
|
6
|
+
toHttpError,
|
|
7
7
|
UnprocessableEntityError
|
|
8
8
|
} from "@getstrata/core/errors/http";
|
|
9
9
|
function isPostgresError(error) {
|
|
@@ -22,8 +22,9 @@ function getPostgresSqlState(error) {
|
|
|
22
22
|
return;
|
|
23
23
|
}
|
|
24
24
|
function mapDatabaseError(error) {
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
const httpError = toHttpError(error);
|
|
26
|
+
if (httpError) {
|
|
27
|
+
return httpError;
|
|
27
28
|
}
|
|
28
29
|
if (!isPostgresError(error)) {
|
|
29
30
|
const message = error instanceof Error ? error.message : "Database operation failed.";
|
|
@@ -11,6 +11,19 @@ class Factory {
|
|
|
11
11
|
...overrides
|
|
12
12
|
};
|
|
13
13
|
}
|
|
14
|
+
async create(overrides = {}) {
|
|
15
|
+
return this.persist(this.insertable(this.make(overrides)));
|
|
16
|
+
}
|
|
17
|
+
insertable(record) {
|
|
18
|
+
const values = { ...record };
|
|
19
|
+
if (values.id === 0 || values.id === undefined || values.id === null) {
|
|
20
|
+
delete values.id;
|
|
21
|
+
}
|
|
22
|
+
return values;
|
|
23
|
+
}
|
|
24
|
+
persist(_values) {
|
|
25
|
+
throw new Error("Factory.persist() must be implemented to use create().");
|
|
26
|
+
}
|
|
14
27
|
}
|
|
15
28
|
export {
|
|
16
29
|
Factory
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// ../../src/core/http/response.ts
|
|
3
|
-
import {
|
|
3
|
+
import { toHttpError as toHttpError3 } from "@getstrata/core/errors/http";
|
|
4
4
|
|
|
5
5
|
// ../../src/core/database/errors.ts
|
|
6
6
|
import {
|
|
7
7
|
BadRequestError,
|
|
8
8
|
ConflictError,
|
|
9
|
-
|
|
9
|
+
toHttpError,
|
|
10
10
|
UnprocessableEntityError
|
|
11
11
|
} from "@getstrata/core/errors/http";
|
|
12
12
|
function isPostgresError(error) {
|
|
@@ -25,8 +25,9 @@ function getPostgresSqlState(error) {
|
|
|
25
25
|
return;
|
|
26
26
|
}
|
|
27
27
|
function mapDatabaseError(error) {
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
const httpError = toHttpError(error);
|
|
29
|
+
if (httpError) {
|
|
30
|
+
return httpError;
|
|
30
31
|
}
|
|
31
32
|
if (!isPostgresError(error)) {
|
|
32
33
|
const message = error instanceof Error ? error.message : "Database operation failed.";
|
|
@@ -66,7 +67,7 @@ async function withDatabaseErrorHandling(operation) {
|
|
|
66
67
|
}
|
|
67
68
|
|
|
68
69
|
// ../../src/core/http/webErrorResponse.ts
|
|
69
|
-
import {
|
|
70
|
+
import { toHttpError as toHttpError2, ValidationError } from "@getstrata/core/errors/http";
|
|
70
71
|
|
|
71
72
|
// ../../src/core/runtime/frontendMode.ts
|
|
72
73
|
function readFrontendMode() {
|
|
@@ -301,11 +302,11 @@ async function webErrorResponse(error, request) {
|
|
|
301
302
|
if (!request || !isViewsEnabled() || requestPrefersJson(request)) {
|
|
302
303
|
return null;
|
|
303
304
|
}
|
|
304
|
-
const mappedError = error
|
|
305
|
-
if (mappedError
|
|
305
|
+
const mappedError = toHttpError2(error) ?? mapDatabaseError(error);
|
|
306
|
+
if (mappedError.status === 401) {
|
|
306
307
|
return Response.redirect(loginRedirectLocation(request), 302);
|
|
307
308
|
}
|
|
308
|
-
const errors = mappedError instanceof ValidationError ? normalizeFieldErrors(mappedError.details) : undefined;
|
|
309
|
+
const errors = mappedError instanceof ValidationError || mappedError.name === "ValidationError" ? normalizeFieldErrors(mappedError.details) : undefined;
|
|
309
310
|
return htmlErrorResponse({
|
|
310
311
|
status: mappedError.status,
|
|
311
312
|
title: errorPageTitle(mappedError.status, mappedError.message),
|
|
@@ -329,7 +330,7 @@ function noContentResponse() {
|
|
|
329
330
|
return new Response(null, { status: 204 });
|
|
330
331
|
}
|
|
331
332
|
function errorResponse(error) {
|
|
332
|
-
const mappedError = error
|
|
333
|
+
const mappedError = toHttpError3(error) ?? mapDatabaseError(error);
|
|
333
334
|
return Response.json({
|
|
334
335
|
error: mappedError.message,
|
|
335
336
|
...mappedError.details === undefined ? {} : { details: mappedError.details }
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// ../../src/core/http/webErrorResponse.ts
|
|
3
|
-
import {
|
|
3
|
+
import { toHttpError as toHttpError2, ValidationError } from "@getstrata/core/errors/http";
|
|
4
4
|
|
|
5
5
|
// ../../src/core/database/errors.ts
|
|
6
6
|
import {
|
|
7
7
|
BadRequestError,
|
|
8
8
|
ConflictError,
|
|
9
|
-
|
|
9
|
+
toHttpError,
|
|
10
10
|
UnprocessableEntityError
|
|
11
11
|
} from "@getstrata/core/errors/http";
|
|
12
12
|
function isPostgresError(error) {
|
|
@@ -25,8 +25,9 @@ function getPostgresSqlState(error) {
|
|
|
25
25
|
return;
|
|
26
26
|
}
|
|
27
27
|
function mapDatabaseError(error) {
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
const httpError = toHttpError(error);
|
|
29
|
+
if (httpError) {
|
|
30
|
+
return httpError;
|
|
30
31
|
}
|
|
31
32
|
if (!isPostgresError(error)) {
|
|
32
33
|
const message = error instanceof Error ? error.message : "Database operation failed.";
|
|
@@ -298,11 +299,11 @@ async function webErrorResponse(error, request) {
|
|
|
298
299
|
if (!request || !isViewsEnabled() || requestPrefersJson(request)) {
|
|
299
300
|
return null;
|
|
300
301
|
}
|
|
301
|
-
const mappedError = error
|
|
302
|
-
if (mappedError
|
|
302
|
+
const mappedError = toHttpError2(error) ?? mapDatabaseError(error);
|
|
303
|
+
if (mappedError.status === 401) {
|
|
303
304
|
return Response.redirect(loginRedirectLocation(request), 302);
|
|
304
305
|
}
|
|
305
|
-
const errors = mappedError instanceof ValidationError ? normalizeFieldErrors(mappedError.details) : undefined;
|
|
306
|
+
const errors = mappedError instanceof ValidationError || mappedError.name === "ValidationError" ? normalizeFieldErrors(mappedError.details) : undefined;
|
|
306
307
|
return htmlErrorResponse({
|
|
307
308
|
status: mappedError.status,
|
|
308
309
|
title: errorPageTitle(mappedError.status, mappedError.message),
|
|
@@ -5,6 +5,30 @@ var OS_CRON_JOB_TITLE = "getstrata-schedule-run";
|
|
|
5
5
|
function parseScheduleExpression(expression, from = new Date) {
|
|
6
6
|
return Bun.cron.parse(expression, from);
|
|
7
7
|
}
|
|
8
|
+
function minuteWindow(now) {
|
|
9
|
+
const start = new Date(now);
|
|
10
|
+
start.setSeconds(0, 0);
|
|
11
|
+
return { start, end: new Date(start.getTime() + 60000) };
|
|
12
|
+
}
|
|
13
|
+
function isScheduleExpressionDue(expression, now = new Date) {
|
|
14
|
+
const { start, end } = minuteWindow(now);
|
|
15
|
+
const next = parseScheduleExpression(expression, new Date(start.getTime() - 1));
|
|
16
|
+
if (!next) {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
const timestamp = next.getTime();
|
|
20
|
+
return timestamp >= start.getTime() && timestamp < end.getTime();
|
|
21
|
+
}
|
|
22
|
+
function assertScheduleExpression(expression) {
|
|
23
|
+
try {
|
|
24
|
+
const next = parseScheduleExpression(expression);
|
|
25
|
+
if (!(next instanceof Date) || Number.isNaN(next.getTime())) {
|
|
26
|
+
throw new Error("unparsable");
|
|
27
|
+
}
|
|
28
|
+
} catch {
|
|
29
|
+
throw new Error(`Unsupported schedule expression "${expression}".`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
8
32
|
function registerInProcessScheduleRunner(run, expression = DEFAULT_SCHEDULE_RUN_EXPRESSION) {
|
|
9
33
|
const register = Bun.cron;
|
|
10
34
|
return register(expression, async () => {
|
|
@@ -20,7 +44,9 @@ async function uninstallOsScheduleRunner(title = OS_CRON_JOB_TITLE) {
|
|
|
20
44
|
export {
|
|
21
45
|
DEFAULT_SCHEDULE_RUN_EXPRESSION,
|
|
22
46
|
OS_CRON_JOB_TITLE,
|
|
47
|
+
assertScheduleExpression,
|
|
23
48
|
installOsScheduleRunner,
|
|
49
|
+
isScheduleExpressionDue,
|
|
24
50
|
parseScheduleExpression,
|
|
25
51
|
registerInProcessScheduleRunner,
|
|
26
52
|
uninstallOsScheduleRunner
|
|
@@ -1,34 +1,57 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
// ../../src/core/scheduler/
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
// ../../src/core/scheduler/osCron.ts
|
|
3
|
+
var DEFAULT_SCHEDULE_RUN_EXPRESSION = "* * * * *";
|
|
4
|
+
var OS_CRON_JOB_TITLE = "getstrata-schedule-run";
|
|
5
|
+
function parseScheduleExpression(expression, from = new Date) {
|
|
6
|
+
return Bun.cron.parse(expression, from);
|
|
7
|
+
}
|
|
8
|
+
function minuteWindow(now) {
|
|
9
|
+
const start = new Date(now);
|
|
10
|
+
start.setSeconds(0, 0);
|
|
11
|
+
return { start, end: new Date(start.getTime() + 60000) };
|
|
12
|
+
}
|
|
13
|
+
function isScheduleExpressionDue(expression, now = new Date) {
|
|
14
|
+
const { start, end } = minuteWindow(now);
|
|
15
|
+
const next = parseScheduleExpression(expression, new Date(start.getTime() - 1));
|
|
16
|
+
if (!next) {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
const timestamp = next.getTime();
|
|
20
|
+
return timestamp >= start.getTime() && timestamp < end.getTime();
|
|
21
|
+
}
|
|
22
|
+
function assertScheduleExpression(expression) {
|
|
23
|
+
try {
|
|
24
|
+
const next = parseScheduleExpression(expression);
|
|
25
|
+
if (!(next instanceof Date) || Number.isNaN(next.getTime())) {
|
|
26
|
+
throw new Error("unparsable");
|
|
27
|
+
}
|
|
28
|
+
} catch {
|
|
29
|
+
throw new Error(`Unsupported schedule expression "${expression}".`);
|
|
6
30
|
}
|
|
7
|
-
|
|
31
|
+
}
|
|
32
|
+
function registerInProcessScheduleRunner(run, expression = DEFAULT_SCHEDULE_RUN_EXPRESSION) {
|
|
33
|
+
const register = Bun.cron;
|
|
34
|
+
return register(expression, async () => {
|
|
35
|
+
await run();
|
|
36
|
+
}).unref();
|
|
37
|
+
}
|
|
38
|
+
async function installOsScheduleRunner(workerScriptPath, expression = DEFAULT_SCHEDULE_RUN_EXPRESSION, title = OS_CRON_JOB_TITLE) {
|
|
39
|
+
await Bun.cron(workerScriptPath, expression, title);
|
|
40
|
+
}
|
|
41
|
+
async function uninstallOsScheduleRunner(title = OS_CRON_JOB_TITLE) {
|
|
42
|
+
await Bun.cron.remove(title);
|
|
8
43
|
}
|
|
9
44
|
|
|
45
|
+
// ../../src/core/scheduler/schedule.ts
|
|
10
46
|
class Schedule {
|
|
11
47
|
tasks = [];
|
|
12
48
|
command(expression, name, run) {
|
|
13
|
-
|
|
14
|
-
throw new Error(`Unsupported schedule expression "${expression}". Only "* * * * *" and "*/N * * * *" are implemented.`);
|
|
15
|
-
}
|
|
49
|
+
assertScheduleExpression(expression);
|
|
16
50
|
this.tasks.push({ expression, name, run });
|
|
17
51
|
return this;
|
|
18
52
|
}
|
|
19
53
|
dueTasks(now = new Date) {
|
|
20
|
-
|
|
21
|
-
return this.tasks.filter((task) => {
|
|
22
|
-
if (task.expression === "* * * * *") {
|
|
23
|
-
return true;
|
|
24
|
-
}
|
|
25
|
-
const intervalMatch = task.expression.match(/^\*\/(\d+)(?: \*){4}$/);
|
|
26
|
-
if (intervalMatch) {
|
|
27
|
-
const interval = Number.parseInt(intervalMatch[1] ?? "", 10);
|
|
28
|
-
return Number.isInteger(interval) && interval > 0 && minute % interval === 0;
|
|
29
|
-
}
|
|
30
|
-
return false;
|
|
31
|
-
});
|
|
54
|
+
return this.tasks.filter((task) => isScheduleExpressionDue(task.expression, now));
|
|
32
55
|
}
|
|
33
56
|
tasksList() {
|
|
34
57
|
return [...this.tasks];
|
|
@@ -10,7 +10,7 @@ export type { AuthUser } from "../core/auth/authContext.ts";
|
|
|
10
10
|
export { authContext, currentAuthUser, runWithAuthUser } from "../core/auth/authContext.ts";
|
|
11
11
|
export type { AuthGuard } from "../core/auth/guard.ts";
|
|
12
12
|
export { ApiTokenGuard, AuthManager, CompositeGuard, DatabaseTokenGuard, GuestGuard, } from "../core/auth/guard.ts";
|
|
13
|
-
export { configureMembershipLookup, currentOrganizationIds, currentOrgRole, hasMinimumOrgRole, hasOrgMembership, } from "../core/auth/membershipContext.ts";
|
|
13
|
+
export { configureMembershipLookup, currentOrganizationIds, currentOrgRole, hasMinimumOrgRole, hasOrgMembership, resolveMembershipLookup, runWithMembershipContext, } from "../core/auth/membershipContext.ts";
|
|
14
14
|
export { createMembershipMiddleware } from "../core/auth/membershipMiddleware.ts";
|
|
15
15
|
export { appendOrganizationScope, appendProjectScope, assertOrganizationReadable, assertResourceInCurrentTenant, emptyPaginateResult, resolveOrganizationScope, scopedOrganizationIds, } from "../core/auth/membershipScope.ts";
|
|
16
16
|
export { default as MembershipService, resolveMembershipService, } from "../core/auth/membershipService.ts";
|
|
@@ -48,7 +48,7 @@ export { runInTransaction } from "../core/database/transaction.ts";
|
|
|
48
48
|
export type { QueryJoin, QueryJoinOn, QueryOptions, QueryOrder, QuerySelectItem, QueryWhere, } from "../core/database/types.ts";
|
|
49
49
|
export type { WhereNode } from "../core/database/whereBuilder.ts";
|
|
50
50
|
export { WhereBuilder } from "../core/database/whereBuilder.ts";
|
|
51
|
-
export { BadRequestError, ConflictError, ForbiddenError, HttpError, NotFoundError, PayloadTooLargeError, PreconditionFailedError, UnauthorizedError, UnprocessableEntityError, ValidationError, } from "../core/errors/http.ts";
|
|
51
|
+
export { BadRequestError, ConflictError, ForbiddenError, HttpError, isHttpErrorLike, NotFoundError, PayloadTooLargeError, PreconditionFailedError, toHttpError, UnauthorizedError, UnprocessableEntityError, ValidationError, } from "../core/errors/http.ts";
|
|
52
52
|
export type { EventListener } from "../core/events/eventBus.ts";
|
|
53
53
|
export { EventBus, eventBus } from "../core/events/eventBus.ts";
|
|
54
54
|
export { modelEventName } from "../core/events/index.ts";
|
package/dist/index.js
CHANGED
|
@@ -120,6 +120,22 @@ class PreconditionFailedError extends HttpError {
|
|
|
120
120
|
super(412, message, details);
|
|
121
121
|
}
|
|
122
122
|
}
|
|
123
|
+
function isHttpErrorLike(error) {
|
|
124
|
+
if (typeof error !== "object" || error === null) {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
const candidate = error;
|
|
128
|
+
return typeof candidate.status === "number" && Number.isInteger(candidate.status) && candidate.status >= 400 && candidate.status < 600 && typeof candidate.message === "string";
|
|
129
|
+
}
|
|
130
|
+
function toHttpError(error) {
|
|
131
|
+
if (error instanceof HttpError) {
|
|
132
|
+
return error;
|
|
133
|
+
}
|
|
134
|
+
if (!isHttpErrorLike(error)) {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
return new HttpError(error.status, error.message, error.details);
|
|
138
|
+
}
|
|
123
139
|
|
|
124
140
|
// ../../src/core/runtime/asyncContextStore.ts
|
|
125
141
|
import { AsyncLocalStorage } from "async_hooks";
|
|
@@ -1575,8 +1591,9 @@ function getPostgresSqlState(error) {
|
|
|
1575
1591
|
return;
|
|
1576
1592
|
}
|
|
1577
1593
|
function mapDatabaseError(error) {
|
|
1578
|
-
|
|
1579
|
-
|
|
1594
|
+
const httpError = toHttpError(error);
|
|
1595
|
+
if (httpError) {
|
|
1596
|
+
return httpError;
|
|
1580
1597
|
}
|
|
1581
1598
|
if (!isPostgresError(error)) {
|
|
1582
1599
|
const message = error instanceof Error ? error.message : "Database operation failed.";
|
|
@@ -5298,11 +5315,11 @@ async function webErrorResponse(error, request) {
|
|
|
5298
5315
|
if (!request || !isViewsEnabled() || requestPrefersJson(request)) {
|
|
5299
5316
|
return null;
|
|
5300
5317
|
}
|
|
5301
|
-
const mappedError = error
|
|
5302
|
-
if (mappedError
|
|
5318
|
+
const mappedError = toHttpError(error) ?? mapDatabaseError(error);
|
|
5319
|
+
if (mappedError.status === 401) {
|
|
5303
5320
|
return Response.redirect(loginRedirectLocation(request), 302);
|
|
5304
5321
|
}
|
|
5305
|
-
const errors = mappedError instanceof ValidationError ? normalizeFieldErrors(mappedError.details) : undefined;
|
|
5322
|
+
const errors = mappedError instanceof ValidationError || mappedError.name === "ValidationError" ? normalizeFieldErrors(mappedError.details) : undefined;
|
|
5306
5323
|
return htmlErrorResponse({
|
|
5307
5324
|
status: mappedError.status,
|
|
5308
5325
|
title: errorPageTitle(mappedError.status, mappedError.message),
|
|
@@ -5326,7 +5343,7 @@ function noContentResponse() {
|
|
|
5326
5343
|
return new Response(null, { status: 204 });
|
|
5327
5344
|
}
|
|
5328
5345
|
function errorResponse(error) {
|
|
5329
|
-
const mappedError = error
|
|
5346
|
+
const mappedError = toHttpError(error) ?? mapDatabaseError(error);
|
|
5330
5347
|
return Response.json({
|
|
5331
5348
|
error: mappedError.message,
|
|
5332
5349
|
...mappedError.details === undefined ? {} : { details: mappedError.details }
|
|
@@ -6556,36 +6573,45 @@ async function collectQueueMetrics() {
|
|
|
6556
6573
|
failedCount
|
|
6557
6574
|
};
|
|
6558
6575
|
}
|
|
6559
|
-
// ../../src/core/scheduler/
|
|
6560
|
-
function
|
|
6561
|
-
|
|
6562
|
-
|
|
6576
|
+
// ../../src/core/scheduler/osCron.ts
|
|
6577
|
+
function parseScheduleExpression(expression, from = new Date) {
|
|
6578
|
+
return Bun.cron.parse(expression, from);
|
|
6579
|
+
}
|
|
6580
|
+
function minuteWindow(now) {
|
|
6581
|
+
const start = new Date(now);
|
|
6582
|
+
start.setSeconds(0, 0);
|
|
6583
|
+
return { start, end: new Date(start.getTime() + 60000) };
|
|
6584
|
+
}
|
|
6585
|
+
function isScheduleExpressionDue(expression, now = new Date) {
|
|
6586
|
+
const { start, end } = minuteWindow(now);
|
|
6587
|
+
const next = parseScheduleExpression(expression, new Date(start.getTime() - 1));
|
|
6588
|
+
if (!next) {
|
|
6589
|
+
return false;
|
|
6590
|
+
}
|
|
6591
|
+
const timestamp = next.getTime();
|
|
6592
|
+
return timestamp >= start.getTime() && timestamp < end.getTime();
|
|
6593
|
+
}
|
|
6594
|
+
function assertScheduleExpression(expression) {
|
|
6595
|
+
try {
|
|
6596
|
+
const next = parseScheduleExpression(expression);
|
|
6597
|
+
if (!(next instanceof Date) || Number.isNaN(next.getTime())) {
|
|
6598
|
+
throw new Error("unparsable");
|
|
6599
|
+
}
|
|
6600
|
+
} catch {
|
|
6601
|
+
throw new Error(`Unsupported schedule expression "${expression}".`);
|
|
6563
6602
|
}
|
|
6564
|
-
return /^(\*\/\d+)( \*){4}$/.test(expression);
|
|
6565
6603
|
}
|
|
6566
6604
|
|
|
6605
|
+
// ../../src/core/scheduler/schedule.ts
|
|
6567
6606
|
class Schedule {
|
|
6568
6607
|
tasks = [];
|
|
6569
6608
|
command(expression, name, run) {
|
|
6570
|
-
|
|
6571
|
-
throw new Error(`Unsupported schedule expression "${expression}". Only "* * * * *" and "*/N * * * *" are implemented.`);
|
|
6572
|
-
}
|
|
6609
|
+
assertScheduleExpression(expression);
|
|
6573
6610
|
this.tasks.push({ expression, name, run });
|
|
6574
6611
|
return this;
|
|
6575
6612
|
}
|
|
6576
6613
|
dueTasks(now = new Date) {
|
|
6577
|
-
|
|
6578
|
-
return this.tasks.filter((task) => {
|
|
6579
|
-
if (task.expression === "* * * * *") {
|
|
6580
|
-
return true;
|
|
6581
|
-
}
|
|
6582
|
-
const intervalMatch = task.expression.match(/^\*\/(\d+)(?: \*){4}$/);
|
|
6583
|
-
if (intervalMatch) {
|
|
6584
|
-
const interval = Number.parseInt(intervalMatch[1] ?? "", 10);
|
|
6585
|
-
return Number.isInteger(interval) && interval > 0 && minute % interval === 0;
|
|
6586
|
-
}
|
|
6587
|
-
return false;
|
|
6588
|
-
});
|
|
6614
|
+
return this.tasks.filter((task) => isScheduleExpressionDue(task.expression, now));
|
|
6589
6615
|
}
|
|
6590
6616
|
tasksList() {
|
|
6591
6617
|
return [...this.tasks];
|
|
@@ -6690,8 +6716,9 @@ function createTenantMiddleware() {
|
|
|
6690
6716
|
});
|
|
6691
6717
|
});
|
|
6692
6718
|
} catch (error) {
|
|
6693
|
-
|
|
6694
|
-
|
|
6719
|
+
const httpError = toHttpError(error);
|
|
6720
|
+
if (httpError) {
|
|
6721
|
+
return Response.json({ error: httpError.message }, { status: httpError.status });
|
|
6695
6722
|
}
|
|
6696
6723
|
throw error;
|
|
6697
6724
|
}
|
|
@@ -7300,6 +7327,7 @@ export {
|
|
|
7300
7327
|
isEtagEnabled,
|
|
7301
7328
|
isGlobalAdmin,
|
|
7302
7329
|
isHtmxRequest,
|
|
7330
|
+
isHttpErrorLike,
|
|
7303
7331
|
isInsideTenantDatabaseScope,
|
|
7304
7332
|
isPublicReadsEnabled,
|
|
7305
7333
|
isTenancyEnabled,
|
|
@@ -7362,6 +7390,7 @@ export {
|
|
|
7362
7390
|
resolveCsrfTokenForRequest,
|
|
7363
7391
|
resolveDatabaseDriver,
|
|
7364
7392
|
resolveHtmlContentSecurityPolicy,
|
|
7393
|
+
resolveMembershipLookup,
|
|
7365
7394
|
resolveMembershipService,
|
|
7366
7395
|
resolveOrganizationScope,
|
|
7367
7396
|
resolveRepositoryConnection,
|
|
@@ -7378,6 +7407,7 @@ export {
|
|
|
7378
7407
|
runSeedersFromDirectory,
|
|
7379
7408
|
runWithAuthUser,
|
|
7380
7409
|
runWithDatabaseConnection,
|
|
7410
|
+
runWithMembershipContext,
|
|
7381
7411
|
runWithRequestMeta,
|
|
7382
7412
|
runWithTenant,
|
|
7383
7413
|
runWithTenantDatabase,
|
|
@@ -7400,6 +7430,7 @@ export {
|
|
|
7400
7430
|
stripMarkdown,
|
|
7401
7431
|
temporarySignedUrl,
|
|
7402
7432
|
textResponse,
|
|
7433
|
+
toHttpError,
|
|
7403
7434
|
toPaginatedResourceCollection,
|
|
7404
7435
|
toResourceCollection,
|
|
7405
7436
|
trustForwardedFor,
|