Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]}}"
}
}
2 changes: 1 addition & 1 deletion modules/verifyAddress/verifyAddress.communication.iml.json
Original file line number Diff line number Diff line change
Expand Up @@ -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]}}"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
},
"output": {
"{{...}}": "{{payload}}",
"property": "{{first(ifempty(body.results.properties, emptyarray))}}"
"property": "{{body.results.properties[1]}}"
}
}
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
8 changes: 4 additions & 4 deletions scripts/lib.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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.';

Expand Down
6 changes: 5 additions & 1 deletion test/iml.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
};
Expand Down
12 changes: 5 additions & 7 deletions test/structure.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
Expand Down
Loading