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/README.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# surreal-zod
|
|
2
|
+
|
|
3
|
+
To install dependencies:
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
bun install
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
To run:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
bun run index.ts
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
This project was created using `bun init` in bun v1.3.0. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
|
package/lib/index.js
ADDED
package/lib/print.js
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
export function zodToSexpr(schema) {
|
|
2
|
+
if ("_zod" in schema) {
|
|
3
|
+
return zod4ToSexpr(schema);
|
|
4
|
+
}
|
|
5
|
+
throw new Error("Zod 3 not yet supported");
|
|
6
|
+
}
|
|
7
|
+
function zod4ToSexpr(_schema, depth = 0) {
|
|
8
|
+
const indent = " ".repeat(depth);
|
|
9
|
+
const childIndent = " ".repeat(depth + 1);
|
|
10
|
+
const schema = _schema;
|
|
11
|
+
const def = schema._zod.def;
|
|
12
|
+
const checks = def.checks ?? [];
|
|
13
|
+
if ("check" in def) {
|
|
14
|
+
checks.push(schema);
|
|
15
|
+
}
|
|
16
|
+
const type = def.type;
|
|
17
|
+
if (type === "object") {
|
|
18
|
+
return `(object)`;
|
|
19
|
+
}
|
|
20
|
+
// Primitives
|
|
21
|
+
switch (type) {
|
|
22
|
+
case "string": {
|
|
23
|
+
const constraints = formatChecks(checks);
|
|
24
|
+
return constraints.length > 0 ? `(string [${constraints}])` : `(string)`;
|
|
25
|
+
}
|
|
26
|
+
case "number": {
|
|
27
|
+
// const constraints = formatChecks(checks);
|
|
28
|
+
// return constraints.length > 0 ? `(number [${constraints}])` : `(number)`;
|
|
29
|
+
return `(number)`;
|
|
30
|
+
}
|
|
31
|
+
case "optional": {
|
|
32
|
+
const inner = zod4ToSexpr(def.innerType, depth + 1);
|
|
33
|
+
return `(optional ${inner})`;
|
|
34
|
+
}
|
|
35
|
+
case "nullable": {
|
|
36
|
+
const inner = zod4ToSexpr(def.innerType, depth + 1);
|
|
37
|
+
return `(nullable ${inner})`;
|
|
38
|
+
}
|
|
39
|
+
case "nonoptional": {
|
|
40
|
+
const inner = zod4ToSexpr(def.innerType, depth + 1);
|
|
41
|
+
return `(nonoptional ${inner})`;
|
|
42
|
+
}
|
|
43
|
+
case "array": {
|
|
44
|
+
const inner = zod4ToSexpr(def.element, depth + 1);
|
|
45
|
+
return `(array ${inner})`;
|
|
46
|
+
}
|
|
47
|
+
case "bigint": {
|
|
48
|
+
return `(bigint)`;
|
|
49
|
+
}
|
|
50
|
+
case "boolean": {
|
|
51
|
+
return `(boolean)`;
|
|
52
|
+
}
|
|
53
|
+
case "symbol": {
|
|
54
|
+
return `(symbol)`;
|
|
55
|
+
}
|
|
56
|
+
case "undefined": {
|
|
57
|
+
return `(undefined)`;
|
|
58
|
+
}
|
|
59
|
+
case "null": {
|
|
60
|
+
return `(null)`;
|
|
61
|
+
}
|
|
62
|
+
case "any": {
|
|
63
|
+
return `(any)`;
|
|
64
|
+
}
|
|
65
|
+
default: {
|
|
66
|
+
return `(unknown-type ${type || "?"})`;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// console.log("unknown type", def);
|
|
70
|
+
// if (type === "number") {
|
|
71
|
+
// const constraints = formatChecks(checks);
|
|
72
|
+
// return constraints.length > 0 ? `(number [${constraints}])` : `(number)`;
|
|
73
|
+
// }
|
|
74
|
+
}
|
|
75
|
+
function breakLine(sexpr, depth) {
|
|
76
|
+
// return sexpr.length > 80
|
|
77
|
+
// ? `\n${" ".repeat(depth)}${sexpr}\n${" ".repeat(depth)}`
|
|
78
|
+
// : sexpr;
|
|
79
|
+
return sexpr;
|
|
80
|
+
}
|
|
81
|
+
function formatChecks(checks) {
|
|
82
|
+
return checks.map((check) => formatCheck(check)).join(" ");
|
|
83
|
+
}
|
|
84
|
+
function formatCheck(_check) {
|
|
85
|
+
const check = _check;
|
|
86
|
+
const def = check._zod.def;
|
|
87
|
+
switch (def.check) {
|
|
88
|
+
case "min_length":
|
|
89
|
+
return `min:${def.minimum}`;
|
|
90
|
+
case "max_length":
|
|
91
|
+
return `max:${def.maximum}`;
|
|
92
|
+
case "length_equals":
|
|
93
|
+
return `length:${def.length}`;
|
|
94
|
+
case "string_format":
|
|
95
|
+
return parseStringFormat(check);
|
|
96
|
+
// case "bigint_format":
|
|
97
|
+
// return `bigint_format=${def.format}`;
|
|
98
|
+
// case "number_format":
|
|
99
|
+
// return `number_format=${def.format}`;
|
|
100
|
+
case "greater_than":
|
|
101
|
+
return `${def.inclusive ? ">=" : ">"}${def.value}`;
|
|
102
|
+
case "less_than":
|
|
103
|
+
return `${def.inclusive ? "<=" : "<"}${def.value}`;
|
|
104
|
+
// case "max_size":
|
|
105
|
+
// return `max_size=${def.maximum}`;
|
|
106
|
+
// case "min_size":
|
|
107
|
+
// return `min_size=${def.minimum}`;
|
|
108
|
+
// case "mime_type":
|
|
109
|
+
// return `mime_type=${def.mime}`;
|
|
110
|
+
// case "multiple_of":
|
|
111
|
+
// return `multiple_of=${def.value}`;
|
|
112
|
+
// case "size_equals":
|
|
113
|
+
// return `size_equals=${def.size}`;
|
|
114
|
+
// case undefined: {
|
|
115
|
+
// return `[unknown ${inspect(def, { colors: true })}]`;
|
|
116
|
+
// }
|
|
117
|
+
default:
|
|
118
|
+
return `[${def.check} ?]`;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function parseStringFormat(_check) {
|
|
122
|
+
const check = _check;
|
|
123
|
+
const def = check._zod.def;
|
|
124
|
+
const coerce = "coerce" in def ? `coerce:${def.coerce} ` : "";
|
|
125
|
+
if (def.format === "starts_with") {
|
|
126
|
+
return `starts_with:"${def.prefix}"`;
|
|
127
|
+
}
|
|
128
|
+
else if (def.format === "ends_with") {
|
|
129
|
+
return `ends_with:"${def.suffix}"`;
|
|
130
|
+
}
|
|
131
|
+
else if (def.format === "includes") {
|
|
132
|
+
return `includes:"${def.includes}"`;
|
|
133
|
+
}
|
|
134
|
+
else if (def.format === "regex") {
|
|
135
|
+
return `regex:${def.pattern}`;
|
|
136
|
+
}
|
|
137
|
+
else if (def.format === "uuid") {
|
|
138
|
+
return `format:uuid${def.version || ""}`;
|
|
139
|
+
}
|
|
140
|
+
else if (def.format === "xid") {
|
|
141
|
+
return `format:xid`;
|
|
142
|
+
}
|
|
143
|
+
else if (def.format === "url") {
|
|
144
|
+
const opts = [
|
|
145
|
+
def.normalize ? `normalize` : null,
|
|
146
|
+
def.hostname ? `hostname:${def.hostname}` : null,
|
|
147
|
+
def.protocol ? `protocol:${def.protocol}` : null,
|
|
148
|
+
]
|
|
149
|
+
.filter(Boolean)
|
|
150
|
+
.join(", ");
|
|
151
|
+
return `format:url${opts.length > 0 ? `(${opts})` : ""}`;
|
|
152
|
+
}
|
|
153
|
+
return `${coerce}format:${def.format}`;
|
|
154
|
+
}
|
|
155
|
+
function zodObjectToSexpr(schema, level = 0) {
|
|
156
|
+
if (!("_zod" in schema)) {
|
|
157
|
+
throw new Error("Invalid schema provided, make sure you are using zod v4 as zod v3 is currently not supported.");
|
|
158
|
+
}
|
|
159
|
+
const def = schema._zod.def;
|
|
160
|
+
let sexpr = `(${def.type}${def.checks ? ` ${formatChecks(def.checks)}` : ""}`;
|
|
161
|
+
for (const [propName, propSchema] of Object.entries(def.shape)) {
|
|
162
|
+
sexpr += `${zod4ToSexpr(propSchema, level + 1)}\n`;
|
|
163
|
+
}
|
|
164
|
+
sexpr += ")";
|
|
165
|
+
return sexpr;
|
|
166
|
+
}
|
package/lib/surql.js
ADDED
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
import { BoundQuery, escapeIdent, escapeIdPart, RecordId, surql, Table, } from "surrealdb";
|
|
2
|
+
import * as z4 from "zod/v4/core";
|
|
3
|
+
import z from "zod";
|
|
4
|
+
import dedent from "dedent";
|
|
5
|
+
export function zodToSurql(options) {
|
|
6
|
+
const table = typeof options.table === "string"
|
|
7
|
+
? new Table(options.table)
|
|
8
|
+
: options.table;
|
|
9
|
+
const schema = options.schema;
|
|
10
|
+
if (!("_zod" in schema)) {
|
|
11
|
+
throw new Error("Invalid schema provided, make sure you are using zod v4 as zod v3 is currently not supported.");
|
|
12
|
+
}
|
|
13
|
+
const def = schema._zod.def;
|
|
14
|
+
const shape = def.shape;
|
|
15
|
+
const query = defineTable(options);
|
|
16
|
+
for (const [key, value] of Object.entries(shape)) {
|
|
17
|
+
query.append(defineField({ name: key, table, type: value, exists: options.exists }));
|
|
18
|
+
}
|
|
19
|
+
// @ts-expect-error - extend is not a method of z4.$ZodObject
|
|
20
|
+
return [schema.extend({ id: z.any() }), query];
|
|
21
|
+
}
|
|
22
|
+
function defineTable(options) {
|
|
23
|
+
const table = typeof options.table === "string"
|
|
24
|
+
? new Table(options.table)
|
|
25
|
+
: options.table;
|
|
26
|
+
const query = surql `DEFINE TABLE`;
|
|
27
|
+
if (options.exists === "ignore") {
|
|
28
|
+
query.append(" IF NOT EXISTS");
|
|
29
|
+
}
|
|
30
|
+
else if (options.exists === "overwrite") {
|
|
31
|
+
query.append(" OVERWRITE");
|
|
32
|
+
}
|
|
33
|
+
// Looks like passing Table instance is not supported yet
|
|
34
|
+
query.append(` ${escapeIdPart(table.name)}`);
|
|
35
|
+
if (options.drop) {
|
|
36
|
+
query.append(" DROP");
|
|
37
|
+
}
|
|
38
|
+
if (options.schemafull) {
|
|
39
|
+
query.append(" SCHEMAFULL");
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
query.append(" SCHEMALESS");
|
|
43
|
+
}
|
|
44
|
+
if (options.comment) {
|
|
45
|
+
query.append(surql ` COMMENT ${options.comment}`);
|
|
46
|
+
}
|
|
47
|
+
query.append(";\n");
|
|
48
|
+
return query;
|
|
49
|
+
}
|
|
50
|
+
function defineField(options) {
|
|
51
|
+
const name = options.name;
|
|
52
|
+
const table = typeof options.table === "string"
|
|
53
|
+
? new Table(options.table)
|
|
54
|
+
: options.table;
|
|
55
|
+
const schema = options.type;
|
|
56
|
+
if (!("_zod" in schema)) {
|
|
57
|
+
throw new Error("Invalid field schema provided, make sure you are using zod v4 as zod v3 is currently not supported.");
|
|
58
|
+
}
|
|
59
|
+
const context = {
|
|
60
|
+
name,
|
|
61
|
+
table,
|
|
62
|
+
rootSchema: schema,
|
|
63
|
+
children: [],
|
|
64
|
+
asserts: [],
|
|
65
|
+
transforms: [],
|
|
66
|
+
};
|
|
67
|
+
const query = surql `DEFINE FIELD`;
|
|
68
|
+
if (options.exists === "ignore") {
|
|
69
|
+
query.append(" IF NOT EXISTS");
|
|
70
|
+
}
|
|
71
|
+
else if (options.exists === "overwrite") {
|
|
72
|
+
query.append(" OVERWRITE");
|
|
73
|
+
}
|
|
74
|
+
query.append(` ${name} ON TABLE ${table.name}`);
|
|
75
|
+
const type = zodTypeToSurrealType(schema, [], context);
|
|
76
|
+
query.append(` TYPE ${type}`);
|
|
77
|
+
if (context.default) {
|
|
78
|
+
query.append(context.default.always
|
|
79
|
+
? ` DEFAULT ALWAYS ${JSON.stringify(context.default.value)}`
|
|
80
|
+
: ` DEFAULT ${JSON.stringify(context.default.value)}`);
|
|
81
|
+
}
|
|
82
|
+
if (context.transforms.length > 0) {
|
|
83
|
+
query.append(` VALUE {\n`);
|
|
84
|
+
for (const transform of context.transforms) {
|
|
85
|
+
query.append(dedent.withOptions({ alignValues: true }) `
|
|
86
|
+
//
|
|
87
|
+
${transform}\n`.slice(3));
|
|
88
|
+
}
|
|
89
|
+
query.append(`}`);
|
|
90
|
+
}
|
|
91
|
+
if (context.asserts.length > 0) {
|
|
92
|
+
query.append(` ASSERT {\n`);
|
|
93
|
+
for (const assert of context.asserts) {
|
|
94
|
+
query.append(dedent.withOptions({ alignValues: true }) `
|
|
95
|
+
//
|
|
96
|
+
${assert}\n`.slice(3));
|
|
97
|
+
}
|
|
98
|
+
query.append(`}`);
|
|
99
|
+
}
|
|
100
|
+
query.append(`;\n`);
|
|
101
|
+
if (context.children.length > 0) {
|
|
102
|
+
for (const { name: childName, type: childType } of context.children) {
|
|
103
|
+
query.append(defineField({
|
|
104
|
+
name: `${name}.${childName}`,
|
|
105
|
+
table,
|
|
106
|
+
type: childType,
|
|
107
|
+
exists: options.exists,
|
|
108
|
+
}));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return query;
|
|
112
|
+
}
|
|
113
|
+
export function zodTypeToSurrealType(type, parents = [], context) {
|
|
114
|
+
const schema = type;
|
|
115
|
+
if (!("_zod" in schema)) {
|
|
116
|
+
throw new Error("Invalid schema provided, make sure you are using zod v4 as zod v3 is currently not supported.");
|
|
117
|
+
}
|
|
118
|
+
const def = schema._zod.def;
|
|
119
|
+
const checks = getChecks(schema);
|
|
120
|
+
parseChecks(context.name, checks, context, def.type);
|
|
121
|
+
// console.log(zodToSexpr(type));
|
|
122
|
+
switch (def.type) {
|
|
123
|
+
case "string":
|
|
124
|
+
return "string";
|
|
125
|
+
case "boolean":
|
|
126
|
+
return "bool";
|
|
127
|
+
case "object": {
|
|
128
|
+
const isInArray = context.rootSchema._zod.def.type === "array";
|
|
129
|
+
// TODO: remove any
|
|
130
|
+
for (const [key, value] of Object.entries(def.shape)) {
|
|
131
|
+
context.children.push({
|
|
132
|
+
name: isInArray ? `*.${key}` : key,
|
|
133
|
+
// TODO: remove as
|
|
134
|
+
type: value,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
return "object";
|
|
138
|
+
}
|
|
139
|
+
case "number":
|
|
140
|
+
return "number";
|
|
141
|
+
case "null":
|
|
142
|
+
return "NULL";
|
|
143
|
+
// case "bigint":
|
|
144
|
+
// return "bigint";
|
|
145
|
+
// case "symbol":
|
|
146
|
+
// return "symbol";
|
|
147
|
+
case "any": {
|
|
148
|
+
//===============================
|
|
149
|
+
// Surreal Specific Types
|
|
150
|
+
//===============================
|
|
151
|
+
if ("surrealType" in def) {
|
|
152
|
+
if (def.surrealType === "record_id") {
|
|
153
|
+
if (def.what) {
|
|
154
|
+
return `record<${Object.keys(def.what).join(" | ")}>`;
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
return "record";
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return "any";
|
|
162
|
+
}
|
|
163
|
+
case "undefined": {
|
|
164
|
+
return "NONE";
|
|
165
|
+
}
|
|
166
|
+
case "default": {
|
|
167
|
+
// if (typeof def.defaultValue === "function") {
|
|
168
|
+
// context.default = { value: def.defaultValue(), always: false };
|
|
169
|
+
// } else {
|
|
170
|
+
// console.log(
|
|
171
|
+
// "default",
|
|
172
|
+
// Object.getOwnPropertyDescriptor(def, "defaultValue").get?.toString(),
|
|
173
|
+
// );
|
|
174
|
+
// TODO: remove any
|
|
175
|
+
context.default = { value: def.defaultValue, always: false };
|
|
176
|
+
// }
|
|
177
|
+
return zodTypeToSurrealType(
|
|
178
|
+
// TODO: remove any
|
|
179
|
+
def.innerType, [...parents, def.type], context);
|
|
180
|
+
}
|
|
181
|
+
case "nullable": {
|
|
182
|
+
const inner = zodTypeToSurrealType(
|
|
183
|
+
// TODO: remove any
|
|
184
|
+
def.innerType, [...parents, def.type], context);
|
|
185
|
+
if (parents.includes("nullable")) {
|
|
186
|
+
return inner;
|
|
187
|
+
}
|
|
188
|
+
return `${inner} | NULL`;
|
|
189
|
+
}
|
|
190
|
+
case "optional": {
|
|
191
|
+
const inner = zodTypeToSurrealType(
|
|
192
|
+
// TODO: remove any
|
|
193
|
+
def.innerType, [...parents, def.type], context);
|
|
194
|
+
if (parents.includes("optional") || parents.includes("nonoptional")) {
|
|
195
|
+
return inner;
|
|
196
|
+
}
|
|
197
|
+
return `option<${inner}>`;
|
|
198
|
+
}
|
|
199
|
+
case "nonoptional": {
|
|
200
|
+
// just a marker for children optional to skip the option<...> wrapper
|
|
201
|
+
return zodTypeToSurrealType(
|
|
202
|
+
// TODO: remove any
|
|
203
|
+
def.innerType, [...parents, def.type], context);
|
|
204
|
+
}
|
|
205
|
+
case "union": {
|
|
206
|
+
// TODO: remove any
|
|
207
|
+
return (def.options
|
|
208
|
+
// TODO: remove any
|
|
209
|
+
.map((option) => zodTypeToSurrealType(option, [...parents, def.type], context))
|
|
210
|
+
.join(" | "));
|
|
211
|
+
}
|
|
212
|
+
case "array": {
|
|
213
|
+
const inner = zodTypeToSurrealType(
|
|
214
|
+
// TODO: remove any
|
|
215
|
+
def.element, [...parents, def.type], context);
|
|
216
|
+
return `array<${inner}>`;
|
|
217
|
+
}
|
|
218
|
+
case "custom": {
|
|
219
|
+
return "any";
|
|
220
|
+
}
|
|
221
|
+
default: {
|
|
222
|
+
console.log("unknown type", def.type);
|
|
223
|
+
return "any";
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
function getChecks(_schema) {
|
|
228
|
+
const schema = _schema;
|
|
229
|
+
const checks = schema._zod.def.checks ?? [];
|
|
230
|
+
if ("check" in schema._zod.def) {
|
|
231
|
+
checks.unshift(schema);
|
|
232
|
+
}
|
|
233
|
+
return checks;
|
|
234
|
+
}
|
|
235
|
+
function parseChecks(name, checks, context, type) {
|
|
236
|
+
for (const check of checks) {
|
|
237
|
+
const { transform, assert } = parseCheck(name, check, type);
|
|
238
|
+
if (transform) {
|
|
239
|
+
context.transforms.push(transform);
|
|
240
|
+
}
|
|
241
|
+
if (assert) {
|
|
242
|
+
context.asserts.push(assert);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
export const checkMap = {
|
|
247
|
+
min_length(name, value, type) {
|
|
248
|
+
if (type === "array") {
|
|
249
|
+
return `$value.len() >= ${value} || { THROW 'Field "${name}" must have at least ${value} ${value === 1 ? "item" : "items"}' };`;
|
|
250
|
+
}
|
|
251
|
+
if (type === "string") {
|
|
252
|
+
return `$value.len() >= ${value} || { THROW 'Field "${name}" must be at least ${value} ${value === 1 ? "character" : "characters"} long' };`;
|
|
253
|
+
}
|
|
254
|
+
throw new Error(`Invalid type: ${type}`);
|
|
255
|
+
},
|
|
256
|
+
max_length(name, value, type) {
|
|
257
|
+
if (type === "array") {
|
|
258
|
+
return `$value.len() <= ${value} || { THROW 'Field "${name}" must have at most ${value} ${value === 1 ? "item" : "items"}' };`;
|
|
259
|
+
}
|
|
260
|
+
if (type === "string") {
|
|
261
|
+
return `$value.len() <= ${value} || { THROW 'Field "${name}" must be at most ${value} ${value === 1 ? "character" : "characters"} long' };`;
|
|
262
|
+
}
|
|
263
|
+
throw new Error(`Invalid type: ${type}`);
|
|
264
|
+
},
|
|
265
|
+
greater_than(name, value, inclusive) {
|
|
266
|
+
return `$value ${inclusive ? ">=" : ">"} ${value} || { THROW 'Field "${name}" must be greater than ${inclusive ? "or equal to" : ""} ${value}' };`;
|
|
267
|
+
},
|
|
268
|
+
less_than(name, value, inclusive) {
|
|
269
|
+
return `$value ${inclusive ? "<=" : "<"} ${value} || { THROW 'Field "${name}" must be less than ${inclusive ? "or equal to" : ""} ${value}' };`;
|
|
270
|
+
},
|
|
271
|
+
length_equals(name, value, type = "string") {
|
|
272
|
+
if (type === "array") {
|
|
273
|
+
return `$value.len() == ${value} || { THROW 'Field "${name}" must have exactly ${value} ${value === 1 ? "item" : "items"}' };`;
|
|
274
|
+
}
|
|
275
|
+
if (type === "string") {
|
|
276
|
+
return `$value.len() == ${value} || { THROW 'Field "${name}" must be exactly ${value} ${value === 1 ? "character" : "characters"} long' };`;
|
|
277
|
+
}
|
|
278
|
+
throw new Error(`Invalid type: ${type}`);
|
|
279
|
+
},
|
|
280
|
+
string_format: {
|
|
281
|
+
email: (name) => {
|
|
282
|
+
const regex = /^[A-Za-z0-9'_+-]+(?:\.[A-Za-z0-9'_+-]+)*@([A-Za-z0-9][A-Za-z0-9\-]*\.)+[A-Za-z]{2,}$/;
|
|
283
|
+
return `string::matches($value, ${regex}) || { THROW "Field '${name}' must be a valid email address" };`;
|
|
284
|
+
},
|
|
285
|
+
url: (name, def) => {
|
|
286
|
+
return dedent `
|
|
287
|
+
LET $url = {
|
|
288
|
+
scheme: parse::url::scheme($value),
|
|
289
|
+
host: parse::url::host($value),
|
|
290
|
+
domain: parse::url::domain($value),
|
|
291
|
+
path: parse::url::path($value),
|
|
292
|
+
port: parse::url::port($value),
|
|
293
|
+
query: parse::url::query($value),
|
|
294
|
+
hash: parse::url::fragment($value),
|
|
295
|
+
};
|
|
296
|
+
$url.scheme || { THROW "Field '${name}' must be a valid URL" };
|
|
297
|
+
${def?.hostname
|
|
298
|
+
? `($url.host ?? "").matches(${def.hostname}) || { THROW "Field '${name}' must match hostname ${def.hostname.toString().replace(/\\/g, "\\\\")}" };`
|
|
299
|
+
: ""}
|
|
300
|
+
${def?.protocol
|
|
301
|
+
? `($url.scheme ?? "").matches(${def.protocol}) || { THROW "Field '${name}' must match protocol ${def.protocol.toString().replace(/\\/g, "\\\\")}" };`
|
|
302
|
+
: ""}
|
|
303
|
+
$url.scheme + "://" + ($url.host ?? "") + (
|
|
304
|
+
IF $url.port && (
|
|
305
|
+
($url.scheme == "http" && $url.port != 80) ||
|
|
306
|
+
($url.scheme == "https" && $url.port != 443)
|
|
307
|
+
) { ":" + <string>$url.port } ?? ""
|
|
308
|
+
)
|
|
309
|
+
+ ($url.path ?? "")
|
|
310
|
+
+ (IF $url.query { "?" + $url.query } ?? "")
|
|
311
|
+
+ (IF $url.fragment { "#" + $url.fragment } ?? "");
|
|
312
|
+
`;
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
};
|
|
316
|
+
function parseCheck(name, _check, type) {
|
|
317
|
+
const check = _check;
|
|
318
|
+
const def = check._zod.def;
|
|
319
|
+
switch (def.check) {
|
|
320
|
+
case "min_length":
|
|
321
|
+
return { assert: checkMap.min_length(name, def.minimum, type) };
|
|
322
|
+
case "max_length":
|
|
323
|
+
return { assert: checkMap.max_length(name, def.maximum, type) };
|
|
324
|
+
case "greater_than":
|
|
325
|
+
return { assert: checkMap.greater_than(name, def.value, def.inclusive) };
|
|
326
|
+
case "less_than":
|
|
327
|
+
return { assert: checkMap.less_than(name, def.value, def.inclusive) };
|
|
328
|
+
case "length_equals":
|
|
329
|
+
return { assert: checkMap.length_equals(name, def.length, type) };
|
|
330
|
+
case "string_format":
|
|
331
|
+
return assertionForStringFormat(name, check);
|
|
332
|
+
default:
|
|
333
|
+
return { assert: `THROW 'Unknown check: ${def.check}';` };
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
// Remove look-around, look-behind, and look-ahead as they are not supported by SurrealDB
|
|
337
|
+
function assertionForStringFormat(name, _check) {
|
|
338
|
+
const check = _check;
|
|
339
|
+
const def = check._zod.def;
|
|
340
|
+
switch (def.format) {
|
|
341
|
+
case "email": {
|
|
342
|
+
return { assert: checkMap.string_format.email(name) };
|
|
343
|
+
}
|
|
344
|
+
case "url": {
|
|
345
|
+
const code = checkMap.string_format.url(name, def);
|
|
346
|
+
return def.normalize ? { transform: code } : { assert: code };
|
|
347
|
+
}
|
|
348
|
+
default:
|
|
349
|
+
return { assert: `THROW 'Unsupported string format: ${def.format}';` };
|
|
350
|
+
}
|
|
351
|
+
}
|
package/lib/zod.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { RecordId } from "surrealdb";
|
|
2
|
+
import z4 from "zod/v4";
|
|
3
|
+
import * as core from "zod/v4/core";
|
|
4
|
+
export const SurrealZodType = core.$constructor("SurrealZodType", (inst, def) => {
|
|
5
|
+
// @ts-expect-error - unknown assertion error
|
|
6
|
+
core.$ZodType.init(inst, def);
|
|
7
|
+
inst._surreal = true;
|
|
8
|
+
return inst;
|
|
9
|
+
});
|
|
10
|
+
export const SurrealZodAny = core.$constructor("SurrealZodAny", (inst, def) => {
|
|
11
|
+
// @ts-expect-error - unknown assertion error
|
|
12
|
+
core.$ZodAny.init(inst, def);
|
|
13
|
+
SurrealZodType.init(inst, def);
|
|
14
|
+
});
|
|
15
|
+
export function any() {
|
|
16
|
+
return core._any(SurrealZodAny);
|
|
17
|
+
}
|
|
18
|
+
export const SurrealZodBoolean = core.$constructor("SurrealZodBoolean", (inst, def) => {
|
|
19
|
+
// @ts-expect-error - unknown assertion error
|
|
20
|
+
core.$ZodBoolean.init(inst, def);
|
|
21
|
+
SurrealZodType.init(inst, def);
|
|
22
|
+
// const originalDefault = inst.default;
|
|
23
|
+
// inst.default = (defaultValue?: any) => {
|
|
24
|
+
// if (typeof defaultValue === "function") {
|
|
25
|
+
// throw new TypeError(
|
|
26
|
+
// "Functions for default values are not supported in surreal-zod",
|
|
27
|
+
// );
|
|
28
|
+
// }
|
|
29
|
+
// return originalDefault(defaultValue);
|
|
30
|
+
// };
|
|
31
|
+
});
|
|
32
|
+
export function boolean(params) {
|
|
33
|
+
return core._boolean(SurrealZodBoolean, params);
|
|
34
|
+
}
|
|
35
|
+
export const SurrealZodString = core.$constructor("SurrealZodString", (inst, def) => {
|
|
36
|
+
// @ts-expect-error - unknown assertion error
|
|
37
|
+
core.$ZodString.init(inst, def);
|
|
38
|
+
SurrealZodType.init(inst, def);
|
|
39
|
+
});
|
|
40
|
+
export function string(params) {
|
|
41
|
+
return core._string(SurrealZodString, params);
|
|
42
|
+
}
|
|
43
|
+
export const SurrealZodObject = core.$constructor("SurrealZodObject", (inst, def) => {
|
|
44
|
+
// TODO: Inline implementation and use core instead
|
|
45
|
+
// @ts-expect-error - unknown assertion error
|
|
46
|
+
z4.ZodObject.init(inst, def);
|
|
47
|
+
SurrealZodType.init(inst, def);
|
|
48
|
+
});
|
|
49
|
+
export function object(shape, params) {
|
|
50
|
+
const def = {
|
|
51
|
+
type: "object",
|
|
52
|
+
shape: shape ?? {},
|
|
53
|
+
...core.util.normalizeParams(params),
|
|
54
|
+
};
|
|
55
|
+
return new SurrealZodObject(def);
|
|
56
|
+
}
|
|
57
|
+
export const SurrealZodRecordId = core.$constructor("SurrealZodRecordId", (inst, def) => {
|
|
58
|
+
// @ts-expect-error - unknown assertion error
|
|
59
|
+
core.$ZodAny.init(inst, def);
|
|
60
|
+
SurrealZodType.init(inst, def);
|
|
61
|
+
inst._surreal = true;
|
|
62
|
+
inst._zod.parse = (payload, _ctx) => {
|
|
63
|
+
if (payload.value instanceof RecordId) {
|
|
64
|
+
if (def.what) {
|
|
65
|
+
if (def.what[payload.value.table.name] === undefined) {
|
|
66
|
+
payload.issues.push({
|
|
67
|
+
code: "invalid_value",
|
|
68
|
+
values: Object.keys(def.what),
|
|
69
|
+
input: payload.value.table.name,
|
|
70
|
+
message: `Expected record table to be one of ${Object.keys(def.what).join(" | ")} but found ${payload.value.table.name}`,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// } else if (typeof payload.value === "string") {
|
|
75
|
+
// let tablePart = '';
|
|
76
|
+
// let idPart = '';
|
|
77
|
+
// let quote = '';
|
|
78
|
+
// for (let i = 0; i < payload.value.length; i++) {
|
|
79
|
+
// const char = payload.value[i];
|
|
80
|
+
// if (char === '`') {
|
|
81
|
+
// if (quote === '`') {
|
|
82
|
+
// tablePart = payload.value.slice(1, i);
|
|
83
|
+
// } else quote = '`';
|
|
84
|
+
// }
|
|
85
|
+
// if (char === ':') {
|
|
86
|
+
// tablePart = payload.value.slice(0, charIndex);
|
|
87
|
+
// idPart = payload.value.slice(tablePart.length + 1);
|
|
88
|
+
// break;
|
|
89
|
+
// }
|
|
90
|
+
// tablePart += char;
|
|
91
|
+
// }
|
|
92
|
+
// for (const char of payload.value.slice(tablePart.length + 1)) {
|
|
93
|
+
// idPart += char;
|
|
94
|
+
// }
|
|
95
|
+
// if (/^(`|\u27e8)/.test(payload.value)) {
|
|
96
|
+
// payload.value = new RecordId(payload.value.split(":")[0], payload.value.split(":")[1]);
|
|
97
|
+
// } else {
|
|
98
|
+
// payload.issues.push({
|
|
99
|
+
// code: 'invalid_format',
|
|
100
|
+
// format: 'record_id',
|
|
101
|
+
// message: 'Invalid record id format',
|
|
102
|
+
// });
|
|
103
|
+
// }
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
payload.issues.push({
|
|
107
|
+
code: "invalid_type",
|
|
108
|
+
expected: "custom",
|
|
109
|
+
input: payload.value,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
return payload;
|
|
113
|
+
};
|
|
114
|
+
return inst;
|
|
115
|
+
});
|
|
116
|
+
export function recordId(what, innerType) {
|
|
117
|
+
return new SurrealZodRecordId({
|
|
118
|
+
// Zod would not be happy if we have a custom type here, so we use any
|
|
119
|
+
type: "any",
|
|
120
|
+
surrealType: "record_id",
|
|
121
|
+
what: what
|
|
122
|
+
? Object.freeze(Object.fromEntries(what.map((v) => [v, v])))
|
|
123
|
+
: undefined,
|
|
124
|
+
innerType: innerType ?? any(),
|
|
125
|
+
});
|
|
126
|
+
}
|