@nullix/zod-mongoose-studio 3.2.0 → 3.3.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.
Files changed (118) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/_fonts/57NSSoFy1VLVs2gqly8Ls9awBnZMFyXGrefpmqvdqmc-zJfbBtpgM4cDmcXBsqZNW79_kFnlpPd62b48glgdydA.woff2 +0 -0
  3. package/.output/public/_fonts/8VR2wSMN-3U4NbWAVYXlkRV6hA0jFBXP-0RtL3X7fko-x2gYI4qfmkRdxyQQUPaBZdZdgl1TeVrquF_TxHeM4lM.woff2 +0 -0
  4. package/.output/public/_fonts/GsKUclqeNLJ96g5AU593ug6yanivOiwjW_7zESNPChw-jHA4tBeM1bjF7LATGUpfBuSTyomIFrWBTzjF7txVYfg.woff2 +0 -0
  5. package/.output/public/_fonts/Ld1FnTo3yTIwDyGfTQ5-Fws9AWsCbKfMvgxduXr7JcY-W25bL8NF1fjpLRSOgJb7RoZPHqGQNwMTM7S9tHVoxx8.woff2 +0 -0
  6. package/.output/public/_fonts/NdzqRASp2bovDUhQT1IRE_EMqKJ2KYQdTCfFcBvL8yw-KhwZiS86o3fErOe5GGMExHUemmI_dBfaEFxjISZrBd0.woff2 +0 -0
  7. package/.output/public/_fonts/iTkrULNFJJkTvihIg1Vqi5IODRH_9btXCioVF5l98I8-AndUyau2HR2felA_ra8V2mutQgschhasE5FD1dXGJX8.woff2 +0 -0
  8. package/.output/public/_nuxt/{qQyvCkbV.js → 5Tu2WUJz.js} +1 -1
  9. package/.output/public/_nuxt/B0N6xv5E.js +1 -0
  10. package/.output/public/_nuxt/{DjPT-NCE.js → B0cUlOSM.js} +1 -1
  11. package/.output/public/_nuxt/B9paGgN0.js +33 -0
  12. package/.output/public/_nuxt/{IxYCG8hB.js → BCIlnd0g.js} +1 -1
  13. package/.output/public/_nuxt/{ikxafTHA.js → BNDnV2op.js} +180 -184
  14. package/.output/public/_nuxt/{Ce-T_1aD.js → BO4hqv6q.js} +1 -1
  15. package/.output/public/_nuxt/{Bd32GOUy.js → BOsJJ_ZU.js} +1 -1
  16. package/.output/public/_nuxt/{BzhSDy30.js → C2BHp-MG.js} +1 -1
  17. package/.output/public/_nuxt/{Cs8GXJCB.js → CKULsFUz.js} +1 -1
  18. package/.output/public/_nuxt/CMH9iX_E.js +6 -0
  19. package/.output/public/_nuxt/{tzj4nvDE.js → CzbCpNnc.js} +2 -2
  20. package/.output/public/_nuxt/{CC3n3Aga.js → D87QMS6j.js} +1 -1
  21. package/.output/public/_nuxt/DA0pQuyP.js +27 -0
  22. package/.output/public/_nuxt/{B7P0wQ35.js → DcJ7oPvZ.js} +1 -1
  23. package/.output/public/_nuxt/{Ick_23I4.js → Dce9SnGa.js} +1 -1
  24. package/.output/public/_nuxt/{CAd-qHfI.js → DpKrXpkK.js} +1 -1
  25. package/.output/public/_nuxt/DsnIpnT9.js +1 -0
  26. package/.output/public/_nuxt/{CNZorjDf.js → I3Ucam2s.js} +1 -1
  27. package/.output/public/_nuxt/builds/latest.json +1 -1
  28. package/.output/public/_nuxt/builds/meta/51f1351c-0667-4235-9ddc-16582a51ce03.json +1 -0
  29. package/.output/public/_nuxt/{entry.J_CbC0oY.css → entry.B-vs1tRH.css} +1 -1
  30. package/.output/public/_nuxt/error-404.CTSXlFYa.css +1 -0
  31. package/.output/public/_nuxt/error-500.DqEfQnVZ.css +1 -0
  32. package/.output/public/_nuxt/{nDZOJ7rN.js → o-NCh2rp.js} +2 -2
  33. package/.output/public/_nuxt/{qwxNOyg7.js → sKEJiAHO.js} +1 -1
  34. package/.output/public/_nuxt/{81vqh61j.js → tqQNlhT9.js} +1 -1
  35. package/.output/public/_nuxt/ypRHVLFO.js +1 -0
  36. package/.output/server/chunks/_/error-500.mjs.map +1 -1
  37. package/.output/server/chunks/build/client.precomputed.mjs +1 -1
  38. package/.output/server/chunks/build/{error-404-BN-7rNE4.mjs → error-404-KRkA0DPI.mjs} +10 -4
  39. package/.output/server/chunks/build/error-404-KRkA0DPI.mjs.map +1 -0
  40. package/.output/server/chunks/build/error-404-styles.I0NYw-Bn.mjs +8 -0
  41. package/.output/server/chunks/build/error-404-styles.I0NYw-Bn.mjs.map +1 -0
  42. package/.output/server/chunks/build/{error-500-DhIQ-2yt.mjs → error-500-DIIFf55p.mjs} +10 -4
  43. package/.output/server/chunks/build/error-500-DIIFf55p.mjs.map +1 -0
  44. package/.output/server/chunks/build/error-500-styles.D1cs9hQy.mjs +8 -0
  45. package/.output/server/chunks/build/error-500-styles.D1cs9hQy.mjs.map +1 -0
  46. package/.output/server/chunks/build/index-CB1N4gax.mjs +557 -0
  47. package/.output/server/chunks/build/index-CB1N4gax.mjs.map +1 -0
  48. package/.output/server/chunks/build/server.mjs +326 -248
  49. package/.output/server/chunks/build/server.mjs.map +1 -1
  50. package/.output/server/chunks/build/styles.mjs +4 -4
  51. package/.output/server/chunks/nitro/nitro.mjs +2302 -835
  52. package/.output/server/chunks/nitro/nitro.mjs.map +1 -1
  53. package/.output/server/chunks/routes/api/editor-types.get.mjs +44 -0
  54. package/.output/server/chunks/routes/api/editor-types.get.mjs.map +1 -0
  55. package/.output/server/chunks/routes/api/parse.post.mjs +26 -1316
  56. package/.output/server/chunks/routes/api/parse.post.mjs.map +1 -1
  57. package/.output/server/chunks/routes/api/resolve.get.mjs +6 -0
  58. package/.output/server/chunks/routes/api/resolve.get.mjs.map +1 -1
  59. package/.output/server/chunks/routes/api/studio/run.post.mjs +35 -0
  60. package/.output/server/chunks/routes/api/studio/run.post.mjs.map +1 -0
  61. package/.output/server/chunks/routes/renderer.mjs +1 -1
  62. package/.output/server/chunks/routes/renderer.mjs.map +1 -1
  63. package/.output/server/index.mjs +7 -1
  64. package/.output/server/index.mjs.map +1 -1
  65. package/.output/server/node_modules/acorn/dist/acorn.mjs +6266 -0
  66. package/.output/server/node_modules/acorn/package.json +50 -0
  67. package/.output/server/node_modules/ip-address/dist/common.js +112 -4
  68. package/.output/server/node_modules/ip-address/dist/ipv4.js +322 -87
  69. package/.output/server/node_modules/ip-address/dist/ipv6.js +654 -199
  70. package/.output/server/node_modules/ip-address/dist/v4/constants.js +45 -2
  71. package/.output/server/node_modules/ip-address/dist/v6/constants.js +55 -3
  72. package/.output/server/node_modules/ip-address/dist/v6/helpers.js +12 -3
  73. package/.output/server/node_modules/ip-address/package.json +49 -37
  74. package/.output/server/node_modules/ufo/dist/index.mjs +2 -2
  75. package/.output/server/node_modules/ufo/package.json +9 -9
  76. package/.output/server/package.json +4 -3
  77. package/README.md +62 -19
  78. package/app/assets/css/main.css +1 -0
  79. package/app/assets/css/studio.css +131 -0
  80. package/app/components/StudioConversion.vue +41 -0
  81. package/app/components/StudioDataPanel.vue +21 -0
  82. package/app/components/StudioEditor.vue +78 -102
  83. package/app/components/StudioExpandButton.vue +11 -0
  84. package/app/components/StudioOutput.vue +7 -1
  85. package/app/components/StudioSourcePanel.vue +16 -0
  86. package/app/components/StudioValidation.vue +47 -0
  87. package/app/components/ZodMongooseStudio.vue +216 -198
  88. package/app/composables/useStudioRunner.ts +142 -0
  89. package/app/pages/index.vue +1 -1
  90. package/app/plugins/monaco.client.ts +2 -0
  91. package/bin/index.mjs +5 -0
  92. package/nuxt.config.ts +4 -3
  93. package/package.json +4 -2
  94. package/server/api/editor-types.get.ts +28 -0
  95. package/server/api/parse.post.ts +17 -175
  96. package/server/api/studio/run.post.ts +17 -0
  97. package/server/middleware/security.ts +29 -7
  98. package/server/utils/example-data.ts +80 -0
  99. package/server/utils/parse-request.ts +28 -0
  100. package/server/utils/runner-client.ts +40 -0
  101. package/server/utils/studio-execution.ts +176 -0
  102. package/.output/public/_nuxt/BD40YYqZ.js +0 -33
  103. package/.output/public/_nuxt/BlQmLjV4.js +0 -1
  104. package/.output/public/_nuxt/CJ4Cv9fY.js +0 -61
  105. package/.output/public/_nuxt/builds/meta/34871c14-b0f4-4898-9ff0-debaff743355.json +0 -1
  106. package/.output/public/_nuxt/error-404.64pZEiDn.css +0 -1
  107. package/.output/public/_nuxt/error-500.DHOpI0Ir.css +0 -1
  108. package/.output/public/_nuxt/rH_O4qXT.js +0 -1
  109. package/.output/server/chunks/build/error-404-BN-7rNE4.mjs.map +0 -1
  110. package/.output/server/chunks/build/error-404-styles.Byx46oLR.mjs +0 -8
  111. package/.output/server/chunks/build/error-404-styles.Byx46oLR.mjs.map +0 -1
  112. package/.output/server/chunks/build/error-500-DhIQ-2yt.mjs.map +0 -1
  113. package/.output/server/chunks/build/error-500-styles.BL3UT0Wa.mjs +0 -8
  114. package/.output/server/chunks/build/error-500-styles.BL3UT0Wa.mjs.map +0 -1
  115. package/.output/server/chunks/build/index-B4T6zHp0.mjs +0 -1904
  116. package/.output/server/chunks/build/index-B4T6zHp0.mjs.map +0 -1
  117. package/.output/server/node_modules/zod/index.js +0 -4
  118. /package/.output/public/_nuxt/{editor.CdBKkuhR.css → monaco.CdBKkuhR.css} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nullix/zod-mongoose-studio",
3
- "version": "3.2.0",
3
+ "version": "3.3.1",
4
4
  "description": "Monaco-powered TypeScript playground for @nullix/zod-mongoose",
5
5
  "main": "./nuxt.config.ts",
6
6
  "bin": {
@@ -39,12 +39,13 @@
39
39
  "provenance": true
40
40
  },
41
41
  "dependencies": {
42
+ "acorn": "8.16.0",
42
43
  "esbuild": "0.28.0",
43
44
  "get-port-please": "3.2.0",
44
45
  "mongoose": "9.6.3",
45
46
  "open": "11.0.0",
46
47
  "zod": "4.3.6",
47
- "@nullix/zod-mongoose": "3.2.0"
48
+ "@nullix/zod-mongoose": "3.3.1"
48
49
  },
49
50
  "devDependencies": {
50
51
  "@nuxt/ui": "4.6.0",
@@ -61,6 +62,7 @@
61
62
  "dev": "nuxt dev -p 34003",
62
63
  "generate": "nuxt generate",
63
64
  "preview": "nuxt preview",
65
+ "test": "node --experimental-strip-types --test test/*.test.ts",
64
66
  "postinstall": "nuxt prepare"
65
67
  }
66
68
  }
@@ -0,0 +1,28 @@
1
+ import { readdir, readFile } from "node:fs/promises";
2
+ import { dirname, join, relative } from "node:path";
3
+ import { createRequire } from "node:module";
4
+ import { setHeader } from "h3";
5
+
6
+ const require = createRequire(import.meta.url);
7
+ const zodRoot = dirname(dirname(require.resolve("zod/v4")));
8
+ let cachedTypes: Promise<Record<string, string>> | undefined;
9
+
10
+ async function collect(directory: string, files: Record<string, string>) {
11
+ for (const entry of await readdir(directory, { withFileTypes: true })) {
12
+ const path = join(directory, entry.name);
13
+ if (entry.isDirectory()) await collect(path, files);
14
+ else if (entry.name.endsWith(".d.ts")) {
15
+ files[`file:///node_modules/zod/${relative(zodRoot, path).replaceAll("\\", "/")}`] = await readFile(path, "utf8");
16
+ }
17
+ }
18
+ }
19
+
20
+ export default defineEventHandler((event) => {
21
+ setHeader(event, "Cache-Control", "public, max-age=3600");
22
+ cachedTypes ||= (async () => {
23
+ const files: Record<string, string> = {};
24
+ await collect(join(zodRoot, "v4"), files);
25
+ return files;
26
+ })();
27
+ return cachedTypes;
28
+ });
@@ -1,187 +1,29 @@
1
- import { defineEventHandler, readBody, createError } from "h3";
2
- import vm from "node:vm";
3
- import { transformSync } from "esbuild";
4
- import util from "node:util";
5
- import { fileURLToPath } from "node:url";
6
- import { dirname } from "node:path";
1
+ import { defineEventHandler, createError } from "h3";
2
+ import { executeStudio, StudioExecutionError } from "../utils/studio-execution";
3
+ import { readStudioRequest } from "../utils/parse-request";
7
4
 
8
- import * as zod from "zod";
9
- import * as mongoose from "mongoose";
10
- import * as mongooseZod from "@nullix/zod-mongoose";
11
-
12
- // Initialize Mongoose in ESM environment
13
- mongooseZod.setMongoose(mongoose.default || mongoose);
14
-
15
- // Shim __filename and __dirname for ESM environments (Nitro)
16
- let _filename = "/sandbox/main.ts";
17
- let _dirname = "/sandbox";
18
-
19
- try {
20
- _filename = fileURLToPath(import.meta.url);
21
- _dirname = dirname(_filename);
22
- } catch {
23
- // Fallback if import.meta.url is not available or valid in the current context
24
- }
25
-
26
- // The converter exposes a module-level Hookable instance. User code can add
27
- // hooks to it, so parse requests must not overlap and must always clean it up
28
- // before the next request starts.
5
+ const MAX_PENDING_PARSES = 3;
29
6
  let parseQueue = Promise.resolve();
7
+ let pendingParses = 0;
30
8
 
31
- async function acquireParseLock(): Promise<() => void> {
9
+ async function acquireLocalLock(): Promise<() => void> {
10
+ if (pendingParses >= MAX_PENDING_PARSES) throw createError({ statusCode: 503, message: "Studio is busy. Try again shortly." });
11
+ pendingParses++;
32
12
  const previous = parseQueue;
33
13
  let release!: () => void;
34
-
35
- parseQueue = new Promise<void>((resolve) => {
36
- release = resolve;
37
- });
38
-
14
+ parseQueue = new Promise<void>((resolve) => { release = () => { pendingParses--; resolve(); }; });
39
15
  await previous;
40
16
  return release;
41
17
  }
42
18
 
43
- function getErrorMessage(error: unknown): string {
44
- if (error instanceof Error) return error.message || error.name;
45
- return String(error);
46
- }
47
-
48
- // @ts-ignore
49
- if (typeof __filename === "undefined") {
50
- // @ts-ignore
51
- globalThis.__filename = _filename;
52
- }
53
- // @ts-ignore
54
- if (typeof __dirname === "undefined") {
55
- // @ts-ignore
56
- globalThis.__dirname = _dirname;
57
- }
58
-
59
19
  export default defineEventHandler(async (event) => {
60
- const config = useRuntimeConfig(event);
61
- const isDocsMode = config.public.isDocsMode;
62
- const { sourceCode } = await readBody(event);
63
-
64
- if (!sourceCode) {
65
- throw createError({ statusCode: 400, message: "Missing sourceCode" });
66
- }
67
-
68
- const releaseParseLock = await acquireParseLock();
69
-
70
- // 1. Transpile exactly what the user wrote
71
- try {
72
- // A user hook can throw before the converter has completed. Since hooks are
73
- // global within the server process, cleanup must happen for both success and
74
- // failure, otherwise the user's hook would affect every later visitor.
75
- mongooseZod.hooks.removeAllHooks();
76
-
77
- let jsCode: string;
78
- try {
79
- jsCode = transformSync(sourceCode, {
80
- loader: "ts",
81
- format: "cjs",
82
- target: "esnext",
83
- // Provide these globals during transpilation just in case
84
- define: {
85
- __filename: '"/sandbox/main.ts"',
86
- __dirname: '"/sandbox"',
87
- },
88
- }).code;
89
- } catch (error: unknown) {
90
- throw createError({
91
- statusCode: 400,
92
- message: `Compilation Error: ${getErrorMessage(error)}`,
93
- });
94
- }
95
- // 2. Map imports securely
96
- const sandboxRequire = (moduleName: string) => {
97
- if (moduleName === "zod" || moduleName === "zod/v4") return zod;
98
- if (moduleName === "@nullix/zod-mongoose") return mongooseZod;
99
- if (moduleName === "mongoose") return mongoose.default || mongoose;
100
- throw new Error(`Security Exception: Module "${moduleName}" is blocked.`);
101
- };
102
-
103
- const sandbox = {
104
- require: sandboxRequire,
105
- console: { log: () => {}, warn: () => {}, error: () => {} },
106
- __filename: "/sandbox/main.ts",
107
- __dirname: "/sandbox",
108
- };
109
- const context = vm.createContext(sandbox);
110
-
111
- const script = new vm.Script(`
112
- ((__filename, __dirname) => {
113
- let exports = {};
114
- let module = { exports };
115
-
116
- ${jsCode}
117
-
118
- return module.exports;
119
- })("/sandbox/main.ts", "/sandbox");
120
- `);
121
-
122
- // 3. Execute code and grab the default export
123
- const rawExport = script.runInContext(context, {
124
- timeout: isDocsMode ? 1000 : 5000,
125
- });
126
-
127
- const schema = rawExport.default || rawExport;
128
-
129
- if (!schema || !schema._def) {
130
- throw new Error("You must `export default` a valid Zod schema.");
131
- }
132
-
133
- // 4. Run BOTH functions on the user's schema
134
- const extractedDef = mongooseZod.extractMongooseDef(schema);
135
- const mongooseSchema = mongooseZod.toMongooseSchema(schema, { modelName: 'ParsedSchema' });
136
-
137
- // Format the outputs nicely
138
- const formatOpts = { depth: null, showHidden: false, colors: false };
139
-
140
- // Mongoose schemas are massive class instances, so we extract the raw .obj definition to display
141
- const formatValue = (val: any): any => {
142
- if (typeof val === "function") {
143
- return val.name || val.toString();
144
- }
145
- if (Array.isArray(val)) {
146
- return val.map(formatValue);
147
- }
148
- if (
149
- val !== null &&
150
- typeof val === "object" &&
151
- val.constructor === Object
152
- ) {
153
- const formatted: any = {};
154
- for (const key in val) {
155
- formatted[key] = formatValue(val[key]);
156
- }
157
- return formatted;
158
- }
159
- return val;
160
- };
161
-
162
- const formattedDef = formatValue(extractedDef);
163
- // schemaObj is a Mongoose Schema instance, we only care about its .obj for display
164
- const formattedSchema = formatValue(mongooseSchema.obj);
165
- // @ts-ignore
166
- const formattedTree = formatValue(mongooseSchema.tree);
167
- // @ts-ignore
168
- const formattedPaths = formatValue(mongooseSchema.paths);
169
-
170
- return {
171
- definition: util.inspect(formattedDef, formatOpts),
172
- schemaObj: util.inspect(formattedSchema, formatOpts),
173
- schemaTree: util.inspect(formattedTree, formatOpts),
174
- schemaPaths: util.inspect(formattedPaths, formatOpts),
175
- };
176
- } catch (err: any) {
177
- const message = getErrorMessage(err);
178
- const isCompilationError = err?.statusCode === 400;
179
- throw createError({
180
- statusCode: isCompilationError ? 400 : 500,
181
- message: isCompilationError ? message : `Execution Error: ${message}`,
182
- });
183
- } finally {
184
- mongooseZod.hooks.removeAllHooks();
185
- releaseParseLock();
20
+ if (useRuntimeConfig(event).public.isDocsMode) {
21
+ throw createError({ statusCode: 403, message: "Local Studio execution is unavailable in docs mode." });
186
22
  }
23
+ const body = await readStudioRequest(event);
24
+ const release = await acquireLocalLock();
25
+ try { return executeStudio(body, 5000); }
26
+ catch (error: any) {
27
+ throw createError({ statusCode: error instanceof StudioExecutionError ? 400 : 500, message: error?.message || "Studio execution failed." });
28
+ } finally { release(); }
187
29
  });
@@ -0,0 +1,17 @@
1
+ import { createError, defineEventHandler } from "h3";
2
+ import { readStudioRequest } from "../../utils/parse-request";
3
+ import { runInContainer } from "../../utils/runner-client";
4
+
5
+ export default defineEventHandler(async (event) => {
6
+ if (!useRuntimeConfig(event).public.isDocsMode) {
7
+ throw createError({ statusCode: 403, message: "Use the local Studio endpoint in CLI mode." });
8
+ }
9
+ const socketPath = process.env.STUDIO_RUNNER_SOCKET;
10
+ if (!socketPath) throw createError({ statusCode: 503, message: "Studio runner is not configured." });
11
+ const body = await readStudioRequest(event);
12
+ try { return await runInContainer(body, socketPath); }
13
+ catch (error: any) {
14
+ if (error?.statusCode) throw error;
15
+ throw createError({ statusCode: 503, message: "Studio runner is unavailable. Try again shortly." });
16
+ }
17
+ });
@@ -1,14 +1,25 @@
1
- import { defineEventHandler, createError, getRequestIP } from "h3";
1
+ import { defineEventHandler, createError, getHeader, getRequestIP, setHeader } from "h3";
2
2
 
3
3
  // Simple in-memory rate limiting map: IP -> array of timestamps
4
4
  const rateLimitMap = new Map<string, number[]>();
5
- const RATE_LIMIT = 2; // 2 requests
5
+ const RATE_LIMIT = 8; // Allow quick edits and shortcut runs without throttling normal use.
6
6
  const TIME_WINDOW = 1000; // per 1 second (1000ms)
7
+ const GLOBAL_RATE_LIMIT = 20;
8
+ let globalWindowStart = 0;
9
+ let globalRequests = 0;
7
10
 
8
11
  export default defineEventHandler((event) => {
9
- const isDocsMode = process.env.DOCS_MODE === "true";
12
+ const isDocsMode = useRuntimeConfig(event).public.isDocsMode;
10
13
  const path = event.path;
11
14
 
15
+ if (!isDocsMode && path.startsWith("/api/")) {
16
+ const host = getHeader(event, "host") || "";
17
+ const origin = getHeader(event, "origin");
18
+ const localHost = /^(localhost|127\.0\.0\.1)(:\d+)?$/.test(host);
19
+ const localOrigin = !origin || /^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/.test(origin);
20
+ if (!localHost || !localOrigin) throw createError({ statusCode: 403, message: "Local Studio accepts only loopback requests." });
21
+ }
22
+
12
23
  // 1. Environment Check: Disable local resolving in Docs mode
13
24
  if (isDocsMode && path.startsWith("/api/resolve")) {
14
25
  throw createError({
@@ -17,8 +28,8 @@ export default defineEventHandler((event) => {
17
28
  });
18
29
  }
19
30
 
20
- // 2. Rate Limiting for Parse endpoint in Docs Mode
21
- if (isDocsMode && path.startsWith("/api/parse")) {
31
+ // 2. Rate limiting for the parse endpoint in Docs Mode
32
+ if (isDocsMode && path === "/api/studio/run") {
22
33
  const ip = getRequestIP(event) || "unknown";
23
34
  const now = Date.now();
24
35
 
@@ -27,14 +38,25 @@ export default defineEventHandler((event) => {
27
38
  // Filter timestamps within the current window
28
39
  const recentRequests = timestamps.filter((t) => now - t < TIME_WINDOW);
29
40
 
30
- if (recentRequests.length >= RATE_LIMIT) {
41
+ if (now - globalWindowStart >= TIME_WINDOW) {
42
+ globalWindowStart = now;
43
+ globalRequests = 0;
44
+ }
45
+ if (rateLimitMap.size > 1024) {
46
+ for (const [key, entries] of rateLimitMap) {
47
+ if (entries.every((time) => now - time >= TIME_WINDOW)) rateLimitMap.delete(key);
48
+ }
49
+ }
50
+ if (recentRequests.length >= RATE_LIMIT || globalRequests >= GLOBAL_RATE_LIMIT || (rateLimitMap.size >= 1024 && !rateLimitMap.has(ip))) {
51
+ setHeader(event, "Retry-After", 1);
31
52
  throw createError({
32
53
  statusCode: 429,
33
- message: "Too Many Requests. Limit is 2 requests per second.",
54
+ message: "Too many runs. Wait a moment and try again.",
34
55
  });
35
56
  }
36
57
 
37
58
  recentRequests.push(now);
38
59
  rateLimitMap.set(ip, recentRequests);
60
+ globalRequests++;
39
61
  }
40
62
  });
@@ -0,0 +1,80 @@
1
+ import type { ZodType } from "zod/v4";
2
+ import { getMongooseMeta } from "@nullix/zod-mongoose";
3
+
4
+ type Schema = ZodType & { _def: Record<string, any> };
5
+
6
+ function checks(schema: Schema): Record<string, any>[] {
7
+ return (schema._def.checks || []).map((check: any) => check._zod?.def || check.def || {});
8
+ }
9
+
10
+ function sampleString(schema: Schema, key: string): string {
11
+ const rules = checks(schema);
12
+ const format = schema._def.format || rules.find((rule) => rule.format)?.format;
13
+ if (format === "email" || /email/i.test(key)) return "alex@example.com";
14
+ if (format === "uuid") return "123e4567-e89b-42d3-a456-426614174000";
15
+ if (format === "url") return "https://example.com";
16
+ if (format === "datetime") return "2026-01-01T12:00:00.000Z";
17
+ if (format === "date") return "2026-01-01";
18
+
19
+ const minimum = Math.max(1, ...rules.filter((rule) => rule.check === "min_length").map((rule) => rule.minimum));
20
+ const base = /name/i.test(key) ? "Alex Morgan" : /title/i.test(key) ? "Example title" : "example";
21
+ return base.padEnd(Math.min(minimum, 100), "x");
22
+ }
23
+
24
+ function sampleNumber(schema: Schema): number {
25
+ const rules = checks(schema);
26
+ const lower = rules.find((rule) => rule.check === "greater_than");
27
+ const upper = rules.find((rule) => rule.check === "less_than");
28
+ const integer = schema._def.type === "int" || rules.some((rule) => rule.check === "number_format" && rule.format === "safeint");
29
+ const minimum = lower ? Number(lower.value) + (lower.inclusive ? 0 : integer ? 1 : 0.1) : 1;
30
+ const maximum = upper ? Number(upper.value) - (upper.inclusive ? 0 : integer ? 1 : 0.1) : Infinity;
31
+ const candidate = Math.min(Math.max(21, minimum), maximum);
32
+ return integer ? Math.ceil(candidate) : candidate;
33
+ }
34
+
35
+ export function createExample(schema: Schema, key = "value", depth = 0, seen = new Set<Schema>()): unknown {
36
+ if (depth > 8 || seen.has(schema)) throw new Error("This recursive schema needs a handwritten example.");
37
+ const nextSeen = new Set(seen).add(schema);
38
+ const def = schema._def;
39
+ const mongooseType = getMongooseMeta(schema).type as any;
40
+ if (mongooseType === "ObjectId" || mongooseType?.schemaName === "ObjectId") return "507f1f77bcf86cd799439011";
41
+
42
+ switch (def.type) {
43
+ case "optional":
44
+ case "default":
45
+ case "prefault":
46
+ return depth === 0 ? createExample(def.innerType, key, depth + 1, nextSeen) : undefined;
47
+ case "nullable":
48
+ case "nonoptional":
49
+ case "readonly":
50
+ case "catch":
51
+ return createExample(def.innerType, key, depth + 1, nextSeen);
52
+ case "pipe":
53
+ return createExample(def.in, key, depth + 1, nextSeen);
54
+ case "lazy":
55
+ return createExample(def.getter(), key, depth + 1, nextSeen);
56
+ case "object": {
57
+ const result: Record<string, unknown> = {};
58
+ for (const [field, fieldSchema] of Object.entries((schema as any).shape || def.shape || {})) {
59
+ const value = createExample(fieldSchema as Schema, field, depth + 1, nextSeen);
60
+ if (value !== undefined) result[field] = value;
61
+ }
62
+ return result;
63
+ }
64
+ case "array": {
65
+ const minimum = Math.max(0, ...checks(schema).filter((rule) => rule.check === "min_length").map((rule) => rule.minimum));
66
+ return Array.from({ length: Math.min(Math.max(1, minimum), 10) }, () => createExample(def.element, key, depth + 1, nextSeen));
67
+ }
68
+ case "tuple": return def.items.map((item: Schema) => createExample(item, key, depth + 1, nextSeen));
69
+ case "string": return sampleString(schema, key);
70
+ case "number":
71
+ case "int": return sampleNumber(schema);
72
+ case "boolean": return true;
73
+ case "enum": return Object.values(def.entries)[0];
74
+ case "literal": return def.values?.[0];
75
+ case "union": return createExample(def.options[0], key, depth + 1, nextSeen);
76
+ case "record": return { example: createExample(def.valueType, key, depth + 1, nextSeen) };
77
+ case "date": return "2026-01-01T12:00:00.000Z";
78
+ default: throw new Error(`Automatic examples do not yet support ${def.type || "this schema"}.`);
79
+ }
80
+ }
@@ -0,0 +1,28 @@
1
+ import { createError } from "h3";
2
+ import type { H3Event } from "h3";
3
+ import type { ParseBody } from "./studio-execution";
4
+
5
+ const MAX_BODY_BYTES = 160_000;
6
+
7
+ export async function readStudioRequest(event: H3Event): Promise<ParseBody> {
8
+ const chunks: Buffer[] = [];
9
+ let size = 0;
10
+ for await (const chunk of event.node.req) {
11
+ const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
12
+ size += bytes.length;
13
+ if (size > MAX_BODY_BYTES) throw createError({ statusCode: 413, message: "Studio input is too large." });
14
+ chunks.push(bytes);
15
+ }
16
+ let body: ParseBody;
17
+ try { body = JSON.parse(Buffer.concat(chunks).toString("utf8")); }
18
+ catch { throw createError({ statusCode: 400, message: "Invalid JSON request." }); }
19
+ if (typeof body?.sourceCode !== "string" || !body.sourceCode.trim()) {
20
+ throw createError({ statusCode: 400, message: "Add a Zod schema to the editor first." });
21
+ }
22
+ if (body.sourceCode.length > 50_000 || typeof body.inputData !== "string" || body.inputData.length > 100_000 ||
23
+ (body.selectedSchema !== undefined && typeof body.selectedSchema !== "string") ||
24
+ (body.action !== undefined && body.action !== "run" && body.action !== "example")) {
25
+ throw createError({ statusCode: 400, message: "Invalid or oversized Studio input." });
26
+ }
27
+ return body;
28
+ }
@@ -0,0 +1,40 @@
1
+ import { createError } from "h3";
2
+ import { request } from "node:http";
3
+ import type { ParseBody } from "./studio-execution";
4
+
5
+ const MAX_RESPONSE_BYTES = 1_000_000;
6
+
7
+ export function runInContainer(body: ParseBody, socketPath: string): Promise<unknown> {
8
+ const payload = JSON.stringify(body);
9
+ return new Promise((resolve, reject) => {
10
+ const req = request({
11
+ socketPath,
12
+ path: "/run",
13
+ method: "POST",
14
+ headers: { "content-type": "application/json", "content-length": Buffer.byteLength(payload) },
15
+ }, (res) => {
16
+ const chunks: Buffer[] = [];
17
+ let size = 0;
18
+ res.on("data", (chunk: Buffer) => {
19
+ size += chunk.length;
20
+ if (size > MAX_RESPONSE_BYTES) {
21
+ req.destroy(new Error("Studio result is too large."));
22
+ return;
23
+ }
24
+ chunks.push(chunk);
25
+ });
26
+ res.on("end", () => {
27
+ try {
28
+ const result = JSON.parse(Buffer.concat(chunks).toString("utf8"));
29
+ if (res.statusCode !== 200) {
30
+ reject(createError({ statusCode: res.statusCode === 400 ? 400 : res.statusCode === 504 ? 504 : 503, message: result.error || "Studio runner failed." }));
31
+ } else resolve(result);
32
+ } catch { reject(createError({ statusCode: 502, message: "Invalid response from Studio runner." })); }
33
+ });
34
+ res.on("error", reject);
35
+ });
36
+ req.setTimeout(5000, () => req.destroy(new Error("Studio runner timed out.")));
37
+ req.on("error", reject);
38
+ req.end(payload);
39
+ });
40
+ }
@@ -0,0 +1,176 @@
1
+ import vm from "node:vm";
2
+ import { transformSync } from "esbuild";
3
+ import util from "node:util";
4
+ import { parse } from "acorn";
5
+ import * as zod from "zod/v4";
6
+ import mongoose from "mongoose";
7
+ import * as mongooseZod from "@nullix/zod-mongoose";
8
+ import { createExample } from "./example-data";
9
+
10
+ mongooseZod.setMongoose(mongoose);
11
+
12
+ export class StudioExecutionError extends Error {
13
+ statusCode = 400;
14
+ }
15
+
16
+ function errorMessage(error: unknown): string {
17
+ return error instanceof Error ? error.message || error.name : String(error);
18
+ }
19
+
20
+ function declaredNames(jsCode: string): string[] {
21
+ const file = parse(jsCode, { ecmaVersion: "latest", sourceType: "script" });
22
+ const names: string[] = [];
23
+ for (const statement of file.body) {
24
+ if (statement.type !== "VariableDeclaration") continue;
25
+ for (const declaration of statement.declarations) {
26
+ if (declaration.id.type === "Identifier") names.push(declaration.id.name);
27
+ }
28
+ }
29
+ return names;
30
+ }
31
+
32
+ function displayValue(value: unknown): string {
33
+ const seen = new WeakSet<object>();
34
+ return JSON.stringify(value, (_key, item) => {
35
+ if (typeof item === "bigint") return `${item.toString()}n`;
36
+ if (typeof item === "undefined") return "[undefined]";
37
+ if (typeof item === "number" && !Number.isFinite(item)) return String(item);
38
+ if (typeof item === "object" && item !== null) {
39
+ if (item instanceof Date) return item.toISOString();
40
+ if (typeof item.toHexString === "function") return item.toHexString();
41
+ if (seen.has(item)) return "[Circular]";
42
+ seen.add(item);
43
+ }
44
+ return item;
45
+ }, 2) || "undefined";
46
+ }
47
+
48
+ function formatMongoose(value: any): any {
49
+ if (typeof value === "function") return value.name || value.toString();
50
+ if (Array.isArray(value)) return value.map(formatMongoose);
51
+ if (value !== null && typeof value === "object" && value.constructor === Object) {
52
+ return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, formatMongoose(entry)]));
53
+ }
54
+ return value;
55
+ }
56
+
57
+ export type ParseBody = {
58
+ sourceCode?: string;
59
+ inputData?: string;
60
+ selectedSchema?: string;
61
+ action?: "run" | "example";
62
+ };
63
+
64
+ export function executeStudio(body: ParseBody, timeoutMs: number) {
65
+ const sourceCode = body.sourceCode!;
66
+ mongooseZod.hooks.removeAllHooks();
67
+ try {
68
+ let jsCode: string;
69
+ try {
70
+ jsCode = transformSync(sourceCode, { loader: "ts", format: "cjs", target: "esnext" }).code;
71
+ } catch (error) {
72
+ throw new StudioExecutionError(`Compilation error: ${errorMessage(error)}`);
73
+ }
74
+ const names = declaredNames(jsCode);
75
+
76
+ const sandboxRequire = (moduleName: string) => {
77
+ if (moduleName === "zod" || moduleName === "zod/v4") return zod;
78
+ if (moduleName === "@nullix/zod-mongoose") return mongooseZod;
79
+ if (moduleName === "mongoose") return mongoose;
80
+ throw new Error(`Module "${moduleName}" is unavailable in Studio.`);
81
+ };
82
+ const context = vm.createContext({
83
+ ...mongooseZod,
84
+ z: zod.z,
85
+ mongoose,
86
+ require: sandboxRequire,
87
+ console: { log: () => {}, warn: () => {}, error: () => {} },
88
+ __filename: "/sandbox/main.ts",
89
+ __dirname: "/sandbox",
90
+ });
91
+ const captured = names.map((name) => `${JSON.stringify(name)}: typeof ${name} === "undefined" ? undefined : ${name}`).join(",");
92
+ const script = new vm.Script(`
93
+ (() => {
94
+ let exports = {};
95
+ let module = { exports };
96
+ ${jsCode}
97
+ return { exported: module.exports, declared: { ${captured} } };
98
+ })()
99
+ `);
100
+ const result = script.runInContext(context, { timeout: timeoutMs });
101
+ const choices = new Map<string, zod.ZodType>();
102
+ for (const [name, value] of Object.entries(result.declared)) {
103
+ if (value instanceof zod.ZodType && ![...choices.values()].includes(value)) choices.set(name, value);
104
+ }
105
+ for (const [name, value] of Object.entries(result.exported || {})) {
106
+ if (value instanceof zod.ZodType && ![...choices.values()].includes(value)) choices.set(name, value);
107
+ }
108
+ const defaultExport = result.exported instanceof zod.ZodType ? result.exported : result.exported?.default;
109
+ if (defaultExport instanceof zod.ZodType && ![...choices.values()].includes(defaultExport)) choices.set("default", defaultExport);
110
+ if (!choices.size) throw new Error("Define a Zod schema, such as const UserSchema = z.object({ name: z.string() }).");
111
+
112
+ const selectedSchema = body.selectedSchema && choices.has(body.selectedSchema)
113
+ ? body.selectedSchema : choices.keys().next().value as string;
114
+ const schema = choices.get(selectedSchema)!;
115
+ const schemaNames = [...choices.keys()];
116
+
117
+ let example: string | undefined;
118
+ let exampleError: string | undefined;
119
+ let inputData = body.inputData;
120
+ if (body.action === "example") {
121
+ try {
122
+ const fixture = result.declared.exampleInput;
123
+ const generated = fixture !== undefined ? fixture : createExample(schema as any);
124
+ const exampleResult = schema.safeParse(generated);
125
+ if (!exampleResult.success) throw new Error("The example does not satisfy this schema. Add or update a valid const exampleInput for custom rules.");
126
+ example = displayValue(generated);
127
+ inputData = example;
128
+ } catch (error) {
129
+ exampleError = errorMessage(error);
130
+ }
131
+ }
132
+
133
+ let validation: { status: "valid" | "invalid" | "json-error" | "parse-error" | "empty"; parsed?: string; issues?: { path: string; message: string; code: string }[]; message?: string } = { status: "empty" };
134
+ if (typeof inputData !== "string" || !inputData.trim()) {
135
+ validation = { status: "empty", message: "Add JSON data to try this schema." };
136
+ } else {
137
+ let input: unknown;
138
+ let inputValid = false;
139
+ try {
140
+ input = JSON.parse(inputData);
141
+ inputValid = true;
142
+ } catch (error) {
143
+ validation = { status: "json-error", message: errorMessage(error) };
144
+ }
145
+ if (inputValid) {
146
+ try {
147
+ const parsed = schema.safeParse(input);
148
+ validation = parsed.success
149
+ ? { status: "valid", parsed: displayValue(parsed.data) }
150
+ : { status: "invalid", issues: parsed.error.issues.map((issue) => ({ path: issue.path.length ? issue.path.join(".") : "(root)", message: issue.message, code: issue.code })) };
151
+ } catch (error) {
152
+ validation = { status: "parse-error", message: `Validation could not run: ${errorMessage(error)}` };
153
+ }
154
+ }
155
+ }
156
+
157
+ let conversion: { definition?: string; schemaObj?: string; schemaTree?: string; schemaPaths?: string; error?: string };
158
+ try {
159
+ const extracted = mongooseZod.extractMongooseDef(schema);
160
+ const converted = mongooseZod.toMongooseSchema(schema, { modelName: "ParsedSchema" });
161
+ const inspect = (value: unknown) => util.inspect(formatMongoose(value), { depth: null, colors: false, showHidden: false });
162
+ conversion = {
163
+ definition: inspect(extracted),
164
+ schemaObj: inspect(converted.obj),
165
+ schemaTree: inspect((converted as any).tree),
166
+ schemaPaths: inspect((converted as any).paths),
167
+ };
168
+ } catch (error) {
169
+ conversion = { error: errorMessage(error) };
170
+ }
171
+
172
+ return { schemaNames, selectedSchema, validation, conversion, example, exampleError };
173
+ } finally {
174
+ mongooseZod.hooks.removeAllHooks();
175
+ }
176
+ }