on-zero 0.1.39 → 0.1.41
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/dist/cjs/createUseQuery.cjs +3 -2
- package/dist/cjs/createUseQuery.js +2 -2
- package/dist/cjs/createUseQuery.js.map +1 -1
- package/dist/cjs/createUseQuery.native.js +3 -2
- package/dist/cjs/createUseQuery.native.js.map +1 -1
- package/dist/cjs/createZeroClient.cjs +28 -5
- package/dist/cjs/createZeroClient.js +19 -4
- package/dist/cjs/createZeroClient.js.map +1 -1
- package/dist/cjs/createZeroClient.native.js +29 -5
- package/dist/cjs/createZeroClient.native.js.map +1 -1
- package/dist/cjs/createZeroServer.cjs +5 -2
- package/dist/cjs/createZeroServer.js +5 -2
- package/dist/cjs/createZeroServer.js.map +1 -1
- package/dist/cjs/createZeroServer.native.js +5 -2
- package/dist/cjs/createZeroServer.native.js.map +1 -1
- package/dist/cjs/generate.cjs +458 -39
- package/dist/cjs/generate.js +485 -31
- package/dist/cjs/generate.js.map +2 -2
- package/dist/cjs/generate.native.js +812 -51
- package/dist/cjs/generate.native.js.map +1 -1
- package/dist/cjs/generate.test.cjs +251 -0
- package/dist/cjs/generate.test.js +252 -0
- package/dist/cjs/generate.test.js.map +1 -1
- package/dist/cjs/generate.test.native.js +251 -0
- package/dist/cjs/generate.test.native.js.map +1 -1
- package/dist/cjs/helpers/createMutators.cjs +21 -8
- package/dist/cjs/helpers/createMutators.js +16 -6
- package/dist/cjs/helpers/createMutators.js.map +1 -1
- package/dist/cjs/helpers/createMutators.native.js +28 -10
- package/dist/cjs/helpers/createMutators.native.js.map +1 -1
- package/dist/esm/createUseQuery.js +3 -3
- package/dist/esm/createUseQuery.js.map +1 -1
- package/dist/esm/createUseQuery.mjs +4 -3
- package/dist/esm/createUseQuery.mjs.map +1 -1
- package/dist/esm/createUseQuery.native.js +4 -3
- package/dist/esm/createUseQuery.native.js.map +1 -1
- package/dist/esm/createZeroClient.js +19 -4
- package/dist/esm/createZeroClient.js.map +1 -1
- package/dist/esm/createZeroClient.mjs +28 -5
- package/dist/esm/createZeroClient.mjs.map +1 -1
- package/dist/esm/createZeroClient.native.js +29 -5
- package/dist/esm/createZeroClient.native.js.map +1 -1
- package/dist/esm/createZeroServer.js +5 -2
- package/dist/esm/createZeroServer.js.map +1 -1
- package/dist/esm/createZeroServer.mjs +5 -2
- package/dist/esm/createZeroServer.mjs.map +1 -1
- package/dist/esm/createZeroServer.native.js +5 -2
- package/dist/esm/createZeroServer.native.js.map +1 -1
- package/dist/esm/generate.js +486 -32
- package/dist/esm/generate.js.map +2 -2
- package/dist/esm/generate.mjs +459 -40
- package/dist/esm/generate.mjs.map +1 -1
- package/dist/esm/generate.native.js +813 -52
- package/dist/esm/generate.native.js.map +1 -1
- package/dist/esm/generate.test.js +252 -0
- package/dist/esm/generate.test.js.map +1 -1
- package/dist/esm/generate.test.mjs +251 -0
- package/dist/esm/generate.test.mjs.map +1 -1
- package/dist/esm/generate.test.native.js +251 -0
- package/dist/esm/generate.test.native.js.map +1 -1
- package/dist/esm/helpers/createMutators.js +6 -4
- package/dist/esm/helpers/createMutators.js.map +1 -1
- package/dist/esm/helpers/createMutators.mjs +6 -4
- package/dist/esm/helpers/createMutators.mjs.map +1 -1
- package/dist/esm/helpers/createMutators.native.js +13 -6
- package/dist/esm/helpers/createMutators.native.js.map +1 -1
- package/package.json +2 -2
- package/readme.md +110 -2
- package/src/createUseQuery.tsx +15 -6
- package/src/createZeroClient.tsx +42 -6
- package/src/createZeroServer.ts +9 -0
- package/src/generate.test.ts +340 -0
- package/src/generate.ts +863 -43
- package/src/helpers/createMutators.ts +22 -8
- package/types/createUseQuery.d.ts +2 -1
- package/types/createUseQuery.d.ts.map +1 -1
- package/types/createZeroClient.d.ts +10 -1
- package/types/createZeroClient.d.ts.map +1 -1
- package/types/createZeroServer.d.ts +7 -1
- package/types/createZeroServer.d.ts.map +1 -1
- package/types/generate.d.ts +1 -0
- package/types/generate.d.ts.map +1 -1
- package/types/helpers/createMutators.d.ts +3 -1
- package/types/helpers/createMutators.d.ts.map +1 -1
package/dist/esm/generate.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { basename, resolve } from "node:path";
|
|
3
|
+
import { basename, dirname, resolve } from "node:path";
|
|
4
4
|
const hash = s => createHash("sha256").update(s).digest("hex");
|
|
5
5
|
let generateCache = {},
|
|
6
6
|
generateCachePath = "";
|
|
@@ -90,6 +90,7 @@ this folder is auto-generated by on-zero. do not edit files here directly.
|
|
|
90
90
|
- \`tables.ts\` - exports table schemas for type inference
|
|
91
91
|
- \`groupedQueries.ts\` - namespaced query re-exports for client setup
|
|
92
92
|
- \`syncedQueries.ts\` - namespaced syncedQuery wrappers for server setup
|
|
93
|
+
- \`syncedMutations.ts\` - valibot validators for mutation args (server auto-validation)
|
|
93
94
|
|
|
94
95
|
## usage guidelines
|
|
95
96
|
|
|
@@ -161,22 +162,7 @@ import * as Queries from './groupedQueries'
|
|
|
161
162
|
`,
|
|
162
163
|
namespaceDefs = sortedFiles.map(file => {
|
|
163
164
|
const queryDefs = queryByFile.get(file).sort((a, b) => a.name.localeCompare(b.name)).map(q => {
|
|
164
|
-
const
|
|
165
|
-
`).filter(l => l.trim()),
|
|
166
|
-
schemaLineIndex = lines.findIndex(l => l.startsWith("export const QueryParams"));
|
|
167
|
-
let validatorDef = "";
|
|
168
|
-
if (schemaLineIndex !== -1) {
|
|
169
|
-
const schemaLines = [];
|
|
170
|
-
let openBraces = 0,
|
|
171
|
-
started = !1;
|
|
172
|
-
for (let i = schemaLineIndex; i < lines.length; i++) {
|
|
173
|
-
const line = lines[i],
|
|
174
|
-
cleaned = started ? line : line.replace("export const QueryParams = ", "");
|
|
175
|
-
if (schemaLines.push(cleaned), started = !0, openBraces += (cleaned.match(/\{/g) || []).length, openBraces -= (cleaned.match(/\}/g) || []).length, openBraces += (cleaned.match(/\(/g) || []).length, openBraces -= (cleaned.match(/\)/g) || []).length, openBraces === 0 && schemaLines.length > 0) break;
|
|
176
|
-
}
|
|
177
|
-
validatorDef = schemaLines.join(`
|
|
178
|
-
`);
|
|
179
|
-
}
|
|
165
|
+
const validatorDef = q.valibotCode.trim();
|
|
180
166
|
if (q.params === "void" || !validatorDef) return ` ${q.name}: defineQuery(() => Queries.${file}.${q.name}()),`;
|
|
181
167
|
const indentedValidator = validatorDef.split(`
|
|
182
168
|
`).map((line, i) => i === 0 ? line : ` ${line}`).join(`
|
|
@@ -203,6 +189,364 @@ ${queriesObject}
|
|
|
203
189
|
})
|
|
204
190
|
`;
|
|
205
191
|
}
|
|
192
|
+
function createTypeResolver(ts, files, dir) {
|
|
193
|
+
const configPath = ts.findConfigFile(dir, ts.sys.fileExists, "tsconfig.json");
|
|
194
|
+
let compilerOptions = {
|
|
195
|
+
target: ts.ScriptTarget.Latest,
|
|
196
|
+
module: ts.ModuleKind.ESNext,
|
|
197
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
198
|
+
strict: !1,
|
|
199
|
+
skipLibCheck: !0,
|
|
200
|
+
noEmit: !0
|
|
201
|
+
};
|
|
202
|
+
if (configPath) {
|
|
203
|
+
const configFile = ts.readConfigFile(configPath, ts.sys.readFile);
|
|
204
|
+
if (configFile.config) {
|
|
205
|
+
const parsed = ts.parseJsonConfigFileContent(configFile.config, ts.sys, dirname(configPath));
|
|
206
|
+
compilerOptions = {
|
|
207
|
+
...compilerOptions,
|
|
208
|
+
...parsed.options
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
const fileMap = /* @__PURE__ */new Map();
|
|
213
|
+
for (const f of files) fileMap.set(f.path, f.content);
|
|
214
|
+
const host = ts.createCompilerHost(compilerOptions),
|
|
215
|
+
originalGetSourceFile = host.getSourceFile.bind(host);
|
|
216
|
+
host.getSourceFile = (fileName, languageVersion, onError) => {
|
|
217
|
+
const content = fileMap.get(fileName);
|
|
218
|
+
return content !== void 0 ? ts.createSourceFile(fileName, content, languageVersion, !0) : originalGetSourceFile(fileName, languageVersion, onError);
|
|
219
|
+
}, host.fileExists = fileName => fileMap.has(fileName) || ts.sys.fileExists(fileName), host.readFile = fileName => fileMap.get(fileName) ?? ts.sys.readFile(fileName);
|
|
220
|
+
const program = ts.createProgram(files.map(f => f.path), compilerOptions, host),
|
|
221
|
+
checker = program.getTypeChecker();
|
|
222
|
+
return {
|
|
223
|
+
program,
|
|
224
|
+
checker,
|
|
225
|
+
// resolve a type annotation node to a ts.Type
|
|
226
|
+
resolveType(node) {
|
|
227
|
+
try {
|
|
228
|
+
return checker.getTypeFromTypeNode(node);
|
|
229
|
+
} catch {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
},
|
|
233
|
+
// convert a resolved type to valibot code
|
|
234
|
+
typeToValibot(type) {
|
|
235
|
+
return tsTypeToValibot(ts, checker, type);
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
function resolveParamType(ts, resolver, sourceFile, exportName, paramIndex) {
|
|
240
|
+
let result = null;
|
|
241
|
+
return ts.forEachChild(sourceFile, node => {
|
|
242
|
+
if (result || !ts.isVariableStatement(node) || !node.modifiers?.some(m => m.kind === ts.SyntaxKind.ExportKeyword)) return;
|
|
243
|
+
const decl = node.declarationList.declarations[0];
|
|
244
|
+
if (!(!decl || !ts.isVariableDeclaration(decl)) && decl.name.getText(sourceFile) === exportName && decl.initializer && ts.isArrowFunction(decl.initializer)) {
|
|
245
|
+
const param = decl.initializer.parameters[paramIndex];
|
|
246
|
+
param?.type && (result = resolver.resolveType(param.type));
|
|
247
|
+
}
|
|
248
|
+
}), result;
|
|
249
|
+
}
|
|
250
|
+
function resolveMutationParamTypes(ts, resolver, sourceFile) {
|
|
251
|
+
const resolved = /* @__PURE__ */new Map();
|
|
252
|
+
return ts.forEachChild(sourceFile, node => {
|
|
253
|
+
if (!ts.isVariableStatement(node) || !node.modifiers?.some(m => m.kind === ts.SyntaxKind.ExportKeyword)) return;
|
|
254
|
+
const decl = node.declarationList.declarations[0];
|
|
255
|
+
if (!decl || !ts.isVariableDeclaration(decl) || decl.name.getText(sourceFile) !== "mutate" || !decl.initializer || !ts.isCallExpression(decl.initializer)) return;
|
|
256
|
+
const args = decl.initializer.arguments;
|
|
257
|
+
let handlersArg = null;
|
|
258
|
+
for (let i = args.length - 1; i >= 0; i--) if (ts.isObjectLiteralExpression(args[i])) {
|
|
259
|
+
handlersArg = args[i];
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
if (handlersArg) for (const prop of handlersArg.properties) {
|
|
263
|
+
if (!ts.isPropertyAssignment(prop) && !ts.isMethodDeclaration(prop)) continue;
|
|
264
|
+
const name = prop.name?.getText(sourceFile);
|
|
265
|
+
if (!name) continue;
|
|
266
|
+
let params = null;
|
|
267
|
+
if (ts.isPropertyAssignment(prop)) {
|
|
268
|
+
const init = prop.initializer;
|
|
269
|
+
(ts.isArrowFunction(init) || ts.isFunctionExpression(init)) && (params = init.parameters);
|
|
270
|
+
} else ts.isMethodDeclaration(prop) && (params = prop.parameters);
|
|
271
|
+
if (!params || params.length < 2) continue;
|
|
272
|
+
const typeNode = params[1].type;
|
|
273
|
+
if (!typeNode) continue;
|
|
274
|
+
const expanded = resolver.resolveType(typeNode);
|
|
275
|
+
expanded && resolved.set(name, expanded);
|
|
276
|
+
}
|
|
277
|
+
}), resolved;
|
|
278
|
+
}
|
|
279
|
+
function extractMutationsFromModel(ts, sourceFile, content, _fileName, silent, typeToValibot, resolvedTypes, resolvedTypeToValibot) {
|
|
280
|
+
let mutateNode = null;
|
|
281
|
+
if (ts.forEachChild(sourceFile, node => {
|
|
282
|
+
if (!ts.isVariableStatement(node) || !node.modifiers?.some(m => m.kind === ts.SyntaxKind.ExportKeyword)) return;
|
|
283
|
+
const decl = node.declarationList.declarations[0];
|
|
284
|
+
!decl || !ts.isVariableDeclaration(decl) || decl.name.getText(sourceFile) === "mutate" && decl.initializer && ts.isCallExpression(decl.initializer) && (mutateNode = decl.initializer);
|
|
285
|
+
}), !mutateNode) return null;
|
|
286
|
+
const args = mutateNode.arguments,
|
|
287
|
+
hasCRUD = args.length >= 2;
|
|
288
|
+
let handlersArg = null;
|
|
289
|
+
args.length === 1 && ts.isObjectLiteralExpression(args[0]) ? handlersArg = args[0] : args.length === 3 && ts.isObjectLiteralExpression(args[2]) && (handlersArg = args[2]);
|
|
290
|
+
const columns = {},
|
|
291
|
+
primaryKeys = [];
|
|
292
|
+
hasCRUD && extractSchemaColumns(ts, sourceFile, columns, primaryKeys);
|
|
293
|
+
const custom = [];
|
|
294
|
+
if (handlersArg) for (const prop of handlersArg.properties) {
|
|
295
|
+
if (!ts.isPropertyAssignment(prop) && !ts.isMethodDeclaration(prop)) continue;
|
|
296
|
+
const name = prop.name?.getText(sourceFile);
|
|
297
|
+
if (!name) continue;
|
|
298
|
+
let params = null;
|
|
299
|
+
if (ts.isPropertyAssignment(prop)) {
|
|
300
|
+
const init = prop.initializer;
|
|
301
|
+
(ts.isArrowFunction(init) || ts.isFunctionExpression(init)) && (params = init.parameters);
|
|
302
|
+
} else ts.isMethodDeclaration(prop) && (params = prop.parameters);
|
|
303
|
+
if (!params) continue;
|
|
304
|
+
if (params.length < 2) {
|
|
305
|
+
custom.push({
|
|
306
|
+
name,
|
|
307
|
+
paramType: "void",
|
|
308
|
+
valibotCode: ""
|
|
309
|
+
});
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
const paramType = params[1].type?.getText(sourceFile) || "unknown";
|
|
313
|
+
if (paramType === "unknown") {
|
|
314
|
+
custom.push({
|
|
315
|
+
name,
|
|
316
|
+
paramType: "unknown",
|
|
317
|
+
valibotCode: ""
|
|
318
|
+
});
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
let valibotCode = typeToValibot(paramType);
|
|
322
|
+
if (!valibotCode && resolvedTypes && resolvedTypeToValibot) {
|
|
323
|
+
const resolvedType = resolvedTypes.get(name);
|
|
324
|
+
resolvedType && (valibotCode = resolvedTypeToValibot(resolvedType));
|
|
325
|
+
}
|
|
326
|
+
custom.push({
|
|
327
|
+
name,
|
|
328
|
+
paramType,
|
|
329
|
+
valibotCode: valibotCode || ""
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
return {
|
|
333
|
+
modelName: "",
|
|
334
|
+
hasCRUD,
|
|
335
|
+
columns,
|
|
336
|
+
primaryKeys,
|
|
337
|
+
custom
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
function extractSchemaColumns(ts, sourceFile, columns, primaryKeys) {
|
|
341
|
+
function visit(node) {
|
|
342
|
+
if (ts.isCallExpression(node)) {
|
|
343
|
+
const text = node.expression.getText(sourceFile);
|
|
344
|
+
if (text.endsWith(".primaryKey")) for (const arg of node.arguments) ts.isStringLiteral(arg) && primaryKeys.push(arg.text);
|
|
345
|
+
if (text.endsWith(".columns") && node.arguments.length === 1) {
|
|
346
|
+
const obj = node.arguments[0];
|
|
347
|
+
if (ts.isObjectLiteralExpression(obj)) for (const prop of obj.properties) {
|
|
348
|
+
if (!ts.isPropertyAssignment(prop)) continue;
|
|
349
|
+
const colName = prop.name?.getText(sourceFile);
|
|
350
|
+
if (!colName) continue;
|
|
351
|
+
const initText = prop.initializer.getText(sourceFile),
|
|
352
|
+
colType = parseColumnType(initText);
|
|
353
|
+
columns[colName] = colType;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
ts.forEachChild(node, visit);
|
|
358
|
+
}
|
|
359
|
+
visit(sourceFile);
|
|
360
|
+
}
|
|
361
|
+
function parseColumnType(initText) {
|
|
362
|
+
const optional = initText.includes(".optional()");
|
|
363
|
+
let type = "string";
|
|
364
|
+
return initText.startsWith("number(") ? type = "number" : initText.startsWith("boolean(") ? type = "boolean" : initText.startsWith("json(") || initText.startsWith("json<") ? type = "json" : initText.startsWith("enumeration(") && (type = "enum"), {
|
|
365
|
+
type,
|
|
366
|
+
optional
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
function columnTypeToValibot(col) {
|
|
370
|
+
let base;
|
|
371
|
+
switch (col.type) {
|
|
372
|
+
case "string":
|
|
373
|
+
base = "v.string()";
|
|
374
|
+
break;
|
|
375
|
+
case "number":
|
|
376
|
+
base = "v.number()";
|
|
377
|
+
break;
|
|
378
|
+
case "boolean":
|
|
379
|
+
base = "v.boolean()";
|
|
380
|
+
break;
|
|
381
|
+
case "json":
|
|
382
|
+
base = "v.unknown()";
|
|
383
|
+
break;
|
|
384
|
+
case "enum":
|
|
385
|
+
base = "v.string()";
|
|
386
|
+
break;
|
|
387
|
+
default:
|
|
388
|
+
base = "v.unknown()";
|
|
389
|
+
}
|
|
390
|
+
return col.optional ? `v.optional(v.nullable(${base}))` : base;
|
|
391
|
+
}
|
|
392
|
+
function schemaColumnsToValibot(columns, primaryKeys, mode) {
|
|
393
|
+
const entries = [];
|
|
394
|
+
if (mode === "delete") for (const pk of primaryKeys) {
|
|
395
|
+
const col = columns[pk];
|
|
396
|
+
col && entries.push(`${pk}: ${columnTypeToValibot({
|
|
397
|
+
...col,
|
|
398
|
+
optional: !1
|
|
399
|
+
})}`);
|
|
400
|
+
} else if (mode === "update") for (const [name, col] of Object.entries(columns)) primaryKeys.includes(name) ? entries.push(`${name}: ${columnTypeToValibot({
|
|
401
|
+
...col,
|
|
402
|
+
optional: !1
|
|
403
|
+
})}`) : entries.push(`${name}: ${columnTypeToValibot({
|
|
404
|
+
...col,
|
|
405
|
+
optional: !0
|
|
406
|
+
})}`);else for (const [name, col] of Object.entries(columns)) entries.push(`${name}: ${columnTypeToValibot(col)}`);
|
|
407
|
+
return `v.object({
|
|
408
|
+
${entries.join(`,
|
|
409
|
+
`)},
|
|
410
|
+
})`;
|
|
411
|
+
}
|
|
412
|
+
function generateSyncedMutationsFile(modelMutations) {
|
|
413
|
+
return `// auto-generated by: on-zero generate
|
|
414
|
+
// mutation validators derived from model schemas and handler types
|
|
415
|
+
import * as v from 'valibot'
|
|
416
|
+
|
|
417
|
+
export const mutationValidators = {
|
|
418
|
+
${[...modelMutations].sort((a, b) => a.modelName.localeCompare(b.modelName)).map(model => {
|
|
419
|
+
const entries = [];
|
|
420
|
+
if (model.hasCRUD && Object.keys(model.columns).length > 0) for (const mode of ["insert", "update", "delete"]) if (model.custom.some(m => m.name === mode)) {
|
|
421
|
+
const customMut = model.custom.find(m => m.name === mode);
|
|
422
|
+
customMut.valibotCode ? entries.push(` ${mode}: ${extractValibotExpression(customMut.valibotCode)},`) : entries.push(` ${mode}: ${schemaColumnsToValibot(model.columns, model.primaryKeys, mode)},`);
|
|
423
|
+
} else entries.push(` ${mode}: ${schemaColumnsToValibot(model.columns, model.primaryKeys, mode)},`);
|
|
424
|
+
for (const mut of model.custom) if (!(model.hasCRUD && ["insert", "update", "delete", "upsert"].includes(mut.name))) {
|
|
425
|
+
if (mut.paramType === "void" || !mut.valibotCode) {
|
|
426
|
+
entries.push(` ${mut.name}: v.void_(),`);
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
entries.push(` ${mut.name}: ${extractValibotExpression(mut.valibotCode)},`);
|
|
430
|
+
}
|
|
431
|
+
return ` ${model.modelName}: {
|
|
432
|
+
${entries.join(`
|
|
433
|
+
`)}
|
|
434
|
+
},`;
|
|
435
|
+
}).join(`
|
|
436
|
+
`)}
|
|
437
|
+
}
|
|
438
|
+
`;
|
|
439
|
+
}
|
|
440
|
+
function extractValibotExpression(valibotCode) {
|
|
441
|
+
return valibotCode.trim() || "v.unknown()";
|
|
442
|
+
}
|
|
443
|
+
function parseTypeString(type) {
|
|
444
|
+
if (type = type.trim(), type === "string") return "v.string()";
|
|
445
|
+
if (type === "number") return "v.number()";
|
|
446
|
+
if (type === "boolean") return "v.boolean()";
|
|
447
|
+
if (type === "void" || type === "undefined") return "v.void_()";
|
|
448
|
+
if (type === "null") return "v.null_()";
|
|
449
|
+
if (type === "any" || type === "unknown") return "v.unknown()";
|
|
450
|
+
if (type.startsWith("{") && type.endsWith("}")) {
|
|
451
|
+
const inner = type.slice(1, -1).trim();
|
|
452
|
+
if (!inner) return "v.object({})";
|
|
453
|
+
const normalized = inner.replace(/\n/g, "; ").replace(/;\s*;/g, ";"),
|
|
454
|
+
entries = [];
|
|
455
|
+
for (const part of normalized.split(";")) {
|
|
456
|
+
const trimmed = part.trim().replace(/,\s*$/, "");
|
|
457
|
+
if (!trimmed) continue;
|
|
458
|
+
const match = trimmed.match(/^(?:readonly\s+)?(\w+)(\?)?:\s*(.+)$/);
|
|
459
|
+
if (!match) continue;
|
|
460
|
+
const [, name, opt, typeStr] = match,
|
|
461
|
+
parsed = parseTypeString(typeStr.trim());
|
|
462
|
+
if (!parsed) return null;
|
|
463
|
+
let val = parsed;
|
|
464
|
+
opt && (val = `v.optional(${val})`), entries.push(`${name}: ${val}`);
|
|
465
|
+
}
|
|
466
|
+
return entries.length === 0 ? "v.object({})" : `v.object({
|
|
467
|
+
${entries.join(`,
|
|
468
|
+
`)},
|
|
469
|
+
})`;
|
|
470
|
+
}
|
|
471
|
+
if (type.endsWith("[]")) {
|
|
472
|
+
const inner = parseTypeString(type.slice(0, -2).trim());
|
|
473
|
+
return inner ? `v.array(${inner})` : null;
|
|
474
|
+
}
|
|
475
|
+
return null;
|
|
476
|
+
}
|
|
477
|
+
function tsTypeToValibot(ts, checker, type, seen) {
|
|
478
|
+
seen || (seen = /* @__PURE__ */new Set());
|
|
479
|
+
const flags = type.getFlags();
|
|
480
|
+
if (flags & (ts.TypeFlags.Object | ts.TypeFlags.Intersection)) {
|
|
481
|
+
if (seen.has(type)) return "v.unknown()";
|
|
482
|
+
seen.add(type);
|
|
483
|
+
}
|
|
484
|
+
const recurse = t => tsTypeToValibot(ts, checker, t, seen);
|
|
485
|
+
if (flags & ts.TypeFlags.String) return "v.string()";
|
|
486
|
+
if (flags & ts.TypeFlags.Number) return "v.number()";
|
|
487
|
+
if (flags & ts.TypeFlags.Boolean) return "v.boolean()";
|
|
488
|
+
if (flags & ts.TypeFlags.Void || flags & ts.TypeFlags.Undefined) return "v.void_()";
|
|
489
|
+
if (flags & ts.TypeFlags.Null) return "v.null_()";
|
|
490
|
+
if (flags & ts.TypeFlags.Any || flags & ts.TypeFlags.Unknown) return "v.unknown()";
|
|
491
|
+
if (flags & ts.TypeFlags.Never) return "v.never()";
|
|
492
|
+
if (flags & ts.TypeFlags.StringLiteral) return `v.literal(${JSON.stringify(type.value)})`;
|
|
493
|
+
if (flags & ts.TypeFlags.NumberLiteral) return `v.literal(${type.value})`;
|
|
494
|
+
if (flags & ts.TypeFlags.BooleanLiteral) return `v.literal(${type.intrinsicName === "true"})`;
|
|
495
|
+
if (type.isUnion()) {
|
|
496
|
+
const members = type.types,
|
|
497
|
+
hasNull = members.some(t => t.getFlags() & ts.TypeFlags.Null),
|
|
498
|
+
hasUndefined = members.some(t => t.getFlags() & (ts.TypeFlags.Undefined | ts.TypeFlags.Void)),
|
|
499
|
+
rest = members.filter(t => !(t.getFlags() & (ts.TypeFlags.Null | ts.TypeFlags.Undefined | ts.TypeFlags.Void)));
|
|
500
|
+
if (rest.length === 2 && rest.every(t => t.getFlags() & ts.TypeFlags.BooleanLiteral)) {
|
|
501
|
+
let inner2 = "v.boolean()";
|
|
502
|
+
return hasNull && (inner2 = `v.nullable(${inner2})`), hasUndefined && (inner2 = `v.optional(${inner2})`), inner2;
|
|
503
|
+
}
|
|
504
|
+
if (rest.length === 0) return "v.unknown()";
|
|
505
|
+
let inner = rest.length === 1 ? recurse(rest[0]) : `v.union([${rest.map(t => recurse(t)).join(", ")}])`;
|
|
506
|
+
return hasNull && (inner = `v.nullable(${inner})`), hasUndefined && (inner = `v.optional(${inner})`), inner;
|
|
507
|
+
}
|
|
508
|
+
const resolveSymbolType = prop => prop.valueDeclaration ? checker.getTypeOfSymbolAtLocation(prop, prop.valueDeclaration) : prop.declarations?.[0] ? checker.getTypeOfSymbolAtLocation(prop, prop.declarations[0]) : checker.getDeclaredTypeOfSymbol(prop);
|
|
509
|
+
if (type.isIntersection()) {
|
|
510
|
+
const props2 = type.getProperties();
|
|
511
|
+
if (props2.length === 0) return "v.object({})";
|
|
512
|
+
const entries = [];
|
|
513
|
+
for (const prop of props2) {
|
|
514
|
+
const propType = resolveSymbolType(prop),
|
|
515
|
+
isOptional = !!(prop.getFlags() & ts.SymbolFlags.Optional);
|
|
516
|
+
let val = recurse(propType);
|
|
517
|
+
isOptional && !val.startsWith("v.optional(") && (val = `v.optional(${val})`), entries.push(`${prop.getName()}: ${val}`);
|
|
518
|
+
}
|
|
519
|
+
return `v.object({
|
|
520
|
+
${entries.join(`,
|
|
521
|
+
`)},
|
|
522
|
+
})`;
|
|
523
|
+
}
|
|
524
|
+
const props = type.getProperties();
|
|
525
|
+
if (props.length > 0 && (type.getFlags() & ts.TypeFlags.Object || type.objectFlags)) {
|
|
526
|
+
const objectFlags = type.objectFlags ?? 0;
|
|
527
|
+
if (objectFlags & ts.ObjectFlags.Reference) {
|
|
528
|
+
const typeRef = type,
|
|
529
|
+
name = type.getSymbol()?.getName();
|
|
530
|
+
if ((name === "Array" || name === "ReadonlyArray") && typeRef.typeArguments?.length === 1) return `v.array(${recurse(typeRef.typeArguments[0])})`;
|
|
531
|
+
}
|
|
532
|
+
if (objectFlags & ts.ObjectFlags.Tuple) return `v.tuple([${(type.typeArguments || []).map(t => recurse(t)).join(", ")}])`;
|
|
533
|
+
const entries = [];
|
|
534
|
+
for (const prop of props) {
|
|
535
|
+
const propType = resolveSymbolType(prop),
|
|
536
|
+
isOptional = !!(prop.getFlags() & ts.SymbolFlags.Optional);
|
|
537
|
+
let val = recurse(propType);
|
|
538
|
+
isOptional && !val.startsWith("v.optional(") && (val = `v.optional(${val})`), entries.push(`${prop.getName()}: ${val}`);
|
|
539
|
+
}
|
|
540
|
+
return entries.length === 0 ? "v.object({})" : `v.object({
|
|
541
|
+
${entries.join(`,
|
|
542
|
+
`)},
|
|
543
|
+
})`;
|
|
544
|
+
}
|
|
545
|
+
const stringIndex = type.getStringIndexType();
|
|
546
|
+
if (stringIndex) return `v.record(v.string(), ${recurse(stringIndex)})`;
|
|
547
|
+
const numberIndex = type.getNumberIndexType();
|
|
548
|
+
return numberIndex ? `v.record(v.number(), ${recurse(numberIndex)})` : "v.unknown()";
|
|
549
|
+
}
|
|
206
550
|
async function generate(options) {
|
|
207
551
|
const {
|
|
208
552
|
dir,
|
|
@@ -219,17 +563,30 @@ async function generate(options) {
|
|
|
219
563
|
const allModelFiles = readdirSync(modelsDir).filter(f => f.endsWith(".ts")).sort(),
|
|
220
564
|
filesWithSchema = allModelFiles.filter(f => readFileSync(resolve(modelsDir, f), "utf-8").includes("export const schema = table("));
|
|
221
565
|
let filesChanged = [writeFileIfChanged(resolve(generatedDir, "models.ts"), generateModelsFile(allModelFiles)), writeFileIfChanged(resolve(generatedDir, "types.ts"), generateTypesFile(filesWithSchema)), writeFileIfChanged(resolve(generatedDir, "tables.ts"), generateTablesFile(filesWithSchema)), writeFileIfChanged(resolve(generatedDir, "README.md"), generateReadmeFile())].filter(Boolean).length,
|
|
222
|
-
queryCount = 0
|
|
566
|
+
queryCount = 0,
|
|
567
|
+
mutationCount = 0;
|
|
568
|
+
const ts = await import("typescript"),
|
|
569
|
+
typeToValibot = paramType => {
|
|
570
|
+
try {
|
|
571
|
+
return parseTypeString(paramType.trim());
|
|
572
|
+
} catch {
|
|
573
|
+
return null;
|
|
574
|
+
}
|
|
575
|
+
};
|
|
223
576
|
if (existsSync(queriesDir)) {
|
|
224
|
-
const
|
|
225
|
-
{
|
|
226
|
-
ModelToValibot
|
|
227
|
-
} = await import("@sinclair/typebox-codegen/model/index.js"),
|
|
228
|
-
{
|
|
229
|
-
TypeScriptToModel
|
|
230
|
-
} = await import("@sinclair/typebox-codegen/typescript/index.js"),
|
|
231
|
-
queryFiles = readdirSync(queriesDir).filter(f => f.endsWith(".ts")),
|
|
577
|
+
const queryFiles = readdirSync(queriesDir).filter(f => f.endsWith(".ts")),
|
|
232
578
|
allQueries = [];
|
|
579
|
+
let queryResolver = null;
|
|
580
|
+
const getQueryResolver = () => {
|
|
581
|
+
if (!queryResolver) {
|
|
582
|
+
const allFiles = readdirSync(queriesDir).filter(f => f.endsWith(".ts")).map(f => ({
|
|
583
|
+
path: resolve(queriesDir, f),
|
|
584
|
+
content: readFileSync(resolve(queriesDir, f), "utf-8")
|
|
585
|
+
}));
|
|
586
|
+
queryResolver = createTypeResolver(ts, allFiles, queriesDir);
|
|
587
|
+
}
|
|
588
|
+
return queryResolver;
|
|
589
|
+
};
|
|
233
590
|
for (const file of queryFiles) {
|
|
234
591
|
const filePath = resolve(queriesDir, file),
|
|
235
592
|
fileBaseName = basename(file, ".ts");
|
|
@@ -247,19 +604,21 @@ async function generate(options) {
|
|
|
247
604
|
const params = declaration.initializer.parameters;
|
|
248
605
|
let paramType = "void";
|
|
249
606
|
params.length > 0 && (paramType = params[0].type?.getText(sourceFile) || "unknown");
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
name,
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
sourceFile: fileBaseName
|
|
259
|
-
});
|
|
260
|
-
} catch (err) {
|
|
261
|
-
silent || console.error(`\u2717 ${name}: ${err}`);
|
|
607
|
+
let valibotCode = typeToValibot(paramType);
|
|
608
|
+
if (!valibotCode && params.length > 0 && params[0].type) {
|
|
609
|
+
const resolver = getQueryResolver(),
|
|
610
|
+
resolverSourceFile = resolver.program.getSourceFile(filePath);
|
|
611
|
+
if (resolverSourceFile) {
|
|
612
|
+
const resolvedType = resolveParamType(ts, resolver, resolverSourceFile, name, 0);
|
|
613
|
+
resolvedType && (valibotCode = resolver.typeToValibot(resolvedType));
|
|
614
|
+
}
|
|
262
615
|
}
|
|
616
|
+
valibotCode ? allQueries.push({
|
|
617
|
+
name,
|
|
618
|
+
params: paramType,
|
|
619
|
+
valibotCode,
|
|
620
|
+
sourceFile: fileBaseName
|
|
621
|
+
}) : !silent && paramType !== "void" && console.error(`\u2717 ${name}: could not resolve type "${paramType}"`);
|
|
263
622
|
}
|
|
264
623
|
}
|
|
265
624
|
});
|
|
@@ -272,7 +631,66 @@ async function generate(options) {
|
|
|
272
631
|
syncedChanged = writeFileIfChanged(resolve(generatedDir, "syncedQueries.ts"), generateSyncedQueriesFile(allQueries));
|
|
273
632
|
groupedChanged && filesChanged++, syncedChanged && filesChanged++;
|
|
274
633
|
}
|
|
275
|
-
|
|
634
|
+
const allModelMutations = [],
|
|
635
|
+
mutationFiles = [],
|
|
636
|
+
unresolvedModels = [];
|
|
637
|
+
for (const file of allModelFiles) {
|
|
638
|
+
const filePath = resolve(modelsDir, file),
|
|
639
|
+
fileBaseName = basename(file, ".ts");
|
|
640
|
+
try {
|
|
641
|
+
const content = readFileSync(filePath, "utf-8");
|
|
642
|
+
if (!content.includes("export const mutate")) continue;
|
|
643
|
+
mutationFiles.push({
|
|
644
|
+
path: filePath,
|
|
645
|
+
content,
|
|
646
|
+
baseName: fileBaseName
|
|
647
|
+
});
|
|
648
|
+
const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, !0),
|
|
649
|
+
result = extractMutationsFromModel(ts, sourceFile, content, filePath, !!silent, typeToValibot);
|
|
650
|
+
result && (result.modelName = fileBaseName, allModelMutations.push(result), result.custom.some(m => m.paramType !== "void" && m.paramType !== "unknown" && !m.valibotCode) && unresolvedModels.push({
|
|
651
|
+
baseName: fileBaseName,
|
|
652
|
+
filePath
|
|
653
|
+
}));
|
|
654
|
+
} catch (err) {
|
|
655
|
+
silent || console.error(`Error extracting mutations from ${file}:`, err);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
if (unresolvedModels.length > 0) {
|
|
659
|
+
const collectTsFiles = dir2 => {
|
|
660
|
+
const results = [];
|
|
661
|
+
for (const entry of readdirSync(dir2, {
|
|
662
|
+
withFileTypes: !0
|
|
663
|
+
})) {
|
|
664
|
+
const fullPath = resolve(dir2, entry.name);
|
|
665
|
+
entry.isDirectory() && entry.name !== "node_modules" ? results.push(...collectTsFiles(fullPath)) : entry.isFile() && entry.name.endsWith(".ts") && !entry.name.endsWith(".d.ts") && results.push({
|
|
666
|
+
path: fullPath,
|
|
667
|
+
content: readFileSync(fullPath, "utf-8")
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
return results;
|
|
671
|
+
},
|
|
672
|
+
allFiles = collectTsFiles(baseDir),
|
|
673
|
+
modelResolver = createTypeResolver(ts, allFiles, baseDir);
|
|
674
|
+
for (const {
|
|
675
|
+
baseName,
|
|
676
|
+
filePath
|
|
677
|
+
} of unresolvedModels) {
|
|
678
|
+
const resolverSourceFile = modelResolver.program.getSourceFile(filePath);
|
|
679
|
+
if (!resolverSourceFile) continue;
|
|
680
|
+
const resolvedTypes = resolveMutationParamTypes(ts, modelResolver, resolverSourceFile);
|
|
681
|
+
if (resolvedTypes.size === 0) continue;
|
|
682
|
+
const content = readFileSync(filePath, "utf-8"),
|
|
683
|
+
sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, !0),
|
|
684
|
+
result = extractMutationsFromModel(ts, sourceFile, content, filePath, !!silent, typeToValibot, resolvedTypes, modelResolver.typeToValibot);
|
|
685
|
+
if (result) {
|
|
686
|
+
result.modelName = baseName;
|
|
687
|
+
const idx = allModelMutations.findIndex(m => m.modelName === baseName);
|
|
688
|
+
idx >= 0 && (allModelMutations[idx] = result);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
for (const model of allModelMutations) model.hasCRUD && (mutationCount += 3), mutationCount += model.custom.filter(m => !model.hasCRUD || !["insert", "update", "delete", "upsert"].includes(m.name)).length;
|
|
693
|
+
if (allModelMutations.length > 0 && writeFileIfChanged(resolve(generatedDir, "syncedMutations.ts"), generateSyncedMutationsFile(allModelMutations)) && filesChanged++, filesChanged > 0 && !silent && console.info(`\u2713 ${allModelFiles.length} models (${filesWithSchema.length} schemas)${queryCount ? `, ${queryCount} queries` : ""}${mutationCount ? `, ${mutationCount} mutations` : ""}`), filesChanged > 0 && after) {
|
|
276
694
|
const {
|
|
277
695
|
execSync
|
|
278
696
|
} = await import("node:child_process");
|
|
@@ -292,7 +710,8 @@ async function generate(options) {
|
|
|
292
710
|
filesChanged,
|
|
293
711
|
modelCount: allModelFiles.length,
|
|
294
712
|
schemaCount: filesWithSchema.length,
|
|
295
|
-
queryCount
|
|
713
|
+
queryCount,
|
|
714
|
+
mutationCount
|
|
296
715
|
};
|
|
297
716
|
}
|
|
298
717
|
async function watch(options) {
|