@xano/xanoscript-language-server 12.21.0 → 12.22.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/lexer/db.js +8 -0
  2. package/lexer/realtime.js +8 -0
  3. package/onHover/functions.md +27 -2
  4. package/package.json +1 -1
  5. package/parser/agent_parser.js +3 -1
  6. package/parser/attributes/descriptionFieldAttribute.js +8 -1
  7. package/parser/attributes/docsFieldAttribute.js +8 -1
  8. package/parser/branch_parser.js +25 -9
  9. package/parser/channel_parser.js +9 -0
  10. package/parser/clauses/flexibleStringClause.js +8 -1
  11. package/parser/clauses/mcpMetadataClauses.js +8 -2
  12. package/parser/clauses/nakedStackFn.js +4 -0
  13. package/parser/clauses/viewClause.js +8 -4
  14. package/parser/definitions/columnDefinition.js +7 -2
  15. package/parser/definitions/enumColumnDefinition.js +7 -2
  16. package/parser/definitions/objectColumnDefinition.js +7 -2
  17. package/parser/error_trigger_parser.js +13 -1
  18. package/parser/functions/api/apiCallFn.js +3 -1
  19. package/parser/functions/controls/switchCaseFn.js +3 -0
  20. package/parser/functions/db/dbAddFn.js +2 -1
  21. package/parser/functions/db/dbAddOrEditFn.js +2 -1
  22. package/parser/functions/db/dbEditFn.js +2 -1
  23. package/parser/functions/db/dbExternalFn.js +2 -0
  24. package/parser/functions/realtime/realtimeKickFn.js +35 -0
  25. package/parser/functions/realtime/register.js +2 -0
  26. package/parser/functions/realtimeFn.js +1 -0
  27. package/parser/functions/register.js +2 -0
  28. package/parser/functions/storage/storageCreateImageFn.js +3 -1
  29. package/parser/functions/workflowTestCallFn.js +47 -0
  30. package/parser/generic/stringArray.js +9 -2
  31. package/parser/mcp_primitive_parser.js +6 -1
  32. package/parser/mcp_server_trigger_parser.js +3 -1
package/lexer/db.js CHANGED
@@ -145,6 +145,12 @@ export const OracleToken = createTokenByName("oracle", {
145
145
  categories: [Identifier],
146
146
  });
147
147
 
148
+ // db.external.snowflake
149
+ export const SnowflakeToken = createTokenByName("snowflake", {
150
+ longer_alt: Identifier,
151
+ categories: [Identifier],
152
+ });
153
+
148
154
  export const DbTokens = [
149
155
  DbToken,
150
156
  QueryToken,
@@ -169,6 +175,7 @@ export const DbTokens = [
169
175
  MysqlToken,
170
176
  PostgresToken,
171
177
  OracleToken,
178
+ SnowflakeToken,
172
179
  WhereToken,
173
180
  ];
174
181
 
@@ -183,6 +190,7 @@ export function mapTokenToType(token) {
183
190
  case MysqlToken.name:
184
191
  case PostgresToken.name:
185
192
  case OracleToken.name:
193
+ case SnowflakeToken.name:
186
194
  case QueryToken.name:
187
195
  case GetToken.name:
188
196
  case AddToken.name:
package/lexer/realtime.js CHANGED
@@ -19,10 +19,17 @@ export const PublishToken = createTokenByName("publish", {
19
19
  categories: [Identifier],
20
20
  });
21
21
 
22
+ // kick
23
+ export const KickToken = createTokenByName("kick", {
24
+ longer_alt: Identifier,
25
+ categories: [Identifier],
26
+ });
27
+
22
28
  export const RealtimeStatementTokens = [
23
29
  RealtimeToken,
24
30
  GetSessionToken,
25
31
  PublishToken,
32
+ KickToken,
26
33
  ];
27
34
 
28
35
  export function mapTokenToType(token) {
@@ -30,6 +37,7 @@ export function mapTokenToType(token) {
30
37
  case RealtimeToken.name:
31
38
  case GetSessionToken.name:
32
39
  case PublishToken.name:
40
+ case KickToken.name:
33
41
  return "function";
34
42
  default:
35
43
  return null;
@@ -444,7 +444,7 @@ api.call "account/{account_id}" verb=POST {
444
444
  } as $account_update_response
445
445
  ```
446
446
 
447
- Calls an internal Xano API endpoint from within a `workflow_test` or `test` clause. This allows you to invoke internal APIs as part of your end-to-end workflow tests or unit tests within queries, functions, and tasks.
447
+ Calls an internal Xano API endpoint from within a `workflow_test`, `tool`, `prompt`, `resource` or `test` clause. This allows you to invoke internal APIs as part of your end-to-end workflow tests, from MCP tools, prompts and resources, or in unit tests within queries, functions, and tasks.
448
448
 
449
449
  To call the endpoint as a logged-in user, pass a token with `auth = {token: "...", ignore_expiration: true}` (a literal token) or an `Authorization` header (`headers = ["Authorization: Bearer " ~ $token]`).
450
450
 
@@ -460,6 +460,18 @@ trigger.call "onNewOrder" {
460
460
 
461
461
  - `trigger.call` can call any trigger defined within your workspace, table, realtime, agentic or API triggers.
462
462
 
463
+ # workflow_test.call
464
+
465
+ ```xs
466
+ workflow_test.call "sign up" {
467
+ datasource = "test"
468
+ } as $signup
469
+ ```
470
+
471
+ **Note:** `workflow_test.call` is only available within `workflow_test`.
472
+
473
+ Runs another workflow test as a step of this one, so a chain of tests can build on each other. `datasource` optionally picks the datasource the called test runs against.
474
+
463
475
  # tool.call
464
476
 
465
477
  ```xs
@@ -470,7 +482,7 @@ tool.call "my_tool" {
470
482
  } as $tool_response
471
483
  ```
472
484
 
473
- Calls an internal Xano AI tool, it allows you to invoke internal tools within your end-to-end workflows tests.
485
+ Calls an internal Xano AI tool, it allows you to invoke internal tools within your end-to-end workflows tests, and from other tools, prompts and resources.
474
486
 
475
487
  - `tool.call` takes the name of the tool as an argument.
476
488
  - `input`: The input parameters for the tool.
@@ -1167,6 +1179,19 @@ db.external.postgres.direct_query {
1167
1179
 
1168
1180
  Performs a SQL query directly on an external PostgreSQL database. The `response_type` indicates if the result is a `list` or a `single` record. The `connection_string` establishes the database connection, and `arg` provides values for placeholders. The results are stored in the `as` variable, here `$recent_customers`.
1169
1181
 
1182
+ # db.external.snowflake.direct_query
1183
+
1184
+ ```xs
1185
+ db.external.snowflake.direct_query {
1186
+ sql = "SELECT * FROM customers WHERE customers.last_purchase > ?"
1187
+ response_type = "list"
1188
+ connection_string = $env.SNOWFLAKE_CONNECTION_STRING
1189
+ arg = $input.date_threshold
1190
+ } as $recent_customers
1191
+ ```
1192
+
1193
+ Performs a SQL query directly on an external Snowflake database. The `response_type` indicates if the result is a `list` or a `single` record. The `connection_string` establishes the database connection, and `arg` provides values for placeholders. The results are stored in the `as` variable, here `$recent_customers`.
1194
+
1170
1195
  # debug.stop
1171
1196
 
1172
1197
  ```xs
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xano/xanoscript-language-server",
3
- "version": "12.21.0",
3
+ "version": "12.22.1",
4
4
  "description": "Language Server Protocol implementation for XanoScript, the language of Xano backends",
5
5
  "keywords": [
6
6
  "xano",
@@ -206,7 +206,9 @@ export function agentDeclaration($) {
206
206
  "api_version?": "[string]",
207
207
  "baseURL?": "[string]",
208
208
  "compatibility?": ["strict", "compatible", ""],
209
- "dynamic_retrival?": ["enabled", "disabled", ""],
209
+ // Free-form JSON text in the Studio form, written back
210
+ // verbatim, so any string is valid.
211
+ "dynamic_retrival?": "[string]",
210
212
  "headers?": "[string]",
211
213
  "include_thoughts?": "[boolean]",
212
214
  "model?": "[string]",
@@ -1,5 +1,8 @@
1
1
  import { EqualToken } from "../../lexer/control.js";
2
- import { StringLiteral } from "../../lexer/literal.js";
2
+ import {
3
+ SingleQuotedStringLiteral,
4
+ StringLiteral,
5
+ } from "../../lexer/literal.js";
3
6
  import { MultiLineStringToken } from "../../lexer/multiline.js";
4
7
  import { DescriptionToken } from "../../lexer/tokens.js";
5
8
 
@@ -15,6 +18,10 @@ export function descriptionFieldAttribute($) {
15
18
  {
16
19
  ALT: () => $.CONSUME(StringLiteral), // e.g., "A description of the field"
17
20
  },
21
+ {
22
+ // the platform writes a value holding a double quote as '...'
23
+ ALT: () => $.CONSUME(SingleQuotedStringLiteral),
24
+ },
18
25
  {
19
26
  ALT: () => $.CONSUME(MultiLineStringToken), // e.g., "fieldDescription"
20
27
  },
@@ -1,5 +1,8 @@
1
1
  import { EqualToken } from "../../lexer/control.js";
2
- import { StringLiteral } from "../../lexer/literal.js";
2
+ import {
3
+ SingleQuotedStringLiteral,
4
+ StringLiteral,
5
+ } from "../../lexer/literal.js";
3
6
  import { MultiLineStringToken } from "../../lexer/multiline.js";
4
7
  import { DocsToken } from "../../lexer/tokens.js";
5
8
 
@@ -15,6 +18,10 @@ export function docsFieldAttribute($) {
15
18
  {
16
19
  ALT: () => $.CONSUME(StringLiteral), // e.g., "A docs of the field"
17
20
  },
21
+ {
22
+ // the platform writes a value holding a double quote as '...'
23
+ ALT: () => $.CONSUME(SingleQuotedStringLiteral),
24
+ },
18
25
  {
19
26
  ALT: () => $.CONSUME(MultiLineStringToken), // e.g., "fieldDocs"
20
27
  },
@@ -15,30 +15,46 @@ export function branchDeclaration($) {
15
15
  ]);
16
16
  $.validateDeclarationName(nameToken, "branch");
17
17
 
18
+ // Mirrors the branch document the platform writes on pull. A middleware
19
+ // entry carries `active: false` when the middleware is disabled.
20
+ const middlewareEntry = { name: "[string]", "active?": "[boolean]" };
18
21
  const middlewareSchema = {
19
- pre: [{ name: "[string]" }],
20
- post: [{ name: "[string]" }],
22
+ pre: [middlewareEntry],
23
+ post: [middlewareEntry],
21
24
  };
22
25
 
26
+ // `0` is a valid limit, and the backend writes `task: 0` (not `false`)
27
+ // for a disabled task history.
28
+ const historyValues = [false, 0, 10, 100, 1000, 10000, "all"];
29
+
23
30
  $.SUBRULE($.schemaParseAttributeFn, {
24
31
  ARGS: [
25
32
  parent,
26
33
  {
27
- color: "[string]",
28
34
  "description?": "[string]",
35
+ // Omitted by the backend when the branch has no color.
36
+ "color?": "[string]",
29
37
  middleware: {
30
38
  function: middlewareSchema,
31
39
  query: middlewareSchema,
32
40
  task: middlewareSchema,
33
41
  tool: middlewareSchema,
42
+ // Always written by the platform.
43
+ "message?": middlewareSchema,
44
+ // Written only when the branch has some.
45
+ "prompt?": middlewareSchema,
46
+ "resource?": middlewareSchema,
34
47
  },
35
48
  history: {
36
- function: [false, 10, 100, 1000, 10000, "all"],
37
- query: [false, 10, 100, 1000, 10000, "all"],
38
- task: [false, 10, 100, 1000, 10000, "all"],
39
- tool: [false, 10, 100, 1000, 10000, "all"],
40
- trigger: [false, 10, 100, 1000, 10000, "all"],
41
- middleware: [false, 10, 100, 1000, 10000, "all"],
49
+ function: historyValues,
50
+ query: historyValues,
51
+ task: historyValues,
52
+ tool: historyValues,
53
+ trigger: historyValues,
54
+ middleware: historyValues,
55
+ // Written only when they differ from the default.
56
+ "prompt?": historyValues,
57
+ "resource?": historyValues,
42
58
  },
43
59
  },
44
60
  ],
@@ -22,6 +22,7 @@ export function channelDeclaration($) {
22
22
  let hasMiddleware = false;
23
23
  let hasInput = false;
24
24
  let hasDescription = false;
25
+ let hasDocs = false;
25
26
  let hasGuid = false;
26
27
  let hasTags = false;
27
28
  let hasActive = false;
@@ -156,6 +157,14 @@ export function channelDeclaration($) {
156
157
  $.SUBRULE($.inputClause);
157
158
  },
158
159
  },
160
+ {
161
+ // written by the platform when the channel has docs
162
+ GATE: () => !hasDocs,
163
+ ALT: () => {
164
+ hasDocs = true;
165
+ $.SUBRULE($.docsFieldAttribute);
166
+ },
167
+ },
159
168
  {
160
169
  GATE: () => !hasDescription,
161
170
  ALT: () => {
@@ -1,5 +1,8 @@
1
1
  import { EqualToken } from "../../lexer/control.js";
2
- import { StringLiteral } from "../../lexer/literal.js";
2
+ import {
3
+ SingleQuotedStringLiteral,
4
+ StringLiteral,
5
+ } from "../../lexer/literal.js";
3
6
  import { MultiLineStringToken } from "../../lexer/multiline.js";
4
7
  import { InstructionsToken } from "../../lexer/tool.js";
5
8
 
@@ -26,6 +29,10 @@ export function flexibleStringClause($) {
26
29
  {
27
30
  ALT: () => $.CONSUME(StringLiteral), // e.g., "A instructions of the field"
28
31
  },
32
+ {
33
+ // the platform writes a value holding a double quote as '...'
34
+ ALT: () => $.CONSUME(SingleQuotedStringLiteral),
35
+ },
29
36
  {
30
37
  ALT: () => $.CONSUME(MultiLineStringToken), // e.g., "fieldinstructions"
31
38
  },
@@ -103,8 +103,14 @@ export function mcpTitleClause($) {
103
103
  $.sectionStack.push("mcpTitleClause");
104
104
  $.CONSUME(TitleToken); // "title"
105
105
  $.CONSUME(EqualToken); // "="
106
- const value = $.CONSUME(StringLiteral); // "..."
107
- if (value.image === `""`) {
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) === "") {
108
114
  $.addMissingError(value, "title cannot be empty");
109
115
  }
110
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 either search or hide is present
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
- const nameToken = $.CONSUME(Identifier);
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.image,
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
- const name = $.CONSUME(Identifier);
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.image, "enum", iterable, nullable, optional);
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
- const name = $.CONSUME(Identifier);
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.image, "enum", iterable, nullable, optional);
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: () => {
@@ -24,6 +24,8 @@ export function apiCallFn($) {
24
24
  const validContexts = [
25
25
  "workflowTestDeclaration",
26
26
  "toolDeclaration",
27
+ "promptDeclaration",
28
+ "resourceDeclaration",
27
29
  "testClause",
28
30
  ];
29
31
  const isInValidContext = validContexts.some((context) =>
@@ -33,7 +35,7 @@ export function apiCallFn($) {
33
35
  if (!isInValidContext) {
34
36
  $.addInvalidValueError(
35
37
  fnToken,
36
- "api.call can only be used within workflow_test, tool, or test contexts",
38
+ "api.call can only be used within workflow_test, tool, prompt, resource, or test contexts",
37
39
  );
38
40
  }
39
41
 
@@ -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);
@@ -24,7 +24,8 @@ export function dbAddFn($) {
24
24
  ARGS: [
25
25
  fnToken,
26
26
  {
27
- data: { "[string]": "[expression]" },
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: { "[string]": "[expression]" },
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: { "[string]": "[expression]" },
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"
@@ -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
+ }
@@ -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
- filename: "[expression]",
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 { StringLiteral } from "../../lexer/literal.js";
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
- token = $.CONSUME(StringLiteral);
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);
@@ -271,7 +271,12 @@ function mcpPrimitiveDeclaration($, kind) {
271
271
  GATE: () => !hasStack,
272
272
  ALT: () => {
273
273
  hasStack = true;
274
- $.SUBRULE($.stackClause);
274
+ // Like a tool's: the backend's prompt/resource runner loads the
275
+ // workspace's tools, addons and middleware, so the call
276
+ // statements resolve here.
277
+ $.SUBRULE($.stackClause, {
278
+ ARGS: [{ allowCallStatements: true }],
279
+ });
275
280
  },
276
281
  },
277
282
  {
@@ -49,7 +49,9 @@ export function mcpServerTriggerDeclaration($) {
49
49
  ARGS: [
50
50
  subParent,
51
51
  {
52
- connection: "[boolean]",
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
  });