@potentiajs/core 0.1.0-preview.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/CHANGELOG.md +45 -0
- package/LICENSE +21 -0
- package/README.md +441 -0
- package/bin/potentia.js +14 -0
- package/examples/file-routing-basic/README.md +156 -0
- package/examples/file-routing-basic/app.js +16 -0
- package/examples/file-routing-basic/generate.js +21 -0
- package/examples/file-routing-basic/routes/health.js +3 -0
- package/examples/file-routing-basic/routes/index.js +3 -0
- package/examples/file-routing-basic/routes/users/[id].js +6 -0
- package/examples/file-routing-basic/routes/users/_routes.js +15 -0
- package/examples/form-rendering-basic/README.md +43 -0
- package/examples/form-rendering-basic/index.js +120 -0
- package/examples/full-flow-basic/README.md +141 -0
- package/examples/full-flow-basic/app.js +16 -0
- package/examples/full-flow-basic/form.js +205 -0
- package/examples/full-flow-basic/generate.js +21 -0
- package/examples/full-flow-basic/routes/index.js +5 -0
- package/examples/full-flow-basic/routes/users/[id].js +5 -0
- package/examples/full-flow-basic/routes/users/_routes.js +8 -0
- package/examples/full-flow-basic/routes/users/index.js +5 -0
- package/examples/full-flow-basic/routes/users/new.js +5 -0
- package/package.json +90 -0
- package/src/cli.js +407 -0
- package/src/dev/file-routing/diagnostics.js +24 -0
- package/src/dev/file-routing/generator.js +126 -0
- package/src/dev/file-routing/index.js +5 -0
- package/src/dev/file-routing/path-mapping.js +125 -0
- package/src/dev/file-routing/scanner.js +154 -0
- package/src/dev/file-routing/writer.js +100 -0
- package/src/file-routing.d.ts +36 -0
- package/src/file-routing.js +1 -0
- package/src/forms.d.ts +9 -0
- package/src/forms.js +334 -0
- package/src/index.d.ts +185 -0
- package/src/index.js +15 -0
- package/src/index.mjs +25 -0
- package/src/kernel/action-projection.js +35 -0
- package/src/kernel/action.js +172 -0
- package/src/kernel/app.js +144 -0
- package/src/kernel/context.js +37 -0
- package/src/kernel/contract-projection.js +129 -0
- package/src/kernel/contract.js +135 -0
- package/src/kernel/diagnostics.js +147 -0
- package/src/kernel/effect.js +119 -0
- package/src/kernel/error.js +93 -0
- package/src/kernel/form-projection.js +251 -0
- package/src/kernel/form-state.js +173 -0
- package/src/kernel/plugin.js +46 -0
- package/src/kernel/response.js +187 -0
- package/src/kernel/result.js +45 -0
- package/src/kernel/route-collection.js +148 -0
- package/src/kernel/route-id.js +11 -0
- package/src/kernel/route-manifest.js +129 -0
- package/src/kernel/route-projection.js +139 -0
- package/src/kernel/route.js +156 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { createRootIssue, normalizeIssue } from './diagnostics.js';
|
|
2
|
+
|
|
3
|
+
const DEFAULT_ROOT_KEY = '_form';
|
|
4
|
+
const DEFAULT_SENSITIVE_FIELDS = new Set([
|
|
5
|
+
'password',
|
|
6
|
+
'confirmpassword',
|
|
7
|
+
'currentpassword',
|
|
8
|
+
'newpassword',
|
|
9
|
+
'token',
|
|
10
|
+
'secret',
|
|
11
|
+
'apikey',
|
|
12
|
+
'authorization',
|
|
13
|
+
'auth',
|
|
14
|
+
'credential',
|
|
15
|
+
'session',
|
|
16
|
+
'cookie'
|
|
17
|
+
]);
|
|
18
|
+
const DANGEROUS_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
19
|
+
|
|
20
|
+
export function createFormState(input = {}) {
|
|
21
|
+
const normalizedInput = isPlainObject(input) ? input : {};
|
|
22
|
+
const ok = Object.prototype.hasOwnProperty.call(normalizedInput, 'ok') ? Boolean(normalizedInput.ok) : false;
|
|
23
|
+
const error = normalizeFormError(normalizedInput.error, ok);
|
|
24
|
+
const issues = normalizeFormIssues(normalizedInput.issues, ok, error);
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
ok: ok,
|
|
28
|
+
kind: 'form',
|
|
29
|
+
values: preserveFormValues(normalizedInput.values),
|
|
30
|
+
errors: groupIssuesByField(issues),
|
|
31
|
+
issues: issues,
|
|
32
|
+
error: error,
|
|
33
|
+
value: Object.prototype.hasOwnProperty.call(normalizedInput, 'value') ? normalizedInput.value : null,
|
|
34
|
+
meta: Object.prototype.hasOwnProperty.call(normalizedInput, 'meta') ? normalizedInput.meta : null
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function preserveFormValues(input, options = {}) {
|
|
39
|
+
if (!isPlainObject(input)) return {};
|
|
40
|
+
|
|
41
|
+
const sensitiveFields = createSensitiveSet(options.sensitiveFields);
|
|
42
|
+
const result = {};
|
|
43
|
+
|
|
44
|
+
for (const key of Object.keys(input)) {
|
|
45
|
+
if (!isSafeKey(key) || isSensitiveSegment(key, sensitiveFields)) continue;
|
|
46
|
+
|
|
47
|
+
const value = preserveValue(input[key], [key], sensitiveFields, new Set());
|
|
48
|
+
if (value !== undefined) result[key] = value;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function groupIssuesByField(issues, options = {}) {
|
|
55
|
+
if (!Array.isArray(issues) || issues.length === 0) return {};
|
|
56
|
+
|
|
57
|
+
const rootKey = typeof options.rootKey === 'string' && options.rootKey.length > 0 ? options.rootKey : DEFAULT_ROOT_KEY;
|
|
58
|
+
const errors = {};
|
|
59
|
+
|
|
60
|
+
for (const issue of issues) {
|
|
61
|
+
const key = issue && typeof issue === 'object' && typeof issue.field === 'string' && issue.field.length > 0 ? issue.field : rootKey;
|
|
62
|
+
if (!Array.isArray(errors[key])) errors[key] = [];
|
|
63
|
+
errors[key].push(issue);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return errors;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function normalizeFormIssues(issues, ok, error) {
|
|
70
|
+
if (Array.isArray(issues) && issues.length > 0) {
|
|
71
|
+
return issues.map((issue) => normalizeIssue(issue, { boundary: 'input', source: 'framework' }));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (!ok && error) {
|
|
75
|
+
return [createRootIssue({
|
|
76
|
+
code: error.code || 'form_failed',
|
|
77
|
+
message: error.message || 'Form submission failed',
|
|
78
|
+
boundary: 'handler',
|
|
79
|
+
source: 'framework'
|
|
80
|
+
})];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function normalizeFormError(error, ok) {
|
|
87
|
+
if (ok && !error) return null;
|
|
88
|
+
if (!error || typeof error !== 'object') {
|
|
89
|
+
return ok ? null : { code: 'FORM_FAILED', message: 'Form submission failed' };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
code: typeof error.code === 'string' && error.code.length > 0 ? error.code : 'FORM_FAILED',
|
|
94
|
+
message: typeof error.message === 'string' && error.message.length > 0 ? error.message : 'Form submission failed'
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function preserveValue(value, path, sensitiveFields, seen) {
|
|
99
|
+
if (isSensitivePath(path, sensitiveFields)) return undefined;
|
|
100
|
+
if (value === null) return null;
|
|
101
|
+
|
|
102
|
+
const type = typeof value;
|
|
103
|
+
if (type === 'string' || type === 'number' || type === 'boolean') return value;
|
|
104
|
+
if (type === 'function' || type === 'symbol' || type === 'bigint' || type === 'undefined') return undefined;
|
|
105
|
+
|
|
106
|
+
if (Array.isArray(value)) {
|
|
107
|
+
const preserved = [];
|
|
108
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
109
|
+
const item = preserveValue(value[index], path.concat(index), sensitiveFields, seen);
|
|
110
|
+
if (item !== undefined && isSafeArrayValue(item)) preserved.push(item);
|
|
111
|
+
}
|
|
112
|
+
return preserved;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (!isPlainObject(value)) return undefined;
|
|
116
|
+
if (seen.has(value)) return undefined;
|
|
117
|
+
|
|
118
|
+
seen.add(value);
|
|
119
|
+
const result = {};
|
|
120
|
+
for (const key of Object.keys(value)) {
|
|
121
|
+
if (!isSafeKey(key) || isSensitiveSegment(key, sensitiveFields)) continue;
|
|
122
|
+
const preserved = preserveValue(value[key], path.concat(key), sensitiveFields, seen);
|
|
123
|
+
if (preserved !== undefined) result[key] = preserved;
|
|
124
|
+
}
|
|
125
|
+
seen.delete(value);
|
|
126
|
+
|
|
127
|
+
return result;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function isSafeArrayValue(value) {
|
|
131
|
+
if (value === null) return true;
|
|
132
|
+
if (Array.isArray(value)) return value.every(isSafeArrayValue);
|
|
133
|
+
if (isPlainObject(value)) return true;
|
|
134
|
+
const type = typeof value;
|
|
135
|
+
return type === 'string' || type === 'number' || type === 'boolean';
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function createSensitiveSet(extra = []) {
|
|
139
|
+
const fields = new Set(DEFAULT_SENSITIVE_FIELDS);
|
|
140
|
+
if (Array.isArray(extra)) {
|
|
141
|
+
for (const field of extra) {
|
|
142
|
+
if (typeof field === 'string' && field.length > 0) fields.add(normalizeFieldName(field));
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return fields;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function isSensitivePath(path, sensitiveFields) {
|
|
149
|
+
return path.some((segment) => typeof segment === 'string' && isSensitiveSegment(segment, sensitiveFields));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function isSensitiveSegment(segment, sensitiveFields) {
|
|
153
|
+
const normalized = normalizeFieldName(segment);
|
|
154
|
+
if (sensitiveFields.has(normalized)) return true;
|
|
155
|
+
for (const field of sensitiveFields) {
|
|
156
|
+
if (normalized.includes(field)) return true;
|
|
157
|
+
}
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function normalizeFieldName(value) {
|
|
162
|
+
return String(value).replace(/[^a-zA-Z0-9]/g, '').toLowerCase();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function isSafeKey(key) {
|
|
166
|
+
return typeof key === 'string' && key.length > 0 && !DANGEROUS_KEYS.has(key);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function isPlainObject(value) {
|
|
170
|
+
if (!value || typeof value !== 'object') return false;
|
|
171
|
+
const prototype = Object.getPrototypeOf(value);
|
|
172
|
+
return prototype === Object.prototype || prototype === null;
|
|
173
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { createRoutes, normalizeHooks } from './route-collection.js';
|
|
2
|
+
|
|
3
|
+
export function createPlugin(options = {}) {
|
|
4
|
+
return {
|
|
5
|
+
kind: 'plugin',
|
|
6
|
+
name: typeof options.name === 'string' ? options.name : 'anonymous',
|
|
7
|
+
routes: Array.isArray(options.routes) ? options.routes.slice() : [],
|
|
8
|
+
hooks: normalizeHooks(options.hooks),
|
|
9
|
+
contracts: normalizeContracts(options.contracts),
|
|
10
|
+
setup: typeof options.setup === 'function' ? options.setup : null,
|
|
11
|
+
meta: Object.prototype.hasOwnProperty.call(options, 'meta') ? options.meta : null
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function pluginRoutes(plugins = [], appContext = {}) {
|
|
16
|
+
if (!Array.isArray(plugins)) return [];
|
|
17
|
+
|
|
18
|
+
const routes = [];
|
|
19
|
+
for (const plugin of plugins) {
|
|
20
|
+
const normalized = plugin && plugin.kind === 'plugin' ? plugin : createPlugin(plugin);
|
|
21
|
+
runSetup(normalized, appContext);
|
|
22
|
+
routes.push(createRoutes({
|
|
23
|
+
routes: normalized.routes,
|
|
24
|
+
hooks: normalized.hooks,
|
|
25
|
+
contracts: normalized.contracts,
|
|
26
|
+
meta: { plugin: normalized.name }
|
|
27
|
+
}));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return routes;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function runSetup(plugin, appContext) {
|
|
34
|
+
if (!plugin.setup) return;
|
|
35
|
+
|
|
36
|
+
plugin.setup(Object.freeze({
|
|
37
|
+
name: plugin.name,
|
|
38
|
+
state: Object.prototype.hasOwnProperty.call(appContext, 'state') ? appContext.state : null,
|
|
39
|
+
meta: plugin.meta
|
|
40
|
+
}));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function normalizeContracts(contracts = {}) {
|
|
44
|
+
if (!contracts || typeof contracts !== 'object') return {};
|
|
45
|
+
return { ...contracts };
|
|
46
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { fail, normalizeError } from './result.js';
|
|
2
|
+
|
|
3
|
+
export function json(value, init = {}) {
|
|
4
|
+
return createResponseDescriptor('json', value, init);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function text(value, init = {}) {
|
|
8
|
+
return createResponseDescriptor('text', String(value), init);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function redirect(location, status = 302) {
|
|
12
|
+
return createResponseDescriptor('redirect', null, {
|
|
13
|
+
status: status,
|
|
14
|
+
headers: { location: location }
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function toResponse(value) {
|
|
19
|
+
if (value instanceof Response) return value;
|
|
20
|
+
|
|
21
|
+
if (isResult(value)) {
|
|
22
|
+
if (value.ok) return toResponse(value.value);
|
|
23
|
+
return errorResponse(value.error);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (isResponseDescriptor(value)) {
|
|
27
|
+
return descriptorToResponse(value);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (value instanceof Error) {
|
|
31
|
+
return errorResponse(normalizeError(value));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (value && typeof value === 'object' && value.name === 'PotentiaError') {
|
|
35
|
+
return errorResponse(normalizeError(value));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (value === undefined) {
|
|
39
|
+
return new Response(null, { status: 204 });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (value === null) {
|
|
43
|
+
return new Response(null, { status: 204 });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (typeof value === 'string') {
|
|
47
|
+
return descriptorToResponse(text(value));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return descriptorToResponse(json(value));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function errorResponse(error) {
|
|
54
|
+
const normalized = normalizeError(error);
|
|
55
|
+
|
|
56
|
+
if (normalized.details && typeof normalized.details === 'object' && normalized.details.kind === 'form') {
|
|
57
|
+
return descriptorToResponse(json(normalized.details, { status: normalized.status }));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const diagnosticShaped = hasDiagnosticDetails(normalized) || isActionOrDomainCode(normalized.code);
|
|
61
|
+
const body = diagnosticShaped ? {
|
|
62
|
+
ok: false,
|
|
63
|
+
error: {
|
|
64
|
+
code: normalized.code,
|
|
65
|
+
message: normalized.message
|
|
66
|
+
},
|
|
67
|
+
boundary: null,
|
|
68
|
+
issues: []
|
|
69
|
+
} : {
|
|
70
|
+
error: {
|
|
71
|
+
code: normalized.code,
|
|
72
|
+
message: normalized.message
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
if (normalized.details && typeof normalized.details === 'object') {
|
|
77
|
+
if (typeof normalized.details.boundary === 'string') {
|
|
78
|
+
if (diagnosticShaped) body.boundary = normalized.details.boundary;
|
|
79
|
+
body.error.boundary = normalized.details.boundary;
|
|
80
|
+
}
|
|
81
|
+
if (Array.isArray(normalized.details.issues)) {
|
|
82
|
+
if (diagnosticShaped) body.issues = normalized.details.issues;
|
|
83
|
+
body.error.issues = normalized.details.issues;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return descriptorToResponse(json(body, { status: normalized.status }));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function hasDiagnosticDetails(error) {
|
|
91
|
+
return Boolean(error && error.details && typeof error.details === 'object' && (typeof error.details.boundary === 'string' || Array.isArray(error.details.issues)));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function isActionOrDomainCode(code) {
|
|
95
|
+
return typeof code === 'string' && (code.startsWith('POTENTIA_ACTION_') || !code.startsWith('POTENTIA_'));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function internalFailure(error) {
|
|
99
|
+
return fail(normalizeError(error));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function isResponseDescriptor(value) {
|
|
103
|
+
return Boolean(value && typeof value === 'object' && value.kind === 'response');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function responseBody(value) {
|
|
107
|
+
if (isResult(value)) return responseBody(value.value);
|
|
108
|
+
if (isResponseDescriptor(value)) return value.body;
|
|
109
|
+
return value;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function replaceResponseBody(value, body) {
|
|
113
|
+
if (isResult(value)) {
|
|
114
|
+
return {
|
|
115
|
+
ok: value.ok,
|
|
116
|
+
value: replaceResponseBody(value.value, body),
|
|
117
|
+
error: value.error,
|
|
118
|
+
meta: value.meta
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (isResponseDescriptor(value)) {
|
|
123
|
+
return {
|
|
124
|
+
kind: value.kind,
|
|
125
|
+
type: value.type,
|
|
126
|
+
body: body,
|
|
127
|
+
status: value.status,
|
|
128
|
+
headers: value.headers
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return body;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function isRedirectResponse(value) {
|
|
136
|
+
if (isResult(value)) return value.ok && isRedirectResponse(value.value);
|
|
137
|
+
return isResponseDescriptor(value) && value.type === 'redirect';
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function createResponseDescriptor(type, body, init) {
|
|
141
|
+
const headers = new Headers(init.headers || {});
|
|
142
|
+
const status = init.status || (type === 'redirect' ? 302 : 200);
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
kind: 'response',
|
|
146
|
+
type: type,
|
|
147
|
+
body: body,
|
|
148
|
+
status: status,
|
|
149
|
+
headers: headers
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function descriptorToResponse(descriptor) {
|
|
154
|
+
const headers = new Headers(descriptor.headers);
|
|
155
|
+
|
|
156
|
+
if (descriptor.type === 'json') {
|
|
157
|
+
if (!headers.has('content-type')) headers.set('content-type', 'application/json');
|
|
158
|
+
return new Response(JSON.stringify(descriptor.body), {
|
|
159
|
+
status: descriptor.status,
|
|
160
|
+
headers: headers
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (descriptor.type === 'text') {
|
|
165
|
+
if (!headers.has('content-type')) headers.set('content-type', 'text/plain; charset=utf-8');
|
|
166
|
+
return new Response(descriptor.body, {
|
|
167
|
+
status: descriptor.status,
|
|
168
|
+
headers: headers
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (descriptor.type === 'redirect') {
|
|
173
|
+
return new Response(null, {
|
|
174
|
+
status: descriptor.status,
|
|
175
|
+
headers: headers
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return new Response(String(descriptor.body), {
|
|
180
|
+
status: descriptor.status,
|
|
181
|
+
headers: headers
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function isResult(value) {
|
|
186
|
+
return Boolean(value && typeof value === 'object' && typeof value.ok === 'boolean' && 'value' in value && 'error' in value && 'meta' in value);
|
|
187
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { normalizeFrameworkError } from './error.js';
|
|
2
|
+
|
|
3
|
+
export function ok(value, meta = null) {
|
|
4
|
+
return {
|
|
5
|
+
ok: true,
|
|
6
|
+
value: value,
|
|
7
|
+
error: null,
|
|
8
|
+
meta: meta
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function fail(error, meta = null) {
|
|
13
|
+
const normalizedInput = typeof meta === 'number' && error && typeof error === 'object' ? { ...error, status: meta } : error;
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
ok: false,
|
|
17
|
+
value: null,
|
|
18
|
+
error: normalizeError(normalizedInput),
|
|
19
|
+
meta: typeof meta === 'number' ? null : meta
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function normalizeError(error) {
|
|
24
|
+
if (isFormState(error)) {
|
|
25
|
+
return {
|
|
26
|
+
code: error.error.code,
|
|
27
|
+
message: error.error.message,
|
|
28
|
+
status: Number.isInteger(error.status) ? error.status : 400,
|
|
29
|
+
details: error
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const normalized = normalizeFrameworkError(error);
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
code: normalized.code,
|
|
37
|
+
message: normalized.message,
|
|
38
|
+
status: normalized.status,
|
|
39
|
+
details: normalized.detail
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isFormState(value) {
|
|
44
|
+
return Boolean(value && typeof value === 'object' && value.kind === 'form' && typeof value.ok === 'boolean' && value.error && typeof value.error === 'object' && typeof value.error.code === 'string');
|
|
45
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { route } from './route.js';
|
|
2
|
+
|
|
3
|
+
export function createRoutes(options = {}) {
|
|
4
|
+
return {
|
|
5
|
+
kind: 'routes',
|
|
6
|
+
prefix: normalizePrefix(options.prefix || ''),
|
|
7
|
+
routes: Array.isArray(options.routes) ? options.routes.slice() : [],
|
|
8
|
+
hooks: normalizeHooks(options.hooks),
|
|
9
|
+
contracts: normalizeContracts(options.contracts),
|
|
10
|
+
meta: Object.prototype.hasOwnProperty.call(options, 'meta') ? options.meta : null
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function mount(source, options = {}) {
|
|
15
|
+
return {
|
|
16
|
+
kind: 'mount',
|
|
17
|
+
source: source,
|
|
18
|
+
prefix: normalizePrefix(options.prefix || ''),
|
|
19
|
+
hooks: normalizeHooks(options.hooks),
|
|
20
|
+
contracts: normalizeContracts(options.contracts),
|
|
21
|
+
meta: Object.prototype.hasOwnProperty.call(options, 'meta') ? options.meta : null
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function composeRoutes(entries = []) {
|
|
26
|
+
const routes = Array.isArray(entries) ? entries : [];
|
|
27
|
+
return flattenRoutes(routes, emptyScope());
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function flattenRoutes(entries, scope) {
|
|
31
|
+
const flattened = [];
|
|
32
|
+
|
|
33
|
+
for (const entry of entries) {
|
|
34
|
+
if (isMount(entry)) {
|
|
35
|
+
flattened.push(...flattenMounted(entry, scope));
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (isRouteCollection(entry)) {
|
|
40
|
+
flattened.push(...flattenCollection(entry, scope));
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (isRoute(entry)) {
|
|
45
|
+
flattened.push(cloneRoute(entry, scope));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return flattened;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function flattenMounted(entry, scope) {
|
|
53
|
+
const nextScope = mergeScope(scope, {
|
|
54
|
+
prefix: entry.prefix,
|
|
55
|
+
contracts: entry.contracts,
|
|
56
|
+
hooks: entry.hooks
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
if (isRoute(entry.source)) return [cloneRoute(entry.source, nextScope)];
|
|
60
|
+
if (isRouteCollection(entry.source)) return flattenCollection(entry.source, nextScope);
|
|
61
|
+
if (Array.isArray(entry.source)) return flattenRoutes(entry.source, nextScope);
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function flattenCollection(collection, scope) {
|
|
66
|
+
const nextScope = mergeScope(scope, {
|
|
67
|
+
prefix: collection.prefix,
|
|
68
|
+
contracts: collection.contracts,
|
|
69
|
+
hooks: collection.hooks
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
return flattenRoutes(collection.routes, nextScope);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function cloneRoute(source, scope) {
|
|
76
|
+
const cloned = route(source.method, composePath(scope.prefix, source.path), source.handler, {
|
|
77
|
+
...scope.contracts,
|
|
78
|
+
...(source.options || {})
|
|
79
|
+
});
|
|
80
|
+
cloned.hooks = normalizeHooks(scope.hooks);
|
|
81
|
+
return cloned;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function emptyScope() {
|
|
85
|
+
return {
|
|
86
|
+
prefix: '',
|
|
87
|
+
contracts: {},
|
|
88
|
+
hooks: normalizeHooks()
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function mergeScope(parent, child) {
|
|
93
|
+
const childHooks = normalizeHooks(child.hooks);
|
|
94
|
+
return {
|
|
95
|
+
prefix: composePath(parent.prefix, child.prefix || ''),
|
|
96
|
+
contracts: {
|
|
97
|
+
...parent.contracts,
|
|
98
|
+
...normalizeContracts(child.contracts)
|
|
99
|
+
},
|
|
100
|
+
hooks: {
|
|
101
|
+
beforeRequest: parent.hooks.beforeRequest.concat(childHooks.beforeRequest),
|
|
102
|
+
afterResponse: childHooks.afterResponse.concat(parent.hooks.afterResponse),
|
|
103
|
+
onError: childHooks.onError.concat(parent.hooks.onError)
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function normalizeHooks(hooks = {}) {
|
|
109
|
+
return {
|
|
110
|
+
beforeRequest: Array.isArray(hooks.beforeRequest) ? hooks.beforeRequest.slice() : [],
|
|
111
|
+
afterResponse: Array.isArray(hooks.afterResponse) ? hooks.afterResponse.slice() : [],
|
|
112
|
+
onError: Array.isArray(hooks.onError) ? hooks.onError.slice() : []
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function normalizeContracts(contracts = {}) {
|
|
117
|
+
if (!contracts || typeof contracts !== 'object') return {};
|
|
118
|
+
return { ...contracts };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function normalizePrefix(prefix) {
|
|
122
|
+
if (!prefix) return '';
|
|
123
|
+
const normalized = `/${String(prefix).split('/').filter(Boolean).join('/')}`;
|
|
124
|
+
return normalized === '/' ? '' : normalized;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function composePath(prefix, path) {
|
|
128
|
+
const parts = [];
|
|
129
|
+
for (const value of [prefix, path]) {
|
|
130
|
+
const segment = String(value || '').split('/').filter(Boolean).join('/');
|
|
131
|
+
if (segment) parts.push(segment);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (parts.length === 0) return '/';
|
|
135
|
+
return `/${parts.join('/')}`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function isMount(value) {
|
|
139
|
+
return Boolean(value && typeof value === 'object' && value.kind === 'mount');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function isRouteCollection(value) {
|
|
143
|
+
return Boolean(value && typeof value === 'object' && value.kind === 'routes' && Array.isArray(value.routes));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function isRoute(value) {
|
|
147
|
+
return Boolean(value && typeof value === 'object' && typeof value.method === 'string' && typeof value.path === 'string' && Object.prototype.hasOwnProperty.call(value, 'handler'));
|
|
148
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function createRouteId(value) {
|
|
2
|
+
if (!value || typeof value !== 'object') return null;
|
|
3
|
+
if (typeof value.method !== 'string' || typeof value.path !== 'string') return null;
|
|
4
|
+
|
|
5
|
+
return `${String(value.method).toUpperCase()} ${normalizeRoutePath(value.path)}`;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function normalizeRoutePath(path) {
|
|
9
|
+
const normalized = `/${String(path || '').split('/').filter(Boolean).join('/')}`;
|
|
10
|
+
return normalized === '' ? '/' : normalized;
|
|
11
|
+
}
|