true-pg 0.0.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/.github/workflows/releases.yml +29 -0
- package/README.md +133 -0
- package/package.json +26 -0
- package/src/bin.ts +81 -0
- package/src/consumer.ts +38 -0
- package/src/index.ts +185 -0
- package/src/kysely/builtins.ts +38 -0
- package/src/kysely/index.ts +315 -0
- package/src/types.ts +297 -0
- package/src/util.ts +1 -0
- package/src/zod/builtins.ts +38 -0
- package/src/zod/index.ts +301 -0
- package/tsconfig.json +28 -0
package/src/zod/index.ts
ADDED
@@ -0,0 +1,301 @@
|
|
1
|
+
import type { CanonicalType, Schema, TableColumn } from "pg-extract";
|
2
|
+
import { allowed_kind_names, createGenerator, Nodes, type SchemaGenerator } from "../types.ts";
|
3
|
+
import { builtins } from "./builtins.ts";
|
4
|
+
import { join } from "../util.ts";
|
5
|
+
|
6
|
+
const isIdentifierInvalid = (str: string) => {
|
7
|
+
const invalid = str.match(/[^a-zA-Z0-9_]/);
|
8
|
+
return invalid !== null;
|
9
|
+
};
|
10
|
+
|
11
|
+
const to_snake_case = (str: string) =>
|
12
|
+
str
|
13
|
+
.replace(/^[^a-zA-Z]+/, "") // remove leading non-alphabetic characters
|
14
|
+
.replace(/[^a-zA-Z0-9]+/g, "_") // replace non-alphanumeric characters with underscores
|
15
|
+
.replace(/([A-Z])/g, "_$1") // insert underscores before uppercase letters
|
16
|
+
.toLowerCase();
|
17
|
+
|
18
|
+
// TODO: create an insert and update zod interface for each type
|
19
|
+
export const Zod = createGenerator(opts => {
|
20
|
+
const defaultSchema = opts?.defaultSchema ?? "public";
|
21
|
+
|
22
|
+
const zod = (imports: Nodes.ImportList, name?: string) =>
|
23
|
+
imports.add(
|
24
|
+
new Nodes.ExternalImport({
|
25
|
+
name: name ?? "z",
|
26
|
+
module: "zod",
|
27
|
+
typeOnly: false,
|
28
|
+
star: !name,
|
29
|
+
}),
|
30
|
+
);
|
31
|
+
|
32
|
+
const add = (imports: Nodes.ImportList, type: CanonicalType) => {
|
33
|
+
if (type.schema === "pg_catalog") zod(imports, "z");
|
34
|
+
else
|
35
|
+
imports.add(
|
36
|
+
new Nodes.InternalImport({
|
37
|
+
name: generator.formatType(type),
|
38
|
+
canonical_type: type,
|
39
|
+
typeOnly: false,
|
40
|
+
star: false,
|
41
|
+
}),
|
42
|
+
);
|
43
|
+
};
|
44
|
+
|
45
|
+
const column = (
|
46
|
+
imports: Nodes.ImportList,
|
47
|
+
/** Information about the column */
|
48
|
+
col: TableColumn,
|
49
|
+
) => {
|
50
|
+
// don't create a property for always generated columns
|
51
|
+
if (col.generated === "ALWAYS") return "";
|
52
|
+
|
53
|
+
let out = col.comment ? `/** ${col.comment} */\n\t` : "";
|
54
|
+
out += col.name;
|
55
|
+
let type = generator.formatType(col.type);
|
56
|
+
add(imports, col.type);
|
57
|
+
if (col.type.dimensions > 0) type += ".array()".repeat(col.type.dimensions);
|
58
|
+
if (col.isNullable || col.generated === "BY DEFAULT" || col.defaultValue) type += `.nullable().optional()`;
|
59
|
+
out += `: ${type}`;
|
60
|
+
|
61
|
+
return `\t${out},\n`;
|
62
|
+
};
|
63
|
+
|
64
|
+
const composite_attribute = (imports: Nodes.ImportList, attr: CanonicalType.CompositeAttribute) => {
|
65
|
+
let out = attr.name;
|
66
|
+
out += `: ${generator.formatType(attr.type)}`;
|
67
|
+
add(imports, attr.type);
|
68
|
+
if (attr.type.dimensions > 0) out += ".array()".repeat(attr.type.dimensions);
|
69
|
+
if (attr.isNullable) out += ".nullable().optional()";
|
70
|
+
|
71
|
+
return out;
|
72
|
+
};
|
73
|
+
|
74
|
+
const generator: SchemaGenerator = {
|
75
|
+
formatSchema(name) {
|
76
|
+
return to_snake_case(name) + "_validators";
|
77
|
+
},
|
78
|
+
|
79
|
+
formatSchemaType(type) {
|
80
|
+
return to_snake_case(type.name);
|
81
|
+
},
|
82
|
+
|
83
|
+
formatType(type) {
|
84
|
+
if (type.schema === "pg_catalog") {
|
85
|
+
const name = type.canonical_name;
|
86
|
+
const format = builtins[name];
|
87
|
+
if (format) return format;
|
88
|
+
opts?.warnings?.push(
|
89
|
+
`Unknown builtin type: ${name}! Pass customBuiltinMap to map this type. Defaulting to "z.unknown()".`,
|
90
|
+
);
|
91
|
+
return "z.unknown()";
|
92
|
+
}
|
93
|
+
return to_snake_case(type.name);
|
94
|
+
},
|
95
|
+
|
96
|
+
table(imports, table) {
|
97
|
+
let out = "";
|
98
|
+
|
99
|
+
if (table.comment) out += `/** ${table.comment} */\n`;
|
100
|
+
out += `export const ${this.formatSchemaType(table)} = z.object({\n`;
|
101
|
+
zod(imports, "z");
|
102
|
+
for (const col of table.columns) out += column(imports, col);
|
103
|
+
out += "});";
|
104
|
+
|
105
|
+
return out;
|
106
|
+
},
|
107
|
+
|
108
|
+
enum(imports, en) {
|
109
|
+
let out = "";
|
110
|
+
|
111
|
+
if (en.comment) out += `/** ${en.comment} */\n`;
|
112
|
+
|
113
|
+
out += `export const ${this.formatSchemaType(en)} = z.union([\n`;
|
114
|
+
out += en.values.map(v => `\tz.literal("${v}")`).join(",\n");
|
115
|
+
out += "\n]);";
|
116
|
+
|
117
|
+
zod(imports, "z");
|
118
|
+
|
119
|
+
return out;
|
120
|
+
},
|
121
|
+
|
122
|
+
composite(imports, type) {
|
123
|
+
let out = "";
|
124
|
+
|
125
|
+
if (type.comment) out += `/** ${type.comment} */\n`;
|
126
|
+
out += `export const ${this.formatSchemaType(type)} = z.object({\n`;
|
127
|
+
|
128
|
+
const props = type.canonical.attributes.map(c => composite_attribute(imports, c)).map(t => `\t${t},`);
|
129
|
+
out += props.join("\n");
|
130
|
+
out += "\n});";
|
131
|
+
|
132
|
+
return out;
|
133
|
+
},
|
134
|
+
|
135
|
+
function(imports, type) {
|
136
|
+
let out = "export const ";
|
137
|
+
out += this.formatSchemaType(type);
|
138
|
+
out += " = {\n";
|
139
|
+
|
140
|
+
out += `\tparameters: z.tuple([`;
|
141
|
+
|
142
|
+
// Get the input parameters (those that appear in function signature)
|
143
|
+
const inputParams = type.parameters.filter(p => p.mode === "IN" || p.mode === "INOUT");
|
144
|
+
|
145
|
+
if (inputParams.length === 0) {
|
146
|
+
out += "])";
|
147
|
+
} else {
|
148
|
+
out += "\n";
|
149
|
+
|
150
|
+
for (const param of inputParams) {
|
151
|
+
// TODO: update imports for non-primitive types based on typeInfo.kind
|
152
|
+
out += "\t\t" + this.formatType(param.type);
|
153
|
+
add(imports, param.type);
|
154
|
+
if (param.type.dimensions > 0) out += ".array()".repeat(param.type.dimensions);
|
155
|
+
if (param.hasDefault) out += ".nullable().optional()";
|
156
|
+
out += `, // ${param.name}\n`;
|
157
|
+
}
|
158
|
+
|
159
|
+
out += "\t])";
|
160
|
+
}
|
161
|
+
|
162
|
+
const variadic = type.parameters.find(p => p.mode === "VARIADIC");
|
163
|
+
if (variadic) {
|
164
|
+
out += ".rest(";
|
165
|
+
out += this.formatType(variadic.type);
|
166
|
+
// reduce by 1 because it's already a rest parameter
|
167
|
+
if (variadic.type.dimensions > 1) out += ".array()".repeat(variadic.type.dimensions - 1);
|
168
|
+
out += ")" + ", // " + variadic.name + "\n";
|
169
|
+
} else out += ",\n";
|
170
|
+
|
171
|
+
out += "\treturnType: ";
|
172
|
+
|
173
|
+
if (type.returnType.kind === "table") {
|
174
|
+
out += "z.object({\n";
|
175
|
+
for (const col of type.returnType.columns) {
|
176
|
+
out += `\t\t${col.name}: `;
|
177
|
+
out += this.formatType(col.type);
|
178
|
+
add(imports, col.type);
|
179
|
+
if (col.type.dimensions > 0) out += ".array()".repeat(col.type.dimensions);
|
180
|
+
out += `,\n`;
|
181
|
+
}
|
182
|
+
out += "\t})";
|
183
|
+
} else {
|
184
|
+
out += this.formatType(type.returnType.type);
|
185
|
+
add(imports, type.returnType.type);
|
186
|
+
if (type.returnType.type.dimensions > 0) out += ".array()".repeat(type.returnType.type.dimensions);
|
187
|
+
}
|
188
|
+
|
189
|
+
// Add additional array brackets if it returns a set
|
190
|
+
if (type.returnType.isSet) out += ".array()";
|
191
|
+
out += ",\n};";
|
192
|
+
|
193
|
+
return out;
|
194
|
+
},
|
195
|
+
|
196
|
+
schemaKindIndex(schema, kind, main_generator) {
|
197
|
+
const imports = schema[kind];
|
198
|
+
if (imports.length === 0) return "";
|
199
|
+
const generator = main_generator ?? this;
|
200
|
+
|
201
|
+
return imports
|
202
|
+
.map(each => {
|
203
|
+
const name = this.formatSchemaType(each);
|
204
|
+
const file = generator.formatSchemaType(each);
|
205
|
+
return `export { ${name} } from "./${file}.ts";`;
|
206
|
+
})
|
207
|
+
.join("\n");
|
208
|
+
},
|
209
|
+
|
210
|
+
schemaIndex(schema, main_generator) {
|
211
|
+
let out = allowed_kind_names.map(kind => `import * as zod_${kind} from "./${kind}/index.ts";`).join("\n");
|
212
|
+
|
213
|
+
out += "\n\n";
|
214
|
+
out += `export const ${this.formatSchema(schema.name)} = {\n`;
|
215
|
+
|
216
|
+
for (const kind of allowed_kind_names) {
|
217
|
+
const items = schema[kind];
|
218
|
+
if (items.length === 0) continue;
|
219
|
+
|
220
|
+
out += `\t${kind}: {\n`;
|
221
|
+
|
222
|
+
const formatted = items
|
223
|
+
.map(each => {
|
224
|
+
const formatted = this.formatSchemaType(each);
|
225
|
+
return { ...each, formatted };
|
226
|
+
})
|
227
|
+
.filter(x => x !== undefined);
|
228
|
+
|
229
|
+
out += formatted
|
230
|
+
.map(t => {
|
231
|
+
let name = t.name;
|
232
|
+
if (isIdentifierInvalid(name)) name = `"${name}"`;
|
233
|
+
return `\t\t${name}: zod_${t.kind}s.${t.formatted},`;
|
234
|
+
})
|
235
|
+
.join("\n");
|
236
|
+
out += "\n\t},\n";
|
237
|
+
}
|
238
|
+
|
239
|
+
out += "}";
|
240
|
+
|
241
|
+
return out;
|
242
|
+
},
|
243
|
+
|
244
|
+
fullIndex(schemas: Schema[], main_generator) {
|
245
|
+
const generator = main_generator ?? this;
|
246
|
+
|
247
|
+
let out = "";
|
248
|
+
|
249
|
+
out += schemas
|
250
|
+
.map(s => `import { ${generator.formatSchema(s.name)} } from "./${s.name}/index.ts";`)
|
251
|
+
.join("\n");
|
252
|
+
|
253
|
+
out += "\n\n";
|
254
|
+
out += `export const Validators = {\n`;
|
255
|
+
out += join(
|
256
|
+
schemas.map(schema => {
|
257
|
+
const schema_validators = join(
|
258
|
+
allowed_kind_names.map(kind => {
|
259
|
+
const current = schema[kind];
|
260
|
+
|
261
|
+
const seen = new Set<string>();
|
262
|
+
const formatted = current
|
263
|
+
.map(each => {
|
264
|
+
const formatted = generator.formatSchemaType(each);
|
265
|
+
// skip clashing names
|
266
|
+
if (seen.has(formatted)) return;
|
267
|
+
seen.add(formatted);
|
268
|
+
return { ...each, formatted };
|
269
|
+
})
|
270
|
+
.filter(x => x !== undefined);
|
271
|
+
|
272
|
+
if (!formatted.length) return "";
|
273
|
+
|
274
|
+
let out = "";
|
275
|
+
out += "\t// " + kind + "\n";
|
276
|
+
out += join(
|
277
|
+
formatted.map(t => {
|
278
|
+
const prefix = defaultSchema === schema.name ? "" : schema.name + ".";
|
279
|
+
let qualified = prefix + t.name;
|
280
|
+
if (isIdentifierInvalid(qualified)) qualified = `"${qualified}"`;
|
281
|
+
return `\t${qualified}: ${this.formatSchema(schema.name)}["${t.kind}s"]["${t.name}"],`;
|
282
|
+
}),
|
283
|
+
"\n",
|
284
|
+
);
|
285
|
+
return out;
|
286
|
+
}),
|
287
|
+
);
|
288
|
+
return `\t/* -- ${schema.name} --*/\n\n` + schema_validators || "\t-- no validators\n\n";
|
289
|
+
}),
|
290
|
+
);
|
291
|
+
|
292
|
+
out += "\n}\n\n";
|
293
|
+
|
294
|
+
out += schemas.map(s => `export type { ${this.formatSchema(s.name)} };`).join("\n");
|
295
|
+
|
296
|
+
return out;
|
297
|
+
},
|
298
|
+
};
|
299
|
+
|
300
|
+
return generator;
|
301
|
+
});
|
package/tsconfig.json
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
{
|
2
|
+
"compilerOptions": {
|
3
|
+
// Environment setup & latest features
|
4
|
+
"lib": ["esnext"],
|
5
|
+
"target": "ESNext",
|
6
|
+
"module": "NodeNext",
|
7
|
+
"moduleDetection": "force",
|
8
|
+
|
9
|
+
// Bundler mode
|
10
|
+
"moduleResolution": "nodenext",
|
11
|
+
"allowImportingTsExtensions": true,
|
12
|
+
"rewriteRelativeImportExtensions": true,
|
13
|
+
"verbatimModuleSyntax": true,
|
14
|
+
|
15
|
+
// Best practices
|
16
|
+
"strict": true,
|
17
|
+
"skipLibCheck": true,
|
18
|
+
"noFallthroughCasesInSwitch": true,
|
19
|
+
"noUncheckedIndexedAccess": true,
|
20
|
+
|
21
|
+
// Some stricter flags (disabled by default)
|
22
|
+
"noUnusedLocals": false,
|
23
|
+
"noUnusedParameters": false,
|
24
|
+
"noPropertyAccessFromIndexSignature": false,
|
25
|
+
"outDir": "lib"
|
26
|
+
},
|
27
|
+
"include": ["src/**/*.ts", "models/**/*.ts"]
|
28
|
+
}
|