rankrunners-cms 0.0.31 → 0.0.33

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.
Files changed (49) hide show
  1. package/package.json +1 -1
  2. package/src/hono/captcha.ts +12 -0
  3. package/src/libs/valibot-serialize/converters/from_valibot.ts +61 -0
  4. package/src/libs/valibot-serialize/converters/index.ts +2 -0
  5. package/src/libs/valibot-serialize/converters/to_valibot.ts +36 -0
  6. package/src/libs/valibot-serialize/index.ts +2 -0
  7. package/src/libs/valibot-serialize/types/any.ts +36 -0
  8. package/src/libs/valibot-serialize/types/array.ts +115 -0
  9. package/src/libs/valibot-serialize/types/bigint.ts +40 -0
  10. package/src/libs/valibot-serialize/types/blob.ts +67 -0
  11. package/src/libs/valibot-serialize/types/boolean.ts +40 -0
  12. package/src/libs/valibot-serialize/types/date.ts +36 -0
  13. package/src/libs/valibot-serialize/types/enum.ts +67 -0
  14. package/src/libs/valibot-serialize/types/exact_optional.ts +58 -0
  15. package/src/libs/valibot-serialize/types/file.ts +86 -0
  16. package/src/libs/valibot-serialize/types/function.ts +32 -0
  17. package/src/libs/valibot-serialize/types/index.ts +39 -0
  18. package/src/libs/valibot-serialize/types/intersect.ts +45 -0
  19. package/src/libs/valibot-serialize/types/lazy.ts +51 -0
  20. package/src/libs/valibot-serialize/types/lib/default_utils.ts +7 -0
  21. package/src/libs/valibot-serialize/types/lib/type_interfaces.ts +41 -0
  22. package/src/libs/valibot-serialize/types/literal.ts +68 -0
  23. package/src/libs/valibot-serialize/types/map.ts +97 -0
  24. package/src/libs/valibot-serialize/types/nan.ts +29 -0
  25. package/src/libs/valibot-serialize/types/never.ts +35 -0
  26. package/src/libs/valibot-serialize/types/non_nullable.ts +41 -0
  27. package/src/libs/valibot-serialize/types/non_nullish.ts +41 -0
  28. package/src/libs/valibot-serialize/types/non_optional.ts +41 -0
  29. package/src/libs/valibot-serialize/types/null.ts +40 -0
  30. package/src/libs/valibot-serialize/types/nullable.ts +44 -0
  31. package/src/libs/valibot-serialize/types/nullish.ts +41 -0
  32. package/src/libs/valibot-serialize/types/number.ts +123 -0
  33. package/src/libs/valibot-serialize/types/object.ts +162 -0
  34. package/src/libs/valibot-serialize/types/optional.ts +44 -0
  35. package/src/libs/valibot-serialize/types/picklist.ts +49 -0
  36. package/src/libs/valibot-serialize/types/promise.ts +31 -0
  37. package/src/libs/valibot-serialize/types/record.ts +53 -0
  38. package/src/libs/valibot-serialize/types/set.ts +89 -0
  39. package/src/libs/valibot-serialize/types/string.ts +416 -0
  40. package/src/libs/valibot-serialize/types/symbol.ts +39 -0
  41. package/src/libs/valibot-serialize/types/tuple.ts +84 -0
  42. package/src/libs/valibot-serialize/types/undefined.ts +39 -0
  43. package/src/libs/valibot-serialize/types/undefinedable.ts +60 -0
  44. package/src/libs/valibot-serialize/types/union.ts +59 -0
  45. package/src/libs/valibot-serialize/types/unknown.ts +37 -0
  46. package/src/libs/valibot-serialize/types/variant.ts +56 -0
  47. package/src/libs/valibot-serialize/types/void.ts +39 -0
  48. package/src/libs/valibot-serialize/types.ts +102 -0
  49. package/src/libs/valibot-serialize/util/guard.ts +62 -0
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "rankrunners-cms",
3
3
  "type": "module",
4
- "version": "0.0.31",
4
+ "version": "0.0.33",
5
5
  "peerDependencies": {
6
6
  "@puckeditor/core": "^0.21.1",
7
7
  "@tanstack/react-router": "^1.166.6",
@@ -3,6 +3,12 @@ import { getConnInfo } from "hono/bun";
3
3
  import { verifyToken as verifyTokenClient } from "../api/client/captcha";
4
4
 
5
5
  export const getIpAddress = (c: Context): string => {
6
+ const cfConnectingIp = c.req.header("cf-connecting-ip")?.trim();
7
+
8
+ if (cfConnectingIp && cfConnectingIp.length > 0) {
9
+ return cfConnectingIp;
10
+ }
11
+
6
12
  const forwardedFor = c.req.header("x-forwarded-for");
7
13
 
8
14
  if (forwardedFor) {
@@ -14,6 +20,12 @@ export const getIpAddress = (c: Context): string => {
14
20
  }
15
21
  }
16
22
 
23
+ // Common fallback headers set by some proxies/CDNs
24
+ const realIp = c.req.header("x-real-ip")?.trim();
25
+ if (realIp && realIp.length > 0) {
26
+ return realIp;
27
+ }
28
+
17
29
  // Fallback to the connection info if x-forwarded-for is not present
18
30
  const info = getConnInfo(c);
19
31
  return info.remote.address || "";
@@ -0,0 +1,61 @@
1
+ import type { BaseIssue, BaseSchema } from "valibot";
2
+ import {
3
+ FORMAT_VERSION,
4
+ type SchemaNode,
5
+ type SerializedSchema,
6
+ } from "../types.ts";
7
+ import * as codecs from "../types/index.ts";
8
+
9
+ type AnySchema = BaseSchema<unknown, unknown, BaseIssue<unknown>>;
10
+
11
+ // Cache schema encodings to support recursive builders like v.lazy.
12
+ const encodeCache = new WeakMap<AnySchema, SchemaNode>();
13
+
14
+ /**
15
+ * Convert a Valibot schema into a portable JSON-friendly representation.
16
+ *
17
+ * The returned value contains a stable envelope plus an AST `node` that
18
+ * captures the schema structure and constraints without functions.
19
+ *
20
+ * @typeParam T - Any Valibot `BaseSchema`.
21
+ * @param schema - The Valibot schema to convert.
22
+ * @returns A `SerializedSchema` envelope with the encoded AST.
23
+ */
24
+ export function fromValibot<T extends AnySchema>(schema: T): SerializedSchema {
25
+ const node = encodeNode(schema);
26
+ return {
27
+ kind: "schema",
28
+ vendor: "valibot",
29
+ version: 1,
30
+ format: FORMAT_VERSION,
31
+ node,
32
+ };
33
+ }
34
+
35
+ function encodeNode(schema: AnySchema): SchemaNode {
36
+ const cached = encodeCache.get(schema);
37
+ if (cached) return cached;
38
+
39
+ const placeholder = {} as SchemaNode;
40
+ encodeCache.set(schema, placeholder);
41
+
42
+ // Read type directly to avoid JSON.stringify BigInt errors
43
+ const any = schema as unknown as { type?: string } & Record<string, unknown>;
44
+ const type = any.type;
45
+ // Dispatch to codecs that can detect from Valibot schema
46
+ try {
47
+ for (const c of Object.values(codecs)) {
48
+ if (c.matches(schema as never)) {
49
+ const node = c.encode(any as never, { encodeNode });
50
+ return Object.assign(placeholder, node);
51
+ }
52
+ }
53
+ } catch (error) {
54
+ encodeCache.delete(schema);
55
+ throw error;
56
+ }
57
+ encodeCache.delete(schema);
58
+ throw new Error(
59
+ `Unsupported schema type for serialization: ${String(type)}`,
60
+ );
61
+ }
@@ -0,0 +1,2 @@
1
+ export { fromValibot } from "./from_valibot.ts";
2
+ export { toValibot } from "./to_valibot.ts";
@@ -0,0 +1,36 @@
1
+ // import removed; decoders are delegated to codecs now
2
+ import type { BaseIssue, BaseSchema } from "valibot";
3
+ import type { SchemaNode, SerializedSchema } from "../types.ts";
4
+ import { isSerializedSchema } from "../util/guard.ts";
5
+ import * as codecs from "../types/index.ts";
6
+
7
+ type AnySchema = BaseSchema<unknown, unknown, BaseIssue<unknown>>;
8
+
9
+ /**
10
+ * Convert a serialized schema back to a Valibot schema.
11
+ *
12
+ * Validates the envelope shape and converts the contained AST into
13
+ * Valibot builder objects.
14
+ *
15
+ * @param data - A `SerializedSchema` envelope produced by {@link fromValibot}.
16
+ * @returns A Valibot schema equivalent to the original.
17
+ * @throws If the input is not a valid serialized schema.
18
+ */
19
+ export function toValibot(data: SerializedSchema): AnySchema {
20
+ if (!isSerializedSchema(data)) {
21
+ throw new Error("Invalid serialized schema format");
22
+ }
23
+ return decodeNode(data.node);
24
+ }
25
+
26
+ function decodeNode(node: SchemaNode): AnySchema {
27
+ for (const c of Object.values(codecs)) {
28
+ if (c.typeName === node.type) {
29
+ return c.decode(node as never, { decodeNode });
30
+ }
31
+ }
32
+
33
+ throw new Error(
34
+ `Unsupported node type: ${(node as { type: string }).type}`,
35
+ );
36
+ }
@@ -0,0 +1,2 @@
1
+ export { fromValibot } from "./converters/from_valibot.ts";
2
+ export { toValibot } from "./converters/to_valibot.ts";
@@ -0,0 +1,36 @@
1
+ import * as v from "valibot";
2
+ import type { BaseNode, IsSchemaNode } from "./lib/type_interfaces.ts";
3
+ import type {
4
+ AnySchema,
5
+ Decoder,
6
+ Encoder,
7
+ Matches,
8
+ } from "./lib/type_interfaces.ts";
9
+
10
+ export const typeName = "any" as const;
11
+
12
+ // Serialized node shape for "any"
13
+ export interface AnyNode extends BaseNode<typeof typeName> {}
14
+
15
+ export const isSchemaNode: IsSchemaNode<AnyNode> = (
16
+ node: unknown,
17
+ _ctx,
18
+ ): node is AnyNode => {
19
+ return Boolean(
20
+ node &&
21
+ typeof node === "object" &&
22
+ (node as { type?: unknown }).type === typeName,
23
+ );
24
+ };
25
+
26
+ export const matches: Matches = (any: AnySchema): boolean => {
27
+ return any?.type === typeName;
28
+ };
29
+
30
+ export const encode: Encoder<AnyNode> = function encodeAny(): AnyNode {
31
+ return { type: typeName };
32
+ };
33
+
34
+ export const decode: Decoder<AnyNode> = function decodeAny(): AnySchema {
35
+ return v.any();
36
+ };
@@ -0,0 +1,115 @@
1
+ import * as v from "valibot";
2
+ import type { AnyNode, BaseNode, IsSchemaNode } from "./lib/type_interfaces.ts";
3
+ import type {
4
+ AnySchema,
5
+ Decoder,
6
+ Encoder,
7
+ Matches,
8
+ } from "./lib/type_interfaces.ts";
9
+
10
+ export const typeName = "array" as const;
11
+
12
+ // Serialized node shape for "array"
13
+ export interface ArrayNode extends BaseNode<typeof typeName> {
14
+ item: AnyNode;
15
+ minLength?: number;
16
+ maxLength?: number;
17
+ length?: number;
18
+ }
19
+
20
+ export const isSchemaNode: IsSchemaNode<ArrayNode> = (
21
+ node: unknown,
22
+ ctx,
23
+ ): node is ArrayNode => {
24
+ if (!node || typeof node !== "object") return false;
25
+ if ((node as { type?: unknown }).type !== typeName) return false;
26
+ const n = node as Record<string, unknown>;
27
+ if (!ctx.isSchemaNode(n.item)) return false;
28
+ if (n.minLength !== undefined && typeof n.minLength !== "number") {
29
+ return false;
30
+ }
31
+ if (n.maxLength !== undefined && typeof n.maxLength !== "number") {
32
+ return false;
33
+ }
34
+ if (n.length !== undefined && typeof n.length !== "number") return false;
35
+ return true;
36
+ };
37
+
38
+ export const matches: Matches = (any: AnySchema): boolean => {
39
+ return any?.type === typeName;
40
+ };
41
+
42
+ export const encode: Encoder<ArrayNode> = function encodeArray(
43
+ any,
44
+ ctx,
45
+ ): ArrayNode {
46
+ const child = (any as { item?: unknown }).item as AnySchema | undefined;
47
+ if (!child) throw new Error("Unsupported array schema: missing item schema");
48
+ const node: ArrayNode = {
49
+ type: typeName,
50
+ item: ctx.encodeNode(child),
51
+ };
52
+ const pipe = (any as { pipe?: unknown[] }).pipe as
53
+ | Array<Record<string, unknown>>
54
+ | undefined;
55
+ if (Array.isArray(pipe)) {
56
+ for (const step of pipe) {
57
+ if (!step || typeof step !== "object") continue;
58
+ if (step.kind !== "validation") continue;
59
+ switch (step.type) {
60
+ case "min_length": {
61
+ const req = step.requirement as number | undefined;
62
+ if (typeof req === "number") node.minLength = req;
63
+ break;
64
+ }
65
+ case "max_length": {
66
+ const req = step.requirement as number | undefined;
67
+ if (typeof req === "number") node.maxLength = req;
68
+ break;
69
+ }
70
+ case "length": {
71
+ const req = step.requirement as number | undefined;
72
+ if (typeof req === "number") node.length = req;
73
+ break;
74
+ }
75
+ case "non_empty": {
76
+ node.minLength = Math.max(1, node.minLength ?? 0);
77
+ break;
78
+ }
79
+ }
80
+ }
81
+ }
82
+ return node;
83
+ };
84
+
85
+ export const decode: Decoder<ArrayNode> = function decodeArray(
86
+ node,
87
+ ctx,
88
+ ): AnySchema {
89
+ const base = v.array(ctx.decodeNode(node.item));
90
+ const validators: unknown[] = [];
91
+ if (node.minLength !== undefined) {
92
+ validators.push(v.minLength(node.minLength));
93
+ }
94
+ if (node.maxLength !== undefined) {
95
+ validators.push(v.maxLength(node.maxLength));
96
+ }
97
+ if (node.length !== undefined) validators.push(v.length(node.length));
98
+ switch (validators.length) {
99
+ case 0:
100
+ return base;
101
+ case 1:
102
+ return v.pipe(base, validators[0] as never);
103
+ case 2:
104
+ return v.pipe(base, validators[0] as never, validators[1] as never);
105
+ case 3:
106
+ return v.pipe(
107
+ base,
108
+ validators[0] as never,
109
+ validators[1] as never,
110
+ validators[2] as never,
111
+ );
112
+ default:
113
+ return v.pipe(base, ...(validators as never[]));
114
+ }
115
+ };
@@ -0,0 +1,40 @@
1
+ import * as v from "valibot";
2
+ import type { BaseNode, IsSchemaNode } from "./lib/type_interfaces.ts";
3
+ import type {
4
+ AnySchema,
5
+ Decoder,
6
+ Encoder,
7
+ Matches,
8
+ } from "./lib/type_interfaces.ts";
9
+
10
+ export const typeName = "bigint" as const;
11
+
12
+ // Serialized node shape for "bigint"
13
+ export interface BigIntNode extends BaseNode<typeof typeName> {}
14
+
15
+ export const isSchemaNode: IsSchemaNode<BigIntNode> = (
16
+ node: unknown,
17
+ _ctx,
18
+ ): node is BigIntNode => {
19
+ return Boolean(
20
+ node &&
21
+ typeof node === "object" &&
22
+ (node as { type?: unknown }).type === typeName,
23
+ );
24
+ };
25
+
26
+ export const matches: Matches = (any: AnySchema): boolean => {
27
+ return any?.type === typeName;
28
+ };
29
+
30
+ export const encode: Encoder<BigIntNode> = function encodeBigInt(
31
+ _any,
32
+ ): BigIntNode {
33
+ return { type: typeName };
34
+ };
35
+
36
+ export const decode: Decoder<BigIntNode> = function decodeBigInt(
37
+ _node,
38
+ ): AnySchema {
39
+ return v.bigint();
40
+ };
@@ -0,0 +1,67 @@
1
+ import * as v from "valibot";
2
+ import type { BaseNode } from "./lib/type_interfaces.ts";
3
+ import type {
4
+ AnySchema,
5
+ Decoder,
6
+ Encoder,
7
+ Matches,
8
+ } from "./lib/type_interfaces.ts";
9
+
10
+ export const typeName = "blob" as const;
11
+
12
+ // Serialized node shape for "blob"
13
+ export interface BlobNode extends BaseNode<typeof typeName> {
14
+ minSize?: number;
15
+ maxSize?: number;
16
+ mimeTypes?: string[];
17
+ }
18
+
19
+ export const matches: Matches = (any: AnySchema): boolean => {
20
+ return any?.type === typeName;
21
+ };
22
+
23
+ export const encode: Encoder<BlobNode> = function encodeBlob(any): BlobNode {
24
+ const node: BlobNode = { type: typeName };
25
+ const pipe = (any as { pipe?: unknown[] }).pipe as
26
+ | Array<Record<string, unknown>>
27
+ | undefined;
28
+ if (Array.isArray(pipe)) {
29
+ for (const step of pipe) {
30
+ if (!step || typeof step !== "object") continue;
31
+ if (step.kind !== "validation") continue;
32
+ switch (step.type) {
33
+ case "min_size": {
34
+ const req = step.requirement as number | undefined;
35
+ if (typeof req === "number") node.minSize = req;
36
+ break;
37
+ }
38
+ case "max_size": {
39
+ const req = step.requirement as number | undefined;
40
+ if (typeof req === "number") node.maxSize = req;
41
+ break;
42
+ }
43
+ case "mime_type": {
44
+ const req = step.requirement as string[] | undefined;
45
+ if (Array.isArray(req)) node.mimeTypes = req;
46
+ break;
47
+ }
48
+ }
49
+ }
50
+ }
51
+ return node;
52
+ };
53
+
54
+ export const decode: Decoder<BlobNode> = function decodeBlob(node): AnySchema {
55
+ let b = v.blob();
56
+ const items: unknown[] = [];
57
+ if (node.minSize !== undefined) items.push(v.minSize(node.minSize));
58
+ if (node.maxSize !== undefined) items.push(v.maxSize(node.maxSize));
59
+ if (node.mimeTypes && node.mimeTypes.length > 0) {
60
+ const mimeType = (
61
+ v as unknown as { mimeType: (req: string[] | string) => unknown }
62
+ ).mimeType;
63
+ items.push(mimeType(node.mimeTypes));
64
+ }
65
+ if (items.length > 0) b = v.pipe(b, ...(items as never[]));
66
+ return b;
67
+ };
@@ -0,0 +1,40 @@
1
+ import * as v from "valibot";
2
+ import type { BaseNode, IsSchemaNode } from "./lib/type_interfaces.ts";
3
+ import type {
4
+ AnySchema,
5
+ Decoder,
6
+ Encoder,
7
+ Matches,
8
+ } from "./lib/type_interfaces.ts";
9
+
10
+ export const typeName = "boolean" as const;
11
+
12
+ // Serialized node shape for "boolean"
13
+ export interface BooleanNode extends BaseNode<typeof typeName> {}
14
+
15
+ export const isSchemaNode: IsSchemaNode<BooleanNode> = (
16
+ node: unknown,
17
+ _ctx,
18
+ ): node is BooleanNode => {
19
+ return Boolean(
20
+ node &&
21
+ typeof node === "object" &&
22
+ (node as { type?: unknown }).type === typeName,
23
+ );
24
+ };
25
+
26
+ export const matches: Matches = (any: AnySchema): boolean => {
27
+ return any?.type === typeName;
28
+ };
29
+
30
+ export const encode: Encoder<BooleanNode> = function encodeBoolean(
31
+ _any,
32
+ ): BooleanNode {
33
+ return { type: typeName };
34
+ };
35
+
36
+ export const decode: Decoder<BooleanNode> = function decodeBoolean(
37
+ _node,
38
+ ): AnySchema {
39
+ return v.boolean();
40
+ };
@@ -0,0 +1,36 @@
1
+ import * as v from "valibot";
2
+ import type { BaseNode, IsSchemaNode } from "./lib/type_interfaces.ts";
3
+ import type {
4
+ AnySchema,
5
+ Decoder,
6
+ Encoder,
7
+ Matches,
8
+ } from "./lib/type_interfaces.ts";
9
+
10
+ export const typeName = "date" as const;
11
+
12
+ // Serialized node shape for "date"
13
+ export interface DateNode extends BaseNode<typeof typeName> {}
14
+
15
+ export const isSchemaNode: IsSchemaNode<DateNode> = (
16
+ node: unknown,
17
+ _ctx,
18
+ ): node is DateNode => {
19
+ return Boolean(
20
+ node &&
21
+ typeof node === "object" &&
22
+ (node as { type?: unknown }).type === typeName,
23
+ );
24
+ };
25
+
26
+ export const matches: Matches = (any: AnySchema): boolean => {
27
+ return any?.type === typeName;
28
+ };
29
+
30
+ export const encode: Encoder<DateNode> = function encodeDate(_any): DateNode {
31
+ return { type: typeName };
32
+ };
33
+
34
+ export const decode: Decoder<DateNode> = function decodeDate(_node): AnySchema {
35
+ return v.date();
36
+ };
@@ -0,0 +1,67 @@
1
+ import * as v from "valibot";
2
+ import type { BaseNode, IsSchemaNode } from "./lib/type_interfaces.ts";
3
+ import type {
4
+ AnySchema,
5
+ Decoder,
6
+ Encoder,
7
+ Matches,
8
+ } from "./lib/type_interfaces.ts";
9
+
10
+ export const typeName = "enum" as const;
11
+
12
+ // Serialized node shape for "enum"
13
+ export interface EnumNode extends BaseNode<typeof typeName> {
14
+ values: Array<string | number | boolean | null>;
15
+ }
16
+
17
+ export const isSchemaNode: IsSchemaNode<EnumNode> = (
18
+ node: unknown,
19
+ _ctx,
20
+ ): node is EnumNode => {
21
+ if (!node || typeof node !== "object") return false;
22
+ if ((node as { type?: unknown }).type !== typeName) return false;
23
+ const values = (node as { values?: unknown }).values as unknown;
24
+ if (!Array.isArray(values)) return false;
25
+ for (const v of values) {
26
+ const t = typeof v;
27
+ if (!(t === "string" || t === "number" || t === "boolean") && v !== null) {
28
+ return false;
29
+ }
30
+ }
31
+ return true;
32
+ };
33
+
34
+ export const matches: Matches = (any: AnySchema): boolean => {
35
+ const type = (any as { type?: string }).type;
36
+ return type === "enum";
37
+ };
38
+
39
+ export const encode: Encoder<EnumNode> = function encodeEnum(any): EnumNode {
40
+ const values = ((any as { options?: unknown[] }).options ??
41
+ (any as { enum?: unknown[] }).enum) as unknown[] | undefined;
42
+ if (!Array.isArray(values)) {
43
+ throw new Error("Unsupported enum schema: missing options");
44
+ }
45
+ const out: Array<string | number | boolean | null> = [];
46
+ for (const val of values) {
47
+ const t = typeof val;
48
+ if (t === "string" || t === "number" || t === "boolean" || val === null) {
49
+ out.push(val as never);
50
+ } else throw new Error("Unsupported enum value type");
51
+ }
52
+ return { type: typeName, values: out };
53
+ };
54
+
55
+ export const decode: Decoder<EnumNode> = function decodeEnum(node): AnySchema {
56
+ const allStrings = node.values.every((val) => typeof val === "string");
57
+ if (allStrings) {
58
+ const mapping = Object.fromEntries(
59
+ (node.values as string[]).map((s) => [s, s] as const),
60
+ );
61
+ // deno-lint-ignore no-explicit-any
62
+ return (v as any).enum(mapping);
63
+ }
64
+ // Fallback: union of literals
65
+ const literals = node.values.map((val) => v.literal(val as never));
66
+ return v.union(literals as never);
67
+ };
@@ -0,0 +1,58 @@
1
+ import * as v from "valibot";
2
+ import type { AnyNode, BaseNode, IsSchemaNode } from "./lib/type_interfaces.ts";
3
+ import type {
4
+ AnySchema,
5
+ Decoder,
6
+ Encoder,
7
+ Matches,
8
+ } from "./lib/type_interfaces.ts";
9
+ import { cloneSerializable } from "./lib/default_utils.ts";
10
+
11
+ export const typeName = "exact_optional" as const;
12
+
13
+ export const supportedTypes = [typeName];
14
+
15
+ export interface ExactOptionalNode extends BaseNode<typeof typeName> {
16
+ base: AnyNode;
17
+ default?: unknown;
18
+ hasDefault?: boolean;
19
+ }
20
+
21
+ export const isSchemaNode: IsSchemaNode<ExactOptionalNode> = (
22
+ node,
23
+ ctx,
24
+ ): node is ExactOptionalNode => {
25
+ if (!node || typeof node !== "object") return false;
26
+ if ((node as { type?: unknown }).type !== typeName) return false;
27
+ if (!ctx.isSchemaNode((node as { base?: unknown }).base)) return false;
28
+ return true;
29
+ };
30
+
31
+ export const matches: Matches = (schema: AnySchema): boolean => {
32
+ return schema?.type === typeName;
33
+ };
34
+
35
+ export const encode: Encoder<ExactOptionalNode> = (schema, ctx) => {
36
+ const wrapped = (schema as { wrapped?: AnySchema }).wrapped;
37
+ if (!wrapped) {
38
+ throw new Error("Unsupported exactOptional schema: missing wrapped schema");
39
+ }
40
+ const node: ExactOptionalNode = {
41
+ type: typeName,
42
+ base: ctx.encodeNode(wrapped),
43
+ };
44
+ if (Object.prototype.hasOwnProperty.call(schema, "default")) {
45
+ const def = (schema as { default?: unknown }).default;
46
+ node.default = cloneSerializable(def);
47
+ node.hasDefault = true;
48
+ }
49
+ return node;
50
+ };
51
+
52
+ export const decode: Decoder<ExactOptionalNode> = (node, ctx) => {
53
+ const base = ctx.decodeNode(node.base);
54
+ if (node.hasDefault) {
55
+ return v.exactOptional(base, cloneSerializable(node.default));
56
+ }
57
+ return v.exactOptional(base, undefined);
58
+ };
@@ -0,0 +1,86 @@
1
+ import * as v from "valibot";
2
+ import type { BaseNode, IsSchemaNode } from "./lib/type_interfaces.ts";
3
+ import type {
4
+ AnySchema,
5
+ Decoder,
6
+ Encoder,
7
+ Matches,
8
+ } from "./lib/type_interfaces.ts";
9
+
10
+ export const typeName = "file" as const;
11
+
12
+ // Serialized node shape for "file"
13
+ export interface FileNode extends BaseNode<typeof typeName> {
14
+ minSize?: number;
15
+ maxSize?: number;
16
+ mimeTypes?: string[];
17
+ }
18
+
19
+ export const isSchemaNode: IsSchemaNode<FileNode> = (
20
+ node: unknown,
21
+ _ctx,
22
+ ): node is FileNode => {
23
+ if (!node || typeof node !== "object") return false;
24
+ if ((node as { type?: unknown }).type !== typeName) return false;
25
+ const n = node as Record<string, unknown>;
26
+ if (n.minSize !== undefined && typeof n.minSize !== "number") return false;
27
+ if (n.maxSize !== undefined && typeof n.maxSize !== "number") return false;
28
+ if (n.mimeTypes !== undefined) {
29
+ if (
30
+ !Array.isArray(n.mimeTypes) ||
31
+ (n.mimeTypes as unknown[]).some((x) => typeof x !== "string")
32
+ )
33
+ return false;
34
+ }
35
+ return true;
36
+ };
37
+
38
+ export const matches: Matches = (any: AnySchema): boolean => {
39
+ return any?.type === typeName;
40
+ };
41
+
42
+ export const encode: Encoder<FileNode> = function encodeFile(any): FileNode {
43
+ const node: FileNode = { type: typeName };
44
+ const pipe = (any as { pipe?: unknown[] }).pipe as
45
+ | Array<Record<string, unknown>>
46
+ | undefined;
47
+ if (Array.isArray(pipe)) {
48
+ for (const step of pipe) {
49
+ if (!step || typeof step !== "object") continue;
50
+ if (step.kind !== "validation") continue;
51
+ switch (step.type) {
52
+ case "min_size": {
53
+ const req = step.requirement as number | undefined;
54
+ if (typeof req === "number") node.minSize = req;
55
+ break;
56
+ }
57
+ case "max_size": {
58
+ const req = step.requirement as number | undefined;
59
+ if (typeof req === "number") node.maxSize = req;
60
+ break;
61
+ }
62
+ case "mime_type": {
63
+ const req = step.requirement as string[] | undefined;
64
+ if (Array.isArray(req)) node.mimeTypes = req;
65
+ break;
66
+ }
67
+ }
68
+ }
69
+ }
70
+ return node;
71
+ };
72
+
73
+ export const decode: Decoder<FileNode> = function decodeFile(node): AnySchema {
74
+ let f = v.file();
75
+ const actions: unknown[] = [];
76
+ if (node.minSize !== undefined) actions.push(v.minSize(node.minSize));
77
+ if (node.maxSize !== undefined) actions.push(v.maxSize(node.maxSize));
78
+ if (node.mimeTypes && node.mimeTypes.length > 0) {
79
+ const mimeType = (
80
+ v as unknown as { mimeType: (req: string[] | string) => unknown }
81
+ ).mimeType;
82
+ actions.push(mimeType(node.mimeTypes));
83
+ }
84
+ if (actions.length > 0) f = v.pipe(f, ...(actions as never[]));
85
+ return f;
86
+ };