@getstrata/core 0.5.52 → 0.5.54
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -3
- package/dist/core/queue/failedJobRepository.d.ts +1 -1
- package/dist/core/queue/failedJobTable.d.ts +1 -1
- package/dist/entries/auth/accessControl.js +60 -1
- package/dist/entries/auth/membershipMiddleware.js +21 -0
- package/dist/entries/auth/membershipScope.js +336 -1
- package/dist/entries/auth/membershipService.js +304 -1
- package/dist/entries/auth/policy.js +81 -1
- package/dist/entries/auth/sessionGuard.js +8 -7
- package/dist/entries/database/repositoryQuery.js +655 -0
- package/dist/entries/database/whereBuilder.js +32 -0
- package/dist/entries/http/formRequest.js +102 -0
- package/dist/entries/http/pagination.js +102 -0
- package/dist/entries/http/routeModelBinding.js +102 -0
- package/dist/entries/http/securedRouteModelBinding.js +102 -0
- package/dist/entries/http/validation.js +145 -0
- package/dist/entries/http/webErrorResponse.js +8 -7
- package/dist/entries/http/webFormRequest.js +102 -0
- package/dist/entries/queue/createAppQueue.js +2 -2
- package/dist/entries/queue/failedJobRepository.js +2 -2
- package/dist/entries/queue/publicQueue.js +2 -2
- package/dist/entries/queue/queueMetrics.js +2 -2
- package/dist/entries/view.js +8 -7
- package/dist/index.js +2215 -2213
- package/dist/modules/user/apiTokenRepository.d.ts +1 -1
- package/dist/modules/user/apiTokenTable.d.ts +1 -1
- package/dist/modules/user/notificationRepository.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/repository.d.ts +1 -1
- package/dist/modules/user/table.d.ts +1 -1
- package/package.json +17 -2
|
@@ -6,12 +6,65 @@ import { ForbiddenError } from "@getstrata/core/errors/http";
|
|
|
6
6
|
import { currentAuthUser } from "@getstrata/core/auth/authContext";
|
|
7
7
|
import { BadRequestError } from "@getstrata/core/errors/http";
|
|
8
8
|
import { currentTenantId } from "@getstrata/core/tenant/tenantContext";
|
|
9
|
+
function buildRequestCacheKey(fallbackPath, request) {
|
|
10
|
+
if (!request) {
|
|
11
|
+
return fallbackPath;
|
|
12
|
+
}
|
|
13
|
+
const url = new URL(request.url);
|
|
14
|
+
const user = currentAuthUser();
|
|
15
|
+
const authScope = user ? `u:${user.id}` : "guest";
|
|
16
|
+
const tenantScope = `t:${currentTenantId()}`;
|
|
17
|
+
return `${authScope}|${tenantScope}|${url.pathname}${url.search}`;
|
|
18
|
+
}
|
|
9
19
|
function getQueryParams(request) {
|
|
10
20
|
if (!request) {
|
|
11
21
|
return new URLSearchParams;
|
|
12
22
|
}
|
|
13
23
|
return new URL(request.url).searchParams;
|
|
14
24
|
}
|
|
25
|
+
function parseOptionalPositiveIntQueryParam(params, name) {
|
|
26
|
+
const value = params.get(name);
|
|
27
|
+
if (value === null || value.trim() === "") {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const parsed = Number.parseInt(value, 10);
|
|
31
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
32
|
+
throw new BadRequestError(`Invalid query parameter "${name}". Expected a positive integer.`);
|
|
33
|
+
}
|
|
34
|
+
return parsed;
|
|
35
|
+
}
|
|
36
|
+
function parseOptionalBooleanQueryParam(params, name) {
|
|
37
|
+
const value = params.get(name);
|
|
38
|
+
if (value === null || value.trim() === "") {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
switch (value.toLowerCase()) {
|
|
42
|
+
case "true":
|
|
43
|
+
case "1":
|
|
44
|
+
return true;
|
|
45
|
+
case "false":
|
|
46
|
+
case "0":
|
|
47
|
+
return false;
|
|
48
|
+
default:
|
|
49
|
+
throw new BadRequestError(`Invalid query parameter "${name}". Expected a boolean.`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function parseOptionalEnumQueryParam(params, name, allowedValues) {
|
|
53
|
+
const value = params.get(name);
|
|
54
|
+
if (value === null || value.trim() === "") {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (!allowedValues.includes(value)) {
|
|
58
|
+
throw new BadRequestError(`Invalid query parameter "${name}". Expected one of: ${allowedValues.join(", ")}.`);
|
|
59
|
+
}
|
|
60
|
+
return value;
|
|
61
|
+
}
|
|
62
|
+
function expectObject(value, label = "request body") {
|
|
63
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
64
|
+
throw new BadRequestError(`${label} must be a JSON object.`);
|
|
65
|
+
}
|
|
66
|
+
return value;
|
|
67
|
+
}
|
|
15
68
|
async function parseJsonBody(request, validator) {
|
|
16
69
|
let payload;
|
|
17
70
|
try {
|
|
@@ -21,6 +74,55 @@ async function parseJsonBody(request, validator) {
|
|
|
21
74
|
}
|
|
22
75
|
return validator(payload);
|
|
23
76
|
}
|
|
77
|
+
function readRequiredString(payload, field, options = {}) {
|
|
78
|
+
const value = payload[field];
|
|
79
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
80
|
+
throw new BadRequestError(`"${field}" is required and must be a string.`);
|
|
81
|
+
}
|
|
82
|
+
const trimmed = value.trim();
|
|
83
|
+
if (options.minLength !== undefined && trimmed.length < options.minLength) {
|
|
84
|
+
throw new BadRequestError(`"${field}" must be at least ${options.minLength} characters.`);
|
|
85
|
+
}
|
|
86
|
+
if (options.maxLength !== undefined && trimmed.length > options.maxLength) {
|
|
87
|
+
throw new BadRequestError(`"${field}" must be at most ${options.maxLength} characters.`);
|
|
88
|
+
}
|
|
89
|
+
if (options.pattern && !options.pattern.test(trimmed)) {
|
|
90
|
+
throw new BadRequestError(`"${field}" has an invalid format.`);
|
|
91
|
+
}
|
|
92
|
+
return trimmed;
|
|
93
|
+
}
|
|
94
|
+
function readOptionalString(payload, field, options = {}) {
|
|
95
|
+
if (!(field in payload) || payload[field] === undefined) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
return readRequiredString(payload, field, options);
|
|
99
|
+
}
|
|
100
|
+
function readRequiredEnum(payload, field, allowedValues) {
|
|
101
|
+
const value = readRequiredString(payload, field);
|
|
102
|
+
if (!allowedValues.includes(value)) {
|
|
103
|
+
throw new BadRequestError(`"${field}" must be one of: ${allowedValues.join(", ")}.`);
|
|
104
|
+
}
|
|
105
|
+
return value;
|
|
106
|
+
}
|
|
107
|
+
function readOptionalEnum(payload, field, allowedValues) {
|
|
108
|
+
if (!(field in payload) || payload[field] === undefined) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
return readRequiredEnum(payload, field, allowedValues);
|
|
112
|
+
}
|
|
113
|
+
function readRequiredPositiveInt(payload, field) {
|
|
114
|
+
const value = payload[field];
|
|
115
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
|
|
116
|
+
throw new BadRequestError(`"${field}" is required and must be a positive integer.`);
|
|
117
|
+
}
|
|
118
|
+
return value;
|
|
119
|
+
}
|
|
120
|
+
function readOptionalPositiveInt(payload, field) {
|
|
121
|
+
if (!(field in payload) || payload[field] === undefined) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
return readRequiredPositiveInt(payload, field);
|
|
125
|
+
}
|
|
24
126
|
function parsePositiveIntParam(value, name = "id") {
|
|
25
127
|
const parsed = Number.parseInt(value, 10);
|
|
26
128
|
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
@@ -17,12 +17,65 @@ function buildPaginationMeta(input) {
|
|
|
17
17
|
import { currentAuthUser } from "@getstrata/core/auth/authContext";
|
|
18
18
|
import { BadRequestError } from "@getstrata/core/errors/http";
|
|
19
19
|
import { currentTenantId } from "@getstrata/core/tenant/tenantContext";
|
|
20
|
+
function buildRequestCacheKey(fallbackPath, request) {
|
|
21
|
+
if (!request) {
|
|
22
|
+
return fallbackPath;
|
|
23
|
+
}
|
|
24
|
+
const url = new URL(request.url);
|
|
25
|
+
const user = currentAuthUser();
|
|
26
|
+
const authScope = user ? `u:${user.id}` : "guest";
|
|
27
|
+
const tenantScope = `t:${currentTenantId()}`;
|
|
28
|
+
return `${authScope}|${tenantScope}|${url.pathname}${url.search}`;
|
|
29
|
+
}
|
|
20
30
|
function getQueryParams(request) {
|
|
21
31
|
if (!request) {
|
|
22
32
|
return new URLSearchParams;
|
|
23
33
|
}
|
|
24
34
|
return new URL(request.url).searchParams;
|
|
25
35
|
}
|
|
36
|
+
function parseOptionalPositiveIntQueryParam(params, name) {
|
|
37
|
+
const value = params.get(name);
|
|
38
|
+
if (value === null || value.trim() === "") {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const parsed = Number.parseInt(value, 10);
|
|
42
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
43
|
+
throw new BadRequestError(`Invalid query parameter "${name}". Expected a positive integer.`);
|
|
44
|
+
}
|
|
45
|
+
return parsed;
|
|
46
|
+
}
|
|
47
|
+
function parseOptionalBooleanQueryParam(params, name) {
|
|
48
|
+
const value = params.get(name);
|
|
49
|
+
if (value === null || value.trim() === "") {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
switch (value.toLowerCase()) {
|
|
53
|
+
case "true":
|
|
54
|
+
case "1":
|
|
55
|
+
return true;
|
|
56
|
+
case "false":
|
|
57
|
+
case "0":
|
|
58
|
+
return false;
|
|
59
|
+
default:
|
|
60
|
+
throw new BadRequestError(`Invalid query parameter "${name}". Expected a boolean.`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function parseOptionalEnumQueryParam(params, name, allowedValues) {
|
|
64
|
+
const value = params.get(name);
|
|
65
|
+
if (value === null || value.trim() === "") {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (!allowedValues.includes(value)) {
|
|
69
|
+
throw new BadRequestError(`Invalid query parameter "${name}". Expected one of: ${allowedValues.join(", ")}.`);
|
|
70
|
+
}
|
|
71
|
+
return value;
|
|
72
|
+
}
|
|
73
|
+
function expectObject(value, label = "request body") {
|
|
74
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
75
|
+
throw new BadRequestError(`${label} must be a JSON object.`);
|
|
76
|
+
}
|
|
77
|
+
return value;
|
|
78
|
+
}
|
|
26
79
|
async function parseJsonBody(request, validator) {
|
|
27
80
|
let payload;
|
|
28
81
|
try {
|
|
@@ -32,6 +85,55 @@ async function parseJsonBody(request, validator) {
|
|
|
32
85
|
}
|
|
33
86
|
return validator(payload);
|
|
34
87
|
}
|
|
88
|
+
function readRequiredString(payload, field, options = {}) {
|
|
89
|
+
const value = payload[field];
|
|
90
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
91
|
+
throw new BadRequestError(`"${field}" is required and must be a string.`);
|
|
92
|
+
}
|
|
93
|
+
const trimmed = value.trim();
|
|
94
|
+
if (options.minLength !== undefined && trimmed.length < options.minLength) {
|
|
95
|
+
throw new BadRequestError(`"${field}" must be at least ${options.minLength} characters.`);
|
|
96
|
+
}
|
|
97
|
+
if (options.maxLength !== undefined && trimmed.length > options.maxLength) {
|
|
98
|
+
throw new BadRequestError(`"${field}" must be at most ${options.maxLength} characters.`);
|
|
99
|
+
}
|
|
100
|
+
if (options.pattern && !options.pattern.test(trimmed)) {
|
|
101
|
+
throw new BadRequestError(`"${field}" has an invalid format.`);
|
|
102
|
+
}
|
|
103
|
+
return trimmed;
|
|
104
|
+
}
|
|
105
|
+
function readOptionalString(payload, field, options = {}) {
|
|
106
|
+
if (!(field in payload) || payload[field] === undefined) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
return readRequiredString(payload, field, options);
|
|
110
|
+
}
|
|
111
|
+
function readRequiredEnum(payload, field, allowedValues) {
|
|
112
|
+
const value = readRequiredString(payload, field);
|
|
113
|
+
if (!allowedValues.includes(value)) {
|
|
114
|
+
throw new BadRequestError(`"${field}" must be one of: ${allowedValues.join(", ")}.`);
|
|
115
|
+
}
|
|
116
|
+
return value;
|
|
117
|
+
}
|
|
118
|
+
function readOptionalEnum(payload, field, allowedValues) {
|
|
119
|
+
if (!(field in payload) || payload[field] === undefined) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
return readRequiredEnum(payload, field, allowedValues);
|
|
123
|
+
}
|
|
124
|
+
function readRequiredPositiveInt(payload, field) {
|
|
125
|
+
const value = payload[field];
|
|
126
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
|
|
127
|
+
throw new BadRequestError(`"${field}" is required and must be a positive integer.`);
|
|
128
|
+
}
|
|
129
|
+
return value;
|
|
130
|
+
}
|
|
131
|
+
function readOptionalPositiveInt(payload, field) {
|
|
132
|
+
if (!(field in payload) || payload[field] === undefined) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
return readRequiredPositiveInt(payload, field);
|
|
136
|
+
}
|
|
35
137
|
function parsePositiveIntParam(value, name = "id") {
|
|
36
138
|
const parsed = Number.parseInt(value, 10);
|
|
37
139
|
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
@@ -3,12 +3,65 @@
|
|
|
3
3
|
import { currentAuthUser } from "@getstrata/core/auth/authContext";
|
|
4
4
|
import { BadRequestError } from "@getstrata/core/errors/http";
|
|
5
5
|
import { currentTenantId } from "@getstrata/core/tenant/tenantContext";
|
|
6
|
+
function buildRequestCacheKey(fallbackPath, request) {
|
|
7
|
+
if (!request) {
|
|
8
|
+
return fallbackPath;
|
|
9
|
+
}
|
|
10
|
+
const url = new URL(request.url);
|
|
11
|
+
const user = currentAuthUser();
|
|
12
|
+
const authScope = user ? `u:${user.id}` : "guest";
|
|
13
|
+
const tenantScope = `t:${currentTenantId()}`;
|
|
14
|
+
return `${authScope}|${tenantScope}|${url.pathname}${url.search}`;
|
|
15
|
+
}
|
|
6
16
|
function getQueryParams(request) {
|
|
7
17
|
if (!request) {
|
|
8
18
|
return new URLSearchParams;
|
|
9
19
|
}
|
|
10
20
|
return new URL(request.url).searchParams;
|
|
11
21
|
}
|
|
22
|
+
function parseOptionalPositiveIntQueryParam(params, name) {
|
|
23
|
+
const value = params.get(name);
|
|
24
|
+
if (value === null || value.trim() === "") {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const parsed = Number.parseInt(value, 10);
|
|
28
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
29
|
+
throw new BadRequestError(`Invalid query parameter "${name}". Expected a positive integer.`);
|
|
30
|
+
}
|
|
31
|
+
return parsed;
|
|
32
|
+
}
|
|
33
|
+
function parseOptionalBooleanQueryParam(params, name) {
|
|
34
|
+
const value = params.get(name);
|
|
35
|
+
if (value === null || value.trim() === "") {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
switch (value.toLowerCase()) {
|
|
39
|
+
case "true":
|
|
40
|
+
case "1":
|
|
41
|
+
return true;
|
|
42
|
+
case "false":
|
|
43
|
+
case "0":
|
|
44
|
+
return false;
|
|
45
|
+
default:
|
|
46
|
+
throw new BadRequestError(`Invalid query parameter "${name}". Expected a boolean.`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function parseOptionalEnumQueryParam(params, name, allowedValues) {
|
|
50
|
+
const value = params.get(name);
|
|
51
|
+
if (value === null || value.trim() === "") {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (!allowedValues.includes(value)) {
|
|
55
|
+
throw new BadRequestError(`Invalid query parameter "${name}". Expected one of: ${allowedValues.join(", ")}.`);
|
|
56
|
+
}
|
|
57
|
+
return value;
|
|
58
|
+
}
|
|
59
|
+
function expectObject(value, label = "request body") {
|
|
60
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
61
|
+
throw new BadRequestError(`${label} must be a JSON object.`);
|
|
62
|
+
}
|
|
63
|
+
return value;
|
|
64
|
+
}
|
|
12
65
|
async function parseJsonBody(request, validator) {
|
|
13
66
|
let payload;
|
|
14
67
|
try {
|
|
@@ -18,6 +71,55 @@ async function parseJsonBody(request, validator) {
|
|
|
18
71
|
}
|
|
19
72
|
return validator(payload);
|
|
20
73
|
}
|
|
74
|
+
function readRequiredString(payload, field, options = {}) {
|
|
75
|
+
const value = payload[field];
|
|
76
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
77
|
+
throw new BadRequestError(`"${field}" is required and must be a string.`);
|
|
78
|
+
}
|
|
79
|
+
const trimmed = value.trim();
|
|
80
|
+
if (options.minLength !== undefined && trimmed.length < options.minLength) {
|
|
81
|
+
throw new BadRequestError(`"${field}" must be at least ${options.minLength} characters.`);
|
|
82
|
+
}
|
|
83
|
+
if (options.maxLength !== undefined && trimmed.length > options.maxLength) {
|
|
84
|
+
throw new BadRequestError(`"${field}" must be at most ${options.maxLength} characters.`);
|
|
85
|
+
}
|
|
86
|
+
if (options.pattern && !options.pattern.test(trimmed)) {
|
|
87
|
+
throw new BadRequestError(`"${field}" has an invalid format.`);
|
|
88
|
+
}
|
|
89
|
+
return trimmed;
|
|
90
|
+
}
|
|
91
|
+
function readOptionalString(payload, field, options = {}) {
|
|
92
|
+
if (!(field in payload) || payload[field] === undefined) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
return readRequiredString(payload, field, options);
|
|
96
|
+
}
|
|
97
|
+
function readRequiredEnum(payload, field, allowedValues) {
|
|
98
|
+
const value = readRequiredString(payload, field);
|
|
99
|
+
if (!allowedValues.includes(value)) {
|
|
100
|
+
throw new BadRequestError(`"${field}" must be one of: ${allowedValues.join(", ")}.`);
|
|
101
|
+
}
|
|
102
|
+
return value;
|
|
103
|
+
}
|
|
104
|
+
function readOptionalEnum(payload, field, allowedValues) {
|
|
105
|
+
if (!(field in payload) || payload[field] === undefined) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
return readRequiredEnum(payload, field, allowedValues);
|
|
109
|
+
}
|
|
110
|
+
function readRequiredPositiveInt(payload, field) {
|
|
111
|
+
const value = payload[field];
|
|
112
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
|
|
113
|
+
throw new BadRequestError(`"${field}" is required and must be a positive integer.`);
|
|
114
|
+
}
|
|
115
|
+
return value;
|
|
116
|
+
}
|
|
117
|
+
function readOptionalPositiveInt(payload, field) {
|
|
118
|
+
if (!(field in payload) || payload[field] === undefined) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
return readRequiredPositiveInt(payload, field);
|
|
122
|
+
}
|
|
21
123
|
function parsePositiveIntParam(value, name = "id") {
|
|
22
124
|
const parsed = Number.parseInt(value, 10);
|
|
23
125
|
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
@@ -109,12 +109,65 @@ function applyConditionalGet(request, response, etag) {
|
|
|
109
109
|
import { currentAuthUser } from "@getstrata/core/auth/authContext";
|
|
110
110
|
import { BadRequestError } from "@getstrata/core/errors/http";
|
|
111
111
|
import { currentTenantId } from "@getstrata/core/tenant/tenantContext";
|
|
112
|
+
function buildRequestCacheKey(fallbackPath, request) {
|
|
113
|
+
if (!request) {
|
|
114
|
+
return fallbackPath;
|
|
115
|
+
}
|
|
116
|
+
const url = new URL(request.url);
|
|
117
|
+
const user = currentAuthUser();
|
|
118
|
+
const authScope = user ? `u:${user.id}` : "guest";
|
|
119
|
+
const tenantScope = `t:${currentTenantId()}`;
|
|
120
|
+
return `${authScope}|${tenantScope}|${url.pathname}${url.search}`;
|
|
121
|
+
}
|
|
112
122
|
function getQueryParams(request) {
|
|
113
123
|
if (!request) {
|
|
114
124
|
return new URLSearchParams;
|
|
115
125
|
}
|
|
116
126
|
return new URL(request.url).searchParams;
|
|
117
127
|
}
|
|
128
|
+
function parseOptionalPositiveIntQueryParam(params, name) {
|
|
129
|
+
const value = params.get(name);
|
|
130
|
+
if (value === null || value.trim() === "") {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const parsed = Number.parseInt(value, 10);
|
|
134
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
135
|
+
throw new BadRequestError(`Invalid query parameter "${name}". Expected a positive integer.`);
|
|
136
|
+
}
|
|
137
|
+
return parsed;
|
|
138
|
+
}
|
|
139
|
+
function parseOptionalBooleanQueryParam(params, name) {
|
|
140
|
+
const value = params.get(name);
|
|
141
|
+
if (value === null || value.trim() === "") {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
switch (value.toLowerCase()) {
|
|
145
|
+
case "true":
|
|
146
|
+
case "1":
|
|
147
|
+
return true;
|
|
148
|
+
case "false":
|
|
149
|
+
case "0":
|
|
150
|
+
return false;
|
|
151
|
+
default:
|
|
152
|
+
throw new BadRequestError(`Invalid query parameter "${name}". Expected a boolean.`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
function parseOptionalEnumQueryParam(params, name, allowedValues) {
|
|
156
|
+
const value = params.get(name);
|
|
157
|
+
if (value === null || value.trim() === "") {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (!allowedValues.includes(value)) {
|
|
161
|
+
throw new BadRequestError(`Invalid query parameter "${name}". Expected one of: ${allowedValues.join(", ")}.`);
|
|
162
|
+
}
|
|
163
|
+
return value;
|
|
164
|
+
}
|
|
165
|
+
function expectObject(value, label = "request body") {
|
|
166
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
167
|
+
throw new BadRequestError(`${label} must be a JSON object.`);
|
|
168
|
+
}
|
|
169
|
+
return value;
|
|
170
|
+
}
|
|
118
171
|
async function parseJsonBody(request, validator) {
|
|
119
172
|
let payload;
|
|
120
173
|
try {
|
|
@@ -124,6 +177,55 @@ async function parseJsonBody(request, validator) {
|
|
|
124
177
|
}
|
|
125
178
|
return validator(payload);
|
|
126
179
|
}
|
|
180
|
+
function readRequiredString(payload, field, options = {}) {
|
|
181
|
+
const value = payload[field];
|
|
182
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
183
|
+
throw new BadRequestError(`"${field}" is required and must be a string.`);
|
|
184
|
+
}
|
|
185
|
+
const trimmed = value.trim();
|
|
186
|
+
if (options.minLength !== undefined && trimmed.length < options.minLength) {
|
|
187
|
+
throw new BadRequestError(`"${field}" must be at least ${options.minLength} characters.`);
|
|
188
|
+
}
|
|
189
|
+
if (options.maxLength !== undefined && trimmed.length > options.maxLength) {
|
|
190
|
+
throw new BadRequestError(`"${field}" must be at most ${options.maxLength} characters.`);
|
|
191
|
+
}
|
|
192
|
+
if (options.pattern && !options.pattern.test(trimmed)) {
|
|
193
|
+
throw new BadRequestError(`"${field}" has an invalid format.`);
|
|
194
|
+
}
|
|
195
|
+
return trimmed;
|
|
196
|
+
}
|
|
197
|
+
function readOptionalString(payload, field, options = {}) {
|
|
198
|
+
if (!(field in payload) || payload[field] === undefined) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
return readRequiredString(payload, field, options);
|
|
202
|
+
}
|
|
203
|
+
function readRequiredEnum(payload, field, allowedValues) {
|
|
204
|
+
const value = readRequiredString(payload, field);
|
|
205
|
+
if (!allowedValues.includes(value)) {
|
|
206
|
+
throw new BadRequestError(`"${field}" must be one of: ${allowedValues.join(", ")}.`);
|
|
207
|
+
}
|
|
208
|
+
return value;
|
|
209
|
+
}
|
|
210
|
+
function readOptionalEnum(payload, field, allowedValues) {
|
|
211
|
+
if (!(field in payload) || payload[field] === undefined) {
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
return readRequiredEnum(payload, field, allowedValues);
|
|
215
|
+
}
|
|
216
|
+
function readRequiredPositiveInt(payload, field) {
|
|
217
|
+
const value = payload[field];
|
|
218
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
|
|
219
|
+
throw new BadRequestError(`"${field}" is required and must be a positive integer.`);
|
|
220
|
+
}
|
|
221
|
+
return value;
|
|
222
|
+
}
|
|
223
|
+
function readOptionalPositiveInt(payload, field) {
|
|
224
|
+
if (!(field in payload) || payload[field] === undefined) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
return readRequiredPositiveInt(payload, field);
|
|
228
|
+
}
|
|
127
229
|
function parsePositiveIntParam(value, name = "id") {
|
|
128
230
|
const parsed = Number.parseInt(value, 10);
|
|
129
231
|
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// ../../src/core/http/validation.ts
|
|
3
|
+
import { currentAuthUser } from "@getstrata/core/auth/authContext";
|
|
4
|
+
import { BadRequestError } from "@getstrata/core/errors/http";
|
|
5
|
+
import { currentTenantId } from "@getstrata/core/tenant/tenantContext";
|
|
6
|
+
function buildRequestCacheKey(fallbackPath, request) {
|
|
7
|
+
if (!request) {
|
|
8
|
+
return fallbackPath;
|
|
9
|
+
}
|
|
10
|
+
const url = new URL(request.url);
|
|
11
|
+
const user = currentAuthUser();
|
|
12
|
+
const authScope = user ? `u:${user.id}` : "guest";
|
|
13
|
+
const tenantScope = `t:${currentTenantId()}`;
|
|
14
|
+
return `${authScope}|${tenantScope}|${url.pathname}${url.search}`;
|
|
15
|
+
}
|
|
16
|
+
function getQueryParams(request) {
|
|
17
|
+
if (!request) {
|
|
18
|
+
return new URLSearchParams;
|
|
19
|
+
}
|
|
20
|
+
return new URL(request.url).searchParams;
|
|
21
|
+
}
|
|
22
|
+
function parseOptionalPositiveIntQueryParam(params, name) {
|
|
23
|
+
const value = params.get(name);
|
|
24
|
+
if (value === null || value.trim() === "") {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const parsed = Number.parseInt(value, 10);
|
|
28
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
29
|
+
throw new BadRequestError(`Invalid query parameter "${name}". Expected a positive integer.`);
|
|
30
|
+
}
|
|
31
|
+
return parsed;
|
|
32
|
+
}
|
|
33
|
+
function parseOptionalBooleanQueryParam(params, name) {
|
|
34
|
+
const value = params.get(name);
|
|
35
|
+
if (value === null || value.trim() === "") {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
switch (value.toLowerCase()) {
|
|
39
|
+
case "true":
|
|
40
|
+
case "1":
|
|
41
|
+
return true;
|
|
42
|
+
case "false":
|
|
43
|
+
case "0":
|
|
44
|
+
return false;
|
|
45
|
+
default:
|
|
46
|
+
throw new BadRequestError(`Invalid query parameter "${name}". Expected a boolean.`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function parseOptionalEnumQueryParam(params, name, allowedValues) {
|
|
50
|
+
const value = params.get(name);
|
|
51
|
+
if (value === null || value.trim() === "") {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (!allowedValues.includes(value)) {
|
|
55
|
+
throw new BadRequestError(`Invalid query parameter "${name}". Expected one of: ${allowedValues.join(", ")}.`);
|
|
56
|
+
}
|
|
57
|
+
return value;
|
|
58
|
+
}
|
|
59
|
+
function expectObject(value, label = "request body") {
|
|
60
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
61
|
+
throw new BadRequestError(`${label} must be a JSON object.`);
|
|
62
|
+
}
|
|
63
|
+
return value;
|
|
64
|
+
}
|
|
65
|
+
async function parseJsonBody(request, validator) {
|
|
66
|
+
let payload;
|
|
67
|
+
try {
|
|
68
|
+
payload = await request.json();
|
|
69
|
+
} catch {
|
|
70
|
+
throw new BadRequestError("Request body must be valid JSON.");
|
|
71
|
+
}
|
|
72
|
+
return validator(payload);
|
|
73
|
+
}
|
|
74
|
+
function readRequiredString(payload, field, options = {}) {
|
|
75
|
+
const value = payload[field];
|
|
76
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
77
|
+
throw new BadRequestError(`"${field}" is required and must be a string.`);
|
|
78
|
+
}
|
|
79
|
+
const trimmed = value.trim();
|
|
80
|
+
if (options.minLength !== undefined && trimmed.length < options.minLength) {
|
|
81
|
+
throw new BadRequestError(`"${field}" must be at least ${options.minLength} characters.`);
|
|
82
|
+
}
|
|
83
|
+
if (options.maxLength !== undefined && trimmed.length > options.maxLength) {
|
|
84
|
+
throw new BadRequestError(`"${field}" must be at most ${options.maxLength} characters.`);
|
|
85
|
+
}
|
|
86
|
+
if (options.pattern && !options.pattern.test(trimmed)) {
|
|
87
|
+
throw new BadRequestError(`"${field}" has an invalid format.`);
|
|
88
|
+
}
|
|
89
|
+
return trimmed;
|
|
90
|
+
}
|
|
91
|
+
function readOptionalString(payload, field, options = {}) {
|
|
92
|
+
if (!(field in payload) || payload[field] === undefined) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
return readRequiredString(payload, field, options);
|
|
96
|
+
}
|
|
97
|
+
function readRequiredEnum(payload, field, allowedValues) {
|
|
98
|
+
const value = readRequiredString(payload, field);
|
|
99
|
+
if (!allowedValues.includes(value)) {
|
|
100
|
+
throw new BadRequestError(`"${field}" must be one of: ${allowedValues.join(", ")}.`);
|
|
101
|
+
}
|
|
102
|
+
return value;
|
|
103
|
+
}
|
|
104
|
+
function readOptionalEnum(payload, field, allowedValues) {
|
|
105
|
+
if (!(field in payload) || payload[field] === undefined) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
return readRequiredEnum(payload, field, allowedValues);
|
|
109
|
+
}
|
|
110
|
+
function readRequiredPositiveInt(payload, field) {
|
|
111
|
+
const value = payload[field];
|
|
112
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
|
|
113
|
+
throw new BadRequestError(`"${field}" is required and must be a positive integer.`);
|
|
114
|
+
}
|
|
115
|
+
return value;
|
|
116
|
+
}
|
|
117
|
+
function readOptionalPositiveInt(payload, field) {
|
|
118
|
+
if (!(field in payload) || payload[field] === undefined) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
return readRequiredPositiveInt(payload, field);
|
|
122
|
+
}
|
|
123
|
+
function parsePositiveIntParam(value, name = "id") {
|
|
124
|
+
const parsed = Number.parseInt(value, 10);
|
|
125
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
126
|
+
throw new BadRequestError(`Invalid ${name}. Expected a positive integer.`);
|
|
127
|
+
}
|
|
128
|
+
return parsed;
|
|
129
|
+
}
|
|
130
|
+
export {
|
|
131
|
+
readRequiredString,
|
|
132
|
+
readRequiredPositiveInt,
|
|
133
|
+
readRequiredEnum,
|
|
134
|
+
readOptionalString,
|
|
135
|
+
readOptionalPositiveInt,
|
|
136
|
+
readOptionalEnum,
|
|
137
|
+
parsePositiveIntParam,
|
|
138
|
+
parseOptionalPositiveIntQueryParam,
|
|
139
|
+
parseOptionalEnumQueryParam,
|
|
140
|
+
parseOptionalBooleanQueryParam,
|
|
141
|
+
parseJsonBody,
|
|
142
|
+
getQueryParams,
|
|
143
|
+
expectObject,
|
|
144
|
+
buildRequestCacheKey
|
|
145
|
+
};
|
|
@@ -168,7 +168,7 @@ function isFeatureEnabled(feature) {
|
|
|
168
168
|
}
|
|
169
169
|
|
|
170
170
|
// ../../src/modules/user/apiTokenRepository.ts
|
|
171
|
-
import { BaseRepository } from "@getstrata/core/database";
|
|
171
|
+
import { BaseRepository } from "@getstrata/core/database/baseRepository";
|
|
172
172
|
|
|
173
173
|
// ../../src/config/database.ts
|
|
174
174
|
function readInteger(name, fallback) {
|
|
@@ -294,7 +294,7 @@ var db = new Proxy(function database() {}, {
|
|
|
294
294
|
var connection_default = db;
|
|
295
295
|
|
|
296
296
|
// ../../src/modules/user/apiTokenTable.ts
|
|
297
|
-
import { defineTable } from "@getstrata/core/database";
|
|
297
|
+
import { defineTable } from "@getstrata/core/database/table";
|
|
298
298
|
var apiTokenTable = defineTable({
|
|
299
299
|
name: "api_token",
|
|
300
300
|
primaryKey: "id",
|
|
@@ -452,10 +452,10 @@ class AuthService {
|
|
|
452
452
|
}
|
|
453
453
|
|
|
454
454
|
// ../../src/modules/user/notificationRepository.ts
|
|
455
|
-
import { BaseRepository as BaseRepository2 } from "@getstrata/core/database";
|
|
455
|
+
import { BaseRepository as BaseRepository2 } from "@getstrata/core/database/baseRepository";
|
|
456
456
|
|
|
457
457
|
// ../../src/modules/user/notificationTable.ts
|
|
458
|
-
import { defineTable as defineTable2 } from "@getstrata/core/database";
|
|
458
|
+
import { defineTable as defineTable2 } from "@getstrata/core/database/table";
|
|
459
459
|
var notificationTable = defineTable2({
|
|
460
460
|
name: "notification",
|
|
461
461
|
primaryKey: "id",
|
|
@@ -467,7 +467,8 @@ var notificationTable = defineTable2({
|
|
|
467
467
|
import { NotFoundError } from "@getstrata/core/errors/http";
|
|
468
468
|
|
|
469
469
|
// ../../src/modules/user/oauthIdentityRepository.ts
|
|
470
|
-
import { BaseRepository as BaseRepository3
|
|
470
|
+
import { BaseRepository as BaseRepository3 } from "@getstrata/core/database/baseRepository";
|
|
471
|
+
import { defineTable as defineTable3 } from "@getstrata/core/database/table";
|
|
471
472
|
var oauthIdentityTable = defineTable3({
|
|
472
473
|
name: "oauth_identity",
|
|
473
474
|
primaryKey: "id",
|
|
@@ -481,11 +482,11 @@ import {
|
|
|
481
482
|
revealEmail
|
|
482
483
|
} from "@getstrata/core/crypto/fieldEncryption";
|
|
483
484
|
import { revealMfaSecret as revealMfaSecret2 } from "@getstrata/core/crypto/mfaSecret";
|
|
484
|
-
import { BaseRepository as BaseRepository4 } from "@getstrata/core/database";
|
|
485
|
+
import { BaseRepository as BaseRepository4 } from "@getstrata/core/database/baseRepository";
|
|
485
486
|
import { currentTenantId as currentTenantId2 } from "@getstrata/core/tenant/tenantContext";
|
|
486
487
|
|
|
487
488
|
// ../../src/modules/user/table.ts
|
|
488
|
-
import { defineTable as defineTable4 } from "@getstrata/core/database";
|
|
489
|
+
import { defineTable as defineTable4 } from "@getstrata/core/database/table";
|
|
489
490
|
var userTable = defineTable4({
|
|
490
491
|
name: "users",
|
|
491
492
|
primaryKey: "id",
|