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.
- package/example/array-query-documentation.md +155 -0
- package/example/array-query-example.ts +90 -0
- package/example/auto-array-parse-example.ts +90 -0
- package/example/recommended-array-example.ts +100 -0
- package/example/single-item-array-documentation.md +217 -0
- package/example/single-item-array-example.ts +121 -0
- package/package.json +1 -1
- package/src/index.ts +32 -6
|
@@ -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
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,
|
|
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
|
|
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" } });
|