bxo 0.0.10-dev.4 → 0.0.10-dev.6

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.
@@ -0,0 +1,155 @@
1
+ # Array Query Parameters in BXO Framework
2
+
3
+ The BXO framework has built-in support for handling arrays from URL search parameters. This guide explains the different ways to pass and receive arrays.
4
+
5
+ ## How It Works
6
+
7
+ The framework automatically parses URL search parameters and converts multiple values with the same key into arrays. This happens in the `parseQuery` function which:
8
+
9
+ 1. **Handles array notation**: Keys ending with `[]` are automatically converted to arrays
10
+ 2. **Multiple values**: When the same key appears multiple times, values are collected into an array
11
+ 3. **Type coercion**: Works with Zod schemas for validation and type conversion
12
+
13
+ ## URL Formats for Arrays
14
+
15
+ ### Method 1: Multiple Parameters with Same Key
16
+ ```
17
+ /api/tags?tags=javascript&tags=typescript&tags=nodejs
18
+ ```
19
+ **Result**: `{ tags: ["javascript", "typescript", "nodejs"] }`
20
+
21
+ ### Method 2: Bracket Notation
22
+ ```
23
+ /api/items?items[]=item1&items[]=item2&items[]=item3
24
+ ```
25
+ **Result**: `{ items: ["item1", "item2", "item3"] }`
26
+
27
+ ### Method 3: Mixed Approach
28
+ ```
29
+ /api/search?categories=tech&categories=web&tags[]=javascript&tags[]=react
30
+ ```
31
+ **Result**: `{ categories: ["tech", "web"], tags: ["javascript", "react"] }`
32
+
33
+ ## Code Examples
34
+
35
+ ### Basic Array Handling
36
+ ```typescript
37
+ app.get("/api/tags", (ctx) => {
38
+ // Access array directly from ctx.query
39
+ const tags = ctx.query.tags; // string[] or string
40
+ return ctx.json({ tags });
41
+ });
42
+ ```
43
+
44
+ ### With Zod Validation
45
+ ```typescript
46
+ app.get("/api/filtered-tags", (ctx) => {
47
+ return ctx.json({ tags: ctx.query.tags });
48
+ }, {
49
+ query: z.object({
50
+ tags: z.array(z.string()).optional(),
51
+ category: z.string().optional()
52
+ })
53
+ });
54
+ ```
55
+
56
+ ### Type Coercion for Numbers
57
+ ```typescript
58
+ app.get("/api/numbers", (ctx) => {
59
+ return ctx.json({ numbers: ctx.query.numbers });
60
+ }, {
61
+ query: z.object({
62
+ numbers: z.array(z.coerce.number()).optional()
63
+ })
64
+ });
65
+ ```
66
+
67
+ ### Complex Validation
68
+ ```typescript
69
+ app.get("/api/advanced-search", (ctx) => {
70
+ return ctx.json({ filters: ctx.query.filters });
71
+ }, {
72
+ query: z.object({
73
+ filters: z.array(z.string()).optional(),
74
+ sort: z.array(z.enum(["name", "date", "price"])).optional(),
75
+ limit: z.coerce.number().min(1).max(100).optional()
76
+ })
77
+ });
78
+ ```
79
+
80
+ ## Framework Implementation Details
81
+
82
+ The array parsing happens in the `parseQuery` function:
83
+
84
+ ```typescript
85
+ function parseQuery(searchParams: URLSearchParams, schema?: z.ZodTypeAny): any {
86
+ const out: QueryObject = {};
87
+ for (const [k, v] of searchParams.entries()) {
88
+ // Handle array notation like fields[] -> fields
89
+ const key = k.endsWith('[]') ? k.slice(0, -2) : k;
90
+
91
+ if (key in out) {
92
+ const existing = out[key];
93
+ if (Array.isArray(existing)) out[key] = [...existing, v];
94
+ else out[key] = [existing as string, v];
95
+ } else out[key] = v;
96
+ }
97
+ // ... schema validation
98
+ return out;
99
+ }
100
+ ```
101
+
102
+ ## Key Features
103
+
104
+ 1. **Automatic Array Detection**: Multiple values with the same key become arrays
105
+ 2. **Bracket Notation Support**: `param[]` syntax is supported
106
+ 3. **Mixed Types**: Single values and arrays can coexist
107
+ 4. **Zod Integration**: Full validation and type coercion support
108
+ 5. **Type Safety**: TypeScript support with proper typing
109
+
110
+ ## Common Use Cases
111
+
112
+ ### Filtering and Search
113
+ ```
114
+ /api/products?categories=electronics&categories=computers&tags=on-sale&tags=featured
115
+ ```
116
+
117
+ ### Bulk Operations
118
+ ```
119
+ /api/users?ids=1&ids=2&ids=3&ids=4
120
+ ```
121
+
122
+ ### Sorting and Ordering
123
+ ```
124
+ /api/posts?sort=date&sort=title&order=desc&order=asc
125
+ ```
126
+
127
+ ### Complex Queries
128
+ ```
129
+ /api/search?filters=active&filters=featured&categories=tech&categories=web&tags[]=javascript&tags[]=react&limit=10
130
+ ```
131
+
132
+ ## Best Practices
133
+
134
+ 1. **Use consistent naming**: Choose either bracket notation or repeated parameters
135
+ 2. **Validate with Zod**: Always validate array parameters for type safety
136
+ 3. **Handle optional arrays**: Use `.optional()` for arrays that might not be present
137
+ 4. **Type coercion**: Use `z.coerce.number()` for numeric arrays
138
+ 5. **Enum validation**: Use `z.enum()` for restricted value arrays
139
+
140
+ ## Testing Your Arrays
141
+
142
+ You can test array parameters using curl:
143
+
144
+ ```bash
145
+ # Simple array
146
+ curl "http://localhost:3000/api/tags?tags=javascript&tags=typescript"
147
+
148
+ # Bracket notation
149
+ curl "http://localhost:3000/api/items?items[]=item1&items[]=item2"
150
+
151
+ # Mixed approach
152
+ curl "http://localhost:3000/api/search?categories=tech&tags[]=javascript&tags[]=react"
153
+ ```
154
+
155
+ The framework handles all these formats automatically, making it easy to work with arrays in your API endpoints.
@@ -0,0 +1,90 @@
1
+ import BXO, { z } from "../src";
2
+
3
+ async function main() {
4
+ const app = new BXO({ serve: { port: 3000 } });
5
+
6
+ // Example 1: Simple array with multiple values
7
+ app.get("/api/tags", (ctx) => {
8
+ return ctx.json({
9
+ message: "Tags received",
10
+ tags: ctx.query.tags,
11
+ query: ctx.query
12
+ });
13
+ });
14
+
15
+ // Example 2: Array with validation using Zod schema
16
+ app.get("/api/filtered-tags", (ctx) => {
17
+ return ctx.json({
18
+ message: "Filtered tags received",
19
+ tags: ctx.query.tags,
20
+ query: ctx.query
21
+ });
22
+ }, {
23
+ query: z.object({
24
+ tags: z.array(z.string()).optional(),
25
+ category: z.string().optional()
26
+ })
27
+ });
28
+
29
+ // Example 3: Multiple arrays in the same query
30
+ app.get("/api/search", (ctx) => {
31
+ return ctx.json({
32
+ message: "Search parameters received",
33
+ categories: ctx.query.categories,
34
+ tags: ctx.query.tags,
35
+ ids: ctx.query.ids,
36
+ query: ctx.query
37
+ });
38
+ });
39
+
40
+ // Example 4: Array with mixed data types
41
+ app.get("/api/numbers", (ctx) => {
42
+ return ctx.json({
43
+ message: "Numbers received",
44
+ numbers: ctx.query.numbers,
45
+ query: ctx.query
46
+ });
47
+ }, {
48
+ query: z.object({
49
+ numbers: z.array(z.coerce.number()).optional()
50
+ })
51
+ });
52
+
53
+ // Example 5: Nested array-like structure (using bracket notation)
54
+ app.get("/api/items", (ctx) => {
55
+ return ctx.json({
56
+ message: "Items received",
57
+ items: ctx.query.items,
58
+ query: ctx.query
59
+ });
60
+ });
61
+
62
+ // Example 6: Complex query with arrays and validation
63
+ app.get("/api/advanced-search", (ctx) => {
64
+ return ctx.json({
65
+ message: "Advanced search parameters received",
66
+ filters: ctx.query.filters,
67
+ sort: ctx.query.sort,
68
+ limit: ctx.query.limit,
69
+ query: ctx.query
70
+ });
71
+ }, {
72
+ query: z.object({
73
+ filters: z.array(z.string()).optional(),
74
+ sort: z.array(z.enum(["name", "date", "price"])).optional(),
75
+ limit: z.coerce.number().min(1).max(100).optional()
76
+ })
77
+ });
78
+
79
+ app.start();
80
+ console.log("šŸš€ Server running on http://localhost:3000");
81
+ console.log("\nšŸ“ Array Query Examples:");
82
+ console.log("1. Simple array: http://localhost:3000/api/tags?tags=javascript&tags=typescript&tags=nodejs");
83
+ console.log("2. With validation: http://localhost:3000/api/filtered-tags?tags=web&tags=api&category=development");
84
+ console.log("3. Multiple arrays: http://localhost:3000/api/search?categories=tech&categories=web&tags=javascript&tags=react&ids=1&ids=2&ids=3");
85
+ console.log("4. Numbers array: http://localhost:3000/api/numbers?numbers=1&numbers=2&numbers=3&numbers=42");
86
+ console.log("5. Bracket notation: http://localhost:3000/api/items?items[]=item1&items[]=item2&items[]=item3");
87
+ console.log("6. Advanced search: http://localhost:3000/api/advanced-search?filters=active&filters=featured&sort=name&sort=date&limit=10");
88
+ }
89
+
90
+ main();
@@ -0,0 +1,90 @@
1
+ import BXO, { z } from "../src";
2
+
3
+ async function main() {
4
+ console.log("šŸš€ Testing BXO autoArrayParse feature\n");
5
+
6
+ // Example 1: Default behavior (autoArrayParse: true)
7
+ console.log("šŸ“ Example 1: Default autoArrayParse (enabled)");
8
+ const app1 = new BXO({ serve: { port: 3001 } }); // Default: autoArrayParse: true
9
+
10
+ app1.get("/api/tags", (ctx) => {
11
+ return ctx.json({
12
+ message: "Default autoArrayParse - always arrays",
13
+ tags: ctx.query.tags,
14
+ isArray: Array.isArray(ctx.query.tags),
15
+ query: ctx.query
16
+ });
17
+ });
18
+
19
+ // Example 2: Explicitly enabled
20
+ console.log("šŸ“ Example 2: Explicitly enabled autoArrayParse");
21
+ const app2 = new BXO({
22
+ serve: { port: 3002 },
23
+ autoArrayParse: true
24
+ });
25
+
26
+ app2.get("/api/tags", (ctx) => {
27
+ return ctx.json({
28
+ message: "Explicitly enabled autoArrayParse",
29
+ tags: ctx.query.tags,
30
+ isArray: Array.isArray(ctx.query.tags),
31
+ query: ctx.query
32
+ });
33
+ });
34
+
35
+ // Example 3: Disabled
36
+ console.log("šŸ“ Example 3: Disabled autoArrayParse");
37
+ const app3 = new BXO({
38
+ serve: { port: 3003 },
39
+ autoArrayParse: false
40
+ });
41
+
42
+ app3.get("/api/tags", (ctx) => {
43
+ return ctx.json({
44
+ message: "Disabled autoArrayParse - original behavior",
45
+ tags: ctx.query.tags,
46
+ isArray: Array.isArray(ctx.query.tags),
47
+ query: ctx.query
48
+ });
49
+ });
50
+
51
+ // Example 4: With Zod validation
52
+ console.log("šŸ“ Example 4: With Zod validation");
53
+ const app4 = new BXO({
54
+ serve: { port: 3004 },
55
+ autoArrayParse: true
56
+ });
57
+
58
+ app4.get("/api/validated-tags", (ctx) => {
59
+ return ctx.json({
60
+ message: "Validated with autoArrayParse",
61
+ tags: ctx.query.tags,
62
+ isArray: Array.isArray(ctx.query.tags),
63
+ query: ctx.query
64
+ });
65
+ }, {
66
+ query: z.object({
67
+ tags: z.array(z.string()).optional()
68
+ })
69
+ });
70
+
71
+ // Start all servers
72
+ app1.start();
73
+ app2.start();
74
+ app3.start();
75
+ app4.start();
76
+
77
+ console.log("āœ… All servers started!");
78
+ console.log("\n🧪 Test URLs:");
79
+ console.log("1. Default (enabled): http://localhost:3001/api/tags?tags=javascript");
80
+ console.log("2. Explicit (enabled): http://localhost:3002/api/tags?tags=javascript");
81
+ console.log("3. Disabled: http://localhost:3003/api/tags?tags=javascript");
82
+ console.log("4. With validation: http://localhost:3004/api/validated-tags?tags=javascript");
83
+ console.log("\nšŸ” Multiple values:");
84
+ console.log("1. Default: http://localhost:3001/api/tags?tags=javascript&tags=typescript");
85
+ console.log("2. Explicit: http://localhost:3002/api/tags?tags=javascript&tags=typescript");
86
+ console.log("3. Disabled: http://localhost:3003/api/tags?tags=javascript&tags=typescript");
87
+ console.log("4. With validation: http://localhost:3004/api/validated-tags?tags=javascript&tags=typescript");
88
+ }
89
+
90
+ main();
@@ -0,0 +1,100 @@
1
+ import BXO, { z } from "../src";
2
+
3
+ async function main() {
4
+ const app = new BXO({ serve: { port: 3000 } });
5
+
6
+ // RECOMMENDED APPROACH: Always parse to array if we expect array
7
+ app.get("/api/tags", (ctx) => {
8
+ return ctx.json({
9
+ message: "Tags endpoint - always returns array",
10
+ tags: ctx.query.tags,
11
+ count: ctx.query.tags?.length || 0,
12
+ isArray: Array.isArray(ctx.query.tags)
13
+ });
14
+ }, {
15
+ query: z.object({
16
+ tags: z.union([
17
+ z.string(),
18
+ z.array(z.string())
19
+ ]).transform(val => Array.isArray(val) ? val : [val]).optional()
20
+ })
21
+ });
22
+
23
+ // Alternative: Handle in code (also good approach)
24
+ app.get("/api/categories", (ctx) => {
25
+ // Normalize to array in code
26
+ let categories = ctx.query.categories;
27
+ if (categories && !Array.isArray(categories)) {
28
+ categories = [categories];
29
+ }
30
+
31
+ return ctx.json({
32
+ message: "Categories endpoint - normalized to array",
33
+ categories: categories || [],
34
+ count: categories?.length || 0,
35
+ isArray: Array.isArray(categories)
36
+ });
37
+ });
38
+
39
+ // For required arrays with default
40
+ app.get("/api/filters", (ctx) => {
41
+ return ctx.json({
42
+ message: "Filters endpoint - required array",
43
+ filters: ctx.query.filters,
44
+ count: ctx.query.filters.length,
45
+ isArray: Array.isArray(ctx.query.filters)
46
+ });
47
+ }, {
48
+ query: z.object({
49
+ filters: z.union([
50
+ z.string(),
51
+ z.array(z.string())
52
+ ]).transform(val => Array.isArray(val) ? val : [val]).default([])
53
+ })
54
+ });
55
+
56
+ // Complex example with multiple array parameters
57
+ app.get("/api/search", (ctx) => {
58
+ return ctx.json({
59
+ message: "Search endpoint with multiple arrays",
60
+ categories: ctx.query.categories,
61
+ tags: ctx.query.tags,
62
+ ids: ctx.query.ids,
63
+ categoryCount: ctx.query.categories?.length || 0,
64
+ tagCount: ctx.query.tags?.length || 0,
65
+ idCount: ctx.query.ids?.length || 0
66
+ });
67
+ }, {
68
+ query: z.object({
69
+ categories: z.union([
70
+ z.string(),
71
+ z.array(z.string())
72
+ ]).transform(val => Array.isArray(val) ? val : [val]).optional(),
73
+ tags: z.union([
74
+ z.string(),
75
+ z.array(z.string())
76
+ ]).transform(val => Array.isArray(val) ? val : [val]).optional(),
77
+ ids: z.union([
78
+ z.string(),
79
+ z.array(z.string())
80
+ ]).transform(val => Array.isArray(val) ? val : [val]).optional()
81
+ })
82
+ });
83
+
84
+ app.start();
85
+ console.log("šŸš€ Server running on http://localhost:3000");
86
+ console.log("\nšŸ“ RECOMMENDED Array Examples:");
87
+ console.log("āœ… BEST: Single value -> array: http://localhost:3000/api/tags?tags=javascript");
88
+ console.log("āœ… BEST: Multiple values -> array: http://localhost:3000/api/tags?tags=javascript&tags=typescript");
89
+ console.log("āœ… BEST: No params: http://localhost:3000/api/tags");
90
+ console.log("");
91
+ console.log("āœ… Alternative: Code normalization: http://localhost:3000/api/categories?categories=web");
92
+ console.log("āœ… Alternative: Multiple: http://localhost:3000/api/categories?categories=web&categories=mobile");
93
+ console.log("");
94
+ console.log("āœ… Required with default: http://localhost:3000/api/filters");
95
+ console.log("āœ… Required with value: http://localhost:3000/api/filters?filters=active");
96
+ console.log("");
97
+ console.log("āœ… Complex search: http://localhost:3000/api/search?categories=tech&tags=javascript&ids=1&ids=2");
98
+ }
99
+
100
+ main();
@@ -0,0 +1,217 @@
1
+ # Single Item Arrays in BXO Framework
2
+
3
+ When you have only one item in what would normally be an array, the BXO framework's behavior depends on how you structure your URL parameters.
4
+
5
+ ## Key Behavior
6
+
7
+ ### **Single Value (No Bracket Notation)**
8
+ ```
9
+ /api/tags?tag=javascript
10
+ ```
11
+ **Result**: `{ tag: "javascript" }` (string, not array)
12
+
13
+ ### **Single Value with Bracket Notation**
14
+ ```
15
+ /api/tags?tag[]=javascript
16
+ ```
17
+ **Result**: `{ tag: ["javascript"] }` (array with one item)
18
+
19
+ ### **Multiple Values**
20
+ ```
21
+ /api/tags?tag=javascript&tag=typescript
22
+ ```
23
+ **Result**: `{ tag: ["javascript", "typescript"] }` (array)
24
+
25
+ ## Framework Logic
26
+
27
+ The framework's `parseQuery` function works like this:
28
+
29
+ ```typescript
30
+ if (key in out) {
31
+ const existing = out[key];
32
+ if (Array.isArray(existing)) out[key] = [...existing, v];
33
+ else out[key] = [existing as string, v]; // Convert to array on second occurrence
34
+ } else out[key] = v; // First occurrence stays as string
35
+ ```
36
+
37
+ **This means:**
38
+ - **First occurrence**: Single value remains a string
39
+ - **Second occurrence**: Converts to array
40
+ - **Bracket notation**: Always creates array
41
+
42
+ ## Handling Single Item Arrays
43
+
44
+ ### **Method 1: Always Use Bracket Notation**
45
+ ```typescript
46
+ // URL: /api/tags?tag[]=javascript
47
+ app.get("/api/tags", (ctx) => {
48
+ const tags = ctx.query.tag; // Always array: ["javascript"]
49
+ return ctx.json({ tags });
50
+ });
51
+ ```
52
+
53
+ ### **Method 2: Handle Both Cases in Code**
54
+ ```typescript
55
+ app.get("/api/tags", (ctx) => {
56
+ let tags = ctx.query.tag;
57
+
58
+ // Normalize to array
59
+ if (!Array.isArray(tags)) {
60
+ tags = [tags];
61
+ }
62
+
63
+ return ctx.json({ tags });
64
+ });
65
+ ```
66
+
67
+ ### **Method 3: Use Zod Union Type**
68
+ ```typescript
69
+ app.get("/api/tags", (ctx) => {
70
+ return ctx.json({ tags: ctx.query.tags });
71
+ }, {
72
+ query: z.object({
73
+ tags: z.union([
74
+ z.string(),
75
+ z.array(z.string())
76
+ ]).optional()
77
+ })
78
+ });
79
+ ```
80
+
81
+ ### **Method 4: Force Array with Zod Transform**
82
+ ```typescript
83
+ app.get("/api/tags", (ctx) => {
84
+ return ctx.json({ tags: ctx.query.tags });
85
+ }, {
86
+ query: z.object({
87
+ tags: z.union([
88
+ z.string(),
89
+ z.array(z.string())
90
+ ]).transform(val => Array.isArray(val) ? val : [val]).optional()
91
+ })
92
+ });
93
+ ```
94
+
95
+ ### **Method 5: Default Empty Array**
96
+ ```typescript
97
+ app.get("/api/tags", (ctx) => {
98
+ return ctx.json({ tags: ctx.query.tags });
99
+ }, {
100
+ query: z.object({
101
+ tags: z.array(z.string()).default([])
102
+ })
103
+ });
104
+ ```
105
+
106
+ ## Real-World Examples
107
+
108
+ ### **Search with Optional Filters**
109
+ ```typescript
110
+ // URL: /api/search?filters=active
111
+ // URL: /api/search?filters=active&filters=featured
112
+ app.get("/api/search", (ctx) => {
113
+ let filters = ctx.query.filters;
114
+
115
+ // Normalize to array
116
+ if (filters && !Array.isArray(filters)) {
117
+ filters = [filters];
118
+ }
119
+
120
+ return ctx.json({
121
+ filters: filters || [],
122
+ count: filters?.length || 0
123
+ });
124
+ });
125
+ ```
126
+
127
+ ### **Bulk Operations**
128
+ ```typescript
129
+ // URL: /api/users?ids=1
130
+ // URL: /api/users?ids=1&ids=2&ids=3
131
+ app.get("/api/users", (ctx) => {
132
+ const ids = Array.isArray(ctx.query.ids)
133
+ ? ctx.query.ids
134
+ : ctx.query.ids ? [ctx.query.ids] : [];
135
+
136
+ return ctx.json({
137
+ userIds: ids,
138
+ count: ids.length
139
+ });
140
+ });
141
+ ```
142
+
143
+ ### **Tag System**
144
+ ```typescript
145
+ // URL: /api/posts?tags=javascript
146
+ // URL: /api/posts?tags=javascript&tags=react
147
+ app.get("/api/posts", (ctx) => {
148
+ const tags = ctx.query.tags;
149
+ const tagArray = Array.isArray(tags) ? tags : (tags ? [tags] : []);
150
+
151
+ return ctx.json({
152
+ posts: [],
153
+ filters: { tags: tagArray }
154
+ });
155
+ });
156
+ ```
157
+
158
+ ## Best Practices
159
+
160
+ ### **1. Be Explicit About Array Expectations**
161
+ ```typescript
162
+ // Good: Always expect array
163
+ app.get("/api/tags", (ctx) => {
164
+ const tags = Array.isArray(ctx.query.tags)
165
+ ? ctx.query.tags
166
+ : ctx.query.tags ? [ctx.query.tags] : [];
167
+ // Process tags...
168
+ });
169
+ ```
170
+
171
+ ### **2. Use Zod for Type Safety**
172
+ ```typescript
173
+ // Good: Zod handles the union type
174
+ app.get("/api/tags", (ctx) => {
175
+ return ctx.json({ tags: ctx.query.tags });
176
+ }, {
177
+ query: z.object({
178
+ tags: z.union([z.string(), z.array(z.string())])
179
+ .transform(val => Array.isArray(val) ? val : [val])
180
+ })
181
+ });
182
+ ```
183
+
184
+ ### **3. Document Expected Format**
185
+ ```typescript
186
+ // Good: Clear documentation
187
+ app.get("/api/tags", (ctx) => {
188
+ // Expects: ?tags=value OR ?tags=value1&tags=value2 OR ?tags[]=value
189
+ const tags = ctx.query.tags;
190
+ // Handle both cases...
191
+ });
192
+ ```
193
+
194
+ ## Testing Single Item Arrays
195
+
196
+ ```bash
197
+ # Single value (string)
198
+ curl "http://localhost:3000/api/tags?tag=javascript"
199
+
200
+ # Single value with bracket (array)
201
+ curl "http://localhost:3000/api/tags?tag[]=javascript"
202
+
203
+ # Multiple values (array)
204
+ curl "http://localhost:3000/api/tags?tag=javascript&tag=typescript"
205
+
206
+ # Mixed approach
207
+ curl "http://localhost:3000/api/tags?tag[]=javascript&tag=typescript"
208
+ ```
209
+
210
+ ## Summary
211
+
212
+ - **Single value without brackets**: Returns string
213
+ - **Single value with brackets**: Returns array with one item
214
+ - **Multiple values**: Returns array
215
+ - **Best practice**: Use bracket notation if you always want arrays
216
+ - **Alternative**: Handle both cases in your code with type checking
217
+ - **Recommended**: Use Zod transforms for consistent array handling
@@ -0,0 +1,121 @@
1
+ import BXO, { z } from "../src";
2
+
3
+ async function main() {
4
+ const app = new BXO({ serve: { port: 3000 } });
5
+
6
+ // Example 1: Single item array - shows the behavior difference
7
+ app.get("/api/single-tag", (ctx) => {
8
+ return ctx.json({
9
+ message: "Single tag analysis",
10
+ tag: ctx.query.tag,
11
+ isArray: Array.isArray(ctx.query.tag),
12
+ type: typeof ctx.query.tag,
13
+ query: ctx.query
14
+ });
15
+ });
16
+
17
+ // Example 2: Single item with bracket notation
18
+ app.get("/api/single-item-bracket", (ctx) => {
19
+ return ctx.json({
20
+ message: "Single item with bracket notation",
21
+ item: ctx.query.item,
22
+ isArray: Array.isArray(ctx.query.item),
23
+ type: typeof ctx.query.item,
24
+ query: ctx.query
25
+ });
26
+ });
27
+
28
+ // Example 3: Mixed single and multiple values
29
+ app.get("/api/mixed", (ctx) => {
30
+ return ctx.json({
31
+ message: "Mixed single and multiple values",
32
+ single: ctx.query.single,
33
+ multiple: ctx.query.multiple,
34
+ singleIsArray: Array.isArray(ctx.query.single),
35
+ multipleIsArray: Array.isArray(ctx.query.multiple),
36
+ query: ctx.query
37
+ });
38
+ });
39
+
40
+ // Example 4: With Zod validation - handles both cases
41
+ app.get("/api/validated-tags", (ctx) => {
42
+ return ctx.json({
43
+ message: "Validated tags",
44
+ tags: ctx.query.tags,
45
+ isArray: Array.isArray(ctx.query.tags),
46
+ query: ctx.query
47
+ });
48
+ }, {
49
+ query: z.object({
50
+ tags: z.union([
51
+ z.string(),
52
+ z.array(z.string())
53
+ ]).optional()
54
+ })
55
+ });
56
+
57
+ // Example 4b: Better approach - always parse to array if we expect array
58
+ app.get("/api/smart-tags", (ctx) => {
59
+ return ctx.json({
60
+ message: "Smart tags - always array",
61
+ tags: ctx.query.tags,
62
+ isArray: Array.isArray(ctx.query.tags),
63
+ query: ctx.query
64
+ });
65
+ }, {
66
+ query: z.object({
67
+ tags: z.union([
68
+ z.string(),
69
+ z.array(z.string())
70
+ ]).transform(val => Array.isArray(val) ? val : [val]).optional()
71
+ })
72
+ });
73
+
74
+ // Example 5: Force array with Zod transform
75
+ app.get("/api/force-array", (ctx) => {
76
+ return ctx.json({
77
+ message: "Force array transformation",
78
+ tags: ctx.query.tags,
79
+ isArray: Array.isArray(ctx.query.tags),
80
+ query: ctx.query
81
+ });
82
+ }, {
83
+ query: z.object({
84
+ tags: z.union([
85
+ z.string(),
86
+ z.array(z.string())
87
+ ]).transform(val => Array.isArray(val) ? val : [val]).optional()
88
+ })
89
+ });
90
+
91
+ // Example 6: Always array with default
92
+ app.get("/api/always-array", (ctx) => {
93
+ return ctx.json({
94
+ message: "Always array with default",
95
+ tags: ctx.query.tags,
96
+ isArray: Array.isArray(ctx.query.tags),
97
+ query: ctx.query
98
+ });
99
+ }, {
100
+ query: z.object({
101
+ tags: z.array(z.string()).default([])
102
+ })
103
+ });
104
+
105
+ app.start();
106
+ console.log("šŸš€ Server running on http://localhost:3000");
107
+ console.log("\nšŸ“ Single Item Array Examples:");
108
+ console.log("1. Single value: http://localhost:3000/api/single-tag?tag=javascript");
109
+ console.log("2. Single bracket: http://localhost:3000/api/single-item-bracket?item[]=single");
110
+ console.log("3. Mixed values: http://localhost:3000/api/mixed?single=one&multiple=first&multiple=second");
111
+ console.log("4. Validated single: http://localhost:3000/api/validated-tags?tags=javascript");
112
+ console.log("5. Validated multiple: http://localhost:3000/api/validated-tags?tags=javascript&tags=typescript");
113
+ console.log("6. Smart tags (BEST): http://localhost:3000/api/smart-tags?tags=javascript");
114
+ console.log("7. Smart tags multiple: http://localhost:3000/api/smart-tags?tags=javascript&tags=typescript");
115
+ console.log("8. Force array single: http://localhost:3000/api/force-array?tags=javascript");
116
+ console.log("9. Force array multiple: http://localhost:3000/api/force-array?tags=javascript&tags=typescript");
117
+ console.log("10. Always array: http://localhost:3000/api/always-array");
118
+ console.log("11. Always array with value: http://localhost:3000/api/always-array?tags=javascript");
119
+ }
120
+
121
+ main();
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  ".": "./src/index.ts",
6
6
  "./plugins": "./plugins/index.ts"
7
7
  },
8
- "version": "0.0.10-dev.4",
8
+ "version": "0.0.10-dev.6",
9
9
  "type": "module",
10
10
  "devDependencies": {
11
11
  "@types/bun": "latest"
package/src/index.ts CHANGED
@@ -191,9 +191,21 @@ function serializeCookie(name: string, value: string, options: CookieOptions = {
191
191
  return cookie;
192
192
  }
193
193
 
194
- function parseQuery(searchParams: URLSearchParams): QueryObject;
195
- function parseQuery<T extends z.ZodTypeAny>(searchParams: URLSearchParams, schema: T): z.infer<T>;
196
- function parseQuery(searchParams: URLSearchParams, schema?: z.ZodTypeAny): any {
194
+ function parseQuery(searchParams: URLSearchParams, autoArrayParse?: boolean): QueryObject;
195
+ function parseQuery<T extends z.ZodTypeAny>(searchParams: URLSearchParams, schema: T, autoArrayParse?: boolean): z.infer<T>;
196
+ function parseQuery(searchParams: URLSearchParams, schemaOrAutoArrayParse?: z.ZodTypeAny | boolean, autoArrayParse?: boolean): any {
197
+ // Handle overloaded parameters
198
+ let schema: z.ZodTypeAny | undefined;
199
+ let shouldAutoArrayParse: boolean;
200
+
201
+ if (typeof schemaOrAutoArrayParse === 'boolean') {
202
+ shouldAutoArrayParse = schemaOrAutoArrayParse;
203
+ schema = undefined;
204
+ } else {
205
+ shouldAutoArrayParse = autoArrayParse ?? false;
206
+ schema = schemaOrAutoArrayParse;
207
+ }
208
+
197
209
  const out: QueryObject = {};
198
210
  for (const [k, v] of searchParams.entries()) {
199
211
  // Handle array notation like fields[] -> fields
@@ -203,8 +215,20 @@ function parseQuery(searchParams: URLSearchParams, schema?: z.ZodTypeAny): any {
203
215
  const existing = out[key];
204
216
  if (Array.isArray(existing)) out[key] = [...existing, v];
205
217
  else out[key] = [existing as string, v];
206
- } else out[key] = v;
218
+ } else {
219
+ out[key] = v;
220
+ }
221
+ }
222
+
223
+ // Apply autoArrayParse transformation if enabled
224
+ if (shouldAutoArrayParse) {
225
+ for (const [key, value] of Object.entries(out)) {
226
+ if (typeof value === 'string') {
227
+ out[key] = [value];
228
+ }
229
+ }
207
230
  }
231
+
208
232
  if (schema) {
209
233
  return (schema as any).parse ? (schema as any).parse(out) : out;
210
234
  }
@@ -359,6 +383,7 @@ function toResponse(body: unknown, init?: ResponseInit): Response {
359
383
  export default class BXO {
360
384
  private routes: InternalRoute[] = [];
361
385
  private serveOptions: ServeOptions;
386
+ private autoArrayParse: boolean;
362
387
  public server?: ReturnType<typeof Bun.serve>;
363
388
 
364
389
  // Lifecycle hooks
@@ -367,8 +392,9 @@ export default class BXO {
367
392
  protected beforeResponseHooks: BeforeResponseHook[] = [];
368
393
  protected onErrorHooks: OnErrorHook[] = [];
369
394
 
370
- constructor(options?: { serve?: ServeOptions }) {
395
+ constructor(options?: { serve?: ServeOptions; autoArrayParse?: boolean }) {
371
396
  this.serveOptions = options?.serve ?? {};
397
+ this.autoArrayParse = options?.autoArrayParse ?? true; // Default to true
372
398
  }
373
399
 
374
400
  getRoutes(): InternalRoute[] {
@@ -714,7 +740,7 @@ export default class BXO {
714
740
 
715
741
  // Parse query using schema if provided
716
742
  try {
717
- queryObj = route.schema?.query ? parseQuery(url.searchParams, route.schema.query as any) : parseQuery(url.searchParams);
743
+ queryObj = route.schema?.query ? parseQuery(url.searchParams, route.schema.query as any, this.autoArrayParse) : parseQuery(url.searchParams, this.autoArrayParse);
718
744
  } catch (err: any) {
719
745
  const payload = err?.issues ? { error: "Validation Error", issues: err.issues } : { error: "Validation Error", issues: [], message: err.message };
720
746
  return new Response(JSON.stringify(payload), { status: 400, headers: { "Content-Type": "application/json" } });