surreal-zod 0.0.0-alpha.1
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 +15 -0
- package/lib/index.js +3 -0
- package/lib/print.js +166 -0
- package/lib/surql.js +351 -0
- package/lib/zod.js +126 -0
- package/package.json +35 -0
- package/src/index.ts +3 -0
- package/src/print.ts +184 -0
- package/src/surql.ts +493 -0
- package/src/zod.ts +302 -0
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "surreal-zod",
|
|
3
|
+
"version": "0.0.0-alpha.1",
|
|
4
|
+
"module": "index.ts",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"build": "tsc"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"lib",
|
|
10
|
+
"src"
|
|
11
|
+
],
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"bun": "./src/index.ts",
|
|
15
|
+
"import": "./lib/index.js"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@biomejs/biome": "^2.3.3",
|
|
20
|
+
"@types/bun": "latest",
|
|
21
|
+
"chalk": "^5.6.2",
|
|
22
|
+
"get-port": "^7.1.0",
|
|
23
|
+
"zod": "^3.25.0 || ^4.0.0"
|
|
24
|
+
},
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"typescript": "^5",
|
|
27
|
+
"zod": "^3.25.0 || ^4.0.0"
|
|
28
|
+
},
|
|
29
|
+
"type": "module",
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@surrealdb/node": "2.3.4",
|
|
32
|
+
"dedent": "^1.7.0",
|
|
33
|
+
"surrealdb": "^2.0.0-alpha.13"
|
|
34
|
+
}
|
|
35
|
+
}
|
package/src/index.ts
ADDED
package/src/print.ts
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
// import type * as z3 from 'zod/v3'
|
|
2
|
+
import type * as z4 from "zod/v4/core";
|
|
3
|
+
|
|
4
|
+
export function zodToSexpr(schema: z4.$ZodType) {
|
|
5
|
+
if ("_zod" in schema) {
|
|
6
|
+
return zod4ToSexpr(schema);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
throw new Error("Zod 3 not yet supported");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function zod4ToSexpr(_schema: z4.$ZodType, depth = 0): string {
|
|
13
|
+
const indent = " ".repeat(depth);
|
|
14
|
+
const childIndent = " ".repeat(depth + 1);
|
|
15
|
+
|
|
16
|
+
const schema = _schema as z4.$ZodTypes;
|
|
17
|
+
const def = schema._zod.def;
|
|
18
|
+
const checks = def.checks ?? [];
|
|
19
|
+
if ("check" in def) {
|
|
20
|
+
checks.push(schema as z4.$ZodCheck);
|
|
21
|
+
}
|
|
22
|
+
const type = def.type;
|
|
23
|
+
|
|
24
|
+
if (type === "object") {
|
|
25
|
+
return `(object)`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Primitives
|
|
29
|
+
switch (type) {
|
|
30
|
+
case "string": {
|
|
31
|
+
const constraints = formatChecks(checks);
|
|
32
|
+
return constraints.length > 0 ? `(string [${constraints}])` : `(string)`;
|
|
33
|
+
}
|
|
34
|
+
case "number": {
|
|
35
|
+
// const constraints = formatChecks(checks);
|
|
36
|
+
// return constraints.length > 0 ? `(number [${constraints}])` : `(number)`;
|
|
37
|
+
return `(number)`;
|
|
38
|
+
}
|
|
39
|
+
case "optional": {
|
|
40
|
+
const inner = zod4ToSexpr(def.innerType, depth + 1);
|
|
41
|
+
return `(optional ${inner})`;
|
|
42
|
+
}
|
|
43
|
+
case "nullable": {
|
|
44
|
+
const inner = zod4ToSexpr(def.innerType, depth + 1);
|
|
45
|
+
return `(nullable ${inner})`;
|
|
46
|
+
}
|
|
47
|
+
case "nonoptional": {
|
|
48
|
+
const inner = zod4ToSexpr(def.innerType, depth + 1);
|
|
49
|
+
return `(nonoptional ${inner})`;
|
|
50
|
+
}
|
|
51
|
+
case "array": {
|
|
52
|
+
const inner = zod4ToSexpr(def.element, depth + 1);
|
|
53
|
+
return `(array ${inner})`;
|
|
54
|
+
}
|
|
55
|
+
case "bigint": {
|
|
56
|
+
return `(bigint)`;
|
|
57
|
+
}
|
|
58
|
+
case "boolean": {
|
|
59
|
+
return `(boolean)`;
|
|
60
|
+
}
|
|
61
|
+
case "symbol": {
|
|
62
|
+
return `(symbol)`;
|
|
63
|
+
}
|
|
64
|
+
case "undefined": {
|
|
65
|
+
return `(undefined)`;
|
|
66
|
+
}
|
|
67
|
+
case "null": {
|
|
68
|
+
return `(null)`;
|
|
69
|
+
}
|
|
70
|
+
case "any": {
|
|
71
|
+
return `(any)`;
|
|
72
|
+
}
|
|
73
|
+
default: {
|
|
74
|
+
return `(unknown-type ${type || "?"})`;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// console.log("unknown type", def);
|
|
79
|
+
|
|
80
|
+
// if (type === "number") {
|
|
81
|
+
// const constraints = formatChecks(checks);
|
|
82
|
+
// return constraints.length > 0 ? `(number [${constraints}])` : `(number)`;
|
|
83
|
+
// }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function breakLine(sexpr: string, depth: number) {
|
|
87
|
+
// return sexpr.length > 80
|
|
88
|
+
// ? `\n${" ".repeat(depth)}${sexpr}\n${" ".repeat(depth)}`
|
|
89
|
+
// : sexpr;
|
|
90
|
+
return sexpr;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function formatChecks(checks: z4.$ZodCheck[]) {
|
|
94
|
+
return checks.map((check) => formatCheck(check)).join(" ");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function formatCheck(_check: z4.$ZodCheck) {
|
|
98
|
+
const check = _check as z4.$ZodChecks;
|
|
99
|
+
const def = check._zod.def;
|
|
100
|
+
switch (def.check) {
|
|
101
|
+
case "min_length":
|
|
102
|
+
return `min:${def.minimum}`;
|
|
103
|
+
case "max_length":
|
|
104
|
+
return `max:${def.maximum}`;
|
|
105
|
+
case "length_equals":
|
|
106
|
+
return `length:${def.length}`;
|
|
107
|
+
case "string_format":
|
|
108
|
+
return parseStringFormat(check);
|
|
109
|
+
// case "bigint_format":
|
|
110
|
+
// return `bigint_format=${def.format}`;
|
|
111
|
+
// case "number_format":
|
|
112
|
+
// return `number_format=${def.format}`;
|
|
113
|
+
case "greater_than":
|
|
114
|
+
return `${def.inclusive ? ">=" : ">"}${def.value}`;
|
|
115
|
+
case "less_than":
|
|
116
|
+
return `${def.inclusive ? "<=" : "<"}${def.value}`;
|
|
117
|
+
// case "max_size":
|
|
118
|
+
// return `max_size=${def.maximum}`;
|
|
119
|
+
// case "min_size":
|
|
120
|
+
// return `min_size=${def.minimum}`;
|
|
121
|
+
// case "mime_type":
|
|
122
|
+
// return `mime_type=${def.mime}`;
|
|
123
|
+
// case "multiple_of":
|
|
124
|
+
// return `multiple_of=${def.value}`;
|
|
125
|
+
// case "size_equals":
|
|
126
|
+
// return `size_equals=${def.size}`;
|
|
127
|
+
// case undefined: {
|
|
128
|
+
// return `[unknown ${inspect(def, { colors: true })}]`;
|
|
129
|
+
// }
|
|
130
|
+
default:
|
|
131
|
+
return `[${def.check} ?]`;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function parseStringFormat(_check: z4.$ZodCheck) {
|
|
136
|
+
const check = _check as z4.$ZodStringFormatChecks;
|
|
137
|
+
const def = check._zod.def;
|
|
138
|
+
|
|
139
|
+
const coerce = "coerce" in def ? `coerce:${def.coerce} ` : "";
|
|
140
|
+
|
|
141
|
+
if (def.format === "starts_with") {
|
|
142
|
+
return `starts_with:"${def.prefix}"`;
|
|
143
|
+
} else if (def.format === "ends_with") {
|
|
144
|
+
return `ends_with:"${def.suffix}"`;
|
|
145
|
+
} else if (def.format === "includes") {
|
|
146
|
+
return `includes:"${def.includes}"`;
|
|
147
|
+
} else if (def.format === "regex") {
|
|
148
|
+
return `regex:${def.pattern}`;
|
|
149
|
+
} else if (def.format === "uuid") {
|
|
150
|
+
return `format:uuid${def.version || ""}`;
|
|
151
|
+
} else if (def.format === "xid") {
|
|
152
|
+
return `format:xid`;
|
|
153
|
+
} else if (def.format === "url") {
|
|
154
|
+
const opts = [
|
|
155
|
+
def.normalize ? `normalize` : null,
|
|
156
|
+
def.hostname ? `hostname:${def.hostname}` : null,
|
|
157
|
+
def.protocol ? `protocol:${def.protocol}` : null,
|
|
158
|
+
]
|
|
159
|
+
.filter(Boolean)
|
|
160
|
+
.join(", ");
|
|
161
|
+
|
|
162
|
+
return `format:url${opts.length > 0 ? `(${opts})` : ""}`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return `${coerce}format:${def.format}`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function zodObjectToSexpr(schema: z4.$ZodObject, level = 0) {
|
|
169
|
+
if (!("_zod" in schema)) {
|
|
170
|
+
throw new Error(
|
|
171
|
+
"Invalid schema provided, make sure you are using zod v4 as zod v3 is currently not supported.",
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
const def = schema._zod.def;
|
|
175
|
+
let sexpr = `(${def.type}${def.checks ? ` ${formatChecks(def.checks)}` : ""}`;
|
|
176
|
+
|
|
177
|
+
for (const [propName, propSchema] of Object.entries(def.shape)) {
|
|
178
|
+
sexpr += `${zod4ToSexpr(propSchema, level + 1)}\n`;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
sexpr += ")";
|
|
182
|
+
|
|
183
|
+
return sexpr;
|
|
184
|
+
}
|
package/src/surql.ts
ADDED
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BoundQuery,
|
|
3
|
+
escapeIdent,
|
|
4
|
+
escapeIdPart,
|
|
5
|
+
RecordId,
|
|
6
|
+
surql,
|
|
7
|
+
Table,
|
|
8
|
+
} from "surrealdb";
|
|
9
|
+
import * as z4 from "zod/v4/core";
|
|
10
|
+
import z from "zod";
|
|
11
|
+
import dedent from "dedent";
|
|
12
|
+
import type sz from ".";
|
|
13
|
+
import type { SurrealZodType, SurrealZodTypes } from "./zod";
|
|
14
|
+
|
|
15
|
+
export type ZodTypeName = z4.$ZodType["_zod"]["def"]["type"];
|
|
16
|
+
export type SurrealZodTypeName = SurrealZodType["_zod"]["def"]["type"];
|
|
17
|
+
|
|
18
|
+
export interface ZodToSurqlOptions<S extends z4.$ZodObject> {
|
|
19
|
+
table: string | Table;
|
|
20
|
+
schemafull?: boolean;
|
|
21
|
+
exists?: "ignore" | "error" | "overwrite";
|
|
22
|
+
drop?: boolean;
|
|
23
|
+
comment?: string;
|
|
24
|
+
schema: S;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function zodToSurql<S extends z4.$ZodObject>(
|
|
28
|
+
options: ZodToSurqlOptions<S>,
|
|
29
|
+
): [
|
|
30
|
+
S extends z4.$ZodObject<infer Shape>
|
|
31
|
+
? z4.$ZodObject<Shape & { id: z4.$ZodCustom<RecordId> }>
|
|
32
|
+
: never,
|
|
33
|
+
BoundQuery,
|
|
34
|
+
] {
|
|
35
|
+
const table =
|
|
36
|
+
typeof options.table === "string"
|
|
37
|
+
? new Table(options.table)
|
|
38
|
+
: options.table;
|
|
39
|
+
|
|
40
|
+
const schema = options.schema;
|
|
41
|
+
if (!("_zod" in schema)) {
|
|
42
|
+
throw new Error(
|
|
43
|
+
"Invalid schema provided, make sure you are using zod v4 as zod v3 is currently not supported.",
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const def = schema._zod.def;
|
|
48
|
+
const shape = def.shape;
|
|
49
|
+
const query = defineTable(options);
|
|
50
|
+
for (const [key, value] of Object.entries(shape)) {
|
|
51
|
+
query.append(
|
|
52
|
+
defineField({ name: key, table, type: value, exists: options.exists }),
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// @ts-expect-error - extend is not a method of z4.$ZodObject
|
|
57
|
+
return [schema.extend({ id: z.any() }), query];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function defineTable<S extends z4.$ZodObject>(options: ZodToSurqlOptions<S>) {
|
|
61
|
+
const table =
|
|
62
|
+
typeof options.table === "string"
|
|
63
|
+
? new Table(options.table)
|
|
64
|
+
: options.table;
|
|
65
|
+
const query = surql`DEFINE TABLE`;
|
|
66
|
+
|
|
67
|
+
if (options.exists === "ignore") {
|
|
68
|
+
query.append(" IF NOT EXISTS");
|
|
69
|
+
} else if (options.exists === "overwrite") {
|
|
70
|
+
query.append(" OVERWRITE");
|
|
71
|
+
}
|
|
72
|
+
// Looks like passing Table instance is not supported yet
|
|
73
|
+
query.append(` ${escapeIdPart(table.name)}`);
|
|
74
|
+
|
|
75
|
+
if (options.drop) {
|
|
76
|
+
query.append(" DROP");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (options.schemafull) {
|
|
80
|
+
query.append(" SCHEMAFULL");
|
|
81
|
+
} else {
|
|
82
|
+
query.append(" SCHEMALESS");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (options.comment) {
|
|
86
|
+
query.append(surql` COMMENT ${options.comment}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
query.append(";\n");
|
|
90
|
+
|
|
91
|
+
return query;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function defineField(options: {
|
|
95
|
+
name: string;
|
|
96
|
+
table: string | Table;
|
|
97
|
+
type: z4.$ZodType;
|
|
98
|
+
exists?: "ignore" | "error" | "overwrite";
|
|
99
|
+
}) {
|
|
100
|
+
const name = options.name;
|
|
101
|
+
const table =
|
|
102
|
+
typeof options.table === "string"
|
|
103
|
+
? new Table(options.table)
|
|
104
|
+
: options.table;
|
|
105
|
+
const schema = options.type as z4.$ZodTypes;
|
|
106
|
+
if (!("_zod" in schema)) {
|
|
107
|
+
throw new Error(
|
|
108
|
+
"Invalid field schema provided, make sure you are using zod v4 as zod v3 is currently not supported.",
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const context: ZodSurrealTypeContext = {
|
|
113
|
+
name,
|
|
114
|
+
table,
|
|
115
|
+
rootSchema: schema,
|
|
116
|
+
children: [],
|
|
117
|
+
asserts: [],
|
|
118
|
+
transforms: [],
|
|
119
|
+
};
|
|
120
|
+
const query = surql`DEFINE FIELD`;
|
|
121
|
+
|
|
122
|
+
if (options.exists === "ignore") {
|
|
123
|
+
query.append(" IF NOT EXISTS");
|
|
124
|
+
} else if (options.exists === "overwrite") {
|
|
125
|
+
query.append(" OVERWRITE");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
query.append(` ${name} ON TABLE ${table.name}`);
|
|
129
|
+
|
|
130
|
+
const type = zodTypeToSurrealType(schema, [], context);
|
|
131
|
+
|
|
132
|
+
query.append(` TYPE ${type}`);
|
|
133
|
+
|
|
134
|
+
if (context.default) {
|
|
135
|
+
query.append(
|
|
136
|
+
context.default.always
|
|
137
|
+
? ` DEFAULT ALWAYS ${JSON.stringify(context.default.value)}`
|
|
138
|
+
: ` DEFAULT ${JSON.stringify(context.default.value)}`,
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (context.transforms.length > 0) {
|
|
143
|
+
query.append(` VALUE {\n`);
|
|
144
|
+
for (const transform of context.transforms) {
|
|
145
|
+
query.append(
|
|
146
|
+
dedent.withOptions({ alignValues: true })`
|
|
147
|
+
//
|
|
148
|
+
${transform}\n`.slice(3),
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
query.append(`}`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (context.asserts.length > 0) {
|
|
155
|
+
query.append(` ASSERT {\n`);
|
|
156
|
+
for (const assert of context.asserts) {
|
|
157
|
+
query.append(
|
|
158
|
+
dedent.withOptions({ alignValues: true })`
|
|
159
|
+
//
|
|
160
|
+
${assert}\n`.slice(3),
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
query.append(`}`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
query.append(`;\n`);
|
|
167
|
+
|
|
168
|
+
if (context.children.length > 0) {
|
|
169
|
+
for (const { name: childName, type: childType } of context.children) {
|
|
170
|
+
query.append(
|
|
171
|
+
defineField({
|
|
172
|
+
name: `${name}.${childName}`,
|
|
173
|
+
table,
|
|
174
|
+
type: childType,
|
|
175
|
+
exists: options.exists,
|
|
176
|
+
}),
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return query;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
type ZodSurrealTypeContext = {
|
|
185
|
+
name: string;
|
|
186
|
+
table: Table;
|
|
187
|
+
rootSchema: z4.$ZodType;
|
|
188
|
+
children: ZodSurrealChildType[];
|
|
189
|
+
asserts: string[];
|
|
190
|
+
transforms: string[];
|
|
191
|
+
default?: { value: any; always: boolean };
|
|
192
|
+
};
|
|
193
|
+
type ZodSurrealChildType = { name: string; type: z4.$ZodType };
|
|
194
|
+
|
|
195
|
+
export function zodTypeToSurrealType(
|
|
196
|
+
type: z4.$ZodType | SurrealZodType,
|
|
197
|
+
parents: string[] = [],
|
|
198
|
+
context: ZodSurrealTypeContext,
|
|
199
|
+
): string {
|
|
200
|
+
const schema = type as z4.$ZodTypes | SurrealZodTypes;
|
|
201
|
+
if (!("_zod" in schema)) {
|
|
202
|
+
throw new Error(
|
|
203
|
+
"Invalid schema provided, make sure you are using zod v4 as zod v3 is currently not supported.",
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const def = schema._zod.def;
|
|
208
|
+
const checks = getChecks(schema);
|
|
209
|
+
parseChecks(context.name, checks, context, def.type);
|
|
210
|
+
// console.log(zodToSexpr(type));
|
|
211
|
+
|
|
212
|
+
switch (def.type) {
|
|
213
|
+
case "string":
|
|
214
|
+
return "string";
|
|
215
|
+
case "boolean":
|
|
216
|
+
return "bool";
|
|
217
|
+
case "object": {
|
|
218
|
+
const isInArray = context.rootSchema._zod.def.type === "array";
|
|
219
|
+
// TODO: remove any
|
|
220
|
+
for (const [key, value] of Object.entries((def as any).shape)) {
|
|
221
|
+
context.children.push({
|
|
222
|
+
name: isInArray ? `*.${key}` : key,
|
|
223
|
+
// TODO: remove as
|
|
224
|
+
type: value as z4.$ZodType,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
return "object";
|
|
228
|
+
}
|
|
229
|
+
case "number":
|
|
230
|
+
return "number";
|
|
231
|
+
case "null":
|
|
232
|
+
return "NULL";
|
|
233
|
+
// case "bigint":
|
|
234
|
+
// return "bigint";
|
|
235
|
+
// case "symbol":
|
|
236
|
+
// return "symbol";
|
|
237
|
+
case "any": {
|
|
238
|
+
//===============================
|
|
239
|
+
// Surreal Specific Types
|
|
240
|
+
//===============================
|
|
241
|
+
if ("surrealType" in def) {
|
|
242
|
+
if (def.surrealType === "record_id") {
|
|
243
|
+
if (def.what) {
|
|
244
|
+
return `record<${Object.keys(def.what).join(" | ")}>`;
|
|
245
|
+
} else {
|
|
246
|
+
return "record";
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return "any";
|
|
251
|
+
}
|
|
252
|
+
case "undefined": {
|
|
253
|
+
return "NONE";
|
|
254
|
+
}
|
|
255
|
+
case "default": {
|
|
256
|
+
// if (typeof def.defaultValue === "function") {
|
|
257
|
+
// context.default = { value: def.defaultValue(), always: false };
|
|
258
|
+
// } else {
|
|
259
|
+
// console.log(
|
|
260
|
+
// "default",
|
|
261
|
+
// Object.getOwnPropertyDescriptor(def, "defaultValue").get?.toString(),
|
|
262
|
+
// );
|
|
263
|
+
// TODO: remove any
|
|
264
|
+
context.default = { value: (def as any).defaultValue, always: false };
|
|
265
|
+
// }
|
|
266
|
+
return zodTypeToSurrealType(
|
|
267
|
+
// TODO: remove any
|
|
268
|
+
(def as any).innerType,
|
|
269
|
+
[...parents, def.type],
|
|
270
|
+
context,
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
case "nullable": {
|
|
274
|
+
const inner = zodTypeToSurrealType(
|
|
275
|
+
// TODO: remove any
|
|
276
|
+
(def as any).innerType,
|
|
277
|
+
[...parents, def.type],
|
|
278
|
+
context,
|
|
279
|
+
);
|
|
280
|
+
if (parents.includes("nullable")) {
|
|
281
|
+
return inner;
|
|
282
|
+
}
|
|
283
|
+
return `${inner} | NULL`;
|
|
284
|
+
}
|
|
285
|
+
case "optional": {
|
|
286
|
+
const inner = zodTypeToSurrealType(
|
|
287
|
+
// TODO: remove any
|
|
288
|
+
(def as any).innerType,
|
|
289
|
+
[...parents, def.type],
|
|
290
|
+
context,
|
|
291
|
+
);
|
|
292
|
+
if (parents.includes("optional") || parents.includes("nonoptional")) {
|
|
293
|
+
return inner;
|
|
294
|
+
}
|
|
295
|
+
return `option<${inner}>`;
|
|
296
|
+
}
|
|
297
|
+
case "nonoptional": {
|
|
298
|
+
// just a marker for children optional to skip the option<...> wrapper
|
|
299
|
+
return zodTypeToSurrealType(
|
|
300
|
+
// TODO: remove any
|
|
301
|
+
(def as any).innerType,
|
|
302
|
+
[...parents, def.type],
|
|
303
|
+
context,
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
case "union": {
|
|
307
|
+
// TODO: remove any
|
|
308
|
+
return (
|
|
309
|
+
(def as any).options
|
|
310
|
+
// TODO: remove any
|
|
311
|
+
.map((option: any) =>
|
|
312
|
+
zodTypeToSurrealType(option, [...parents, def.type], context),
|
|
313
|
+
)
|
|
314
|
+
.join(" | ")
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
case "array": {
|
|
318
|
+
const inner = zodTypeToSurrealType(
|
|
319
|
+
// TODO: remove any
|
|
320
|
+
(def as any).element,
|
|
321
|
+
[...parents, def.type],
|
|
322
|
+
context,
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
return `array<${inner}>`;
|
|
326
|
+
}
|
|
327
|
+
case "custom": {
|
|
328
|
+
return "any";
|
|
329
|
+
}
|
|
330
|
+
default: {
|
|
331
|
+
console.log("unknown type", def.type);
|
|
332
|
+
return "any";
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function getChecks(_schema: z4.$ZodType | SurrealZodType) {
|
|
338
|
+
const schema = _schema as z4.$ZodTypes | SurrealZodTypes;
|
|
339
|
+
const checks = schema._zod.def.checks ?? [];
|
|
340
|
+
if ("check" in schema._zod.def) {
|
|
341
|
+
checks.unshift(schema as z4.$ZodCheck);
|
|
342
|
+
}
|
|
343
|
+
return checks;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function parseChecks(
|
|
347
|
+
name: string,
|
|
348
|
+
checks: z4.$ZodCheck[],
|
|
349
|
+
context: ZodSurrealTypeContext,
|
|
350
|
+
type: ZodTypeName | SurrealZodTypeName,
|
|
351
|
+
) {
|
|
352
|
+
for (const check of checks) {
|
|
353
|
+
const { transform, assert } = parseCheck(name, check, type);
|
|
354
|
+
if (transform) {
|
|
355
|
+
context.transforms.push(transform);
|
|
356
|
+
}
|
|
357
|
+
if (assert) {
|
|
358
|
+
context.asserts.push(assert);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
export const checkMap = {
|
|
364
|
+
min_length(name: string, value: number, type: ZodTypeName) {
|
|
365
|
+
if (type === "array") {
|
|
366
|
+
return `$value.len() >= ${value} || { THROW 'Field "${name}" must have at least ${value} ${value === 1 ? "item" : "items"}' };`;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (type === "string") {
|
|
370
|
+
return `$value.len() >= ${value} || { THROW 'Field "${name}" must be at least ${value} ${value === 1 ? "character" : "characters"} long' };`;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
throw new Error(`Invalid type: ${type}`);
|
|
374
|
+
},
|
|
375
|
+
max_length(name: string, value: number, type: ZodTypeName) {
|
|
376
|
+
if (type === "array") {
|
|
377
|
+
return `$value.len() <= ${value} || { THROW 'Field "${name}" must have at most ${value} ${value === 1 ? "item" : "items"}' };`;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (type === "string") {
|
|
381
|
+
return `$value.len() <= ${value} || { THROW 'Field "${name}" must be at most ${value} ${value === 1 ? "character" : "characters"} long' };`;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
throw new Error(`Invalid type: ${type}`);
|
|
385
|
+
},
|
|
386
|
+
greater_than(name: string, value: z4.util.Numeric, inclusive: boolean) {
|
|
387
|
+
return `$value ${inclusive ? ">=" : ">"} ${value} || { THROW 'Field "${name}" must be greater than ${inclusive ? "or equal to" : ""} ${value}' };`;
|
|
388
|
+
},
|
|
389
|
+
less_than(name: string, value: z4.util.Numeric, inclusive: boolean) {
|
|
390
|
+
return `$value ${inclusive ? "<=" : "<"} ${value} || { THROW 'Field "${name}" must be less than ${inclusive ? "or equal to" : ""} ${value}' };`;
|
|
391
|
+
},
|
|
392
|
+
length_equals(name: string, value: number, type: ZodTypeName = "string") {
|
|
393
|
+
if (type === "array") {
|
|
394
|
+
return `$value.len() == ${value} || { THROW 'Field "${name}" must have exactly ${value} ${value === 1 ? "item" : "items"}' };`;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (type === "string") {
|
|
398
|
+
return `$value.len() == ${value} || { THROW 'Field "${name}" must be exactly ${value} ${value === 1 ? "character" : "characters"} long' };`;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
throw new Error(`Invalid type: ${type}`);
|
|
402
|
+
},
|
|
403
|
+
|
|
404
|
+
string_format: {
|
|
405
|
+
email: (name: string) => {
|
|
406
|
+
const regex =
|
|
407
|
+
/^[A-Za-z0-9'_+-]+(?:\.[A-Za-z0-9'_+-]+)*@([A-Za-z0-9][A-Za-z0-9\-]*\.)+[A-Za-z]{2,}$/;
|
|
408
|
+
return `string::matches($value, ${regex}) || { THROW "Field '${name}' must be a valid email address" };`;
|
|
409
|
+
},
|
|
410
|
+
url: (
|
|
411
|
+
name: string,
|
|
412
|
+
def?: Pick<z4.$ZodCheckURLParams, "hostname" | "protocol" | "normalize">,
|
|
413
|
+
) => {
|
|
414
|
+
return dedent`
|
|
415
|
+
LET $url = {
|
|
416
|
+
scheme: parse::url::scheme($value),
|
|
417
|
+
host: parse::url::host($value),
|
|
418
|
+
domain: parse::url::domain($value),
|
|
419
|
+
path: parse::url::path($value),
|
|
420
|
+
port: parse::url::port($value),
|
|
421
|
+
query: parse::url::query($value),
|
|
422
|
+
hash: parse::url::fragment($value),
|
|
423
|
+
};
|
|
424
|
+
$url.scheme || { THROW "Field '${name}' must be a valid URL" };
|
|
425
|
+
${
|
|
426
|
+
def?.hostname
|
|
427
|
+
? `($url.host ?? "").matches(${def.hostname}) || { THROW "Field '${name}' must match hostname ${def.hostname.toString().replace(/\\/g, "\\\\")}" };`
|
|
428
|
+
: ""
|
|
429
|
+
}
|
|
430
|
+
${
|
|
431
|
+
def?.protocol
|
|
432
|
+
? `($url.scheme ?? "").matches(${def.protocol}) || { THROW "Field '${name}' must match protocol ${def.protocol.toString().replace(/\\/g, "\\\\")}" };`
|
|
433
|
+
: ""
|
|
434
|
+
}
|
|
435
|
+
$url.scheme + "://" + ($url.host ?? "") + (
|
|
436
|
+
IF $url.port && (
|
|
437
|
+
($url.scheme == "http" && $url.port != 80) ||
|
|
438
|
+
($url.scheme == "https" && $url.port != 443)
|
|
439
|
+
) { ":" + <string>$url.port } ?? ""
|
|
440
|
+
)
|
|
441
|
+
+ ($url.path ?? "")
|
|
442
|
+
+ (IF $url.query { "?" + $url.query } ?? "")
|
|
443
|
+
+ (IF $url.fragment { "#" + $url.fragment } ?? "");
|
|
444
|
+
`;
|
|
445
|
+
},
|
|
446
|
+
},
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
function parseCheck(
|
|
450
|
+
name: string,
|
|
451
|
+
_check: z4.$ZodCheck,
|
|
452
|
+
type: ZodTypeName,
|
|
453
|
+
): { transform?: string; assert?: string } {
|
|
454
|
+
const check = _check as z4.$ZodChecks;
|
|
455
|
+
const def = check._zod.def;
|
|
456
|
+
switch (def.check) {
|
|
457
|
+
case "min_length":
|
|
458
|
+
return { assert: checkMap.min_length(name, def.minimum, type) };
|
|
459
|
+
case "max_length":
|
|
460
|
+
return { assert: checkMap.max_length(name, def.maximum, type) };
|
|
461
|
+
case "greater_than":
|
|
462
|
+
return { assert: checkMap.greater_than(name, def.value, def.inclusive) };
|
|
463
|
+
case "less_than":
|
|
464
|
+
return { assert: checkMap.less_than(name, def.value, def.inclusive) };
|
|
465
|
+
case "length_equals":
|
|
466
|
+
return { assert: checkMap.length_equals(name, def.length, type) };
|
|
467
|
+
case "string_format":
|
|
468
|
+
return assertionForStringFormat(name, check);
|
|
469
|
+
default:
|
|
470
|
+
return { assert: `THROW 'Unknown check: ${def.check}';` };
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Remove look-around, look-behind, and look-ahead as they are not supported by SurrealDB
|
|
475
|
+
function assertionForStringFormat(
|
|
476
|
+
name: string,
|
|
477
|
+
_check: z4.$ZodCheck,
|
|
478
|
+
): { transform?: string; assert?: string } {
|
|
479
|
+
const check = _check as z4.$ZodStringFormatChecks;
|
|
480
|
+
const def = check._zod.def;
|
|
481
|
+
|
|
482
|
+
switch (def.format) {
|
|
483
|
+
case "email": {
|
|
484
|
+
return { assert: checkMap.string_format.email(name) };
|
|
485
|
+
}
|
|
486
|
+
case "url": {
|
|
487
|
+
const code = checkMap.string_format.url(name, def);
|
|
488
|
+
return def.normalize ? { transform: code } : { assert: code };
|
|
489
|
+
}
|
|
490
|
+
default:
|
|
491
|
+
return { assert: `THROW 'Unsupported string format: ${def.format}';` };
|
|
492
|
+
}
|
|
493
|
+
}
|