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.
Files changed (84) hide show
  1. package/dist/cjs/createUseQuery.cjs +3 -2
  2. package/dist/cjs/createUseQuery.js +2 -2
  3. package/dist/cjs/createUseQuery.js.map +1 -1
  4. package/dist/cjs/createUseQuery.native.js +3 -2
  5. package/dist/cjs/createUseQuery.native.js.map +1 -1
  6. package/dist/cjs/createZeroClient.cjs +28 -5
  7. package/dist/cjs/createZeroClient.js +19 -4
  8. package/dist/cjs/createZeroClient.js.map +1 -1
  9. package/dist/cjs/createZeroClient.native.js +29 -5
  10. package/dist/cjs/createZeroClient.native.js.map +1 -1
  11. package/dist/cjs/createZeroServer.cjs +5 -2
  12. package/dist/cjs/createZeroServer.js +5 -2
  13. package/dist/cjs/createZeroServer.js.map +1 -1
  14. package/dist/cjs/createZeroServer.native.js +5 -2
  15. package/dist/cjs/createZeroServer.native.js.map +1 -1
  16. package/dist/cjs/generate.cjs +458 -39
  17. package/dist/cjs/generate.js +485 -31
  18. package/dist/cjs/generate.js.map +2 -2
  19. package/dist/cjs/generate.native.js +812 -51
  20. package/dist/cjs/generate.native.js.map +1 -1
  21. package/dist/cjs/generate.test.cjs +251 -0
  22. package/dist/cjs/generate.test.js +252 -0
  23. package/dist/cjs/generate.test.js.map +1 -1
  24. package/dist/cjs/generate.test.native.js +251 -0
  25. package/dist/cjs/generate.test.native.js.map +1 -1
  26. package/dist/cjs/helpers/createMutators.cjs +21 -8
  27. package/dist/cjs/helpers/createMutators.js +16 -6
  28. package/dist/cjs/helpers/createMutators.js.map +1 -1
  29. package/dist/cjs/helpers/createMutators.native.js +28 -10
  30. package/dist/cjs/helpers/createMutators.native.js.map +1 -1
  31. package/dist/esm/createUseQuery.js +3 -3
  32. package/dist/esm/createUseQuery.js.map +1 -1
  33. package/dist/esm/createUseQuery.mjs +4 -3
  34. package/dist/esm/createUseQuery.mjs.map +1 -1
  35. package/dist/esm/createUseQuery.native.js +4 -3
  36. package/dist/esm/createUseQuery.native.js.map +1 -1
  37. package/dist/esm/createZeroClient.js +19 -4
  38. package/dist/esm/createZeroClient.js.map +1 -1
  39. package/dist/esm/createZeroClient.mjs +28 -5
  40. package/dist/esm/createZeroClient.mjs.map +1 -1
  41. package/dist/esm/createZeroClient.native.js +29 -5
  42. package/dist/esm/createZeroClient.native.js.map +1 -1
  43. package/dist/esm/createZeroServer.js +5 -2
  44. package/dist/esm/createZeroServer.js.map +1 -1
  45. package/dist/esm/createZeroServer.mjs +5 -2
  46. package/dist/esm/createZeroServer.mjs.map +1 -1
  47. package/dist/esm/createZeroServer.native.js +5 -2
  48. package/dist/esm/createZeroServer.native.js.map +1 -1
  49. package/dist/esm/generate.js +486 -32
  50. package/dist/esm/generate.js.map +2 -2
  51. package/dist/esm/generate.mjs +459 -40
  52. package/dist/esm/generate.mjs.map +1 -1
  53. package/dist/esm/generate.native.js +813 -52
  54. package/dist/esm/generate.native.js.map +1 -1
  55. package/dist/esm/generate.test.js +252 -0
  56. package/dist/esm/generate.test.js.map +1 -1
  57. package/dist/esm/generate.test.mjs +251 -0
  58. package/dist/esm/generate.test.mjs.map +1 -1
  59. package/dist/esm/generate.test.native.js +251 -0
  60. package/dist/esm/generate.test.native.js.map +1 -1
  61. package/dist/esm/helpers/createMutators.js +6 -4
  62. package/dist/esm/helpers/createMutators.js.map +1 -1
  63. package/dist/esm/helpers/createMutators.mjs +6 -4
  64. package/dist/esm/helpers/createMutators.mjs.map +1 -1
  65. package/dist/esm/helpers/createMutators.native.js +13 -6
  66. package/dist/esm/helpers/createMutators.native.js.map +1 -1
  67. package/package.json +2 -2
  68. package/readme.md +110 -2
  69. package/src/createUseQuery.tsx +15 -6
  70. package/src/createZeroClient.tsx +42 -6
  71. package/src/createZeroServer.ts +9 -0
  72. package/src/generate.test.ts +340 -0
  73. package/src/generate.ts +863 -43
  74. package/src/helpers/createMutators.ts +22 -8
  75. package/types/createUseQuery.d.ts +2 -1
  76. package/types/createUseQuery.d.ts.map +1 -1
  77. package/types/createZeroClient.d.ts +10 -1
  78. package/types/createZeroClient.d.ts.map +1 -1
  79. package/types/createZeroServer.d.ts +7 -1
  80. package/types/createZeroServer.d.ts.map +1 -1
  81. package/types/generate.d.ts +1 -0
  82. package/types/generate.d.ts.map +1 -1
  83. package/types/helpers/createMutators.d.ts +3 -1
  84. package/types/helpers/createMutators.d.ts.map +1 -1
@@ -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 lines = q.valibotCode.split(`
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 ts = await import("typescript"),
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
- try {
251
- const typeString = `type QueryParams = ${paramType}`,
252
- model = TypeScriptToModel.Generate(typeString),
253
- valibotCode = ModelToValibot.Generate(model);
254
- allQueries.push({
255
- name,
256
- params: paramType,
257
- valibotCode,
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
- if (filesChanged > 0 && !silent && console.info(`\u2713 ${allModelFiles.length} models (${filesWithSchema.length} schemas)${queryCount ? `, ${queryCount} queries` : ""}`), filesChanged > 0 && after) {
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) {