akanjs 3.1.0-rc.4 → 3.1.0-rc.6
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/base/baseEnv.ts +5 -2
- package/client/device.ts +1 -1
- package/client/frameConfig.ts +2 -2
- package/client/router.ts +3 -3
- package/client/rscNavigation.ts +4 -1
- package/client/types.ts +3 -2
- package/client/webManifest.ts +5 -3
- package/common/TrustedProxy.ts +14 -0
- package/common/index.ts +1 -0
- package/common/interpolateTranslation.ts +17 -3
- package/common/isEmail.ts +1 -1
- package/common/localeConfig.ts +10 -4
- package/common/markdownSpans.ts +1 -1
- package/common/restClient.ts +3 -1
- package/common/routeConvention.ts +4 -4
- package/common/trimSlashes.ts +11 -0
- package/common/types.ts +1 -1
- package/constant/fieldInfo.ts +2 -2
- package/constant/fieldPlan.ts +80 -0
- package/constant/index.ts +3 -0
- package/constant/inputParser.ts +148 -0
- package/constant/invalidInputError.ts +13 -0
- package/constant/serialize.ts +36 -20
- package/constant/types.ts +1 -1
- package/constant/via.ts +2 -0
- package/dictionary/base.dictionary.ts +3 -0
- package/dictionary/trans.ts +8 -0
- package/document/database.ts +3 -6
- package/document/documentSchema.ts +1 -1
- package/fetch/client/fetchClient.ts +2 -1
- package/fetch/fetchType/appliedReturn.type.ts +3 -1
- package/fetch/types.ts +4 -2
- package/package.json +1 -1
- package/server/akanApp.ts +5 -2
- package/server/akanServer.ts +34 -14
- package/server/artifact/routeClientCache.ts +4 -5
- package/server/artifact/routeSeedIndexStore.ts +3 -20
- package/server/artifact/routeSeedMatcher.ts +70 -0
- package/server/console.ts +1 -1
- package/server/di/diLifecycle.ts +16 -7
- package/server/imageOptimizer.ts +1 -1
- package/server/machine/machineAccount.ts +28 -3
- package/server/machine/machineToken.ts +27 -3
- package/server/mcp/McpExecutionContext.ts +9 -3
- package/server/resolver/signal.resolver.ts +35 -55
- package/server/rscClient.tsx +22 -19
- package/server/rscNavigationState.ts +3 -5
- package/server/rscScrollRestoration.ts +116 -0
- package/server/sitemap.ts +4 -4
- package/server/tunnel/TunnelAgent.ts +9 -1
- package/server/webRouter.ts +17 -2
- package/service/injectInfo.ts +2 -2
- package/service/predefinedAdaptor/sql/SqlDocumentStore.ts +6 -2
- package/service/predefinedAdaptor/sql/dialect/postgres.ts +0 -1
- package/service/predefinedAdaptor/sql/dialect/sqlite.ts +0 -1
- package/signal/endpointCache.ts +7 -1
- package/signal/endpointErrorContract.ts +37 -0
- package/signal/endpointIdempotency.ts +5 -3
- package/signal/endpointInfo.ts +4 -1
- package/signal/endpointRateLimit.ts +6 -2
- package/signal/index.ts +1 -0
- package/signal/mcp/McpDocument.ts +16 -1
- package/signal/mcp/McpUriTemplate.ts +1 -1
- package/signal/middlewareChain.ts +105 -0
- package/signal/openapi/openapi.ts +46 -2
- package/signal/serializer/fetch.serializer.ts +1 -0
- package/signal/signalContext.ts +104 -81
- package/signal/slice.ts +2 -2
- package/signal/trace.ts +8 -2
- package/signal/types.ts +13 -0
- package/store/agent/ScreenReader.ts +2 -1
- package/store/stateBuilder.ts +1 -1
- package/store/storeInstance.ts +2 -2
- package/store/withSelector.ts +2 -2
- package/types/client/rscNavigation.d.ts +1 -0
- package/types/common/index.d.ts +1 -0
- package/types/common/trimSlashes.d.ts +2 -0
- package/types/common/types.d.ts +1 -1
- package/types/constant/fieldInfo.d.ts +2 -2
- package/types/constant/fieldPlan.d.ts +33 -0
- package/types/constant/index.d.ts +3 -0
- package/types/constant/inputParser.d.ts +11 -0
- package/types/constant/invalidInputError.d.ts +8 -0
- package/types/constant/types.d.ts +1 -1
- package/types/dictionary/base.dictionary.d.ts +1 -1
- package/types/dictionary/dictionary.d.ts +3 -3
- package/types/dictionary/trans.d.ts +3 -0
- package/types/document/database.d.ts +2 -3
- package/types/document/documentSchema.d.ts +1 -1
- package/types/fetch/fetchType/appliedReturn.type.d.ts +3 -1
- package/types/fetch/types.d.ts +2 -2
- package/types/server/akanServer.d.ts +1 -1
- package/types/server/artifact/routeSeedMatcher.d.ts +8 -0
- package/types/server/di/diLifecycle.d.ts +1 -0
- package/types/server/machine/machineAccount.d.ts +1 -0
- package/types/server/machine/machineToken.d.ts +9 -0
- package/types/server/rscNavigationState.d.ts +2 -3
- package/types/server/rscScrollRestoration.d.ts +14 -0
- package/types/server/sitemap.d.ts +1 -1
- package/types/service/injectInfo.d.ts +2 -2
- package/types/signal/endpointCache.d.ts +2 -0
- package/types/signal/endpointErrorContract.d.ts +10 -0
- package/types/signal/endpointIdempotency.d.ts +1 -0
- package/types/signal/endpointRateLimit.d.ts +1 -1
- package/types/signal/index.d.ts +1 -0
- package/types/signal/middlewareChain.d.ts +11 -0
- package/types/signal/signalContext.d.ts +2 -6
- package/types/signal/slice.d.ts +1 -1
- package/types/signal/trace.d.ts +1 -1
- package/types/signal/types.d.ts +12 -0
- package/types/store/stateBuilder.d.ts +1 -1
- package/types/store/storeInstance.d.ts +2 -2
- package/types/store/withSelector.d.ts +1 -1
- package/types/ui/Data/CardList.d.ts +1 -1
- package/types/ui/Data/Dashboard.d.ts +1 -1
- package/types/ui/Data/Insight.d.ts +1 -1
- package/types/ui/Data/Item.d.ts +2 -2
- package/types/ui/Data/ListContainer.d.ts +1 -1
- package/types/ui/Data/Pagination.d.ts +1 -1
- package/types/ui/Data/TableList.d.ts +1 -1
- package/types/ui/Field/Relation.d.ts +13 -10
- package/types/ui/Field/index.d.ts +4 -4
- package/types/ui/Field/useRelationOptions.d.ts +3 -1
- package/types/ui/Field/useRelationWindow.d.ts +14 -0
- package/types/ui/Load/View.d.ts +1 -1
- package/types/ui/Pagination.d.ts +1 -1
- package/types/ui/Signal/Arg.d.ts +1 -1
- package/types/ui/System/CSR.d.ts +1 -1
- package/types/ui/System/Client.d.ts +1 -1
- package/types/ui/System/SSR.d.ts +1 -1
- package/types/vendor/@akanjs/native/packages/core/src/plugin.d.ts +3 -3
- package/types/vendor/@akanjs/native/packages/desktop/src/page-veto.d.ts +1 -1
- package/types/vendor/@akanjs/native/packages/desktop/src/plugin.d.ts +1 -1
- package/types/vendor/@akanjs/native/plugins/auth-session/src/desktop.d.ts +2 -2
- package/types/vendor/@akanjs/native/plugins/auth-session/src/index.d.ts +1 -1
- package/types/vendor/@akanjs/native/plugins/autostart/src/desktop.d.ts +2 -2
- package/types/vendor/@akanjs/native/plugins/autostart/src/index.d.ts +1 -1
- package/types/vendor/@akanjs/native/plugins/badge/src/desktop.d.ts +1 -1
- package/types/vendor/@akanjs/native/plugins/badge/src/index.d.ts +1 -1
- package/types/vendor/@akanjs/native/plugins/badge/src/web.d.ts +1 -1
- package/types/vendor/@akanjs/native/plugins/biometric/src/index.d.ts +1 -1
- package/types/vendor/@akanjs/native/plugins/camera/src/desktop.d.ts +1 -1
- package/types/vendor/@akanjs/native/plugins/camera/src/index.d.ts +1 -1
- package/types/vendor/@akanjs/native/plugins/camera/src/web.d.ts +1 -1
- package/types/vendor/@akanjs/native/plugins/clipboard/src/desktop.d.ts +2 -2
- package/types/vendor/@akanjs/native/plugins/clipboard/src/index.d.ts +1 -1
- package/types/vendor/@akanjs/native/plugins/clipboard/src/web.d.ts +1 -1
- package/types/vendor/@akanjs/native/plugins/contacts/src/index.d.ts +1 -1
- package/types/vendor/@akanjs/native/plugins/device/src/desktop.d.ts +2 -2
- package/types/vendor/@akanjs/native/plugins/device/src/index.d.ts +1 -1
- package/types/vendor/@akanjs/native/plugins/device/src/web.d.ts +1 -1
- package/types/vendor/@akanjs/native/plugins/dialog/src/desktop.d.ts +1 -1
- package/types/vendor/@akanjs/native/plugins/dialog/src/index.d.ts +1 -1
- package/types/vendor/@akanjs/native/plugins/dialog/src/web.d.ts +1 -1
- package/types/vendor/@akanjs/native/plugins/dock/src/desktop.d.ts +1 -1
- package/types/vendor/@akanjs/native/plugins/dock/src/index.d.ts +1 -1
- package/types/vendor/@akanjs/native/plugins/file-picker/src/desktop.d.ts +2 -2
- package/types/vendor/@akanjs/native/plugins/file-picker/src/web.d.ts +1 -1
- package/types/vendor/@akanjs/native/plugins/filesystem/src/desktop.d.ts +2 -2
- package/types/vendor/@akanjs/native/plugins/filesystem/src/web.d.ts +1 -1
- package/types/vendor/@akanjs/native/plugins/haptics/src/index.d.ts +1 -1
- package/types/vendor/@akanjs/native/plugins/haptics/src/web.d.ts +1 -1
- package/types/vendor/@akanjs/native/plugins/http/src/desktop.d.ts +2 -2
- package/types/vendor/@akanjs/native/plugins/http/src/web.d.ts +1 -1
- package/types/vendor/@akanjs/native/plugins/keep-awake/src/desktop.d.ts +2 -2
- package/types/vendor/@akanjs/native/plugins/keep-awake/src/index.d.ts +1 -1
- package/types/vendor/@akanjs/native/plugins/keep-awake/src/web.d.ts +1 -1
- package/types/vendor/@akanjs/native/plugins/opener/src/desktop.d.ts +2 -2
- package/types/vendor/@akanjs/native/plugins/opener/src/index.d.ts +1 -1
- package/types/vendor/@akanjs/native/plugins/opener/src/web.d.ts +1 -1
- package/types/vendor/@akanjs/native/plugins/preferences/src/desktop.d.ts +1 -1
- package/types/vendor/@akanjs/native/plugins/preferences/src/index.d.ts +1 -1
- package/types/vendor/@akanjs/native/plugins/preferences/src/web.d.ts +1 -1
- package/types/vendor/@akanjs/native/plugins/secure-storage/src/desktop.d.ts +2 -2
- package/types/vendor/@akanjs/native/plugins/secure-storage/src/index.d.ts +1 -1
- package/types/vendor/@akanjs/native/plugins/share/src/index.d.ts +1 -1
- package/types/vendor/@akanjs/native/plugins/share/src/web.d.ts +1 -1
- package/types/vendor/@akanjs/native/plugins/single-instance/src/desktop.d.ts +2 -2
- package/types/vendor/@akanjs/native/plugins/splash-screen/src/index.d.ts +1 -1
- package/types/vendor/@akanjs/native/plugins/splash-screen/src/web.d.ts +1 -1
- package/types/vendor/@akanjs/native/plugins/sqlite/src/desktop.d.ts +2 -2
- package/types/vendor/@akanjs/native/plugins/toast/src/index.d.ts +1 -1
- package/types/vendor/@akanjs/native/plugins/toast/src/web.d.ts +1 -1
- package/types/vendor/@akanjs/native/plugins/window/src/index.d.ts +2 -2
- package/types/vendor/@akanjs/native/plugins/window-state/src/desktop.d.ts +2 -2
- package/types/vendor/@akanjs/native/plugins/window-state/src/index.d.ts +1 -1
- package/types/vendor/use-agentic/Reference.d.ts +4 -1
- package/ui/Data/CardList.tsx +1 -1
- package/ui/Data/Dashboard.tsx +1 -1
- package/ui/Data/Insight.tsx +1 -1
- package/ui/Data/Item.tsx +2 -2
- package/ui/Data/ListContainer.tsx +2 -2
- package/ui/Data/Pagination.tsx +1 -1
- package/ui/Data/TableList.tsx +1 -1
- package/ui/Field/Relation.tsx +33 -14
- package/ui/Field/useRelationOptions.ts +33 -5
- package/ui/Field/useRelationWindow.ts +50 -0
- package/ui/Load/View.tsx +2 -2
- package/ui/Model/ViewEditModal.tsx +1 -1
- package/ui/Model/ViewModal.tsx +2 -2
- package/ui/Pagination.tsx +1 -1
- package/ui/Signal/Arg.tsx +1 -3
- package/ui/System/CSR.tsx +1 -1
- package/ui/System/Client.tsx +1 -1
- package/ui/System/Reconnect.tsx +1 -1
- package/ui/System/SSR.tsx +1 -1
- package/vendor/@akanjs/native/packages/cli/src/lib/addons.ts +4 -1
- package/vendor/@akanjs/native/packages/cli/src/lib/apk.ts +2 -2
- package/vendor/@akanjs/native/packages/cli/src/lib/codegen.ts +1 -1
- package/vendor/@akanjs/native/packages/cli/src/lib/csp.ts +4 -3
- package/vendor/@akanjs/native/packages/cli/src/lib/googleservices.ts +7 -1
- package/vendor/@akanjs/native/packages/cli/src/lib/hmr.ts +8 -5
- package/vendor/@akanjs/native/packages/cli/src/lib/html.ts +18 -3
- package/vendor/@akanjs/native/packages/cli/src/lib/manifestmerge.ts +1 -1
- package/vendor/@akanjs/native/packages/cli/src/lib/plist.ts +2 -4
- package/vendor/@akanjs/native/packages/cli/src/lib/toolchains.ts +1 -1
- package/vendor/@akanjs/native/packages/cli/src/lib/updates.ts +10 -4
- package/vendor/@akanjs/native/packages/cli/src/lib/xml.ts +4 -3
- package/vendor/@akanjs/native/packages/cli/src/platforms/android.ts +3 -3
- package/vendor/@akanjs/native/packages/core/src/kernel.ts +3 -3
- package/vendor/@akanjs/native/packages/core/src/live.ts +1 -0
- package/vendor/@akanjs/native/packages/core/src/plugin.ts +5 -3
- package/vendor/@akanjs/native/packages/desktop/src/host.ts +13 -5
- package/vendor/@akanjs/native/packages/desktop/src/lifecycle.ts +1 -0
- package/vendor/@akanjs/native/packages/desktop/src/page-veto.ts +1 -1
- package/vendor/@akanjs/native/packages/desktop/src/plugin.ts +4 -1
- package/vendor/@akanjs/native/plugins/camera/src/web.ts +3 -3
- package/vendor/@akanjs/native/plugins/file-picker/src/index.ts +4 -1
- package/vendor/@akanjs/native/plugins/local-notifications/src/web.ts +1 -1
- package/vendor/@akanjs/native/plugins/secure-storage/src/desktop.ts +1 -1
- package/vendor/@akanjs/native/plugins/share/src/options.ts +1 -1
- package/vendor/@akanjs/native/plugins/single-instance/src/desktop.ts +3 -2
- package/vendor/@akanjs/native/plugins/updates/src/desktop.ts +4 -1
- package/vendor/@akanjs/native/plugins/window/src/index.ts +2 -2
- package/vendor/use-agentic/Reference.ts +5 -2
package/base/baseEnv.ts
CHANGED
|
@@ -81,8 +81,11 @@ const globalWithPrefix = globalThis as typeof globalThis & { __AKAN_PREFIX__?: R
|
|
|
81
81
|
export const normalizeRoutePrefix = (value: string | undefined | null): string | undefined => {
|
|
82
82
|
const trimmed = value?.trim();
|
|
83
83
|
if (!trimmed) return undefined;
|
|
84
|
-
|
|
85
|
-
|
|
84
|
+
let start = 0;
|
|
85
|
+
let end = trimmed.length;
|
|
86
|
+
while (start < end && trimmed[start] === "/") start += 1;
|
|
87
|
+
while (end > start && trimmed[end - 1] === "/") end -= 1;
|
|
88
|
+
return start === end ? undefined : `/${trimmed.slice(start, end)}`;
|
|
86
89
|
};
|
|
87
90
|
|
|
88
91
|
/**
|
package/client/device.ts
CHANGED
|
@@ -11,7 +11,7 @@ type DebugPayload = Record<string, unknown>;
|
|
|
11
11
|
|
|
12
12
|
const globalWithProcess = globalThis as typeof globalThis & { process?: ProcessEnvLike };
|
|
13
13
|
|
|
14
|
-
const debugSessionId =
|
|
14
|
+
const debugSessionId = crypto.getRandomValues(new Uint32Array(1))[0].toString(36);
|
|
15
15
|
let debugSeq = 0;
|
|
16
16
|
|
|
17
17
|
const FRAME_TRACE_LIMIT = 2000;
|
package/client/frameConfig.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isRecord } from "akanjs/common";
|
|
1
|
+
import { isRecord, trimSlashes } from "akanjs/common";
|
|
2
2
|
import type { PageConfig, PageSafeAreaConfig, PageState, ResponsiveInset, SsrRenderMode } from "./csrTypes";
|
|
3
3
|
import { transitionTypes } from "./transitionTypes";
|
|
4
4
|
|
|
@@ -246,7 +246,7 @@ const getPlatformFrameProfile = (platform: DevicePlatform): PageConfig => {
|
|
|
246
246
|
};
|
|
247
247
|
|
|
248
248
|
const getRouteRoleFrameDefaults = (path: string, platform: DevicePlatform, basePath?: string): PageConfig => {
|
|
249
|
-
const normalizedBasePath = basePath
|
|
249
|
+
const normalizedBasePath = trimSlashes(basePath ?? "");
|
|
250
250
|
const depth = path
|
|
251
251
|
.split("/")
|
|
252
252
|
.filter(Boolean)
|
package/client/router.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getEnv } from "akanjs/base";
|
|
2
|
-
import { getBasePathFromPathname, Logger, parseAkanI18nEnv, parseBasePaths } from "akanjs/common";
|
|
2
|
+
import { getBasePathFromPathname, Logger, parseAkanI18nEnv, parseBasePaths, trimSlashes } from "akanjs/common";
|
|
3
3
|
import { csrLocation } from "./csrLocation";
|
|
4
4
|
import { isMcpView } from "./mcpView";
|
|
5
5
|
|
|
@@ -99,7 +99,7 @@ const normalizeRoutePath = (path: string | undefined) => {
|
|
|
99
99
|
if (!normalized) return undefined;
|
|
100
100
|
const [pathWithoutHash, hash = ""] = normalized.split("#");
|
|
101
101
|
const [pathname, search = ""] = pathWithoutHash.split("?");
|
|
102
|
-
const routePath = `/${pathname
|
|
102
|
+
const routePath = `/${trimSlashes(pathname)}`;
|
|
103
103
|
return `${routePath === "/" ? "/" : routePath}${search ? `?${search}` : ""}${hash ? `#${hash}` : ""}`;
|
|
104
104
|
};
|
|
105
105
|
|
|
@@ -453,7 +453,7 @@ class Router {
|
|
|
453
453
|
const h = requestHeaders();
|
|
454
454
|
const req = getRequest();
|
|
455
455
|
const reqPathname = req ? new URL(req.url).pathname : "";
|
|
456
|
-
const langFromPath = reqPathname.split("/").
|
|
456
|
+
const langFromPath = reqPathname.split("/").find(Boolean);
|
|
457
457
|
const lang = (h.get("x-locale") ?? langFromPath ?? this.#lang) as string;
|
|
458
458
|
const basePath = getServerBasePath(reqPathname, lang, h.get("x-base-path") ?? undefined, this.#prefix);
|
|
459
459
|
const { pathname, href: fullHref } = getPathInfo(href, lang, shouldExposeBasePath() ? basePath : "");
|
package/client/rscNavigation.ts
CHANGED
|
@@ -3,7 +3,10 @@ declare global {
|
|
|
3
3
|
var __AKAN_RSC_IS_FROM_CACHE__: (() => boolean) | undefined;
|
|
4
4
|
var __AKAN_RSC_REFRESH__: ((options?: { buildId?: number }) => Promise<void>) | undefined;
|
|
5
5
|
var __AKAN_RSC_NAVIGATE__:
|
|
6
|
-
| ((
|
|
6
|
+
| ((
|
|
7
|
+
href: string,
|
|
8
|
+
options?: { replace?: boolean; scrollToTop?: boolean; scrollY?: number; shallow?: boolean },
|
|
9
|
+
) => Promise<void>)
|
|
7
10
|
| undefined;
|
|
8
11
|
var __AKAN_DEV_SYNC_NAVIGATION__: ((href: string, kind: "push" | "replace" | "back" | "pop") => void) | undefined;
|
|
9
12
|
var __AKAN_DEV_SYNC_NAVIGATION_APPLYING__: boolean | undefined;
|
package/client/types.ts
CHANGED
|
@@ -102,8 +102,9 @@ const slugFontPart = (value: string) =>
|
|
|
102
102
|
value
|
|
103
103
|
.trim()
|
|
104
104
|
.toLowerCase()
|
|
105
|
-
.
|
|
106
|
-
.
|
|
105
|
+
.split(/[^a-z0-9]+/)
|
|
106
|
+
.filter(Boolean)
|
|
107
|
+
.join("-") || "font";
|
|
107
108
|
|
|
108
109
|
export interface ModelsProps<M extends { id: string }> {
|
|
109
110
|
className?: string;
|
package/client/webManifest.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { trimSlashes } from "akanjs/common";
|
|
2
|
+
|
|
1
3
|
/** Where an SSR document links its root layout's `.manifest()`; the server answers it from that layout. */
|
|
2
4
|
export const webManifestHref = (basePath?: string | null) =>
|
|
3
5
|
basePath ? `/${basePath}/manifest.webmanifest` : "/manifest.webmanifest";
|
|
4
6
|
|
|
5
|
-
const
|
|
7
|
+
const trimBasePath = (value: string) => trimSlashes(value.trim());
|
|
6
8
|
|
|
7
9
|
/** The basePath a `/manifest.webmanifest` request asks for: by a visible prefix, else by the request's basePath. */
|
|
8
10
|
export const getWebManifestBasePath = (
|
|
@@ -12,9 +14,9 @@ export const getWebManifestBasePath = (
|
|
|
12
14
|
): string | null | undefined => {
|
|
13
15
|
const segments = pathname.trim().split("/").filter(Boolean);
|
|
14
16
|
if (segments.at(-1) !== "manifest.webmanifest" || segments.length > 2) return undefined;
|
|
15
|
-
const known = basePaths.map(
|
|
17
|
+
const known = basePaths.map(trimBasePath);
|
|
16
18
|
if (segments.length === 2) return known.includes(segments[0] ?? "") ? (segments[0] ?? null) : undefined;
|
|
17
|
-
const fromHeader =
|
|
19
|
+
const fromHeader = trimBasePath(headerBasePath ?? "");
|
|
18
20
|
return known.includes(fromHeader) ? fromHeader : null;
|
|
19
21
|
};
|
|
20
22
|
|
package/common/TrustedProxy.ts
CHANGED
|
@@ -3,8 +3,12 @@ import { clientAddressFromHeaders, normalizeIpAddress } from "./clientAddress";
|
|
|
3
3
|
export class TrustedProxy {
|
|
4
4
|
static #cidrs: { bytes: Uint8Array; bits: number }[] | null = null;
|
|
5
5
|
static #trustAll: boolean | null = null;
|
|
6
|
+
|
|
7
|
+
static readonly #verdicts = new Map<string, boolean>();
|
|
8
|
+
static readonly #verdictLimit = 1024;
|
|
6
9
|
|
|
7
10
|
static configure(list: string | null | undefined) {
|
|
11
|
+
TrustedProxy.#verdicts.clear();
|
|
8
12
|
TrustedProxy.#trustAll = list?.trim() === "*";
|
|
9
13
|
TrustedProxy.#cidrs = (list ?? "")
|
|
10
14
|
.split(",")
|
|
@@ -19,6 +23,7 @@ export class TrustedProxy {
|
|
|
19
23
|
static reset() {
|
|
20
24
|
TrustedProxy.#cidrs = null;
|
|
21
25
|
TrustedProxy.#trustAll = null;
|
|
26
|
+
TrustedProxy.#verdicts.clear();
|
|
22
27
|
}
|
|
23
28
|
|
|
24
29
|
static #load() {
|
|
@@ -29,6 +34,15 @@ export class TrustedProxy {
|
|
|
29
34
|
if (!address) return false;
|
|
30
35
|
TrustedProxy.#load();
|
|
31
36
|
if (TrustedProxy.#trustAll) return true;
|
|
37
|
+
const known = TrustedProxy.#verdicts.get(address);
|
|
38
|
+
if (known !== undefined) return known;
|
|
39
|
+
const verdict = TrustedProxy.#judge(address);
|
|
40
|
+
if (TrustedProxy.#verdicts.size >= TrustedProxy.#verdictLimit) TrustedProxy.#verdicts.clear();
|
|
41
|
+
TrustedProxy.#verdicts.set(address, verdict);
|
|
42
|
+
return verdict;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
static #judge(address: string): boolean {
|
|
32
46
|
const bytes = TrustedProxy.#toBytes(normalizeIpAddress(address));
|
|
33
47
|
if (!bytes) return false;
|
|
34
48
|
if (TrustedProxy.#isLocalRange(bytes)) return true;
|
package/common/index.ts
CHANGED
|
@@ -199,6 +199,7 @@ export { sleep } from "./sleep";
|
|
|
199
199
|
export { getBasePathFromPathname, parseBasePaths, parseSubRouteHosts, resolveSubRouteHosts } from "./subRoute";
|
|
200
200
|
export { TrustedProxy } from "./TrustedProxy";
|
|
201
201
|
export { toError } from "./toError";
|
|
202
|
+
export { trimSlashes, trimTrailingSlashes } from "./trimSlashes";
|
|
202
203
|
export {
|
|
203
204
|
type TunnelAgentIdentity,
|
|
204
205
|
type TunnelAttachedFrame,
|
|
@@ -1,7 +1,21 @@
|
|
|
1
1
|
/** A `{name}` placeholder whose value is absent is left as written, never replaced by "undefined". */
|
|
2
2
|
export const interpolateTranslation = (message: string, data: Record<string, unknown> | undefined) => {
|
|
3
3
|
if (!data) return message;
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
|
|
5
|
+
let result = "";
|
|
6
|
+
let copied = 0;
|
|
7
|
+
let open = message.indexOf("{");
|
|
8
|
+
while (open !== -1) {
|
|
9
|
+
const close = message.indexOf("}", open + 1);
|
|
10
|
+
if (close === -1) break;
|
|
11
|
+
if (close === open + 1) {
|
|
12
|
+
open = message.indexOf("{", close);
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
const value = data[message.slice(open + 1, close)];
|
|
16
|
+
result += message.slice(copied, open) + (value === undefined ? message.slice(open, close + 1) : String(value));
|
|
17
|
+
copied = close + 1;
|
|
18
|
+
open = message.indexOf("{", copied);
|
|
19
|
+
}
|
|
20
|
+
return result + message.slice(copied);
|
|
7
21
|
};
|
package/common/isEmail.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const isEmail = (email?: string | null) => !!email && /^\w+([.-]
|
|
1
|
+
export const isEmail = (email?: string | null) => !!email && /^\w+(?:[.-]\w+)*@\w+(?:[.-]\w+)*\.\w{2,8}$/.test(email);
|
package/common/localeConfig.ts
CHANGED
|
@@ -28,14 +28,20 @@ export function resolveAkanI18nConfig(input: AkanI18nConfigInput = {}): AkanI18n
|
|
|
28
28
|
return { defaultLocale, locales };
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
let lastParsed: { defaultLocale?: string; locales?: string; config: AkanI18nConfig } | null = null;
|
|
32
|
+
|
|
31
33
|
export function parseAkanI18nEnv(
|
|
32
34
|
env: Record<string, string | undefined> = {
|
|
33
35
|
AKAN_PUBLIC_DEFAULT_LOCALE: typeof process !== "undefined" ? process.env.AKAN_PUBLIC_DEFAULT_LOCALE : undefined,
|
|
34
36
|
AKAN_PUBLIC_LOCALES: typeof process !== "undefined" ? process.env.AKAN_PUBLIC_LOCALES : undefined,
|
|
35
37
|
},
|
|
36
38
|
): AkanI18nConfig {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
const defaultLocale = env.AKAN_PUBLIC_DEFAULT_LOCALE;
|
|
40
|
+
const locales = env.AKAN_PUBLIC_LOCALES;
|
|
41
|
+
if (lastParsed && lastParsed.defaultLocale === defaultLocale && lastParsed.locales === locales)
|
|
42
|
+
return lastParsed.config;
|
|
43
|
+
const resolved = resolveAkanI18nConfig({ defaultLocale, locales: locales?.split(",") });
|
|
44
|
+
const config = Object.freeze({ ...resolved, locales: Object.freeze(resolved.locales) as unknown as string[] });
|
|
45
|
+
lastParsed = { defaultLocale, locales, config };
|
|
46
|
+
return config;
|
|
41
47
|
}
|
package/common/markdownSpans.ts
CHANGED
|
@@ -9,7 +9,7 @@ export type MarkdownSpan =
|
|
|
9
9
|
| { kind: "plain"; text: string };
|
|
10
10
|
|
|
11
11
|
const inline =
|
|
12
|
-
/(!?)\[([
|
|
12
|
+
/(!?)\[([^[\]]*)\]\(((?:[^\s()]|\([^\s()]*\))+)\)|`([^`]+)`|\*\*([\s\S]+?)\*\*|\*([^*\n]+?)\*|~~([\s\S]+?)~~/g;
|
|
13
13
|
|
|
14
14
|
export class MarkdownSpans {
|
|
15
15
|
static of(text: string): MarkdownSpan[] {
|
package/common/restClient.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { trimTrailingSlashes } from "./trimSlashes";
|
|
2
|
+
|
|
1
3
|
export interface RestClientOptions {
|
|
2
4
|
baseUrl?: string;
|
|
3
5
|
headers?: HeadersInit;
|
|
@@ -16,7 +18,7 @@ export class RestClient {
|
|
|
16
18
|
|
|
17
19
|
constructor(options: string | RestClientOptions = {}) {
|
|
18
20
|
const normalized = typeof options === "string" ? { baseUrl: options } : options;
|
|
19
|
-
this.baseUrl = normalized.baseUrl
|
|
21
|
+
this.baseUrl = trimTrailingSlashes(normalized.baseUrl ?? "");
|
|
20
22
|
this.headers = new Headers(normalized.headers);
|
|
21
23
|
this.timeout = normalized.timeout;
|
|
22
24
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { trimSlashes, trimTrailingSlashes } from "./trimSlashes";
|
|
2
|
+
|
|
1
3
|
const ROUTE_SOURCE_RE = /^\.\/(.+)\.(tsx|ts|jsx|js)$/;
|
|
2
4
|
const SOURCE_EXT_RE = /\.(tsx|ts|jsx|js)$/;
|
|
3
5
|
const RESERVED_ROUTE_FILES = new Set(["_layout", "_index", "_overrides"]);
|
|
@@ -97,9 +99,7 @@ export function validateSubRoutePageKey(
|
|
|
97
99
|
basePaths: Iterable<string>,
|
|
98
100
|
options: ValidateSubRoutePageKeyOptions = {},
|
|
99
101
|
): void {
|
|
100
|
-
const allowedBasePaths = [
|
|
101
|
-
...new Set([...basePaths].map((basePath) => basePath.trim().replace(/^\/+|\/+$/g, "")).filter(Boolean)),
|
|
102
|
-
];
|
|
102
|
+
const allowedBasePaths = [...new Set([...basePaths].map((basePath) => trimSlashes(basePath.trim())).filter(Boolean))];
|
|
103
103
|
if (allowedBasePaths.length === 0) return;
|
|
104
104
|
|
|
105
105
|
const match = ROUTE_SOURCE_RE.exec(key);
|
|
@@ -165,7 +165,7 @@ export function tryParseRouteModuleKey(key: string): ParsedRouteModuleKey | null
|
|
|
165
165
|
|
|
166
166
|
export function normalizeRoutePattern(segments: string[]): string {
|
|
167
167
|
const parts = segments.map(routeSegmentToPatternPart).filter((part) => part.length > 0);
|
|
168
|
-
return `/${parts.join("/")}
|
|
168
|
+
return trimTrailingSlashes(`/${parts.join("/")}`) || "/";
|
|
169
169
|
}
|
|
170
170
|
|
|
171
171
|
export function routeSegmentToPatternPart(segment: string): string {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export const trimTrailingSlashes = (value: string) => {
|
|
2
|
+
let end = value.length;
|
|
3
|
+
while (end > 0 && value[end - 1] === "/") end -= 1;
|
|
4
|
+
return value.slice(0, end);
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export const trimSlashes = (value: string) => {
|
|
8
|
+
let start = 0;
|
|
9
|
+
while (start < value.length && value[start] === "/") start += 1;
|
|
10
|
+
return trimTrailingSlashes(value.slice(start));
|
|
11
|
+
};
|
package/common/types.ts
CHANGED
package/constant/fieldInfo.ts
CHANGED
|
@@ -276,7 +276,7 @@ export type FieldInfoObjectToFieldObject<Obj extends FieldInfoObject> = {
|
|
|
276
276
|
|
|
277
277
|
export class ConstantField<
|
|
278
278
|
FieldType extends ConstantFieldKind = ConstantFieldKind,
|
|
279
|
-
|
|
279
|
+
_Value extends ConstantFieldTypeInput | null = any,
|
|
280
280
|
FieldValue = any,
|
|
281
281
|
MapValue = any,
|
|
282
282
|
Nullable extends boolean = boolean,
|
|
@@ -472,7 +472,7 @@ type FieldOption<
|
|
|
472
472
|
>
|
|
473
473
|
>[];
|
|
474
474
|
|
|
475
|
-
export type PlainTypeToFieldType<PlainType> = PlainType extends [infer First, ...
|
|
475
|
+
export type PlainTypeToFieldType<PlainType> = PlainType extends [infer First, ...unknown[]]
|
|
476
476
|
? PlainTypeToFieldType<First>[]
|
|
477
477
|
: PlainType extends number
|
|
478
478
|
? typeof Int | typeof Float
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { FIELD_META, PrimitiveRegistry, type PrimitiveScalar } from "akanjs/base";
|
|
2
|
+
|
|
3
|
+
import type { ConstantField, FieldObject } from "./fieldInfo";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* What a response does with a field: leaves it out (`hidden` / `secret`), computes it (`resolve`), copies it (a
|
|
7
|
+
* primitive or a Map), walks into it (an embedded scalar), or loads what it points at (a relation).
|
|
8
|
+
*/
|
|
9
|
+
export type FieldPlanKind = "masked" | "computed" | "value" | "scalar" | "relation";
|
|
10
|
+
|
|
11
|
+
export interface FieldPlanEntry {
|
|
12
|
+
key: string;
|
|
13
|
+
field: ConstantField;
|
|
14
|
+
kind: FieldPlanKind;
|
|
15
|
+
/** Points at another stored model, so an input carries its id; independent of `kind`, as `serialize` reads it. */
|
|
16
|
+
isRelationRef: boolean;
|
|
17
|
+
/** The scalar class of a primitive field, which `serialize` calls straight; `null` for everything else. */
|
|
18
|
+
primitive: typeof PrimitiveScalar | null;
|
|
19
|
+
serializeOption: { nullable: boolean; key: string };
|
|
20
|
+
}
|
|
21
|
+
interface PlanOwner {
|
|
22
|
+
[FIELD_META]: FieldObject;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class FieldPlan {
|
|
26
|
+
static #byClass = new WeakMap<object, FieldPlan>();
|
|
27
|
+
static #generation = 0;
|
|
28
|
+
static #computingAsync = new Set<FieldPlan>();
|
|
29
|
+
static of(cls: PlanOwner): FieldPlan {
|
|
30
|
+
const cached = FieldPlan.#byClass.get(cls);
|
|
31
|
+
if (cached) return cached;
|
|
32
|
+
const plan = new FieldPlan(cls[FIELD_META]);
|
|
33
|
+
FieldPlan.#byClass.set(cls, plan);
|
|
34
|
+
return plan;
|
|
35
|
+
}
|
|
36
|
+
static reset(cls: object) {
|
|
37
|
+
FieldPlan.#byClass.delete(cls);
|
|
38
|
+
FieldPlan.#generation += 1;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
readonly entries: readonly FieldPlanEntry[];
|
|
42
|
+
#resolvesAsync: { value: boolean; generation: number } | null = null;
|
|
43
|
+
constructor(fieldObj: FieldObject) {
|
|
44
|
+
this.entries = Object.entries(fieldObj).map(([key, field]) => ({
|
|
45
|
+
key,
|
|
46
|
+
field,
|
|
47
|
+
kind: FieldPlan.#kindOf(field),
|
|
48
|
+
isRelationRef: field.isClass && !field.isScalar,
|
|
49
|
+
primitive: PrimitiveRegistry.has(field.modelRef) ? (field.modelRef as unknown as typeof PrimitiveScalar) : null,
|
|
50
|
+
serializeOption: { nullable: field.nullable, key },
|
|
51
|
+
}));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
static #kindOf(field: ConstantField): FieldPlanKind {
|
|
55
|
+
if (field.fieldType === "hidden" || field.fieldType === "secret") return "masked";
|
|
56
|
+
if (field.fieldType === "resolve") return "computed";
|
|
57
|
+
if (!field.isClass) return "value";
|
|
58
|
+
return field.isScalar ? "scalar" : "relation";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Whether resolving a value of this class awaits anything: a relation, a resolve field, or one in a scalar below. */
|
|
62
|
+
get resolvesAsync(): boolean {
|
|
63
|
+
if (this.#resolvesAsync?.generation === FieldPlan.#generation) return this.#resolvesAsync.value;
|
|
64
|
+
|
|
65
|
+
if (FieldPlan.#computingAsync.has(this)) return true;
|
|
66
|
+
FieldPlan.#computingAsync.add(this);
|
|
67
|
+
try {
|
|
68
|
+
const value = this.entries.some(
|
|
69
|
+
({ kind, field }) =>
|
|
70
|
+
kind === "computed" ||
|
|
71
|
+
kind === "relation" ||
|
|
72
|
+
(kind === "scalar" && FieldPlan.of(field.modelRef as unknown as PlanOwner).resolvesAsync),
|
|
73
|
+
);
|
|
74
|
+
this.#resolvesAsync = { value, generation: FieldPlan.#generation };
|
|
75
|
+
return value;
|
|
76
|
+
} finally {
|
|
77
|
+
FieldPlan.#computingAsync.delete(this);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
package/constant/index.ts
CHANGED
|
@@ -3,10 +3,13 @@ export * from "./constantRegistry";
|
|
|
3
3
|
export * from "./crystalize";
|
|
4
4
|
export * from "./dateSlot";
|
|
5
5
|
export * from "./fieldInfo";
|
|
6
|
+
export * from "./fieldPlan";
|
|
6
7
|
export * from "./fieldQueryMeta";
|
|
7
8
|
export * from "./getDefault";
|
|
8
9
|
export * from "./hydrationPlan";
|
|
9
10
|
export * from "./immerify";
|
|
11
|
+
export * from "./inputParser";
|
|
12
|
+
export * from "./invalidInputError";
|
|
10
13
|
export * from "./labelOf";
|
|
11
14
|
export * from "./mask";
|
|
12
15
|
export * from "./purify";
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Any,
|
|
3
|
+
type Cls,
|
|
4
|
+
dayjs,
|
|
5
|
+
type EnumInstance,
|
|
6
|
+
FIELD_META,
|
|
7
|
+
Float,
|
|
8
|
+
getNonArrayModel,
|
|
9
|
+
ID,
|
|
10
|
+
Int,
|
|
11
|
+
PrimitiveRegistry,
|
|
12
|
+
type PrimitiveScalar,
|
|
13
|
+
} from "akanjs/base";
|
|
14
|
+
|
|
15
|
+
import { ConstantRegistry } from "./constantRegistry";
|
|
16
|
+
import type { ConstantField, FieldObject } from "./fieldInfo";
|
|
17
|
+
import { FieldPlan, type FieldPlanEntry } from "./fieldPlan";
|
|
18
|
+
import { defaultPlanOf } from "./getDefault";
|
|
19
|
+
import { InvalidInputError } from "./invalidInputError";
|
|
20
|
+
import { deserialize } from "./serialize";
|
|
21
|
+
|
|
22
|
+
type ModelCls = Cls & { [FIELD_META]: FieldObject };
|
|
23
|
+
type Parse = (value: unknown, path: string) => unknown;
|
|
24
|
+
|
|
25
|
+
interface ParseOption {
|
|
26
|
+
key: string;
|
|
27
|
+
nullable?: boolean;
|
|
28
|
+
enum?: EnumInstance;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const isString = (value: unknown) => typeof value === "string";
|
|
32
|
+
const isNumber = (value: unknown) => typeof value === "number";
|
|
33
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
34
|
+
typeof value === "object" && value !== null && !Array.isArray(value);
|
|
35
|
+
|
|
36
|
+
export class InputParser {
|
|
37
|
+
|
|
38
|
+
static readonly #wireTypes = new Map<unknown, (value: unknown) => boolean>([
|
|
39
|
+
[String, isString],
|
|
40
|
+
[ID, isString],
|
|
41
|
+
[Int, isNumber],
|
|
42
|
+
[Float, isNumber],
|
|
43
|
+
[Boolean, (value) => typeof value === "boolean"],
|
|
44
|
+
[Date, (value) => isString(value) || isNumber(value) || value instanceof Date || dayjs.isDayjs(value)],
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
static parse(argRef: Cls, arrDepth: number, value: unknown, option: ParseOption) {
|
|
48
|
+
if (PrimitiveRegistry.has(argRef)) return deserialize(argRef, arrDepth, value, option);
|
|
49
|
+
const { key, nullable = false } = option;
|
|
50
|
+
if (value === null || value === undefined) {
|
|
51
|
+
if (nullable) return null;
|
|
52
|
+
throw new InvalidInputError(key, null);
|
|
53
|
+
}
|
|
54
|
+
return InputParser.#walk(value, arrDepth, key, (item, path) => InputParser.#value(argRef, item, path));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
static #walk(value: unknown, arrDepth: number, path: string, parse: Parse): unknown {
|
|
58
|
+
if (!arrDepth) return parse(value, path);
|
|
59
|
+
if (!Array.isArray(value)) throw new InvalidInputError(path, "array");
|
|
60
|
+
return value.map((item, idx) => InputParser.#walk(item, arrDepth - 1, `${path}.${idx}`, parse));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
static #value(ref: Cls, value: unknown, path: string, enumRef?: EnumInstance): unknown {
|
|
64
|
+
if (value === null || value === undefined) {
|
|
65
|
+
if (ref === Any) return value ?? null;
|
|
66
|
+
throw new InvalidInputError(path, null);
|
|
67
|
+
}
|
|
68
|
+
if (PrimitiveRegistry.has(ref)) return InputParser.#primitive(ref as typeof PrimitiveScalar, value, path, enumRef);
|
|
69
|
+
if (ConstantRegistry.isScalar(ref)) return InputParser.#scalar(ref as ModelCls, value, path);
|
|
70
|
+
return InputParser.#model(ref as ModelCls, value, path);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
static #primitive(primitive: typeof PrimitiveScalar, value: unknown, path: string, enumRef?: EnumInstance) {
|
|
74
|
+
const hasWireType = InputParser.#wireTypes.get(primitive);
|
|
75
|
+
if (hasWireType && !hasWireType(value)) throw new InvalidInputError(path, primitive.refName);
|
|
76
|
+
let parsed: unknown;
|
|
77
|
+
try {
|
|
78
|
+
parsed = primitive._parse(value as never);
|
|
79
|
+
} catch {
|
|
80
|
+
throw new InvalidInputError(path, primitive.refName);
|
|
81
|
+
}
|
|
82
|
+
if (enumRef && !enumRef.has(parsed as never))
|
|
83
|
+
throw new InvalidInputError(path, `one of ${enumRef.values.join(", ")}`);
|
|
84
|
+
return parsed;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
static #scalar(ref: ModelCls, value: unknown, path: string) {
|
|
88
|
+
if (!isRecord(value)) throw new InvalidInputError(path, "object");
|
|
89
|
+
const defaults = defaultPlanOf(ref[FIELD_META]);
|
|
90
|
+
const parsed: Record<string, unknown> = {};
|
|
91
|
+
for (const entry of FieldPlan.of(ref).entries) {
|
|
92
|
+
if (entry.kind === "computed") continue;
|
|
93
|
+
const fieldPath = `${path}.${entry.key}`;
|
|
94
|
+
const raw = value[entry.key];
|
|
95
|
+
if (raw !== undefined) parsed[entry.key] = InputParser.#field(entry, raw, fieldPath);
|
|
96
|
+
else if (InputParser.#isRequired(entry.field)) throw new InvalidInputError(fieldPath, null);
|
|
97
|
+
else {
|
|
98
|
+
const make = defaults.perCall.get(entry.key);
|
|
99
|
+
parsed[entry.key] = make ? make() : defaults.shared[entry.key];
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return parsed;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
static #model(ref: ModelCls, value: unknown, path: string) {
|
|
106
|
+
if (!isRecord(value)) throw new InvalidInputError(path, "object");
|
|
107
|
+
const parsed: Record<string, unknown> = {};
|
|
108
|
+
for (const entry of FieldPlan.of(ref).entries) {
|
|
109
|
+
if (entry.kind === "computed") continue;
|
|
110
|
+
const fieldPath = `${path}.${entry.key}`;
|
|
111
|
+
const raw = value[entry.key];
|
|
112
|
+
if (raw !== undefined) parsed[entry.key] = InputParser.#field(entry, raw, fieldPath);
|
|
113
|
+
else if (InputParser.#isRequired(entry.field)) throw new InvalidInputError(fieldPath, null);
|
|
114
|
+
}
|
|
115
|
+
return parsed;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
static #field({ field, isRelationRef }: FieldPlanEntry, value: unknown, path: string): unknown {
|
|
119
|
+
if (value === null) {
|
|
120
|
+
if (field.nullable || (field.modelRef as Cls) === Any) return null;
|
|
121
|
+
throw new InvalidInputError(path, null);
|
|
122
|
+
}
|
|
123
|
+
return InputParser.#walk(value, field.arrDepth, path, (item, itemPath) => {
|
|
124
|
+
if (field.isMap) return InputParser.#map(field.of as Cls | Cls[], item, itemPath);
|
|
125
|
+
if (isRelationRef) return InputParser.#value(ID, item, itemPath);
|
|
126
|
+
return InputParser.#value(field.modelRef, item, itemPath, field.enum);
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
static #map(of: Cls | Cls[], value: unknown, path: string) {
|
|
131
|
+
if (!isRecord(value)) throw new InvalidInputError(path, "object");
|
|
132
|
+
const [valueRef, valueArrDepth] = getNonArrayModel(of);
|
|
133
|
+
return Object.fromEntries(
|
|
134
|
+
Object.entries(value).map(([key, item]) => [
|
|
135
|
+
key,
|
|
136
|
+
InputParser.#walk(item, valueArrDepth, `${path}.${key}`, (entry, entryPath) =>
|
|
137
|
+
InputParser.#value(valueRef as Cls, entry, entryPath),
|
|
138
|
+
),
|
|
139
|
+
]),
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
static #isRequired(field: ConstantField) {
|
|
144
|
+
if (field.nullable || (field.modelRef as Cls) === Any) return false;
|
|
145
|
+
if (field.default !== null && field.default !== undefined) return false;
|
|
146
|
+
return !(field.isClass && field.isScalar);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** A body value the request schema refuses: `path` names it from the argument down (`data.items.2.price`). */
|
|
2
|
+
export class InvalidInputError extends Error {
|
|
3
|
+
readonly path: string;
|
|
4
|
+
readonly reason: "missing" | "invalid";
|
|
5
|
+
/** What the field takes — a type name, `object`, `array`, or the enum's values; `null` when it is missing. */
|
|
6
|
+
readonly expected: string | null;
|
|
7
|
+
constructor(path: string, expected: string | null) {
|
|
8
|
+
super(expected ? `Invalid input at ${path}: expected ${expected}` : `Missing input at ${path}`);
|
|
9
|
+
this.path = path;
|
|
10
|
+
this.reason = expected ? "invalid" : "missing";
|
|
11
|
+
this.expected = expected;
|
|
12
|
+
}
|
|
13
|
+
}
|