@xano/xanoscript-language-server 11.8.3 → 11.8.5

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 (58) hide show
  1. package/.claude/settings.local.json +2 -1
  2. package/cache/documentCache.js +58 -10
  3. package/lexer/db.js +9 -1
  4. package/lexer/security.js +16 -0
  5. package/onCompletion/onCompletion.js +61 -1
  6. package/onDefinition/onDefinition.js +150 -0
  7. package/onDefinition/onDefinition.spec.js +313 -0
  8. package/onDidChangeContent/onDidChangeContent.js +52 -5
  9. package/onHover/functions.md +28 -0
  10. package/package.json +1 -1
  11. package/parser/base_parser.js +61 -3
  12. package/parser/clauses/middlewareClause.js +16 -0
  13. package/parser/definitions/columnDefinition.js +5 -0
  14. package/parser/functions/api/apiCallFn.js +5 -3
  15. package/parser/functions/controls/functionCallFn.js +5 -3
  16. package/parser/functions/controls/functionRunFn.js +61 -5
  17. package/parser/functions/controls/taskCallFn.js +5 -3
  18. package/parser/functions/db/captureFieldName.js +63 -0
  19. package/parser/functions/db/dbAddFn.js +5 -3
  20. package/parser/functions/db/dbAddOrEditFn.js +13 -3
  21. package/parser/functions/db/dbBulkAddFn.js +5 -3
  22. package/parser/functions/db/dbBulkDeleteFn.js +5 -3
  23. package/parser/functions/db/dbBulkPatchFn.js +5 -3
  24. package/parser/functions/db/dbBulkUpdateFn.js +5 -3
  25. package/parser/functions/db/dbDelFn.js +10 -3
  26. package/parser/functions/db/dbEditFn.js +13 -3
  27. package/parser/functions/db/dbGetFn.js +10 -3
  28. package/parser/functions/db/dbHasFn.js +9 -3
  29. package/parser/functions/db/dbPatchFn.js +10 -3
  30. package/parser/functions/db/dbQueryFn.js +29 -3
  31. package/parser/functions/db/dbSchemaFn.js +5 -3
  32. package/parser/functions/db/dbTruncateFn.js +5 -3
  33. package/parser/functions/middlewareCallFn.js +3 -1
  34. package/parser/functions/security/register.js +19 -9
  35. package/parser/functions/security/securityCreateAuthTokenFn.js +22 -0
  36. package/parser/functions/security/securityJweDecodeLegacyFn.js +24 -0
  37. package/parser/functions/security/securityJweDecodeLegacyFn.spec.js +26 -0
  38. package/parser/functions/security/securityJweEncodeLegacyFn.js +24 -0
  39. package/parser/functions/security/securityJweEncodeLegacyFn.spec.js +25 -0
  40. package/parser/functions/securityFn.js +2 -0
  41. package/parser/functions/varFn.js +1 -1
  42. package/parser/generic/asVariable.js +2 -0
  43. package/parser/generic/assignableVariableAs.js +1 -0
  44. package/parser/generic/assignableVariableProperty.js +5 -2
  45. package/parser/table_trigger_parser.js +21 -0
  46. package/parser/table_trigger_parser.spec.js +29 -0
  47. package/parser/tests/variable_test/coverage_check.xs +293 -0
  48. package/parser/variableScanner.js +64 -0
  49. package/parser/variableValidator.js +44 -0
  50. package/parser/variableValidator.spec.js +179 -0
  51. package/server.js +164 -10
  52. package/utils.js +32 -0
  53. package/utils.spec.js +93 -1
  54. package/workspace/crossFileValidator.js +166 -0
  55. package/workspace/crossFileValidator.spec.js +654 -0
  56. package/workspace/referenceTracking.spec.js +420 -0
  57. package/workspace/workspaceIndex.js +149 -0
  58. package/workspace/workspaceIndex.spec.js +189 -0
@@ -2,6 +2,7 @@ import { CallToken } from "../../lexer/action.js";
2
2
  import { StringLiteral } from "../../lexer/literal.js";
3
3
  import { MiddlewareToken } from "../../lexer/middleware.js";
4
4
  import { DotToken, Identifier } from "../../lexer/tokens.js";
5
+ import { getVarName } from "../generic/utils.js";
5
6
 
6
7
  /**
7
8
  *
@@ -15,10 +16,11 @@ export function middlewareCallFn($) {
15
16
  $.CONSUME(DotToken); // "."
16
17
  const fnToken = $.CONSUME(CallToken); // "call"
17
18
 
18
- $.OR([
19
+ const nameToken = $.OR([
19
20
  { ALT: () => $.CONSUME(Identifier) }, // user
20
21
  { ALT: () => $.CONSUME1(StringLiteral) }, // "user auth"
21
22
  ]);
23
+ $.addReference("middleware", getVarName(nameToken), nameToken);
22
24
 
23
25
  $.OPTION(() => {
24
26
  $.SUBRULE($.schemaParseAttributeFn, {
@@ -8,7 +8,9 @@ import { securityCreateUuidFn } from "./securityCreateUuidFn.js";
8
8
  import { securityDecryptFn } from "./securityDecryptFn.js";
9
9
  import { securityEncryptFn } from "./securityEncryptFn.js";
10
10
  import { securityJweDecodeFn } from "./securityJweDecodeFn.js";
11
+ import { securityJweDecodeLegacyFn } from "./securityJweDecodeLegacyFn.js";
11
12
  import { securityJweEncodeFn } from "./securityJweEncodeFn.js";
13
+ import { securityJweEncodeLegacyFn } from "./securityJweEncodeLegacyFn.js";
12
14
  import { securityJwsDecodeFn } from "./securityJwsDecodeFn.js";
13
15
  import { securityJwsEncodeFn } from "./securityJwsEncodeFn.js";
14
16
  import { securityRandomBytesFn } from "./securityRandomBytesFn.js";
@@ -21,44 +23,52 @@ import { securityRandomNumberFn } from "./securityRandomNumberFn.js";
21
23
  export const register = ($) => {
22
24
  $.securityCheckPasswordFn = $.RULE(
23
25
  "securityCheckPasswordFn",
24
- securityCheckPasswordFn($)
26
+ securityCheckPasswordFn($),
25
27
  );
26
28
  $.securityCreateAuthTokenFn = $.RULE(
27
29
  "securityCreateAuthTokenFn",
28
- securityCreateAuthTokenFn($)
30
+ securityCreateAuthTokenFn($),
29
31
  );
30
32
  $.securityCreateCurveKeyFn = $.RULE(
31
33
  "securityCreateCurveKeyFn",
32
- securityCreateCurveKeyFn($)
34
+ securityCreateCurveKeyFn($),
33
35
  );
34
36
  $.securityCreatePasswordFn = $.RULE(
35
37
  "securityCreatePasswordFn",
36
- securityCreatePasswordFn($)
38
+ securityCreatePasswordFn($),
37
39
  );
38
40
  $.securityCreateRsaKeyFn = $.RULE(
39
41
  "securityCreateRsaKeyFn",
40
- securityCreateRsaKeyFn($)
42
+ securityCreateRsaKeyFn($),
41
43
  );
42
44
  $.securityCreateSecretKeyFn = $.RULE(
43
45
  "securityCreateSecretKeyFn",
44
- securityCreateSecretKeyFn($)
46
+ securityCreateSecretKeyFn($),
45
47
  );
46
48
  $.securityCreateUuidFn = $.RULE(
47
49
  "securityCreateUuidFn",
48
- securityCreateUuidFn($)
50
+ securityCreateUuidFn($),
49
51
  );
50
52
  $.securityDecryptFn = $.RULE("securityDecryptFn", securityDecryptFn($));
51
53
  $.securityEncryptFn = $.RULE("securityEncryptFn", securityEncryptFn($));
52
54
  $.securityJweDecodeFn = $.RULE("securityJweDecodeFn", securityJweDecodeFn($));
53
55
  $.securityJweEncodeFn = $.RULE("securityJweEncodeFn", securityJweEncodeFn($));
56
+ $.securityJweDecodeLegacyFn = $.RULE(
57
+ "securityJweDecodeLegacyFn",
58
+ securityJweDecodeLegacyFn($),
59
+ );
60
+ $.securityJweEncodeLegacyFn = $.RULE(
61
+ "securityJweEncodeLegacyFn",
62
+ securityJweEncodeLegacyFn($),
63
+ );
54
64
  $.securityJwsDecodeFn = $.RULE("securityJwsDecodeFn", securityJwsDecodeFn($));
55
65
  $.securityJwsEncodeFn = $.RULE("securityJwsEncodeFn", securityJwsEncodeFn($));
56
66
  $.securityRandomBytesFn = $.RULE(
57
67
  "securityRandomBytesFn",
58
- securityRandomBytesFn($)
68
+ securityRandomBytesFn($),
59
69
  );
60
70
  $.securityRandomNumberFn = $.RULE(
61
71
  "securityRandomNumberFn",
62
- securityRandomNumberFn($)
72
+ securityRandomNumberFn($),
63
73
  );
64
74
  };
@@ -1,4 +1,8 @@
1
+ import { SingleQuotedStringLiteral,StringLiteral } from "../../../lexer/literal.js";
1
2
  import { CreateAuthTokenToken } from "../../../lexer/security.js";
3
+ import { getVarName } from "../../generic/utils.js";
4
+
5
+ const stringTokens = new Set([StringLiteral.name, SingleQuotedStringLiteral.name]);
2
6
 
3
7
  /**
4
8
  * @param {import('../../base_parser.js').XanoBaseParser} $
@@ -8,6 +12,7 @@ export function securityCreateAuthTokenFn($) {
8
12
  $.sectionStack.push("securityCreateAuthTokenFn");
9
13
  const fnToken = $.CONSUME(CreateAuthTokenToken); // "create_auth_token"
10
14
 
15
+ const captured = {};
11
16
  $.SUBRULE($.schemaParseAttributeFn, {
12
17
  ARGS: [
13
18
  fnToken,
@@ -19,9 +24,26 @@ export function securityCreateAuthTokenFn($) {
19
24
  "description?": "[string]",
20
25
  "disabled?": "[boolean]",
21
26
  },
27
+ captured,
22
28
  ],
23
29
  });
24
30
 
31
+ $.ACTION(() => {
32
+ if (!captured.table) return;
33
+ const keyOffset = captured.table.key?.startOffset;
34
+ if (keyOffset == null) return;
35
+ const tokens = $.input;
36
+ for (let i = 0; i < tokens.length - 2; i++) {
37
+ if (tokens[i].startOffset === keyOffset) {
38
+ const valueToken = tokens[i + 2];
39
+ if (valueToken && stringTokens.has(valueToken.tokenType.name)) {
40
+ $.addReference("table", getVarName(valueToken), valueToken);
41
+ }
42
+ break;
43
+ }
44
+ }
45
+ });
46
+
25
47
  $.SUBRULE($.asVariable, { ARGS: [fnToken] });
26
48
  $.sectionStack.pop();
27
49
  };
@@ -0,0 +1,24 @@
1
+ import { EqualToken, LCurly, RCurly } from "../../../lexer/control.js";
2
+ import { JweDecodeLegacyToken } from "../../../lexer/security.js";
3
+ import { Identifier, NewlineToken } from "../../../lexer/tokens.js";
4
+
5
+ /**
6
+ * @param {import('../../base_parser.js').XanoBaseParser} $
7
+ */
8
+ export function securityJweDecodeLegacyFn($) {
9
+ return () => {
10
+ $.sectionStack.push("securityJweDecodeLegacyFn");
11
+ const fnToken = $.CONSUME(JweDecodeLegacyToken); // "jwe_decode_legacy"
12
+ $.CONSUME(LCurly); // "{"
13
+ $.MANY(() => {
14
+ $.AT_LEAST_ONE(() => $.CONSUME(NewlineToken));
15
+ $.CONSUME1(Identifier); // "field_name"
16
+ $.CONSUME(EqualToken); // "="
17
+ $.SUBRULE($.expressionFn);
18
+ });
19
+ $.MANY1(() => $.CONSUME1(NewlineToken));
20
+ $.CONSUME(RCurly); // "}"
21
+ $.SUBRULE($.asVariable, { ARGS: [fnToken] });
22
+ $.sectionStack.pop();
23
+ };
24
+ }
@@ -0,0 +1,26 @@
1
+ import { expect } from "chai";
2
+ import { describe, it } from "mocha";
3
+ import { lexDocument } from "../../../lexer/lexer.js";
4
+ import { parser } from "../../test_parser.js";
5
+
6
+ function parse(inputText) {
7
+ parser.reset();
8
+ const lexResult = lexDocument(inputText);
9
+ parser.input = lexResult.tokens;
10
+ parser.securityJweDecodeLegacyFn();
11
+ return parser;
12
+ }
13
+
14
+ describe("securityJweDecodeLegacyFn", () => {
15
+ it("securityJweDecodeLegacyFn accepts attributes and store value in a variable", () => {
16
+ const parser = parse(`jwe_decode_legacy {
17
+ token = $input.magic_token
18
+ key = $env.magic_jwt_secret
19
+ audience = "Xano"
20
+ key_algorithm = "A256KW"
21
+ content_algorithm = "A256CBC-HS512"
22
+ } as $decoded_magic_token`);
23
+ console.log(parser.errors);
24
+ expect(parser.errors).to.be.empty;
25
+ });
26
+ });
@@ -0,0 +1,24 @@
1
+ import { EqualToken, LCurly, RCurly } from "../../../lexer/control.js";
2
+ import { JweEncodeLegacyToken } from "../../../lexer/security.js";
3
+ import { Identifier, NewlineToken } from "../../../lexer/tokens.js";
4
+
5
+ /**
6
+ * @param {import('../../base_parser.js').XanoBaseParser} $
7
+ */
8
+ export function securityJweEncodeLegacyFn($) {
9
+ return () => {
10
+ $.sectionStack.push("securityJweEncodeLegacyFn");
11
+ const fnToken = $.CONSUME(JweEncodeLegacyToken); // "jwe_encode_legacy"
12
+ $.CONSUME(LCurly); // "{"
13
+ $.MANY(() => {
14
+ $.AT_LEAST_ONE(() => $.CONSUME(NewlineToken));
15
+ $.CONSUME1(Identifier); // "field_name"
16
+ $.CONSUME(EqualToken); // "="
17
+ $.SUBRULE($.expressionFn);
18
+ });
19
+ $.MANY1(() => $.CONSUME1(NewlineToken));
20
+ $.CONSUME(RCurly); // "}"
21
+ $.SUBRULE($.asVariable, { ARGS: [fnToken] });
22
+ $.sectionStack.pop();
23
+ };
24
+ }
@@ -0,0 +1,25 @@
1
+ import { expect } from "chai";
2
+ import { describe, it } from "mocha";
3
+ import { lexDocument } from "../../../lexer/lexer.js";
4
+ import { parser } from "../../test_parser.js";
5
+
6
+ function parse(inputText) {
7
+ parser.reset();
8
+ const lexResult = lexDocument(inputText);
9
+ parser.input = lexResult.tokens;
10
+ parser.securityJweEncodeLegacyFn();
11
+ return parser;
12
+ }
13
+
14
+ describe("securityJweEncodeLegacyFn", () => {
15
+ it("securityJweEncodeLegacyFn accepts attributes and store value in a variable", () => {
16
+ const parser = parse(`jwe_encode_legacy {
17
+ payload = $jwt_payload
18
+ audience = "Xano"
19
+ key = $env.magic_jwt_secret
20
+ key_algorithm = "A256KW"
21
+ content_algorithm = "A256CBC-HS512"
22
+ } as $jwt`);
23
+ expect(parser.errors).to.be.empty;
24
+ });
25
+ });
@@ -20,6 +20,8 @@ export function securityFn($) {
20
20
  { ALT: () => $.SUBRULE($.securityEncryptFn) }, // security.encrypt
21
21
  { ALT: () => $.SUBRULE($.securityJweDecodeFn) }, // security.jwe_decode
22
22
  { ALT: () => $.SUBRULE($.securityJweEncodeFn) }, // security.jwe_encode
23
+ { ALT: () => $.SUBRULE($.securityJweDecodeLegacyFn) }, // security.jwe_decode_legacy
24
+ { ALT: () => $.SUBRULE($.securityJweEncodeLegacyFn) }, // security.jwe_encode_legacy
23
25
  { ALT: () => $.SUBRULE($.securityJwsDecodeFn) }, // security.jws_decode
24
26
  { ALT: () => $.SUBRULE($.securityJwsEncodeFn) }, // security.jws_encode
25
27
  { ALT: () => $.SUBRULE($.securityRandomBytesFn) }, // security.random_bytes
@@ -68,7 +68,7 @@ export function varFn($) {
68
68
  // we can directly add the variable here but
69
69
  // not necessary for the `assignableVariableProperty` below
70
70
  // which will add it on its own
71
- $.addVariable(variable.image, "unknown");
71
+ $.addVariable(variable.image, "unknown", variable);
72
72
  }
73
73
  },
74
74
  },
@@ -13,7 +13,9 @@ export function asVariable($) {
13
13
  $.OPTION(() => {
14
14
  hasAs = true;
15
15
  $.CONSUME(AsToken); // "as"
16
+ $.__assignableIsDeclaration = true;
16
17
  $.SUBRULE($.assignableVariableProperty);
18
+ $.__assignableIsDeclaration = false;
17
19
  });
18
20
 
19
21
  if (!hasAs && parent) {
@@ -45,6 +45,7 @@ export function assignableVariableAs($) {
45
45
  )
46
46
  );
47
47
  }
48
+ $.addVariable(variableName, "unknown", variableToken);
48
49
  }
49
50
  };
50
51
  }
@@ -128,8 +128,11 @@ export function assignableVariableProperty($) {
128
128
  },
129
129
  });
130
130
 
131
- if (variableName) {
132
- $.addVariable(variableName, "unknown");
131
+ // Only register as a declaration when used in an "as $var" context.
132
+ // Other callers (var.update, math.*, text.*) are usages, not declarations,
133
+ // and the token scanner will pick them up automatically.
134
+ if (variableName && $.__assignableIsDeclaration) {
135
+ $.addVariable(variableName, "unknown", variableToken);
133
136
  }
134
137
  };
135
138
  }
@@ -1,4 +1,5 @@
1
1
  import { EqualToken, LCurly, RCurly } from "../lexer/control.js";
2
+ import { WhereToken } from "../lexer/db.js";
2
3
  import { StringLiteral } from "../lexer/literal.js";
3
4
  import {
4
5
  ActionsToken,
@@ -20,6 +21,7 @@ export function tableTriggerDeclaration($) {
20
21
  let hasInput = false;
21
22
  let hasStack = false;
22
23
  let hasTags = false;
24
+ let hasWhere = false;
23
25
 
24
26
  // @TODO: search
25
27
 
@@ -127,6 +129,25 @@ export function tableTriggerDeclaration($) {
127
129
  $.SUBRULE($.tagsAttribute);
128
130
  },
129
131
  },
132
+ {
133
+ GATE: () => !hasWhere,
134
+ ALT: () => {
135
+ hasWhere = true;
136
+ const token = $.CONSUME(WhereToken, { LABEL: "where" });
137
+ $.CONSUME1(EqualToken);
138
+ $.SUBRULE($.expressionFn, {
139
+ ARGS: [
140
+ token,
141
+ {
142
+ allowQueryExpression: true,
143
+ allowVariable: true,
144
+ allowExpression: false,
145
+ allowComparison: true,
146
+ },
147
+ ],
148
+ });
149
+ },
150
+ },
130
151
  ]);
131
152
  });
132
153
 
@@ -121,6 +121,35 @@ describe("table_trigger", () => {
121
121
  expect(parser.errors).to.be.empty;
122
122
  });
123
123
 
124
+ it("should accept a where clause in actions", () => {
125
+ const parser =
126
+ xanoscriptParser(`// Sends a Slack notification when a conversation's markdown is updated
127
+ table_trigger conversation_slack_notification {
128
+ table = "conversations"
129
+
130
+ input {
131
+ json new
132
+ json old
133
+ enum action {
134
+ values = ["insert", "update", "delete", "truncate"]
135
+ }
136
+
137
+ text datasource
138
+ }
139
+
140
+ stack {
141
+ function.run "notifications/send_conversation_slack_notification" {
142
+ input = {id: $input.new.id}
143
+ } as $slack_result
144
+ }
145
+
146
+ where = $db.NEW.markdown != null
147
+ actions = {insert: true, update: true}
148
+ history = 100
149
+ }`);
150
+ expect(parser.errors).to.be.empty;
151
+ });
152
+
124
153
  it("should parse actions with fields in any order", () => {
125
154
  const parser = xanoscriptParser(`table_trigger foo {
126
155
  table = "blah"
@@ -0,0 +1,293 @@
1
+ // Valide a couverture pour un assuré
2
+ query "coverage/check" verb=GET {
3
+ api_group = "coverage"
4
+
5
+ input {
6
+ // VIN (17 caractères)
7
+ text vin filters=trim|upper
8
+
9
+ // Code garage (optionnel)
10
+ text shop_code? filters=trim|upper
11
+ }
12
+
13
+ stack {
14
+ // Recherche et validation du véhicule
15
+ group {
16
+ stack {
17
+ // Trouve le vehicle a partir du VIN
18
+ db.query vehicle {
19
+ where = $db.vehicle.vin == $input.vin
20
+ return = {type: "single"}
21
+ mock = {
22
+ "devrait retourner véhicule sans police" : {id: 2, vin: "2HGCM82633A004353", make: "Toyota", model: "Camry", year: 2021, owner_name: "Marie Martin"}
23
+ "devrait échouer si VIN introuvable" : null
24
+ "devrait avoir coverages vide si pas de police": {id: 2, vin: "2HGCM82633A004353", make: "Toyota", model: "Camry", year: 2021, owner_name: "Marie Martin"}
25
+ }
26
+ } as $vehicle
27
+
28
+ // Véhicule requis
29
+ precondition ($vehicle != null) {
30
+ error_type = "notfound"
31
+ error = "Vehicle not found with VIN: " ~ $input.vin
32
+ }
33
+
34
+
35
+ var $unused_variable {
36
+ value = "This variable is not used anywhere in the code"
37
+ }
38
+
39
+ // Réponse véhicule
40
+ var $vehicle_response {
41
+ value = {
42
+ vin : $vehicle.vin
43
+ make : $vehicle.make
44
+ model : $vehicle.model
45
+ year : $vehicle.year
46
+ owner_name: $vehicle.owner_name
47
+ }
48
+ }
49
+ }
50
+ }
51
+
52
+ // Recherche de la police d'assurance active
53
+ group {
54
+ stack {
55
+ // Police active du véhicule
56
+ db.query insurance_policy {
57
+ where = $db.insurance_policy.vehicle_id == $vehicle.id && $db.insurance_policy.status == "active"
58
+ return = {type: "single"}
59
+ mock = {
60
+ "devrait retourner véhicule sans police" : null
61
+ "devrait avoir coverages vide si pas de police": null
62
+ }
63
+ } as $policy
64
+
65
+ // Init réponses
66
+ var $policy_response {
67
+ value = null
68
+ }
69
+
70
+ var $coverages_response {
71
+ value = []
72
+ }
73
+
74
+ var $claims_summary {
75
+ value = {total_claims: 0, recent_claims: []}
76
+ }
77
+ }
78
+ }
79
+
80
+ // Traitement si police active
81
+ conditional {
82
+ if ($policy != null) {
83
+ // Construction de la réponse police
84
+ group {
85
+ stack {
86
+ // Réponse police
87
+ var.update $policy_response {
88
+ value = {
89
+ policy_number : $policy.policy_number
90
+ status : $policy.status
91
+ effective_date : $policy.effective_date
92
+ expiration_date: $policy.expiration_date
93
+ insurer_name : $policy.insurer_name
94
+ }
95
+ }
96
+ }
97
+ }
98
+
99
+ // Récupération des couvertures
100
+ group {
101
+ stack {
102
+ db.query policy_coverage {
103
+ where = $db.policy_coverage.policy_id == $policy.id
104
+ return = {type: "list"}
105
+ } as $policy_coverages
106
+
107
+ // Requête par couverture
108
+ foreach ($policy_coverages) {
109
+ each as $pc {
110
+ db.get coverage_type {
111
+ field_name = "id"
112
+ field_value = $pc.coverage_type_id
113
+ } as $ctype
114
+
115
+ // Objet couverture
116
+ var $coverage_item {
117
+ value = {
118
+ code : $ctype.code
119
+ name : $ctype.name
120
+ coverage_limit: $pc.coverage_limit
121
+ deductible : $pc.deductible
122
+ is_active : $pc.is_active
123
+ }
124
+ }
125
+
126
+ // Ajout au tableau
127
+ array.push $coverages_response {
128
+ value = $coverage_item
129
+ }
130
+ }
131
+ }
132
+ }
133
+ }
134
+
135
+ // Récupération et filtrage des sinistres
136
+ group {
137
+ stack {
138
+ // Récupère TOUS les sinistres puis filtre en code
139
+ db.query claim {
140
+ where = $db.claim.policy_id == $policy.id
141
+ return = {type: "list"}
142
+ } as $all_claims
143
+
144
+ // Total sinistres
145
+ var $total_claims_count {
146
+ value = $all_claims|count
147
+ }
148
+
149
+ // Date il y a 2 ans
150
+ var $two_years_ago {
151
+ value = now|transform_timestamp:"-2 years"
152
+ }
153
+
154
+ // Filtrage en code (au lieu de SQL)
155
+ var $recent_claims_list {
156
+ value = []
157
+ }
158
+
159
+ foreach ($all_claims) {
160
+ each as $claim {
161
+ // Date en timestamp
162
+ var $claim_date_ts {
163
+ value = $claim.incident_date|to_timestamp
164
+ }
165
+
166
+ // ⚠️ Filtre en code - devrait être en SQL
167
+ conditional {
168
+ if ($claim_date_ts >= $two_years_ago) {
169
+ var $recent_claim_item {
170
+ value = {
171
+ claim_number : $claim.claim_number
172
+ incident_date : $claim.incident_date
173
+ status : $claim.status
174
+ amount_claimed: $claim.amount_claimed
175
+ amount_paid : $claim.amount_paid
176
+ }
177
+ }
178
+
179
+ array.push $recent_claims_list {
180
+ value = $recent_claim_item
181
+ }
182
+ }
183
+ }
184
+ }
185
+ }
186
+
187
+ // Résumé sinistres
188
+ var.update $claims_summary {
189
+ value = {
190
+ total_claims : $total_claims_count
191
+ recent_claims: $recent_claims_list
192
+ }
193
+ }
194
+ }
195
+ }
196
+ }
197
+ }
198
+
199
+ // Recherche du garage si code fourni
200
+ group {
201
+ stack {
202
+ var $queried_by {
203
+ value = null
204
+ }
205
+
206
+ conditional {
207
+ if ($input.shop_code != null && ($input.shop_code|strlen) > 0) {
208
+ // Récupère garage
209
+ db.query repair_shop {
210
+ where = $db.repair_shop.shop_code == $input.shop_code
211
+ return = {type: "single"}
212
+ } as $shop
213
+
214
+ // Si garage trouvé
215
+ conditional {
216
+ if ($shop != null) {
217
+ var.update $queried_by {
218
+ value = {shop_code: $shop.shop_code, shop_name: $shop.name}
219
+ }
220
+ }
221
+ }
222
+ }
223
+ }
224
+ }
225
+ }
226
+
227
+ // Construction de la réponse finale
228
+ group {
229
+ stack {
230
+ var $result {
231
+ value = {
232
+ vehicle : $vehicle_response
233
+ policy : $policy_response
234
+ coverages : $coverages_response
235
+ claims_summary: $claims_summary
236
+ }
237
+ }
238
+
239
+ // Ajoute queried_by si présent
240
+ conditional {
241
+ if ($queried_by != null) {
242
+ var.update $result {
243
+ value = $result|set:"queried_by":$queried_by
244
+ }
245
+ }
246
+ }
247
+ }
248
+ }
249
+ }
250
+
251
+ response = $result
252
+ history = 100
253
+
254
+ test "devrait retourner véhicule sans police" {
255
+ input = {vin: "2HGCM82633A004353"}
256
+
257
+ expect.to_equal ($response.vehicle.vin) {
258
+ value = "2HGCM82633A004353"
259
+ }
260
+
261
+ expect.to_equal ($response.vehicle.make) {
262
+ value = "Toyota"
263
+ }
264
+
265
+ expect.to_be_null ($response.policy)
266
+ expect.to_be_empty ($response.coverages)
267
+ expect.to_equal ($response.claims_summary.total_claims) {
268
+ value = 0
269
+ }
270
+ }
271
+
272
+ test "devrait échouer si VIN introuvable" {
273
+ input = {vin: "INVALID12345678901"}
274
+
275
+ expect.to_throw {
276
+ exception = "ERROR_CODE_NOT_FOUND"
277
+ }
278
+ }
279
+
280
+ test "devrait avoir coverages vide si pas de police" {
281
+ input = {vin: "2HGCM82633A004353"}
282
+
283
+ expect.to_be_empty ($response.coverages)
284
+ expect.to_be_null ($response.policy)
285
+ expect.to_equal ($response.claims_summary.total_claims) {
286
+ value = 0
287
+ }
288
+
289
+ expect.to_be_empty ($response.claims_summary.recent_claims)
290
+ }
291
+
292
+ guid = "n20O4mlUR72ZWVYiGAE-QFk_ZO4"
293
+ }