@xano/developer-mcp 1.0.47 → 1.0.49
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/xanoscript.js +1 -1
- package/dist/xanoscript_docs/quickstart.md +22 -0
- package/dist/xanoscript_docs/syntax.md +51 -7
- package/dist/xanoscript_docs/types.md +13 -0
- package/dist/xanoscript_docs/unit-testing.md +99 -0
- package/dist/xanoscript_docs/workflow-tests.md +1 -0
- package/dist/xanoscript_docs/workspace.md +3 -2
- package/package.json +1 -1
package/dist/xanoscript.js
CHANGED
|
@@ -178,7 +178,7 @@ export const XANOSCRIPT_DOCS_V2 = {
|
|
|
178
178
|
},
|
|
179
179
|
workspace: {
|
|
180
180
|
file: "workspace.md",
|
|
181
|
-
applyTo: ["workspace
|
|
181
|
+
applyTo: ["workspace/**/*.xs"],
|
|
182
182
|
description: "Workspace-level settings: environment variables, preferences, realtime",
|
|
183
183
|
},
|
|
184
184
|
};
|
|
@@ -611,6 +611,28 @@ stack {
|
|
|
611
611
|
}
|
|
612
612
|
```
|
|
613
613
|
|
|
614
|
+
### 13. Multiple constructs in one file
|
|
615
|
+
Each `.xs` file must contain exactly **one** construct. Placing two constructs in the same file causes a parse error.
|
|
616
|
+
|
|
617
|
+
```xs
|
|
618
|
+
// ❌ Wrong - two constructs in one file
|
|
619
|
+
function "helper_a" {
|
|
620
|
+
stack { ... }
|
|
621
|
+
response = $result
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function "helper_b" {
|
|
625
|
+
stack { ... }
|
|
626
|
+
response = $result
|
|
627
|
+
}
|
|
628
|
+
```
|
|
629
|
+
|
|
630
|
+
```
|
|
631
|
+
// ✅ Correct - one construct per file
|
|
632
|
+
// function/helper_a.xs → contains only "helper_a"
|
|
633
|
+
// function/helper_b.xs → contains only "helper_b"
|
|
634
|
+
```
|
|
635
|
+
|
|
614
636
|
---
|
|
615
637
|
|
|
616
638
|
## Related Topics
|
|
@@ -112,22 +112,42 @@ Working with...
|
|
|
112
112
|
|
|
113
113
|
> **Note:** There is no `default` filter. Use conditional blocks or `first_notnull`/`first_notempty` instead.
|
|
114
114
|
|
|
115
|
-
###
|
|
115
|
+
### Parentheses and Filter Precedence
|
|
116
116
|
|
|
117
|
-
|
|
117
|
+
> **Rule:** Filters bind greedily to the left. Without parentheses, the parser extends the filter expression into the operator that follows — producing invalid or unexpected results. **When in doubt, wrap `$var|filter` in parentheses.**
|
|
118
|
+
|
|
119
|
+
The filter `|` grabs everything to its right until it hits a boundary. This means `$arr|count > 3` is parsed as `$arr | (length > 3)` — treating `count > 3` as the filter argument, which is invalid.
|
|
118
120
|
|
|
119
121
|
```xs
|
|
120
|
-
//
|
|
122
|
+
// ❌ Wrong — parser reads filter argument as "count == 0" (invalid)
|
|
123
|
+
if ($arr|count == 0) { ... }
|
|
124
|
+
|
|
125
|
+
// ❌ Wrong — parse error in concatenation
|
|
121
126
|
var $message {
|
|
122
|
-
value =
|
|
127
|
+
value = $status|to_text ~ ": " ~ $data|json_encode
|
|
123
128
|
}
|
|
129
|
+
```
|
|
124
130
|
|
|
125
|
-
|
|
131
|
+
```xs
|
|
132
|
+
// ✅ Correct — evaluate filter first, then apply operator
|
|
133
|
+
|
|
134
|
+
// ✅ Correct
|
|
135
|
+
if (($arr|count) == 0) { ... }
|
|
136
|
+
|
|
137
|
+
// ✅ Correct — wrap each filtered expression for concatenation
|
|
126
138
|
var $message {
|
|
127
|
-
value = $status|to_text ~ ": " ~ $data|json_encode
|
|
139
|
+
value = ($status|to_text) ~ ": " ~ ($data|json_encode)
|
|
128
140
|
}
|
|
141
|
+
|
|
142
|
+
// ✅ Correct — filter result used in arithmetic
|
|
143
|
+
var $total { value = ($prices|sum) + $tax }
|
|
144
|
+
|
|
145
|
+
// ✅ Correct — filter result compared
|
|
146
|
+
var $is_long { value = ($text|strlen) > 100 }
|
|
129
147
|
```
|
|
130
148
|
|
|
149
|
+
**Summary:** Any time you apply an operator (`>`, `<`, `==`, `!=`, `~`, `+`, `-`, etc.) to a filtered value, wrap the `$var|filter` portion in its own parentheses.
|
|
150
|
+
|
|
131
151
|
---
|
|
132
152
|
|
|
133
153
|
## Conditional Blocks
|
|
@@ -285,7 +305,7 @@ $db.post.date >=? $input.start_date
|
|
|
285
305
|
| `unique` | `[1,1,2]\|unique` | `[1,2]` |
|
|
286
306
|
| `flatten` | `[[1,2],[3]]\|flatten` | `[1,2,3]` |
|
|
287
307
|
| `shuffle` | `[1,2,3]\|shuffle` | Random order |
|
|
288
|
-
| `slice` | `[1,2,3,4]\|slice:1:2` | `[2,3]` |
|
|
308
|
+
| `slice` | `[1,2,3,4]\|slice:1:2` | `[2,3]` — offset 1, length 2 |
|
|
289
309
|
| `push` | `[1,2]\|push:3` | `[1,2,3]` |
|
|
290
310
|
| `pop` | `[1,2,3]\|pop` | `3` |
|
|
291
311
|
| `shift` | `[1,2,3]\|shift` | `1` |
|
|
@@ -331,6 +351,30 @@ Generate numeric ranges with the `..` operator:
|
|
|
331
351
|
[1,2,3,4]|reduce:$$+$result:0 // 10
|
|
332
352
|
```
|
|
333
353
|
|
|
354
|
+
### Array Element Access: `|get` vs `|slice`
|
|
355
|
+
|
|
356
|
+
> **`|get:N`** returns a **single element** by zero-based index.
|
|
357
|
+
> **`|slice:offset:length`** returns a **sub-array**, skipping `offset` elements and returning the next `length` elements.
|
|
358
|
+
|
|
359
|
+
| Method | Use For | Example | Result |
|
|
360
|
+
|--------|---------|---------|--------|
|
|
361
|
+
| `\|get:N` | Single element by index (0-based) | `[10,20,30]\|get:0` | `10` |
|
|
362
|
+
| `\|slice:offset:length` | Sub-array — skip N, take M | `[10,20,30,40]\|slice:1:2` | `[20,30]` |
|
|
363
|
+
| `\|first` | First element | `[10,20,30]\|first` | `10` |
|
|
364
|
+
| `\|last` | Last element | `[10,20,30]\|last` | `30` |
|
|
365
|
+
|
|
366
|
+
```xs
|
|
367
|
+
// Single element by index
|
|
368
|
+
var $third { value = $items|get:2 } // 0-based: 3rd element
|
|
369
|
+
|
|
370
|
+
// Sub-array: skip first 10, return the next 5
|
|
371
|
+
var $page { value = $items|slice:10:5 }
|
|
372
|
+
|
|
373
|
+
// ⚠️ slice:offset:length — NOT slice:start:end
|
|
374
|
+
// $arr|slice:2:3 → skip 2, return 3 elements (indices 2, 3, 4)
|
|
375
|
+
// NOT "from index 2 to index 3"
|
|
376
|
+
```
|
|
377
|
+
|
|
334
378
|
### Grouping & Indexing
|
|
335
379
|
```xs
|
|
336
380
|
// Group by property
|
|
@@ -325,6 +325,19 @@ input {
|
|
|
325
325
|
}
|
|
326
326
|
```
|
|
327
327
|
|
|
328
|
+
### Required Fields Also Reject Empty Strings
|
|
329
|
+
|
|
330
|
+
> **Important for testing:** A required `text` field (no `?` after the name) rejects both `null` **and** empty strings (`""`). Sending `""` triggers a platform-level validation error **before your stack runs** — your preconditions and custom error handling never execute.
|
|
331
|
+
|
|
332
|
+
| Value sent | `text name` | `text? name` | `text name?` | `text? name?` |
|
|
333
|
+
|------------|-------------|--------------|--------------|---------------|
|
|
334
|
+
| `"Alice"` | ✅ valid | ✅ valid | ✅ valid | ✅ valid |
|
|
335
|
+
| `""` | ❌ validation error | ❌ validation error | ✅ valid (empty) | ✅ valid (empty) |
|
|
336
|
+
| `null` | ❌ validation error | ✅ valid | ❌ validation error | ✅ valid |
|
|
337
|
+
| *(omitted)* | ❌ validation error | ❌ validation error | ✅ valid | ✅ valid |
|
|
338
|
+
|
|
339
|
+
To test the "empty input" scenario for a text field, the field must be declared optional (`text name?`). Testing a required field with `""` will always produce a validation error — never your custom logic.
|
|
340
|
+
|
|
328
341
|
---
|
|
329
342
|
|
|
330
343
|
## Foreign Key References
|
|
@@ -35,6 +35,46 @@ function "<name>" {
|
|
|
35
35
|
|
|
36
36
|
---
|
|
37
37
|
|
|
38
|
+
## Scope Limitations
|
|
39
|
+
|
|
40
|
+
> **Unit tests can only access `$response`** — the value declared in the construct's `response = ...` field. Tests do **not** have access to intermediate stack variables.
|
|
41
|
+
|
|
42
|
+
```xs
|
|
43
|
+
function "calculate" {
|
|
44
|
+
input { int x }
|
|
45
|
+
stack {
|
|
46
|
+
var $doubled { value = $input.x * 2 } // NOT accessible in tests
|
|
47
|
+
var $result { value = $doubled + 1 } // NOT accessible in tests
|
|
48
|
+
}
|
|
49
|
+
response = $result // This becomes $response in tests
|
|
50
|
+
|
|
51
|
+
test "doubles and adds one" {
|
|
52
|
+
input = { x: 5 }
|
|
53
|
+
// ✅ Can access $response (the declared response)
|
|
54
|
+
expect.to_equal ($response) { value = 11 }
|
|
55
|
+
|
|
56
|
+
// ❌ Stack variables are not in scope — this will fail
|
|
57
|
+
// expect.to_equal ($doubled) { value = 10 }
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
To assert on intermediate values, expose them through the response:
|
|
63
|
+
|
|
64
|
+
```xs
|
|
65
|
+
response = { result: $result, doubled: $doubled }
|
|
66
|
+
|
|
67
|
+
test "intermediate values" {
|
|
68
|
+
input = { x: 5 }
|
|
69
|
+
expect.to_equal ($response.doubled) { value = 10 }
|
|
70
|
+
expect.to_equal ($response.result) { value = 11 }
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
> **Note:** Function stack variables (variables defined in other functions called via `function.run`) are also not accessible — tests are fully isolated to the construct's own declared response.
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
38
78
|
## Basic Test
|
|
39
79
|
|
|
40
80
|
```xs
|
|
@@ -271,6 +311,65 @@ function "validate_age" {
|
|
|
271
311
|
|
|
272
312
|
---
|
|
273
313
|
|
|
314
|
+
## Common Mistakes
|
|
315
|
+
|
|
316
|
+
### Testing Required Fields with Empty Strings
|
|
317
|
+
|
|
318
|
+
A required input field (no `?` after the name) rejects both `null` and empty strings at the platform level — **before your stack runs**. Writing a test that sends `""` to a required field will always get a validation error, never your custom precondition or error logic.
|
|
319
|
+
|
|
320
|
+
❌ **Wrong — will always throw a validation error, not your custom error:**
|
|
321
|
+
```xs
|
|
322
|
+
function "create_user" {
|
|
323
|
+
input { text name } // required: rejects null AND ""
|
|
324
|
+
stack {
|
|
325
|
+
precondition ($input.name != "") {
|
|
326
|
+
error_type = "inputerror"
|
|
327
|
+
error = "Name cannot be blank"
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
response = { ok: true }
|
|
331
|
+
|
|
332
|
+
// This test gets a validation error from the input layer, not "Name cannot be blank"
|
|
333
|
+
test "rejects empty name" {
|
|
334
|
+
input = { name: "" }
|
|
335
|
+
expect.to_throw { value = "Name cannot be blank" }
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
✅ **Correct — use an optional field if you need to test the empty/omitted case:**
|
|
341
|
+
```xs
|
|
342
|
+
function "create_user" {
|
|
343
|
+
input { text name? } // optional: accepts "", null, or omission
|
|
344
|
+
stack {
|
|
345
|
+
precondition ($input.name != null && $input.name != "") {
|
|
346
|
+
error_type = "inputerror"
|
|
347
|
+
error = "Name cannot be blank"
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
response = { ok: true }
|
|
351
|
+
|
|
352
|
+
test "rejects empty name" {
|
|
353
|
+
input = { name: "" }
|
|
354
|
+
expect.to_throw { value = "Name cannot be blank" }
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
test "rejects null name" {
|
|
358
|
+
input = { name: null }
|
|
359
|
+
expect.to_throw { value = "Name cannot be blank" }
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
test "accepts valid name" {
|
|
363
|
+
input = { name: "Alice" }
|
|
364
|
+
expect.to_equal ($response.ok) { value = true }
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
For a full breakdown of what each required/optional/nullable combination accepts, see `xanoscript_docs({ topic: "types" })`.
|
|
370
|
+
|
|
371
|
+
---
|
|
372
|
+
|
|
274
373
|
## Complete Example
|
|
275
374
|
|
|
276
375
|
```xs
|
|
@@ -323,6 +323,7 @@ workflow_test "comprehensive_test" {
|
|
|
323
323
|
4. **Keep tests independent** — Each workflow test should be self-contained and not depend on other tests
|
|
324
324
|
5. **Use `function.call`, not `function.run`** — `function.call` handles errors so that `expect` assertions work correctly. `function.run` is for calling functions outside of workflow tests
|
|
325
325
|
6. **Use assertions over preconditions** — Prefer `expect.*` assertions for clearer test intent
|
|
326
|
+
7. **Don't test required fields with empty strings** — Required inputs reject both `null` and `""` at the platform level before your stack runs. If you send `""` to a required `text` field, you'll always get a platform validation error — never your custom logic. To test the empty/blank case, the input must be declared optional (`text name?`). See `xanoscript_docs({ topic: "types" })` for the full breakdown.
|
|
326
327
|
|
|
327
328
|
---
|
|
328
329
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
applyTo: "workspace
|
|
2
|
+
applyTo: "workspace/**/*.xs"
|
|
3
3
|
---
|
|
4
4
|
|
|
5
5
|
# Workspace Configuration
|
|
@@ -21,6 +21,7 @@ workspace "<name>" {
|
|
|
21
21
|
### Attributes
|
|
22
22
|
| Attribute | Type | Required | Description |
|
|
23
23
|
|-----------|------|----------|-------------|
|
|
24
|
+
| `name` | text | **Yes** | Workspace name (matches the filename, e.g. `workspace/my_project.xs` → `"my_project"`) |
|
|
24
25
|
| `description` | text | No | Human-readable workspace description |
|
|
25
26
|
| `env` | object | No | Environment variable definitions |
|
|
26
27
|
| `acceptance` | object | No | Terms and acceptance settings |
|
|
@@ -171,7 +172,7 @@ workspace "ecommerce_platform" {
|
|
|
171
172
|
|
|
172
173
|
## File Location
|
|
173
174
|
|
|
174
|
-
Workspace configuration is stored
|
|
175
|
+
Workspace configuration is stored as `workspace/<name>.xs`, where `<name>` matches the name declared in the `workspace` block.
|
|
175
176
|
|
|
176
177
|
```
|
|
177
178
|
project/
|
package/package.json
CHANGED