@xano/xanoscript-language-server 12.20.1 → 12.22.0
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/README.md +84 -149
- package/embedded/README.md +1 -2
- package/lexer/db.js +8 -0
- package/lexer/mcp.js +2 -3
- package/lexer/realtime.js +8 -0
- package/lexer/realtime_server_trigger.js +1 -1
- package/onHover/functions.md +29 -1
- package/package.json +15 -5
- package/parser/agent_parser.js +3 -1
- package/parser/attributes/descriptionFieldAttribute.js +8 -1
- package/parser/attributes/docsFieldAttribute.js +8 -1
- package/parser/branch_parser.js +25 -9
- package/parser/channel_parser.js +9 -0
- package/parser/clauses/flexibleStringClause.js +8 -1
- package/parser/clauses/historyClause.js +1 -2
- package/parser/clauses/mcpMetadataClauses.js +12 -8
- package/parser/clauses/nakedStackFn.js +4 -0
- package/parser/clauses/viewClause.js +8 -4
- package/parser/definitions/columnDefinition.js +7 -2
- package/parser/definitions/enumColumnDefinition.js +7 -2
- package/parser/definitions/objectColumnDefinition.js +7 -2
- package/parser/error_trigger_parser.js +13 -1
- package/parser/functions/controls/switchCaseFn.js +3 -0
- package/parser/functions/controls/throwFn.js +28 -2
- package/parser/functions/db/dbAddFn.js +2 -1
- package/parser/functions/db/dbAddOrEditFn.js +2 -1
- package/parser/functions/db/dbEditFn.js +2 -1
- package/parser/functions/db/dbExternalFn.js +2 -0
- package/parser/functions/mcp/mcpElicitFn.js +1 -1
- package/parser/functions/mcp/mcpOauthFn.js +2 -4
- package/parser/functions/mcp/mcpProgressFn.js +2 -2
- package/parser/functions/realtime/realtimeKickFn.js +35 -0
- package/parser/functions/realtime/realtimePublishFn.js +2 -2
- package/parser/functions/realtime/register.js +2 -0
- package/parser/functions/realtimeFn.js +1 -0
- package/parser/functions/register.js +2 -0
- package/parser/functions/storage/storageCreateImageFn.js +3 -1
- package/parser/functions/workflowTestCallFn.js +47 -0
- package/parser/generic/stringArray.js +9 -2
- package/parser/mcp_primitive_parser.js +4 -4
- package/parser/mcp_server_parser.js +1 -2
- package/parser/mcp_server_trigger_parser.js +3 -1
- package/parser/message_parser.js +3 -3
- package/parser/microservice_parser.js +1 -2
- package/parser/parser.js +1 -1
- package/parser/policy/catalogue.js +2 -2
- package/parser/policy/policyLiteral.js +1 -1
- package/parser/policy/validatePolicyRule.js +10 -11
- package/parser/policy_parser.js +3 -3
- package/parser/realtime_channel_parser.js +1 -1
|
@@ -7,10 +7,8 @@ import { AnnotationsToken, IconsToken, TitleToken } from "../../lexer/mcp.js";
|
|
|
7
7
|
import { MultiLineStringToken } from "../../lexer/multiline.js";
|
|
8
8
|
|
|
9
9
|
// Shared MCP metadata blocks for `tool`, `prompt` and `resource` (MCP spec
|
|
10
|
-
// 2026-07-28). The rules mirror
|
|
11
|
-
// reports what a push would refuse
|
|
12
|
-
// - `helper/mvp/mcpserver/ToolMetadata.php` (icons, tool annotations)
|
|
13
|
-
// - `helper/script/transform/Resource.php` (resource annotations)
|
|
10
|
+
// 2026-07-28). The rules mirror the Xano backend's decode checks (icons, tool annotations,
|
|
11
|
+
// resource annotations) so the editor reports what a push would refuse.
|
|
14
12
|
|
|
15
13
|
export const ICON_MIME_TYPES = ["image/png", "image/jpeg", "image/webp"];
|
|
16
14
|
export const ICON_URL_MAX_LENGTH = 2048;
|
|
@@ -19,7 +17,7 @@ export const RESOURCE_AUDIENCES = ["user", "assistant"];
|
|
|
19
17
|
|
|
20
18
|
const ICON_SCHEMA = {
|
|
21
19
|
src: "[string]",
|
|
22
|
-
// validated below:
|
|
20
|
+
// validated below: the backend compares it lowercased and allows ""
|
|
23
21
|
"mime_type?": "[string]",
|
|
24
22
|
"sizes?": ["[string]"],
|
|
25
23
|
"theme?": ["light", "dark"],
|
|
@@ -47,7 +45,7 @@ function stringValue(token) {
|
|
|
47
45
|
}
|
|
48
46
|
|
|
49
47
|
/**
|
|
50
|
-
* Why an icon `src` is refused, or null. Same rules as
|
|
48
|
+
* Why an icon `src` is refused, or null. Same rules as the platform:
|
|
51
49
|
* `https:` or a base64 `data:` URI of png/jpeg/webp. SVG and every other scheme
|
|
52
50
|
* (`http:`, `javascript:`, `file:`, ...) are refused.
|
|
53
51
|
* @param {string} src
|
|
@@ -105,8 +103,14 @@ export function mcpTitleClause($) {
|
|
|
105
103
|
$.sectionStack.push("mcpTitleClause");
|
|
106
104
|
$.CONSUME(TitleToken); // "title"
|
|
107
105
|
$.CONSUME(EqualToken); // "="
|
|
108
|
-
|
|
109
|
-
|
|
106
|
+
// The platform writes a title holding a double quote as '...' and one
|
|
107
|
+
// holding a newline as a """ block.
|
|
108
|
+
const value = $.OR([
|
|
109
|
+
{ ALT: () => $.CONSUME(StringLiteral) }, // "..."
|
|
110
|
+
{ ALT: () => $.CONSUME(SingleQuotedStringLiteral) }, // '...'
|
|
111
|
+
{ ALT: () => $.CONSUME(MultiLineStringToken) }, // """..."""
|
|
112
|
+
]);
|
|
113
|
+
if (stringValue(value) === "") {
|
|
110
114
|
$.addMissingError(value, "title cannot be empty");
|
|
111
115
|
}
|
|
112
116
|
$.sectionStack.pop();
|
|
@@ -71,6 +71,10 @@ export function nakedStackFn($) {
|
|
|
71
71
|
GATE: () => options.allowCallStatements,
|
|
72
72
|
ALT: () => $.SUBRULE($.triggerCallFn),
|
|
73
73
|
},
|
|
74
|
+
{
|
|
75
|
+
GATE: () => options.allowCallStatements,
|
|
76
|
+
ALT: () => $.SUBRULE($.workflowTestCallFn),
|
|
77
|
+
},
|
|
74
78
|
{ ALT: () => $.SUBRULE($.workflowExpectFn) },
|
|
75
79
|
]);
|
|
76
80
|
// write-before-elicit lint (see functions/mcp/elicitLint.js)
|
|
@@ -22,10 +22,13 @@ export function viewClause($) {
|
|
|
22
22
|
ARGS: [
|
|
23
23
|
parent,
|
|
24
24
|
{
|
|
25
|
+
// The platform writes each of alias/term/search/sort/hide only
|
|
26
|
+
// when the view has it.
|
|
25
27
|
"[string]": {
|
|
28
|
+
"term?": "[string]",
|
|
26
29
|
"search?": "[query]",
|
|
27
30
|
"hide?": ["[string]"],
|
|
28
|
-
sort: { "[string]": ["asc", "desc", "rand"] },
|
|
31
|
+
"sort?": { "[string]": ["asc", "desc", "rand"] },
|
|
29
32
|
id: "[string]",
|
|
30
33
|
"alias?": "[string]",
|
|
31
34
|
},
|
|
@@ -34,14 +37,15 @@ export function viewClause($) {
|
|
|
34
37
|
],
|
|
35
38
|
});
|
|
36
39
|
|
|
37
|
-
// each key is the view name, ensure
|
|
40
|
+
// each key is the view name, ensure it has some criteria
|
|
38
41
|
for (const viewName in captured) {
|
|
42
|
+
const hasTerm = !!captured[viewName].term;
|
|
39
43
|
const hasSearch = !!captured[viewName].search;
|
|
40
44
|
const hasSort = !!captured[viewName].sort;
|
|
41
|
-
if (!hasSearch && !hasSort && !captured[viewName].hide) {
|
|
45
|
+
if (!hasTerm && !hasSearch && !hasSort && !captured[viewName].hide) {
|
|
42
46
|
$.addMissingError(
|
|
43
47
|
parent,
|
|
44
|
-
`view ${viewName} must have either a search, sort or hide criteria`,
|
|
48
|
+
`view ${viewName} must have either a term, search, sort or hide criteria`,
|
|
45
49
|
);
|
|
46
50
|
}
|
|
47
51
|
}
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
} from "../../lexer/columns.js";
|
|
25
25
|
import { Question } from "../../lexer/control.js";
|
|
26
26
|
import { Identifier } from "../../lexer/identifier.js";
|
|
27
|
+
import { StringLiteral } from "../../lexer/literal.js";
|
|
27
28
|
import { getVarName } from "../generic/utils.js";
|
|
28
29
|
|
|
29
30
|
const DEFAULT_CONFIG = {
|
|
@@ -87,7 +88,11 @@ export function columnDefinition($) {
|
|
|
87
88
|
});
|
|
88
89
|
|
|
89
90
|
// Column name (e.g., "user_id", "optional_column", "required_column")
|
|
90
|
-
|
|
91
|
+
// A name that is not an identifier (e.g. `created-at`) is written quoted.
|
|
92
|
+
const nameToken = $.OR9([
|
|
93
|
+
{ ALT: () => $.CONSUME(Identifier) },
|
|
94
|
+
{ ALT: () => $.CONSUME(StringLiteral) },
|
|
95
|
+
]);
|
|
91
96
|
|
|
92
97
|
// ? (is it an optional field)
|
|
93
98
|
$.OPTION6(() => {
|
|
@@ -147,7 +152,7 @@ export function columnDefinition($) {
|
|
|
147
152
|
|
|
148
153
|
// add the variable to the registry
|
|
149
154
|
$.addInput(
|
|
150
|
-
nameToken
|
|
155
|
+
getVarName(nameToken),
|
|
151
156
|
inputTypeToken.image,
|
|
152
157
|
iterable,
|
|
153
158
|
nullable,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { EnumToken } from "../../lexer/columns.js";
|
|
2
2
|
import { Question } from "../../lexer/control.js";
|
|
3
3
|
import { Identifier } from "../../lexer/identifier.js";
|
|
4
|
+
import { StringLiteral } from "../../lexer/literal.js";
|
|
4
5
|
import { getVarName } from "../generic/utils.js";
|
|
5
6
|
|
|
6
7
|
/**
|
|
@@ -30,7 +31,11 @@ export function enumColumnDefinition($) {
|
|
|
30
31
|
}); // ? (optional field)
|
|
31
32
|
|
|
32
33
|
// Column name (e.g., "user_id", "optional_column", "required_column")
|
|
33
|
-
|
|
34
|
+
// A name that is not an identifier (e.g. `created-at`) is written quoted.
|
|
35
|
+
const name = $.OR9([
|
|
36
|
+
{ ALT: () => $.CONSUME(Identifier) },
|
|
37
|
+
{ ALT: () => $.CONSUME(StringLiteral) },
|
|
38
|
+
]);
|
|
34
39
|
|
|
35
40
|
// add variable to schema
|
|
36
41
|
schema[getVarName(name)] = {
|
|
@@ -49,7 +54,7 @@ export function enumColumnDefinition($) {
|
|
|
49
54
|
$.SUBRULE($.enumColumnMetadataDefinition, { ARGS: [parent] }); // Enum metadata
|
|
50
55
|
|
|
51
56
|
// add the variable to the registry
|
|
52
|
-
$.addInput(name
|
|
57
|
+
$.addInput(getVarName(name), "enum", iterable, nullable, optional);
|
|
53
58
|
|
|
54
59
|
$.sectionStack.pop();
|
|
55
60
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ObjectToken } from "../../lexer/columns.js";
|
|
2
2
|
import { Question } from "../../lexer/control.js";
|
|
3
|
+
import { StringLiteral } from "../../lexer/literal.js";
|
|
3
4
|
import { Identifier } from "../../lexer/tokens.js";
|
|
4
5
|
import { getVarName } from "../generic/utils.js";
|
|
5
6
|
|
|
@@ -35,7 +36,11 @@ export function objectColumnDefinition($) {
|
|
|
35
36
|
});
|
|
36
37
|
|
|
37
38
|
// Column name (e.g., "user_id", "optional_column", "required_column")
|
|
38
|
-
|
|
39
|
+
// A name that is not an identifier (e.g. `created-at`) is written quoted.
|
|
40
|
+
const name = $.OR9([
|
|
41
|
+
{ ALT: () => $.CONSUME(Identifier) },
|
|
42
|
+
{ ALT: () => $.CONSUME(StringLiteral) },
|
|
43
|
+
]);
|
|
39
44
|
|
|
40
45
|
// add variable to schema
|
|
41
46
|
schema[getVarName(name)] = {
|
|
@@ -52,7 +57,7 @@ export function objectColumnDefinition($) {
|
|
|
52
57
|
|
|
53
58
|
$.SUBRULE($.objectColumnMetadataDefinition); // Object metadata
|
|
54
59
|
|
|
55
|
-
$.addInput(name
|
|
60
|
+
$.addInput(getVarName(name), "enum", iterable, nullable, optional);
|
|
56
61
|
|
|
57
62
|
$.sectionStack.pop();
|
|
58
63
|
};
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import { LCurly, RCurly } from "../lexer/control.js";
|
|
1
|
+
import { EqualToken, LCurly, RCurly } from "../lexer/control.js";
|
|
2
2
|
import { ErrorTriggerToken } from "../lexer/error_trigger.js";
|
|
3
3
|
import { StringLiteral } from "../lexer/literal.js";
|
|
4
|
+
import { ActiveToken } from "../lexer/mcp_server_trigger.js";
|
|
4
5
|
import { Identifier, NewlineToken } from "../lexer/tokens.js";
|
|
5
6
|
|
|
6
7
|
export function errorTriggerDeclaration($) {
|
|
7
8
|
return () => {
|
|
9
|
+
let hasActive = false;
|
|
8
10
|
let hasDescription = false;
|
|
9
11
|
let hasGuid = false;
|
|
10
12
|
let hasHistory = false;
|
|
@@ -27,6 +29,16 @@ export function errorTriggerDeclaration($) {
|
|
|
27
29
|
$.AT_LEAST_ONE(() => $.CONSUME(NewlineToken)); // at least one new line
|
|
28
30
|
$.OR2([
|
|
29
31
|
{ ALT: () => $.SUBRULE($.commentBlockFn) },
|
|
32
|
+
{
|
|
33
|
+
// the platform writes `active = false` for a paused trigger
|
|
34
|
+
GATE: () => !hasActive,
|
|
35
|
+
ALT: () => {
|
|
36
|
+
hasActive = true;
|
|
37
|
+
$.CONSUME(ActiveToken);
|
|
38
|
+
$.CONSUME(EqualToken);
|
|
39
|
+
$.SUBRULE($.booleanValue);
|
|
40
|
+
},
|
|
41
|
+
},
|
|
30
42
|
{
|
|
31
43
|
GATE: () => !hasDescription,
|
|
32
44
|
ALT: () => {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ExclamationToken } from "../../../lexer/cast.js";
|
|
1
2
|
import {
|
|
2
3
|
BreakToken,
|
|
3
4
|
CaseToken,
|
|
@@ -12,6 +13,8 @@ export function switchCaseFn($) {
|
|
|
12
13
|
return () => {
|
|
13
14
|
$.sectionStack.push("switchCaseFn");
|
|
14
15
|
$.SUBRULE($.optionalCommentBlockFn);
|
|
16
|
+
// a disabled case is written `!case`, like a disabled statement
|
|
17
|
+
$.OPTION2(() => $.CONSUME(ExclamationToken));
|
|
15
18
|
$.CONSUME(CaseToken); // "case"
|
|
16
19
|
$.CONSUME(LParent); // "("
|
|
17
20
|
$.SUBRULE($.expressionFn);
|
|
@@ -1,4 +1,25 @@
|
|
|
1
1
|
import { ThrowToken } from "../../../lexer/control.js";
|
|
2
|
+
import { IntegerLiteral } from "../../../lexer/literal.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* `status` is the HTTP status an UNCAUGHT throw answers with
|
|
6
|
+
* (mvp:throw_error, `status?: static:int`).
|
|
7
|
+
* It is a static integer from 100 to 599, never an expression, so a variable
|
|
8
|
+
* or an out-of-range number is caught in the editor rather than at run time.
|
|
9
|
+
* @param {import('../../base_parser.js').XanoBaseParser} $
|
|
10
|
+
*/
|
|
11
|
+
function statusValue($) {
|
|
12
|
+
const token = $.CONSUME(IntegerLiteral, {
|
|
13
|
+
ERR_MSG: "status must be a whole number from 100 to 599",
|
|
14
|
+
});
|
|
15
|
+
const value = Number(token.image);
|
|
16
|
+
if (!Number.isInteger(value) || value < 100 || value > 599) {
|
|
17
|
+
$.addInvalidValueError(
|
|
18
|
+
token,
|
|
19
|
+
`Invalid status ${token.image}. Must be a whole number from 100 to 599`,
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
2
23
|
|
|
3
24
|
/**
|
|
4
25
|
* represent $var.x or $x, the only format accepting an assigment
|
|
@@ -6,14 +27,19 @@ import { ThrowToken } from "../../../lexer/control.js";
|
|
|
6
27
|
*/
|
|
7
28
|
export function throwFn($) {
|
|
8
29
|
const requiredAttrs = ["value", "name"];
|
|
9
|
-
const optionalAttrs = ["description", "disabled"];
|
|
30
|
+
const optionalAttrs = ["description", "disabled", "status"];
|
|
10
31
|
|
|
11
32
|
return () => {
|
|
12
33
|
$.sectionStack.push("throw");
|
|
13
34
|
const tokenFn = $.CONSUME(ThrowToken); // "throw"
|
|
14
35
|
// value is a required field
|
|
15
36
|
$.SUBRULE($.functionAttrReq, {
|
|
16
|
-
ARGS: [
|
|
37
|
+
ARGS: [
|
|
38
|
+
tokenFn,
|
|
39
|
+
requiredAttrs,
|
|
40
|
+
optionalAttrs,
|
|
41
|
+
{ types: { status: statusValue } },
|
|
42
|
+
],
|
|
17
43
|
});
|
|
18
44
|
|
|
19
45
|
$.sectionStack.pop();
|
|
@@ -24,7 +24,8 @@ export function dbAddFn($) {
|
|
|
24
24
|
ARGS: [
|
|
25
25
|
fnToken,
|
|
26
26
|
{
|
|
27
|
-
data
|
|
27
|
+
// The platform writes `data = {}` when no field is set.
|
|
28
|
+
data: { "[string]?": "[expression]" },
|
|
28
29
|
"description?": "[string]",
|
|
29
30
|
"disabled?": "[boolean]",
|
|
30
31
|
"enforce_hidden_fields?": "[boolean]",
|
|
@@ -28,7 +28,8 @@ export function dbAddOrEditFn($) {
|
|
|
28
28
|
{
|
|
29
29
|
field_name: "[expression]",
|
|
30
30
|
field_value: "[expression]",
|
|
31
|
-
data
|
|
31
|
+
// The platform writes `data = {}` when no field is set.
|
|
32
|
+
data: { "[string]?": "[expression]" },
|
|
32
33
|
"description?": "[string]",
|
|
33
34
|
"disabled?": "[boolean]",
|
|
34
35
|
"enforce_hidden_fields?": "[boolean]",
|
|
@@ -28,7 +28,8 @@ export function dbEditFn($) {
|
|
|
28
28
|
{
|
|
29
29
|
field_name: "[expression]",
|
|
30
30
|
field_value: "[expression]",
|
|
31
|
-
data
|
|
31
|
+
// The platform writes `data = {}` when no field is set.
|
|
32
|
+
data: { "[string]?": "[expression]" },
|
|
32
33
|
"description?": "[string]",
|
|
33
34
|
"disabled?": "[boolean]",
|
|
34
35
|
"enforce_hidden_fields?": "[boolean]",
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
MysqlToken,
|
|
5
5
|
OracleToken,
|
|
6
6
|
PostgresToken,
|
|
7
|
+
SnowflakeToken,
|
|
7
8
|
} from "../../../lexer/db.js";
|
|
8
9
|
import { DotToken } from "../../../lexer/tokens.js";
|
|
9
10
|
|
|
@@ -20,6 +21,7 @@ export function dbExternalFn($) {
|
|
|
20
21
|
{ ALT: () => $.CONSUME(MysqlToken) }, // "external.mysql"
|
|
21
22
|
{ ALT: () => $.CONSUME(OracleToken) }, // "external.oracle"
|
|
22
23
|
{ ALT: () => $.CONSUME(PostgresToken) }, // "external.postgres"
|
|
24
|
+
{ ALT: () => $.CONSUME(SnowflakeToken) }, // "external.snowflake"
|
|
23
25
|
]);
|
|
24
26
|
$.CONSUME1(DotToken); // "."
|
|
25
27
|
$.SUBRULE($.dbExternalDirectQueryFn); // "external.direct_query"
|
|
@@ -13,7 +13,7 @@ import { checkElicit } from "./elicitLint.js";
|
|
|
13
13
|
// }
|
|
14
14
|
// } as $answer
|
|
15
15
|
//
|
|
16
|
-
// Asks the MCP client for input
|
|
16
|
+
// Asks the MCP client for input. Input is only
|
|
17
17
|
// collected from 2026-07-28 clients with form elicitation; every other client
|
|
18
18
|
// (and a non-MCP run) resolves to {action: "cancel"}. The `input` block is the
|
|
19
19
|
// same grammar as a tool's `input` block.
|
|
@@ -5,7 +5,7 @@ import { CompleteToken, OauthToken, RevokeToken } from "../../../lexer/mcp.js";
|
|
|
5
5
|
import { DotToken, NewlineToken } from "../../../lexer/tokens.js";
|
|
6
6
|
import { getVarName } from "../../generic/utils.js";
|
|
7
7
|
|
|
8
|
-
// The values
|
|
8
|
+
// The values `mcp.oauth.complete` accepts for `decision`.
|
|
9
9
|
const DECISIONS = ["approve", "deny"];
|
|
10
10
|
|
|
11
11
|
/**
|
|
@@ -35,9 +35,7 @@ function decisionValue($, identifier) {
|
|
|
35
35
|
$.SUBRULE5($.expressionFn, { ARGS: [identifier, {}] });
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
// The `mcp.oauth.*` login hand-off statements
|
|
39
|
-
// mvp:mcp_oauth_{complete,request,revoke}, kind schemas
|
|
40
|
-
// statement/mcp.oauth.{complete,request,revoke}.yaml). Each takes an optional
|
|
38
|
+
// The `mcp.oauth.*` login hand-off statements. Each takes an optional
|
|
41
39
|
// `as $var` binding.
|
|
42
40
|
|
|
43
41
|
/**
|
|
@@ -6,8 +6,8 @@ import { ProgressToken } from "../../../lexer/mcp.js";
|
|
|
6
6
|
// message = "Working" // optional
|
|
7
7
|
// }
|
|
8
8
|
//
|
|
9
|
-
// Emits `notifications/progress` on the current MCP response stream
|
|
10
|
-
//
|
|
9
|
+
// Emits `notifications/progress` on the current MCP response stream.
|
|
10
|
+
// A value that does not strictly increase is
|
|
11
11
|
// dropped at run time. NO `as $var` binding.
|
|
12
12
|
|
|
13
13
|
/**
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { KickToken } from "../../../lexer/realtime.js";
|
|
2
|
+
|
|
3
|
+
// realtime.kick {
|
|
4
|
+
// realtime_server = "main"
|
|
5
|
+
// channel = "rooms/42"
|
|
6
|
+
// auth_table = "user" // the identity to remove
|
|
7
|
+
// auth_id = $input.user_id
|
|
8
|
+
// reason = "removed" // optional — sent to the removed connections
|
|
9
|
+
// }
|
|
10
|
+
//
|
|
11
|
+
// Takes one identity's sockets out of one realtime v2 channel
|
|
12
|
+
// (mvp:realtime_kick). A channel's join
|
|
13
|
+
// gate runs once, so this is the step that removes a member whose access was
|
|
14
|
+
// revoked. Each of their sockets leaves the channel and is told it was kicked;
|
|
15
|
+
// it stays connected to its other channels.
|
|
16
|
+
// The identity is required (a kick names who to remove), so auth_table and
|
|
17
|
+
// auth_id are required here, though the kind schema lists them optional the way
|
|
18
|
+
// realtime.publish does. Like realtime.publish it has NO `as $var` binding.
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {import('../../base_parser.js').XanoBaseParser} $
|
|
22
|
+
*/
|
|
23
|
+
export function realtimeKickFn($) {
|
|
24
|
+
const requiredAttrs = ["realtime_server", "channel", "auth_table", "auth_id"];
|
|
25
|
+
const optionalAttrs = ["reason", "description", "disabled"];
|
|
26
|
+
|
|
27
|
+
return () => {
|
|
28
|
+
$.sectionStack.push("realtimeKickFn");
|
|
29
|
+
const fnToken = $.CONSUME(KickToken); // "kick"
|
|
30
|
+
$.SUBRULE($.functionAttrReq, {
|
|
31
|
+
ARGS: [fnToken, requiredAttrs, optionalAttrs],
|
|
32
|
+
});
|
|
33
|
+
$.sectionStack.pop();
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -9,8 +9,8 @@ import { PublishToken } from "../../../lexer/realtime.js";
|
|
|
9
9
|
// auth_id = $auth.id // optional
|
|
10
10
|
// }
|
|
11
11
|
//
|
|
12
|
-
// Injects a server-authored event onto a realtime v2 channel
|
|
13
|
-
//
|
|
12
|
+
// Injects a server-authored event onto a realtime v2 channel.
|
|
13
|
+
// `realtime_server` + `channel` + `data` are required;
|
|
14
14
|
// `message`/`auth_table`/`auth_id` are optional. Flat attrs (the PHP transform
|
|
15
15
|
// maps auth_table/auth_id into the nested context.auth.{dbo_id,row_id} shape).
|
|
16
16
|
// Unlike realtime.get_session it has NO `as $var` binding.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { realtimeGetSessionFn } from "./realtimeGetSessionFn.js";
|
|
2
|
+
import { realtimeKickFn } from "./realtimeKickFn.js";
|
|
2
3
|
import { realtimePublishFn } from "./realtimePublishFn.js";
|
|
3
4
|
|
|
4
5
|
/**
|
|
@@ -11,4 +12,5 @@ export const register = ($) => {
|
|
|
11
12
|
realtimeGetSessionFn($),
|
|
12
13
|
);
|
|
13
14
|
$.realtimePublishFn = $.RULE("realtimePublishFn", realtimePublishFn($));
|
|
15
|
+
$.realtimeKickFn = $.RULE("realtimeKickFn", realtimeKickFn($));
|
|
14
16
|
};
|
|
@@ -13,6 +13,7 @@ export function realtimeFn($) {
|
|
|
13
13
|
$.OR([
|
|
14
14
|
{ ALT: () => $.SUBRULE($.realtimeGetSessionFn) }, // "realtime.get_session"
|
|
15
15
|
{ ALT: () => $.SUBRULE($.realtimePublishFn) }, // "realtime.publish"
|
|
16
|
+
{ ALT: () => $.SUBRULE($.realtimeKickFn) }, // "realtime.kick"
|
|
16
17
|
]);
|
|
17
18
|
$.sectionStack.pop();
|
|
18
19
|
};
|
|
@@ -45,6 +45,7 @@ import { utilFn } from "./utilFn.js";
|
|
|
45
45
|
import { varFn } from "./varFn.js";
|
|
46
46
|
import { webflowFn } from "./webflowFn.js";
|
|
47
47
|
import { workflowExpectFn } from "./workflowExpectFn.js";
|
|
48
|
+
import { workflowTestCallFn } from "./workflowTestCallFn.js";
|
|
48
49
|
import { register as register_zip } from "./zip/register.js";
|
|
49
50
|
import { zipFn } from "./zipFn.js";
|
|
50
51
|
|
|
@@ -81,6 +82,7 @@ export const register = ($) => {
|
|
|
81
82
|
$.workflowExpectFn = $.RULE("workflowExpectFn", workflowExpectFn($));
|
|
82
83
|
$.toolCallFn = $.RULE("toolCallFn", toolCallFn($));
|
|
83
84
|
$.triggerCallFn = $.RULE("triggerCallFn", triggerCallFn($));
|
|
85
|
+
$.workflowTestCallFn = $.RULE("workflowTestCallFn", workflowTestCallFn($));
|
|
84
86
|
register_ai($);
|
|
85
87
|
register_schema($);
|
|
86
88
|
register_api($);
|
|
@@ -13,7 +13,9 @@ export function storageCreateImageFn($) {
|
|
|
13
13
|
{
|
|
14
14
|
value: "[expression]",
|
|
15
15
|
access: ["public", "private"],
|
|
16
|
-
|
|
16
|
+
// Optional, as on create_audio/create_video: the platform omits it
|
|
17
|
+
// when unset.
|
|
18
|
+
"filename?": "[expression]",
|
|
17
19
|
"description?": "[string]",
|
|
18
20
|
"disabled?": "[boolean]",
|
|
19
21
|
},
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { CallToken } from "../../lexer/function.js";
|
|
2
|
+
import { StringLiteral } from "../../lexer/literal.js";
|
|
3
|
+
import { DotToken, Identifier } from "../../lexer/tokens.js";
|
|
4
|
+
import { WorkflowTestToken } from "../../lexer/workflow_test.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* workflow_test.call "other test" { datasource = "test" } as $result
|
|
8
|
+
*
|
|
9
|
+
* Runs another workflow test as a step. The platform resolves the target
|
|
10
|
+
* only inside a workflow test.
|
|
11
|
+
* @param {import('../base_parser.js').XanoBaseParser} $
|
|
12
|
+
*/
|
|
13
|
+
export function workflowTestCallFn($) {
|
|
14
|
+
return () => {
|
|
15
|
+
$.sectionStack.push("workflowTestCallFn");
|
|
16
|
+
|
|
17
|
+
$.CONSUME(WorkflowTestToken); // "workflow_test"
|
|
18
|
+
$.CONSUME(DotToken); // "."
|
|
19
|
+
const fnToken = $.CONSUME(CallToken); // "call"
|
|
20
|
+
|
|
21
|
+
if (!$.sectionStack.includes("workflowTestDeclaration")) {
|
|
22
|
+
$.addInvalidValueError(
|
|
23
|
+
fnToken,
|
|
24
|
+
"workflow_test.call can only be used within workflow_test contexts",
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
$.OR([
|
|
29
|
+
{ ALT: () => $.CONSUME(StringLiteral) }, // "other test"
|
|
30
|
+
{ ALT: () => $.CONSUME(Identifier) }, // other_test
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
$.SUBRULE($.schemaParseAttributeFn, {
|
|
34
|
+
ARGS: [
|
|
35
|
+
fnToken,
|
|
36
|
+
{
|
|
37
|
+
"datasource?": "[string]",
|
|
38
|
+
"description?": "[string]",
|
|
39
|
+
"disabled?": "[boolean]",
|
|
40
|
+
"mock?": { "![string]": "[expression]" },
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
});
|
|
44
|
+
$.SUBRULE($.asVariable, { ARGS: [fnToken] });
|
|
45
|
+
$.sectionStack.pop();
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { CommaToken, LSquare, RSquare } from "../../lexer/control.js";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
SingleQuotedStringLiteral,
|
|
4
|
+
StringLiteral,
|
|
5
|
+
} from "../../lexer/literal.js";
|
|
3
6
|
import { NewlineToken } from "../../lexer/tokens.js";
|
|
4
7
|
|
|
5
8
|
/**
|
|
@@ -18,7 +21,11 @@ export const stringArray = ($) => () => {
|
|
|
18
21
|
"Missing comma or newline between string values in the array"
|
|
19
22
|
);
|
|
20
23
|
}
|
|
21
|
-
|
|
24
|
+
// '...' is how the platform writes a value holding a double quote
|
|
25
|
+
token = $.OR([
|
|
26
|
+
{ ALT: () => $.CONSUME(StringLiteral) },
|
|
27
|
+
{ ALT: () => $.CONSUME(SingleQuotedStringLiteral) },
|
|
28
|
+
]);
|
|
22
29
|
hasSeparator = false;
|
|
23
30
|
$.OPTION(() => {
|
|
24
31
|
$.CONSUME(CommaToken);
|
|
@@ -11,7 +11,7 @@ import { getVarName } from "./generic/utils.js";
|
|
|
11
11
|
|
|
12
12
|
// First-class MCP `prompt` and `resource` workspace objects (MCP 2026-07-28),
|
|
13
13
|
// attached to an `mcp_server` through its `prompts` / `resources` blocks.
|
|
14
|
-
// Grounded against
|
|
14
|
+
// Grounded against the Xano backend's XanoScript schema.
|
|
15
15
|
//
|
|
16
16
|
// prompt code_review { resource regional_order {
|
|
17
17
|
// title = "Code review" uri = "orders://{region}/{order_id}"
|
|
@@ -22,7 +22,7 @@ import { getVarName } from "./generic/utils.js";
|
|
|
22
22
|
// } response = $order
|
|
23
23
|
// }
|
|
24
24
|
|
|
25
|
-
/** The input field types a resource uri variable can be bound to
|
|
25
|
+
/** The input field types a resource uri variable can be bound to. */
|
|
26
26
|
export const RESOURCE_URI_VARIABLE_TYPES = [
|
|
27
27
|
"text",
|
|
28
28
|
"int",
|
|
@@ -39,7 +39,7 @@ const MIME_TYPE =
|
|
|
39
39
|
|
|
40
40
|
/**
|
|
41
41
|
* Parse a resource uri: a literal URI, or an RFC 6570 LEVEL-1 template.
|
|
42
|
-
* Mirrors
|
|
42
|
+
* Mirrors the backend's resource URI parser.
|
|
43
43
|
* @param {string} uri
|
|
44
44
|
* @returns {{ problem: string|null, variables: string[] }}
|
|
45
45
|
*/
|
|
@@ -103,7 +103,7 @@ export function parseResourceUri(uri) {
|
|
|
103
103
|
|
|
104
104
|
/**
|
|
105
105
|
* Why a resource's uri variables and its input block disagree, or null.
|
|
106
|
-
* Mirrors
|
|
106
|
+
* Mirrors the backend's check that URI variables match `input`.
|
|
107
107
|
* @param {string[]} variables
|
|
108
108
|
* @param {Object<string, {type: string, iterable: boolean}>} inputs
|
|
109
109
|
* @returns {string|null}
|
|
@@ -98,8 +98,7 @@ export function mcpServerDeclaration($) {
|
|
|
98
98
|
},
|
|
99
99
|
},
|
|
100
100
|
{
|
|
101
|
-
// Per-server OAuth (
|
|
102
|
-
// validated server-side by helper/mvp/mcpoauth/OauthConfig.php).
|
|
101
|
+
// Per-server OAuth (values are validated server-side).
|
|
103
102
|
// An assignment block like a channel's `publish`. mcp_server ONLY:
|
|
104
103
|
// deliberately absent from agent_parser.js. Mode-specific rules
|
|
105
104
|
// (hosted needs login_url, external needs issuer, ...) stay
|
|
@@ -49,7 +49,9 @@ export function mcpServerTriggerDeclaration($) {
|
|
|
49
49
|
ARGS: [
|
|
50
50
|
subParent,
|
|
51
51
|
{
|
|
52
|
-
|
|
52
|
+
// Only the actions that are on are written, so a trigger
|
|
53
|
+
// with connection off is written as `actions = {}`.
|
|
54
|
+
"connection?": "[boolean]",
|
|
53
55
|
},
|
|
54
56
|
],
|
|
55
57
|
});
|
package/parser/message_parser.js
CHANGED
|
@@ -82,8 +82,8 @@ export function messageDeclaration($) {
|
|
|
82
82
|
},
|
|
83
83
|
},
|
|
84
84
|
{
|
|
85
|
-
// backend
|
|
86
|
-
// documented message; round-tripped
|
|
85
|
+
// the backend emits `docs = "..."` for a
|
|
86
|
+
// documented message; round-tripped on push.
|
|
87
87
|
GATE: () => !hasDocs,
|
|
88
88
|
ALT: () => {
|
|
89
89
|
hasDocs = true;
|
|
@@ -91,7 +91,7 @@ export function messageDeclaration($) {
|
|
|
91
91
|
},
|
|
92
92
|
},
|
|
93
93
|
{
|
|
94
|
-
// backend
|
|
94
|
+
// the backend emits `active = false` for a
|
|
95
95
|
// disabled message; mirrors the sibling `channel` declaration.
|
|
96
96
|
GATE: () => !hasActive,
|
|
97
97
|
ALT: () => {
|
|
@@ -25,8 +25,7 @@ import { Identifier, NewlineToken, TypeToken } from "../lexer/tokens.js";
|
|
|
25
25
|
|
|
26
26
|
// Grammar for the top-level `microservice <name> { ... }` entity.
|
|
27
27
|
//
|
|
28
|
-
// Grounded against
|
|
29
|
-
// (extensions/MVP/includes/xano/script/kind/schema/{core,component}/*.yaml):
|
|
28
|
+
// Grounded against the Xano backend's XanoScript schema:
|
|
30
29
|
//
|
|
31
30
|
// microservice <name> {
|
|
32
31
|
// description|guid|kind|tenant_deploy = "..." # static:text (optional)
|
package/parser/parser.js
CHANGED
|
@@ -80,7 +80,7 @@ export function xanoscriptParser(text, scheme, preTokenized = null) {
|
|
|
80
80
|
}
|
|
81
81
|
for (const error of lexResult.errors) {
|
|
82
82
|
// The platform names the first character it cannot read; the lexer skips a run of them.
|
|
83
|
-
// Quoted as PHP's escapeshellarg quotes it
|
|
83
|
+
// Quoted as PHP's escapeshellarg quotes it: a `'` is written `'\''`.
|
|
84
84
|
const first = String.fromCodePoint(text.codePointAt(error.offset)).replaceAll("'", "'\\''");
|
|
85
85
|
parser.addInvalidValueError({
|
|
86
86
|
image: text.slice(error.offset, error.offset + error.length),
|