diff --git a/CHANGELOG.md b/CHANGELOG.md index 89c4479..ad4f3a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,14 @@ # Changelog -## 0.2.0 (unreleased) +## 0.2.1 + +- Published to npm from GitHub Actions with provenance. The package `repository` field points at this repo (`batchdataco/n8n-nodes-batchdata`). +- Safer billed defaults: Property Search, Get Property Permits, Find Investors, and Get Investor Properties default to 1 result; the billed search-session trigger defaults to 5 per check. The shared catalog.json is unchanged; these are n8n-only overrides. +- HTTP 402 now explains that the account balance is too low and funds need to be added. +- README: install from npm, current Creator Portal docs link, and a single license section. + + +## 0.2.0 - New resources: Investor, Search Session, Property Monitor. - New operations: reverse skip trace, permits, batch skip trace (V3), batch look up, autocomplete, geocode, reverse geocode, batch verify addresses, investor buy box and properties, list/create/delete search sessions, list/get/create/delete property monitors (stream delivery). @@ -10,7 +18,7 @@ - Official BatchData logo. - Operations come from a shared catalog (`nodes/BatchData/catalog.json`) with a drift test. -## 0.1.0 (unreleased) +## 0.1.0 - Credential: BatchData API token (Bearer) with a connection test. - Property: Search, Look Up (all attributes), Skip Trace (V1 and V3). diff --git a/README.md b/README.md index 7d3cf84..58ff615 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Three [n8n](https://n8n.io) community nodes for the [BatchData](https://batchdata.io) API: **BatchData** (actions), **BatchData Trigger** (polling) and **BatchData Monitor Trigger** (instant). Search properties, pull full property data, find owner contacts, check permits, find investors, verify addresses and phones, run search sessions and monitor properties inside your workflows. -> Status: 0.2.0, not yet published to npm. See [Publishing](#publishing). +> Status: published on npm as `n8n-nodes-batchdata` 0.2.1. To appear in n8n as a verified community node, the package still needs to be submitted through the [n8n Creator Portal](https://creators.n8n.io/nodes). See [Publishing](#publishing). ## Install @@ -10,7 +10,7 @@ In n8n, go to **Settings > Community Nodes > Install** and enter `n8n-nodes-batc ## Credentials -Create a **BatchData API** credential and paste your API token (the same token you use as `Authorization: Bearer `). Tokens are created in your BatchData account. +Create a **BatchData API** credential and paste your API token (the same token you use as `Authorization: Bearer `). Tokens are created in your BatchData account. API reference: [developer.batchdata.com](https://developer.batchdata.com). **Test button.** The test posts an empty batch to `POST /api/v1/address/verify`. A valid token gets HTTP 400 (empty batch, treated as success). An invalid token gets 401 or 403 (shown as an error). The 400 is rejected before any lookup, so no address is processed. Billing for rejected requests is assumed, not confirmed in the API spec. @@ -108,7 +108,7 @@ Tests run the node against a fake n8n context with response fixtures built from ## Publishing -n8n requires community nodes to be published from GitHub Actions with npm provenance. `.github/workflows/publish.yml` does this on a version tag (`0.2.0`). Setup (npm package ownership and trusted publisher or `NPM_TOKEN` secret) is described in that file. To list the node in n8n's verified catalog, submit it through the [n8n Creator Portal](https://docs.n8n.io/integrations/creating-nodes/deploy/submit-community-nodes/) after publishing. +n8n requires community nodes submitted for verification to be published from GitHub Actions with npm provenance. `.github/workflows/publish.yml` does this on a version tag (`0.2.1`). Setup (npm package ownership and trusted publisher or `NPM_TOKEN` secret) is described in that file. After publishing, submit the package through the [n8n Creator Portal](https://creators.n8n.io/nodes) (docs: [Submit community nodes](https://docs.n8n.io/connect/create-nodes/deploy-your-node/submit-community-nodes/)). ## Support @@ -116,8 +116,4 @@ support@batchservice.com ## License -[MIT](LICENSE) - -## License - -The code in this repository is released under the MIT License (see `LICENSE`). The BatchData API and the data it returns are proprietary to BatchData (batchdata.io) and are subject to BatchData's terms of service; the MIT License does not cover them. Support: support@batchservice.com +The code in this repository is released under the [MIT License](LICENSE). The BatchData API and the data it returns are proprietary to BatchData ([batchdata.io](https://batchdata.io)) and are subject to BatchData's terms of service; the MIT License does not cover them. Support: support@batchservice.com diff --git a/nodes/BatchData/GenericFunctions.ts b/nodes/BatchData/GenericFunctions.ts index 8a1a178..ff1d500 100644 --- a/nodes/BatchData/GenericFunctions.ts +++ b/nodes/BatchData/GenericFunctions.ts @@ -201,6 +201,8 @@ export function describeHttpError(httpCode: string | undefined): string | undefi return `The BatchData API token was rejected. Check the token in your BatchData API credential. Questions: ${SUPPORT_EMAIL}`; case '403': return `The request was refused. The token may lack permission for this endpoint. Questions: ${SUPPORT_EMAIL}`; + case '402': + return 'The BatchData account balance is too low. Add funds and try again.'; case '429': return 'BatchData rate limit reached. Slow the workflow down or retry later.'; default: diff --git a/nodes/BatchData/catalogProperties.ts b/nodes/BatchData/catalogProperties.ts index 38e4c67..1474158 100644 --- a/nodes/BatchData/catalogProperties.ts +++ b/nodes/BatchData/catalogProperties.ts @@ -5,6 +5,37 @@ import type { Catalog, CatalogField, CatalogOp } from './catalogTypes'; export const catalog = catalogJson as unknown as Catalog; +// n8n-only cost overrides. The vendored catalog.json stays unchanged so it can +// stay in sync with the shared catalog. Billed list operations default to 1 +// result; the billed polling trigger defaults to 5 per check. +const N8N_TAKE_DEFAULTS: Record = { + get_permits: [ + 1, + 'Whole number from 1 to 100. Default 1 to avoid unexpected billing. Billed per result.', + ], + investor_buybox: [ + 1, + 'Whole number from 1 to 100. Default 1 to avoid unexpected billing. Billed per result.', + ], + investor_properties: [ + 1, + 'Whole number from 1 to 100. Default 1 to avoid unexpected billing. Billed per result.', + ], + new_session_property: [ + 5, + 'Whole number from 1 to 100. Default 5. You are billed per property delivered on each check.', + ], +}; +for (const op of [...catalog.ops, ...catalog.triggers]) { + const override = N8N_TAKE_DEFAULTS[op.key]; + const field = override && (op.fields ?? []).find((f) => f.key === 'take'); + if (field) { + field.default = override[0]; + field.help = override[1]; + } +} + + /** Catalog resource key -> n8n resource value. `custom` is the existing Custom API Call resource. */ export const RESOURCE_VALUE: Record = { property: 'property', diff --git a/nodes/BatchData/descriptions/PropertyDescription.ts b/nodes/BatchData/descriptions/PropertyDescription.ts index 05ed3cb..372ef9a 100644 --- a/nodes/BatchData/descriptions/PropertyDescription.ts +++ b/nodes/BatchData/descriptions/PropertyDescription.ts @@ -197,9 +197,9 @@ export const propertySearchFields: INodeProperties[] = [ name: 'take', type: 'number', typeOptions: { minValue: 0, maxValue: 100 }, - default: 25, + default: 1, description: - 'Max number of results to return (0 to 100). A value of 0 returns only the count and is billed as one record.', + 'Max number of results to return (0 to 100). Default 1 to avoid unexpected billing. A value of 0 returns only the count and is billed as one record. Billed per returned property record.', displayOptions: { show: showFor('search') }, }, { diff --git a/package-lock.json b/package-lock.json index b9266a4..725b853 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "n8n-nodes-batchdata", - "version": "0.2.0", + "version": "0.2.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "n8n-nodes-batchdata", - "version": "0.2.0", + "version": "0.2.1", "license": "MIT", "devDependencies": { "@n8n/node-cli": "*", diff --git a/package.json b/package.json index 5198381..ee47d31 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "n8n-nodes-batchdata", - "version": "0.2.0", + "version": "0.2.1", "description": "n8n community nodes for the BatchData API: property search and lookup, skip trace, reverse skip trace, permits, investors, address and phone checks, search sessions, property monitors, polling and instant triggers", "license": "MIT", "homepage": "https://github.com/batchdataco/n8n-nodes-batchdata#readme", diff --git a/test/catalog.test.mjs b/test/catalog.test.mjs index 325135f..42d4b16 100644 --- a/test/catalog.test.mjs +++ b/test/catalog.test.mjs @@ -362,3 +362,59 @@ test('package registers all three nodes and ships the official logo', () => { assert.ok(!banned.test(readFileSync(file, 'utf8')), file); } }); + +test('billed searches and the session trigger default to a small Max Results', () => { + const action = new BatchData(); + const trigger = new BatchDataTrigger(); + const takeDefault = (node, name) => node.description.properties.find((p) => p.name === name)?.default; + assert.equal(takeDefault(action, 'get_permits_take'), 1); + assert.equal(takeDefault(action, 'investor_buybox_take'), 1); + assert.equal(takeDefault(action, 'investor_properties_take'), 1); + assert.equal(takeDefault(trigger, 'new_session_property_take'), 5); + const searchLimit = action.description.properties.find( + (p) => p.name === 'take' && p.displayOptions?.show?.operation?.includes('search'), + ); + assert.equal(searchLimit?.default, 1); + const raw = JSON.parse(readFileSync(join(root, 'nodes/BatchData/catalog.json'), 'utf8')); + assert.equal(raw.ops.find((op) => op.key === 'get_permits').fields.find((f) => f.key === 'take').default, 25); +}); + +test('row-level Your Reference ID reaches the request in batch modules', async () => { + const cases = [ + [ + 'batch_skip_trace', + 'batch_skip_trace_properties', + { result: { data: [{ persons: [] }], meta: { results: { matchCount: 0, requestCount: 1 } } } }, + ], + [ + 'batch_lookup_property', + 'batch_lookup_property_addresses', + { results: { properties: [] } }, + ], + ]; + for (const [operation, field, response] of cases) { + const { calls } = await runNode({ + params: [ + { + resource: 'property', + operation, + [field]: { + rows: [{ street: '1 A St', city: 'X', state: 'AZ', zip: '85001', request_id: 'ROW-1' }], + }, + }, + ], + responses: [response], + }); + assert.ok(JSON.stringify(calls[0].body).includes('ROW-1'), `${operation} dropped the row reference`); + } +}); + +test('package metadata points at this GitHub repo and is a 0.2.1 release', () => { + const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')); + assert.equal(pkg.version, '0.2.1'); + assert.equal(pkg.repository.url, 'git+https://github.com/batchdataco/n8n-nodes-batchdata.git'); + assert.match(pkg.homepage, /github\.com\/batchdataco\/n8n-nodes-batchdata/); + assert.ok(pkg.keywords.includes('n8n-community-node-package')); + assert.equal(pkg.license, 'MIT'); + assert.ok(!('dependencies' in pkg) || Object.keys(pkg.dependencies ?? {}).length === 0); +}); diff --git a/test/execute.test.mjs b/test/execute.test.mjs index 51891f2..c60c05f 100644 --- a/test/execute.test.mjs +++ b/test/execute.test.mjs @@ -292,7 +292,7 @@ test('custom call: sends body, strips webhook fields, GET drops body', async () assert.equal(get.calls[0].body, undefined); }); -test('errors: 401, 429 and 500 surface with the right message; continueOnFail keeps going', async () => { +test('errors: 401, 402, 429 and 500 surface with the right message; continueOnFail keeps going', async () => { const body401 = { status: { code: 401, text: 'Unauthorized', message: 'Invalid token' } }; await assert.rejects( runNode({ @@ -312,6 +312,13 @@ test('errors: 401, 429 and 500 surface with the right message; continueOnFail ke }), (error) => /rate limit/i.test(error.description), ); + await assert.rejects( + runNode({ + params: [{ resource: 'phone', operation: 'dnc', phoneNumbers: '2024561111' }], + responses: [httpError(402, {})], + }), + (error) => error.httpCode === '402' && /balance/i.test(error.description), + ); await assert.rejects( runNode({ params: [{ resource: 'phone', operation: 'dnc', phoneNumbers: '2024561111' }],