diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index ca755e8..7467a67 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -33,7 +33,7 @@ Not verified, needs a Make account and a BatchData test token (a sandbox token r - Import with the VS Code extension (the file layout follows public examples, not a Make validator). - Connection check. It POSTs a fixed autocomplete query (`searchCriteria.query` = `1600 Penn`, `take` 1) to `/v1/address/autocomplete` and is valid on HTTP 2xx only when `body.result` is present, so a 200 HTML error page is rejected as `InvalidAccessTokenError`. An earlier empty `/v1/address/verify` check (valid on 400 or 2xx) failed in Make because HTTP 400-599 are errors and `response.valid` cannot override that. Autocomplete was chosen over `GET /v2/property/search-sessions?take=1` because search-sessions can return 403 for tokens without that permission. -- `first()` mappings (Get Property Details, Verify Address, and the billed instant trigger's property lookup) use `first(ifempty(path, emptyarray))` and `response.valid` so a 401 JSON body or a 200 HTML page surfaces as `InvalidAccessTokenError` instead of Make's `IMLError: Function 'first' finished with error! Invalid array.` +- List-item outputs (Get Property Details, Verify Address, and the billed instant trigger's property lookup) use IML 1-based index access (`path[1]`) instead of `first()`, which throws `IMLError` when the value is not an array. `response.valid` marks a missing API object as `InvalidAccessTokenError`, so a 401 JSON body or a 200 HTML page does not surface as `IMLError: Function 'first' finished with error! Invalid array.` A first attempt used `first(ifempty(path, emptyarray))`, but `emptyarray` is a scenario-mapper constant and is not defined in custom-app IML. - Optional fields. The templates use `ifempty(x, undefined)` and `omit()` so empty fields are not sent. Nested optional filters (for example Permit Status) are collection parameters that are dropped when empty. Check that an unfilled nested collection (for example Building) is not sent as `{}`. - Instant triggers. Check that turning a scenario on creates the property subscription (attach), that a test event arrives with `eventId` set, and that turning it off deletes the subscription (detach, `externalHookId`). Create hooks only with a sandbox token: a subscription counts towards the team's limit of 5 and cannot be edited. - Polling triggers (`Watch New Search Sessions`, `Watch New Properties in a Search Session`): confirm the date ordering and that a first run does not replay old items as expected. diff --git a/modules/lookupProperty/lookupProperty.communication.iml.json b/modules/lookupProperty/lookupProperty.communication.iml.json index 2ab438f..e10e666 100644 --- a/modules/lookupProperty/lookupProperty.communication.iml.json +++ b/modules/lookupProperty/lookupProperty.communication.iml.json @@ -23,6 +23,6 @@ "type": "InvalidAccessTokenError", "message": "The BatchData API did not return a usable response. Check that the API token is valid. Contact support@batchservice.com if it still fails." }, - "output": "{{first(ifempty(body.results.properties, emptyarray))}}" + "output": "{{body.results.properties[1]}}" } } diff --git a/modules/verifyAddress/verifyAddress.communication.iml.json b/modules/verifyAddress/verifyAddress.communication.iml.json index 2b89062..1f49c7a 100644 --- a/modules/verifyAddress/verifyAddress.communication.iml.json +++ b/modules/verifyAddress/verifyAddress.communication.iml.json @@ -17,6 +17,6 @@ "type": "InvalidAccessTokenError", "message": "The BatchData API did not return a usable response. Check that the API token is valid. Contact support@batchservice.com if it still fails." }, - "output": "{{first(ifempty(body.results.addresses, emptyarray))}}" + "output": "{{body.results.addresses[1]}}" } } diff --git a/modules/watchMonitorEventsWithProperty/watchMonitorEventsWithProperty.communication.iml.json b/modules/watchMonitorEventsWithProperty/watchMonitorEventsWithProperty.communication.iml.json index e76a907..643cc6c 100644 --- a/modules/watchMonitorEventsWithProperty/watchMonitorEventsWithProperty.communication.iml.json +++ b/modules/watchMonitorEventsWithProperty/watchMonitorEventsWithProperty.communication.iml.json @@ -16,7 +16,7 @@ }, "output": { "{{...}}": "{{payload}}", - "property": "{{first(ifempty(body.results.properties, emptyarray))}}" + "property": "{{body.results.properties[1]}}" } } } diff --git a/package.json b/package.json index b4359c3..b3c09c3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "batchdata-make", - "version": "0.3.2", + "version": "0.3.3", "private": true, "description": "BatchData custom app for Make", "type": "module", diff --git a/scripts/lib.mjs b/scripts/lib.mjs index 490b29a..c5fedf9 100644 --- a/scripts/lib.mjs +++ b/scripts/lib.mjs @@ -43,10 +43,10 @@ export const toInterface = (value) => { }; // Make's first() throws IMLError ("Invalid array") when the value is not an array, -// which happens on a 401 JSON body or a 200 HTML error page. Guard with ifempty so -// first() always receives an array; mark the response invalid when the parent object -// is missing so the user sees InvalidAccessTokenError instead of IMLError. -export const firstItem = (listPath) => `{{first(ifempty(${listPath}, emptyarray))}}`; +// including when ifempty(..., emptyarray) is used — emptyarray is a scenario-mapper +// constant and is not defined in custom-app IML, so first() still receives undefined. +// IML 1-based index access returns empty on a missing path instead of throwing. +export const firstItem = (listPath) => `{{${listPath}[1]}}`; export const UNUSABLE_RESPONSE = 'The BatchData API did not return a usable response. Check that the API token is valid. Contact support@batchservice.com if it still fails.'; diff --git a/test/iml.mjs b/test/iml.mjs index b797ea1..f958a22 100644 --- a/test/iml.mjs +++ b/test/iml.mjs @@ -21,7 +21,11 @@ const fns = { const evalExpr = (expr, scope) => { // IML reads missing paths as empty instead of failing, so use optional chaining. - const js = expr.replace(/\bif\(/g, '__if(').replace(/([\w)\]])\.(?=[A-Za-z_])/g, '$1?.'); + // IML array indexes are 1-based (and [-1] is the last item); JS arrays are 0-based. + const js = expr + .replace(/\bif\(/g, '__if(') + .replace(/([\w)\]])\.(?=[A-Za-z_])/g, '$1?.') + .replace(/\[(\d+)\]/g, (_, n) => `[${Number(n) - 1}]`); const f = new Function('scope', 'fns', `with (fns) { with (scope) { return (${js}); } }`); return f(scope, fns); }; diff --git a/test/structure.test.mjs b/test/structure.test.mjs index a03b80f..4505b2b 100644 --- a/test/structure.test.mjs +++ b/test/structure.test.mjs @@ -200,19 +200,17 @@ test('billed search modules default to Limit 1, and the billed session trigger d }); -test('every first() mapping is guarded and marks a missing API object as InvalidAccessTokenError', () => { +test('list outputs use a 1-based index instead of first(), and mark a missing API object as InvalidAccessTokenError', () => { const files = jsonFiles.filter((f) => f.endsWith('.communication.iml.json') || f.endsWith('batchdata.communication.iml.json')); let found = 0; for (const f of files) { const text = fs.readFileSync(f, 'utf8'); const rel = path.relative(root, f); - const matches = [...text.matchAll(/\{\{first\(([^}]+)\)\}\}/g)]; - for (const m of matches) { - found += 1; - assert.match(m[1], /ifempty\(.+, emptyarray\)/, `${rel} unguarded first(${m[1]})`); - } + assert.doesNotMatch(text, /\{\{first\(/, `${rel} still uses first(), which throws IMLError on a 401 or HTML body`); + const matches = [...text.matchAll(/\{\{([^{}\]]+\[1\])\}\}/g)]; + found += matches.length; } - assert.ok(found >= 3, `expected at least 3 first() mappings, found ${found}`); + assert.ok(found >= 3, `expected at least 3 [1] list-item mappings, found ${found}`); for (const [rel, parent] of [ ['modules/lookupProperty/lookupProperty.communication.iml.json', 'body.results'],