@livemapleads/piece-live-map-leads 0.1.0

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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Live Map Leads
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,92 @@
1
+ # Live Map Leads — Activepieces piece
2
+
3
+ Pull filtered local-business lists from map data inside your [Activepieces](https://www.activepieces.com)
4
+ flows. Start a search and act on the results the moment they're ready.
5
+
6
+ - **Action — Start a Search** — kick off a local-business search (`POST /api/v1/jobs`).
7
+ - **Trigger — New Search Completed** — fires when one of your searches finishes, with its
8
+ record count, the credits charged, and ready-to-use download links (CSV, XLSX, JSON).
9
+ - **Connection — API Key** — paste an API key from your Live Map Leads account settings.
10
+
11
+ ## How it talks to Live Map Leads
12
+
13
+ The piece is a thin, typed client of the public [Live Map Leads API](https://livemapleads.com/api/v1/openapi.yaml)
14
+ (`/api/v1`). It does **not** re-describe request or response shapes by hand:
15
+
16
+ - `openapi.yaml` is vendored from the live, unauthenticated `GET /api/v1/openapi.yaml`.
17
+ - `src/generated/v1.ts` holds the spec-derived **types** (build-time only — erased at runtime).
18
+ - [`openapi-fetch`](https://openapi-ts.dev/openapi-fetch/) (~6 KB) is the single runtime
19
+ dependency; a wrong path, field, or query param fails to **compile**.
20
+
21
+ ## Trigger: polling now, instant later
22
+
23
+ "New Search Completed" is a **polling** trigger: Activepieces checks the read-only list
24
+ endpoint (`GET /api/v1/jobs?status=completed&since=…`) on its schedule and emits one item per
25
+ newly-finished search. Dedupe is time-based on each search's completion time — exactly the
26
+ `since` cursor the API exposes. Listing spends **no credits**.
27
+
28
+ Polling-first is deliberate: the connector ships on the API alone, with no dependency on push
29
+ delivery. An **instant** (webhook) variant is a drop-in upgrade when Live Map Leads adds
30
+ push: only [`src/lib/triggers/new-search-completed.ts`](src/lib/triggers/new-search-completed.ts)
31
+ changes its trigger strategy — the action, the API client, and the connection stay as they are.
32
+
33
+ ### Known limit
34
+
35
+ Each poll fetches the newest 100 completed searches (the API maximum; the API has no forward
36
+ pagination). If **more than 100** of your searches were to complete inside a single polling
37
+ interval, the overflow beyond the newest 100 would not be emitted. This is the upstream API's
38
+ accepted polling residual and is effectively unreachable in practice — an account runs one
39
+ search at a time, so a poll interval can't accumulate 100+ completions. The instant (webhook)
40
+ trigger removes the ceiling entirely.
41
+
42
+ ## Develop
43
+
44
+ ```bash
45
+ npm install
46
+ npm run typecheck # tsc (sources + tests)
47
+ npm run lint # eslint
48
+ npm run test # vitest — runs against a MOCKED API, never spends credits
49
+ npm run build # emits dist/
50
+ npm run check # all of the above
51
+ ```
52
+
53
+ To run it inside Activepieces locally, copy/symlink this package into your Activepieces
54
+ `packages/pieces/community/live-map-leads` and use the standard piece hot-reload dev loop
55
+ (`npm start` in the Activepieces repo). See the Activepieces
56
+ [piece development docs](https://www.activepieces.com/docs/build-pieces/building-pieces/start-building).
57
+
58
+ ### Updating the client when the API changes
59
+
60
+ The API contract is the OpenAPI spec. To pull in API changes:
61
+
62
+ ```bash
63
+ # 1. Refresh the vendored spec from production (byte-identical to docs/api/openapi.yaml).
64
+ curl -fsS https://livemapleads.com/api/v1/openapi.yaml -o openapi.yaml
65
+
66
+ # 2. Regenerate the typed client (pinned generator → reproducible output).
67
+ npm run gen:types # openapi-typescript 7.13.0 → src/generated/v1.ts
68
+
69
+ # 3. tsc will flag any action/trigger that no longer matches the new contract.
70
+ npm run check
71
+ ```
72
+
73
+ `--default-non-nullable false` (already in the `gen:types` script) makes request-body fields
74
+ that carry a server-side default optional, so callers don't have to send them.
75
+
76
+ ## Publishing
77
+
78
+ Two sanctioned routes (operator-gated — needs the npm scope / marketplace account):
79
+
80
+ 1. **Community / public npm (soft launch).** `npm publish --access public` under the
81
+ `@livemapleads` scope, then install it in Activepieces by package name. Fast; no SEO surface.
82
+ 2. **Contribute (cloud marketplace).** Open a PR adding this package to the Activepieces
83
+ monorepo under `packages/pieces/community/live-map-leads`. This earns the indexed
84
+ `activepieces.com/pieces/live-map-leads` listing page — the SEO-bearing path — and is done
85
+ once the piece is proven.
86
+
87
+ Before publishing, confirm `minimumSupportedRelease` (in [`src/index.ts`](src/index.ts)) against
88
+ your target Activepieces version, and that the `logoUrl` resolves.
89
+
90
+ ## License
91
+
92
+ [MIT](../../LICENSE).
@@ -0,0 +1,320 @@
1
+ /**
2
+ * This file was auto-generated by openapi-typescript.
3
+ * Do not make direct changes to the file.
4
+ */
5
+ export interface paths {
6
+ "/jobs": {
7
+ parameters: {
8
+ query?: never;
9
+ header?: never;
10
+ path?: never;
11
+ cookie?: never;
12
+ };
13
+ /**
14
+ * List completed searches
15
+ * @description List the calling account's completed searches, newest first, each with
16
+ * its result count and download links. Only the calling account's own
17
+ * searches are ever returned — no cross-account identifiers are exposed.
18
+ * This is the polling source for a "New search completed" automation
19
+ * trigger.
20
+ *
21
+ * For incremental polling, pass `since` = the most recent `completedAt` you
22
+ * have already seen; the response returns only searches that finished
23
+ * strictly after it, ordered by `completedAt` descending. De-duplicate by
24
+ * the stable `jobId`. See `hasMore` for the rare case where a single page
25
+ * does not cover everything since `since`.
26
+ */
27
+ get: operations["listSearches"];
28
+ put?: never;
29
+ /**
30
+ * Start a search
31
+ * @description Start a new local-business search. Returns immediately with a `jobId`
32
+ * and `status: "queued"`; the search runs in the background. Watch for it
33
+ * to finish via `GET /jobs?status=completed&since=…`.
34
+ *
35
+ * Provide a location as **exactly one** of `locationQuery` (text) or
36
+ * `customGeolocation` (a drawn area) — one is required, and supplying both
37
+ * is rejected.
38
+ */
39
+ post: operations["startSearch"];
40
+ delete?: never;
41
+ options?: never;
42
+ head?: never;
43
+ patch?: never;
44
+ trace?: never;
45
+ };
46
+ }
47
+ export type webhooks = Record<string, never>;
48
+ export interface components {
49
+ schemas: {
50
+ /**
51
+ * @description Search definition. `additionalProperties: false` is a HARD part of the
52
+ * contract: the v1 route MUST validate against this restricted field set and
53
+ * reject any other field — it must NOT forward the raw body to the full
54
+ * internal search schema. That internal schema also accepts add-on /
55
+ * enrichment toggles (reviews, images, extra details — each billable),
56
+ * premium / personal-data add-ons, and webhook routing (`deliverTo`); all of
57
+ * those are DEFERRED from v1 (Phase 24.5) and MUST stay unreachable through
58
+ * this endpoint (no billable add-on or personal-data path via v1). The field
59
+ * names that DO appear here match the internal schema so the validated subset
60
+ * maps onto the existing job-start path with no new credit logic. Exactly one
61
+ * of `locationQuery` or `customGeolocation` is required.
62
+ */
63
+ StartSearchRequest: {
64
+ /**
65
+ * @description One or more search terms, e.g. "dentist" or "italian restaurant". Each term runs against the location.
66
+ * @example [
67
+ * "dentist"
68
+ * ]
69
+ */
70
+ searchStrings: string[];
71
+ /**
72
+ * @description Free-text location, e.g. "Austin, TX" or "Lyon, France". Mutually exclusive with `customGeolocation`.
73
+ * @example Austin, TX
74
+ */
75
+ locationQuery?: string;
76
+ customGeolocation?: components["schemas"]["GeoArea"];
77
+ /**
78
+ * @description Result language as a BCP-47 code.
79
+ * @default en
80
+ */
81
+ language?: string;
82
+ /** @description Optional cap on the number of places returned per search term. */
83
+ maxResults?: number;
84
+ /**
85
+ * @description How a business name must match your search term. `all` = no name filter; `contains` = name contains the term; `exact` = name equals the term.
86
+ * @default all
87
+ * @enum {string}
88
+ */
89
+ nameMatch?: "all" | "contains" | "exact";
90
+ /**
91
+ * @description Filter by website presence. `any` = no filter; `with` = only places that have a website; `without` = only places that do not.
92
+ * @default any
93
+ * @enum {string}
94
+ */
95
+ website?: "any" | "with" | "without";
96
+ /** @description Restrict results to these business categories, e.g. "dentist" or "art gallery". Values are lowercase and must each be a recognised category — an unknown value is rejected with 400. The recognised set is large (thousands of entries) and is versioned with the data provider, so it is not enumerated inline here; a future GET /api/v1/categories endpoint will expose the current list for connector dropdowns. Omit this field to apply no category filter. */
97
+ categories?: string[];
98
+ /**
99
+ * @description Exclude permanently or temporarily closed places.
100
+ * @default false
101
+ */
102
+ skipClosed?: boolean;
103
+ /** @description Optional per-search spending ceiling, in credits. The search stops before exceeding it. Recommended for unattended automations. */
104
+ capCredits?: number;
105
+ };
106
+ /** @description A drawn search area as GeoJSON. Coordinates are [longitude, latitude] pairs. Mutually exclusive with `locationQuery`. */
107
+ GeoArea: {
108
+ /** @enum {string} */
109
+ type: "Polygon" | "MultiPolygon";
110
+ /** @description GeoJSON coordinate array. For `Polygon`: an array of linear rings (each an array of [lng, lat] positions). For `MultiPolygon`: an array of polygons. */
111
+ coordinates: unknown[];
112
+ };
113
+ /**
114
+ * @example {
115
+ * "jobId": "8f1d6c2a-3b4e-4f7a-9c1d-2e3f4a5b6c7d",
116
+ * "status": "queued"
117
+ * }
118
+ */
119
+ StartSearchResponse: {
120
+ /**
121
+ * Format: uuid
122
+ * @description Unique identifier for the search. Use it to match results later.
123
+ * @example 8f1d6c2a-3b4e-4f7a-9c1d-2e3f4a5b6c7d
124
+ */
125
+ jobId: string;
126
+ /**
127
+ * @description Always "queued" on a successful start.
128
+ * @enum {string}
129
+ */
130
+ status: "queued";
131
+ };
132
+ SearchList: {
133
+ /** @description Completed searches, newest first. */
134
+ data: components["schemas"]["SearchSummary"][];
135
+ /** @description True when more completed searches matched than fit in `limit` — the page was truncated to the newest `limit`. Rare for a single account. To retrieve the remainder, repeat the call with a larger `limit` (max 100) and the SAME `since`; only advance `since` to the newest `completedAt` you have seen once `hasMore` is false. Advancing `since` while `hasMore` is true would skip the older, un-returned searches. Results are de-duplicated by `jobId` regardless. */
136
+ hasMore: boolean;
137
+ };
138
+ /**
139
+ * @description A finished search. Mirrors the fields delivered to outbound webhooks (Phase 23), the existing "completed search" payload.
140
+ * @example {
141
+ * "jobId": "8f1d6c2a-3b4e-4f7a-9c1d-2e3f4a5b6c7d",
142
+ * "status": "completed",
143
+ * "recordCount": 142,
144
+ * "creditsCharged": 142,
145
+ * "completedAt": "2026-06-12T14:22:05Z",
146
+ * "downloads": {
147
+ * "csv": "https://livemapleads.com/api/exports/download?token=eyJhbGciOi...",
148
+ * "xlsx": "https://livemapleads.com/api/exports/download?token=eyJhbGciOi...",
149
+ * "json": "https://livemapleads.com/api/exports/download?token=eyJhbGciOi..."
150
+ * },
151
+ * "summary": "Your search finished with 142 records."
152
+ * }
153
+ */
154
+ SearchSummary: {
155
+ /**
156
+ * Format: uuid
157
+ * @description Stable identifier — use it as the de-duplication key when polling.
158
+ */
159
+ jobId: string;
160
+ /**
161
+ * @description Always `completed` for this endpoint (it lists completed searches); maps from the internal `complete` state. The full public lifecycle (`queued`/`running`/`completed`/`cancelled`/`failed`, mapping from the internal `queued`/`running`/`complete`/`aborted`/`failed`+`timed_out`) arrives with the single-search detail route in a later phase (24.5).
162
+ * @enum {string}
163
+ */
164
+ status: "completed";
165
+ /** @description Number of business records the search returned. */
166
+ recordCount: number;
167
+ /** @description Credits charged for this search (charge-on-success). */
168
+ creditsCharged: number;
169
+ /**
170
+ * Format: date-time
171
+ * @description When the search finished (RFC 3339).
172
+ */
173
+ completedAt: string;
174
+ /** @description Ready-to-use download links, one per format. Each is a signed, time-limited URL (about 7 days) that needs no API key. Null if download links are unavailable for this search. */
175
+ downloads: components["schemas"]["Downloads"] | null;
176
+ /**
177
+ * @description Short human-readable summary of the result.
178
+ * @example Your search finished with 142 records.
179
+ */
180
+ summary?: string;
181
+ };
182
+ /** @description Signed, time-limited download URLs by format. No API key needed to fetch. */
183
+ Downloads: {
184
+ /** Format: uri */
185
+ csv?: string;
186
+ /** Format: uri */
187
+ xlsx?: string;
188
+ /** Format: uri */
189
+ json?: string;
190
+ };
191
+ Error: {
192
+ /**
193
+ * @description Stable machine-readable error code.
194
+ * @example invalid_request
195
+ */
196
+ error: string;
197
+ /** @description Human-readable explanation. Do not parse — read `error`. */
198
+ message: string;
199
+ };
200
+ };
201
+ responses: {
202
+ /** @description The request body or query failed validation (e.g. unknown category, both location forms supplied, value out of range). */
203
+ BadRequest: {
204
+ headers: {
205
+ [name: string]: unknown;
206
+ };
207
+ content: {
208
+ /**
209
+ * @example {
210
+ * "error": "invalid_request",
211
+ * "message": "Provide exactly one of locationQuery or customGeolocation."
212
+ * }
213
+ */
214
+ "application/json": components["schemas"]["Error"];
215
+ };
216
+ };
217
+ /** @description Missing, invalid, or revoked API key. */
218
+ Unauthorized: {
219
+ headers: {
220
+ [name: string]: unknown;
221
+ };
222
+ content: {
223
+ /**
224
+ * @example {
225
+ * "error": "unauthorized",
226
+ * "message": "Missing or invalid API key."
227
+ * }
228
+ */
229
+ "application/json": components["schemas"]["Error"];
230
+ };
231
+ };
232
+ /** @description Rate limit exceeded for this key. */
233
+ TooManyRequests: {
234
+ headers: {
235
+ /** @description Seconds to wait before retrying. */
236
+ "Retry-After"?: number;
237
+ [name: string]: unknown;
238
+ };
239
+ content: {
240
+ /**
241
+ * @example {
242
+ * "error": "rate_limited",
243
+ * "message": "Too many requests. Retry after the indicated delay."
244
+ * }
245
+ */
246
+ "application/json": components["schemas"]["Error"];
247
+ };
248
+ };
249
+ };
250
+ parameters: never;
251
+ requestBodies: never;
252
+ headers: never;
253
+ pathItems: never;
254
+ }
255
+ export type $defs = Record<string, never>;
256
+ export interface operations {
257
+ listSearches: {
258
+ parameters: {
259
+ query?: {
260
+ /**
261
+ * @description Lifecycle filter. v1 supports only `completed` (the value a polling
262
+ * trigger needs); it is also the default.
263
+ */
264
+ status?: "completed";
265
+ /**
266
+ * @description Return only searches that completed strictly after this RFC 3339 timestamp. Omit on the first poll.
267
+ * @example 2026-06-12T00:00:00Z
268
+ */
269
+ since?: string;
270
+ /** @description Maximum number of searches to return. */
271
+ limit?: number;
272
+ };
273
+ header?: never;
274
+ path?: never;
275
+ cookie?: never;
276
+ };
277
+ requestBody?: never;
278
+ responses: {
279
+ /** @description A page of completed searches, newest first. */
280
+ 200: {
281
+ headers: {
282
+ [name: string]: unknown;
283
+ };
284
+ content: {
285
+ "application/json": components["schemas"]["SearchList"];
286
+ };
287
+ };
288
+ 400: components["responses"]["BadRequest"];
289
+ 401: components["responses"]["Unauthorized"];
290
+ 429: components["responses"]["TooManyRequests"];
291
+ };
292
+ };
293
+ startSearch: {
294
+ parameters: {
295
+ query?: never;
296
+ header?: never;
297
+ path?: never;
298
+ cookie?: never;
299
+ };
300
+ requestBody: {
301
+ content: {
302
+ "application/json": components["schemas"]["StartSearchRequest"];
303
+ };
304
+ };
305
+ responses: {
306
+ /** @description Search accepted and queued. */
307
+ 202: {
308
+ headers: {
309
+ [name: string]: unknown;
310
+ };
311
+ content: {
312
+ "application/json": components["schemas"]["StartSearchResponse"];
313
+ };
314
+ };
315
+ 400: components["responses"]["BadRequest"];
316
+ 401: components["responses"]["Unauthorized"];
317
+ 429: components["responses"]["TooManyRequests"];
318
+ };
319
+ };
320
+ }
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ /**
3
+ * This file was auto-generated by openapi-typescript.
4
+ * Do not make direct changes to the file.
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,3 @@
1
+ import { liveMapLeadsAuth } from './lib/common/auth';
2
+ export { liveMapLeadsAuth };
3
+ export declare const liveMapLeads: import("@activepieces/pieces-framework").Piece<import("@activepieces/pieces-framework").SecretTextProperty<true>>;
package/dist/index.js ADDED
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.liveMapLeads = exports.liveMapLeadsAuth = void 0;
4
+ // Live Map Leads — Activepieces piece.
5
+ //
6
+ // Pull filtered local-business lists from map data: start a search, then act on the results
7
+ // when the search completes. A thin, typed client of the public Live Map Leads API
8
+ // (https://livemapleads.com/api/v1) — the OpenAPI spec is the contract.
9
+ const shared_1 = require("@activepieces/shared");
10
+ const pieces_framework_1 = require("@activepieces/pieces-framework");
11
+ const start_search_1 = require("./lib/actions/start-search");
12
+ const auth_1 = require("./lib/common/auth");
13
+ Object.defineProperty(exports, "liveMapLeadsAuth", { enumerable: true, get: function () { return auth_1.liveMapLeadsAuth; } });
14
+ const new_search_completed_1 = require("./lib/triggers/new-search-completed");
15
+ exports.liveMapLeads = (0, pieces_framework_1.createPiece)({
16
+ displayName: 'Live Map Leads',
17
+ description: 'Pull filtered local-business lists from map data. Start a search and get a notification when your results are ready to download.',
18
+ auth: auth_1.liveMapLeadsAuth,
19
+ // The lowest Activepieces release this piece is known to run on. Confirm/raise against
20
+ // your target Activepieces version before publishing (see README § Publishing).
21
+ minimumSupportedRelease: '0.36.1',
22
+ logoUrl: 'https://livemapleads.com/logo-icon.png',
23
+ categories: [shared_1.PieceCategory.MARKETING, shared_1.PieceCategory.SALES_AND_CRM],
24
+ authors: ['livemapleads'],
25
+ actions: [start_search_1.startSearchAction],
26
+ triggers: [new_search_completed_1.newSearchCompleted],
27
+ });
@@ -0,0 +1,31 @@
1
+ import { type StartSearchBody } from '../common/api';
2
+ /** Shape of this action's prop values (kept permissive so the framework's inferred type assigns). */
3
+ export interface StartSearchProps {
4
+ searchTerms: unknown;
5
+ location: string;
6
+ maxResults?: number;
7
+ nameMatch?: 'all' | 'contains' | 'exact';
8
+ website?: 'any' | 'with' | 'without';
9
+ categories?: unknown;
10
+ skipClosed?: boolean;
11
+ capCredits?: number;
12
+ language?: string;
13
+ }
14
+ /**
15
+ * Map the action's prop values to the API request body. Pure + exported so it is unit-
16
+ * tested without the Activepieces runtime. Optional fields are omitted entirely when unset
17
+ * so the API applies its own defaults. Throws when no search term survives trimming (the
18
+ * field is required, but an array of blanks would otherwise send an empty search).
19
+ */
20
+ export declare function buildStartSearchBody(props: StartSearchProps): StartSearchBody;
21
+ export declare const startSearchAction: import("@activepieces/pieces-framework").IAction<import("@activepieces/pieces-framework").SecretTextProperty<true>, {
22
+ searchTerms: import("@activepieces/pieces-framework").ArrayProperty<true>;
23
+ location: import("@activepieces/pieces-framework").ShortTextProperty<true>;
24
+ maxResults: import("@activepieces/pieces-framework").NumberProperty<false>;
25
+ nameMatch: import("@activepieces/pieces-framework").StaticDropdownProperty<"all" | "contains" | "exact", false> | import("@activepieces/pieces-framework").StaticDropdownProperty<"all" | "contains" | "exact", true>;
26
+ website: import("@activepieces/pieces-framework").StaticDropdownProperty<"any" | "with" | "without", false> | import("@activepieces/pieces-framework").StaticDropdownProperty<"any" | "with" | "without", true>;
27
+ categories: import("@activepieces/pieces-framework").ArrayProperty<false>;
28
+ skipClosed: import("@activepieces/pieces-framework").CheckboxProperty<false>;
29
+ capCredits: import("@activepieces/pieces-framework").NumberProperty<false>;
30
+ language: import("@activepieces/pieces-framework").ShortTextProperty<false>;
31
+ }>;
@@ -0,0 +1,130 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.startSearchAction = void 0;
4
+ exports.buildStartSearchBody = buildStartSearchBody;
5
+ // Action: "Start a Search" — POST /api/v1/jobs.
6
+ //
7
+ // Exposes the restricted public search fields (openapi.yaml StartSearchRequest). The
8
+ // location is a plain text query (the natural input for a no-code user); the drawn-area
9
+ // (GeoJSON) form the API also accepts is intentionally not surfaced here. Billable add-ons
10
+ // and personal-data options are not part of the v1 surface at all, so they can't be
11
+ // reached from this action.
12
+ const pieces_framework_1 = require("@activepieces/pieces-framework");
13
+ const api_1 = require("../common/api");
14
+ const auth_1 = require("../common/auth");
15
+ const client_1 = require("../common/client");
16
+ /** Coerce an Activepieces array prop (its items are `unknown`) to clean, non-empty strings. */
17
+ function toStringList(value) {
18
+ if (!Array.isArray(value))
19
+ return [];
20
+ return value
21
+ .map((item) => (typeof item === 'string' ? item.trim() : String(item ?? '').trim()))
22
+ .filter((item) => item.length > 0);
23
+ }
24
+ /**
25
+ * Map the action's prop values to the API request body. Pure + exported so it is unit-
26
+ * tested without the Activepieces runtime. Optional fields are omitted entirely when unset
27
+ * so the API applies its own defaults. Throws when no search term survives trimming (the
28
+ * field is required, but an array of blanks would otherwise send an empty search).
29
+ */
30
+ function buildStartSearchBody(props) {
31
+ const searchStrings = toStringList(props.searchTerms);
32
+ if (searchStrings.length === 0) {
33
+ throw new Error('Enter at least one search term, e.g. "dentist".');
34
+ }
35
+ const location = (props.location ?? '').trim();
36
+ if (location.length === 0) {
37
+ throw new Error('Enter a location, e.g. "Austin, TX".');
38
+ }
39
+ const body = {
40
+ searchStrings,
41
+ locationQuery: location,
42
+ };
43
+ if (props.maxResults != null)
44
+ body.maxResults = props.maxResults;
45
+ if (props.nameMatch)
46
+ body.nameMatch = props.nameMatch;
47
+ if (props.website)
48
+ body.website = props.website;
49
+ if (props.skipClosed != null)
50
+ body.skipClosed = props.skipClosed;
51
+ if (props.capCredits != null)
52
+ body.capCredits = props.capCredits;
53
+ if (props.language && props.language.trim().length > 0)
54
+ body.language = props.language.trim();
55
+ const categories = toStringList(props.categories);
56
+ if (categories.length > 0)
57
+ body.categories = categories;
58
+ return body;
59
+ }
60
+ exports.startSearchAction = (0, pieces_framework_1.createAction)({
61
+ auth: auth_1.liveMapLeadsAuth,
62
+ name: 'start_search',
63
+ displayName: 'Start a Search',
64
+ description: 'Start a new local-business search. Returns right away with a search ID while the search runs in the background — use the "New Search Completed" trigger to pick up the results when they are ready.',
65
+ props: {
66
+ searchTerms: pieces_framework_1.Property.Array({
67
+ displayName: 'Search Terms',
68
+ description: 'What to look for, e.g. "dentist" or "italian restaurant". Add one term per line; each runs against your location.',
69
+ required: true,
70
+ }),
71
+ location: pieces_framework_1.Property.ShortText({
72
+ displayName: 'Location',
73
+ description: 'Where to search, e.g. "Austin, TX" or "Lyon, France".',
74
+ required: true,
75
+ }),
76
+ maxResults: pieces_framework_1.Property.Number({
77
+ displayName: 'Max Results per Term',
78
+ description: 'Optional cap on how many businesses to return for each search term. Leave empty for no cap.',
79
+ required: false,
80
+ }),
81
+ nameMatch: pieces_framework_1.Property.StaticDropdown({
82
+ displayName: 'Business Name Match',
83
+ description: 'How strictly a business name must match your search term.',
84
+ required: false,
85
+ options: {
86
+ options: [
87
+ { label: 'Any name (no filter)', value: 'all' },
88
+ { label: 'Name contains the term', value: 'contains' },
89
+ { label: 'Name is exactly the term', value: 'exact' },
90
+ ],
91
+ },
92
+ }),
93
+ website: pieces_framework_1.Property.StaticDropdown({
94
+ displayName: 'Website Filter',
95
+ description: 'Keep only businesses with or without a website.',
96
+ required: false,
97
+ options: {
98
+ options: [
99
+ { label: 'Any (no filter)', value: 'any' },
100
+ { label: 'Only with a website', value: 'with' },
101
+ { label: 'Only without a website', value: 'without' },
102
+ ],
103
+ },
104
+ }),
105
+ categories: pieces_framework_1.Property.Array({
106
+ displayName: 'Categories',
107
+ description: 'Optional. Restrict results to specific business categories (e.g. "dentist", "art gallery"). Each must be a recognised category — an unknown value is rejected. Leave empty to apply no category filter.',
108
+ required: false,
109
+ }),
110
+ skipClosed: pieces_framework_1.Property.Checkbox({
111
+ displayName: 'Skip Closed Businesses',
112
+ description: 'Exclude permanently or temporarily closed businesses.',
113
+ required: false,
114
+ }),
115
+ capCredits: pieces_framework_1.Property.Number({
116
+ displayName: 'Credit Limit',
117
+ description: 'Optional ceiling on how many credits this search may spend. Recommended for unattended automations.',
118
+ required: false,
119
+ }),
120
+ language: pieces_framework_1.Property.ShortText({
121
+ displayName: 'Result Language',
122
+ description: 'Optional language code for results (e.g. "en", "fr", "pt-br"). Defaults to English.',
123
+ required: false,
124
+ }),
125
+ },
126
+ async run(context) {
127
+ const body = buildStartSearchBody(context.propsValue);
128
+ return await (0, api_1.startSearch)((0, client_1.resolveApiKey)(context.auth), body);
129
+ },
130
+ });
@@ -0,0 +1,36 @@
1
+ import type { components } from '../../generated/v1';
2
+ /** The restricted public search request body (openapi.yaml StartSearchRequest). */
3
+ export type StartSearchBody = components['schemas']['StartSearchRequest'];
4
+ /** 202 response from starting a search. */
5
+ export type StartSearchResult = components['schemas']['StartSearchResponse'];
6
+ /** One completed search in the polling list. */
7
+ export type SearchSummary = components['schemas']['SearchSummary'];
8
+ /** A page of completed searches. */
9
+ export type SearchList = components['schemas']['SearchList'];
10
+ /**
11
+ * A failed Live Map Leads API call. `status` is the HTTP status (0 if the request never
12
+ * reached the server); `code` is the stable machine code from the API (`unauthorized`,
13
+ * `rate_limited`, `invalid_request`, …) or a synthetic one for transport failures.
14
+ * `message` is already ICP-friendly — safe to surface to the user.
15
+ */
16
+ export declare class LiveMapLeadsApiError extends Error {
17
+ readonly status: number;
18
+ readonly code: string;
19
+ constructor(status: number, code: string, message: string);
20
+ }
21
+ /**
22
+ * Start a search (POST /jobs). Returns the `{ jobId, status: "queued" }` accept response.
23
+ * The search runs in the background; credits are charged only on success, and never for a
24
+ * search that fails. Throws `LiveMapLeadsApiError` on any non-2xx / transport failure.
25
+ */
26
+ export declare function startSearch(apiKey: string, body: StartSearchBody, baseUrl?: string): Promise<StartSearchResult>;
27
+ /**
28
+ * List the account's completed searches, newest first (GET /jobs). `since` (RFC 3339)
29
+ * returns only searches that finished strictly after it — pass the newest `completedAt`
30
+ * you have already seen for incremental polling. Read-only: spends no credits. Throws
31
+ * `LiveMapLeadsApiError` on any non-2xx / transport failure.
32
+ */
33
+ export declare function listCompletedSearches(apiKey: string, params: {
34
+ since?: string;
35
+ limit?: number;
36
+ }, baseUrl?: string): Promise<SearchList>;
@@ -0,0 +1,107 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LiveMapLeadsApiError = void 0;
4
+ exports.startSearch = startSearch;
5
+ exports.listCompletedSearches = listCompletedSearches;
6
+ const client_1 = require("./client");
7
+ const constants_1 = require("./constants");
8
+ /**
9
+ * A failed Live Map Leads API call. `status` is the HTTP status (0 if the request never
10
+ * reached the server); `code` is the stable machine code from the API (`unauthorized`,
11
+ * `rate_limited`, `invalid_request`, …) or a synthetic one for transport failures.
12
+ * `message` is already ICP-friendly — safe to surface to the user.
13
+ */
14
+ class LiveMapLeadsApiError extends Error {
15
+ status;
16
+ code;
17
+ constructor(status, code, message) {
18
+ super(message);
19
+ this.name = 'LiveMapLeadsApiError';
20
+ this.status = status;
21
+ this.code = code;
22
+ }
23
+ }
24
+ exports.LiveMapLeadsApiError = LiveMapLeadsApiError;
25
+ /** Narrow an unknown error body to the API's `{ error, message }` Error schema. */
26
+ function readErrorBody(body) {
27
+ if (body && typeof body === 'object') {
28
+ const record = body;
29
+ return {
30
+ code: typeof record.error === 'string' ? record.error : undefined,
31
+ message: typeof record.message === 'string' ? record.message : undefined,
32
+ };
33
+ }
34
+ return {};
35
+ }
36
+ /**
37
+ * Turn an `openapi-fetch` failure into a `LiveMapLeadsApiError` with copy a non-technical
38
+ * user can act on. Known statuses get tailored guidance; the API's own human `message` is
39
+ * preferred for validation (400) errors, which are already specific and readable.
40
+ */
41
+ function toApiError(errorBody, response) {
42
+ const status = response?.status ?? 0;
43
+ const { code: bodyCode, message: bodyMessage } = readErrorBody(errorBody);
44
+ if (status === 401 || status === 403) {
45
+ return new LiveMapLeadsApiError(status, bodyCode ?? 'unauthorized', 'Your API key was not accepted. Open your Live Map Leads account settings, check the key is correct and still active, then reconnect.');
46
+ }
47
+ if (status === 429) {
48
+ const retryAfter = response?.headers.get('retry-after');
49
+ const wait = retryAfter ? ` Wait ${retryAfter} seconds and try again.` : ' Please slow down and try again shortly.';
50
+ return new LiveMapLeadsApiError(status, bodyCode ?? 'rate_limited', `Too many requests.${wait}`);
51
+ }
52
+ if (status === 400 && bodyMessage) {
53
+ // The API's 400 message is human-readable and specific (e.g. an unrecognised category).
54
+ return new LiveMapLeadsApiError(status, bodyCode ?? 'invalid_request', bodyMessage);
55
+ }
56
+ if (status === 0) {
57
+ return new LiveMapLeadsApiError(0, 'network_error', "Couldn't reach Live Map Leads. Check your connection and try again.");
58
+ }
59
+ return new LiveMapLeadsApiError(status, bodyCode ?? 'request_failed', bodyMessage ?? 'Something went wrong talking to Live Map Leads. Please try again in a moment.');
60
+ }
61
+ /**
62
+ * Start a search (POST /jobs). Returns the `{ jobId, status: "queued" }` accept response.
63
+ * The search runs in the background; credits are charged only on success, and never for a
64
+ * search that fails. Throws `LiveMapLeadsApiError` on any non-2xx / transport failure.
65
+ */
66
+ async function startSearch(apiKey, body, baseUrl = constants_1.PROD_BASE_URL) {
67
+ let result;
68
+ try {
69
+ result = await (0, client_1.makeClient)(apiKey, baseUrl).POST('/jobs', { body });
70
+ }
71
+ catch {
72
+ // fetch rejected (DNS / connection / abort) — openapi-fetch surfaces HTTP errors via
73
+ // `error`, but transport failures throw, so convert them to the friendly network error.
74
+ throw toApiError(undefined, undefined);
75
+ }
76
+ if (result.data === undefined) {
77
+ throw toApiError(result.error, result.response);
78
+ }
79
+ return result.data;
80
+ }
81
+ /**
82
+ * List the account's completed searches, newest first (GET /jobs). `since` (RFC 3339)
83
+ * returns only searches that finished strictly after it — pass the newest `completedAt`
84
+ * you have already seen for incremental polling. Read-only: spends no credits. Throws
85
+ * `LiveMapLeadsApiError` on any non-2xx / transport failure.
86
+ */
87
+ async function listCompletedSearches(apiKey, params, baseUrl = constants_1.PROD_BASE_URL) {
88
+ let result;
89
+ try {
90
+ result = await (0, client_1.makeClient)(apiKey, baseUrl).GET('/jobs', {
91
+ params: {
92
+ query: {
93
+ status: 'completed',
94
+ limit: params.limit ?? constants_1.MAX_LIST_LIMIT,
95
+ ...(params.since ? { since: params.since } : {}),
96
+ },
97
+ },
98
+ });
99
+ }
100
+ catch {
101
+ throw toApiError(undefined, undefined);
102
+ }
103
+ if (result.data === undefined) {
104
+ throw toApiError(result.error, result.response);
105
+ }
106
+ return result.data;
107
+ }
@@ -0,0 +1 @@
1
+ export declare const liveMapLeadsAuth: import("@activepieces/pieces-framework").SecretTextProperty<true>;
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.liveMapLeadsAuth = void 0;
4
+ // The connection users set up once: their Live Map Leads API key.
5
+ const pieces_framework_1 = require("@activepieces/pieces-framework");
6
+ const api_1 = require("./api");
7
+ const client_1 = require("./client");
8
+ const AUTH_DESCRIPTION = `Connect your Live Map Leads account with an API key.
9
+
10
+ 1. Sign in at https://livemapleads.com and open **Account settings → API keys**.
11
+ 2. Create a key and copy it — it is shown only once.
12
+ 3. Paste it below.
13
+
14
+ The key acts on behalf of your account: it can start searches (which spend credits) and read your results. Keep it secret.`;
15
+ exports.liveMapLeadsAuth = pieces_framework_1.PieceAuth.SecretText({
16
+ displayName: 'API Key',
17
+ description: AUTH_DESCRIPTION,
18
+ required: true,
19
+ // Confirm the key works the moment it is entered, using the read-only "list searches"
20
+ // endpoint (spends no credits). Only an explicit key rejection blocks the connection; a
21
+ // transient outage is not treated as a bad key.
22
+ validate: async ({ auth }) => {
23
+ try {
24
+ await (0, api_1.listCompletedSearches)((0, client_1.resolveApiKey)(auth), { limit: 1 });
25
+ return { valid: true };
26
+ }
27
+ catch (error) {
28
+ if (error instanceof api_1.LiveMapLeadsApiError && (error.status === 401 || error.status === 403)) {
29
+ return { valid: false, error: 'That API key was not accepted. Check it is correct and still active in your account settings.' };
30
+ }
31
+ // Network/server hiccup — don't reject a possibly-good key over a transient error.
32
+ return { valid: true };
33
+ }
34
+ },
35
+ });
@@ -0,0 +1,17 @@
1
+ import { type Client } from 'openapi-fetch';
2
+ import type { paths } from '../../generated/v1';
3
+ export type V1Client = Client<paths>;
4
+ /**
5
+ * Resolve the API key string from an Activepieces auth value. A `SecretText` connection is
6
+ * typed as `{ type, secret_text }` by the SDK but has historically been delivered to piece
7
+ * code as the bare string — this accepts BOTH shapes so the key resolves correctly across
8
+ * Activepieces versions. Throws if neither form yields a non-empty key.
9
+ */
10
+ export declare function resolveApiKey(auth: unknown): string;
11
+ /**
12
+ * Build an authenticated client. `apiKey` is the account API key from Live Map Leads
13
+ * account settings; it is sent on the `x-api-key` header of every request. `baseUrl` is
14
+ * overridable only so tests can point at a mock server — published actions/triggers always
15
+ * use the production base.
16
+ */
17
+ export declare function makeClient(apiKey: string, baseUrl?: string): V1Client;
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.resolveApiKey = resolveApiKey;
7
+ exports.makeClient = makeClient;
8
+ // The typed HTTP client for the Live Map Leads API.
9
+ //
10
+ // The client is generated from the OpenAPI spec (`openapi.yaml`, vendored from the live
11
+ // `GET /api/v1/openapi.yaml`): `src/generated/v1.ts` holds the spec-derived TYPES
12
+ // (build-time only, erased at runtime) and `openapi-fetch` is the single ~6 KB runtime
13
+ // dependency. A wrong path, field, or query param fails to COMPILE — the spec is the
14
+ // contract, nothing here re-describes request or response shapes by hand. To refresh after
15
+ // the API changes: `npm run gen:types` (see README § Updating the client).
16
+ const openapi_fetch_1 = __importDefault(require("openapi-fetch"));
17
+ const constants_1 = require("./constants");
18
+ /**
19
+ * Resolve the API key string from an Activepieces auth value. A `SecretText` connection is
20
+ * typed as `{ type, secret_text }` by the SDK but has historically been delivered to piece
21
+ * code as the bare string — this accepts BOTH shapes so the key resolves correctly across
22
+ * Activepieces versions. Throws if neither form yields a non-empty key.
23
+ */
24
+ function resolveApiKey(auth) {
25
+ if (typeof auth === 'string') {
26
+ const key = auth.trim();
27
+ if (key.length > 0)
28
+ return key;
29
+ }
30
+ if (auth && typeof auth === 'object' && 'secret_text' in auth) {
31
+ const raw = auth.secret_text;
32
+ if (typeof raw === 'string') {
33
+ const key = raw.trim();
34
+ if (key.length > 0)
35
+ return key;
36
+ }
37
+ }
38
+ throw new Error('Missing Live Map Leads API key. Reconnect your account in the connection settings.');
39
+ }
40
+ /**
41
+ * Build an authenticated client. `apiKey` is the account API key from Live Map Leads
42
+ * account settings; it is sent on the `x-api-key` header of every request. `baseUrl` is
43
+ * overridable only so tests can point at a mock server — published actions/triggers always
44
+ * use the production base.
45
+ */
46
+ function makeClient(apiKey, baseUrl = constants_1.PROD_BASE_URL) {
47
+ return (0, openapi_fetch_1.default)({
48
+ baseUrl,
49
+ headers: { 'x-api-key': apiKey },
50
+ });
51
+ }
@@ -0,0 +1,9 @@
1
+ /** The public Live Map Leads API base. Every action and trigger talks to this. */
2
+ export declare const PROD_BASE_URL = "https://livemapleads.com/api/v1";
3
+ /**
4
+ * Page size for listing completed searches. The API caps `limit` at 100; we ask for the
5
+ * max so the "New Search Completed" trigger picks up everything finished since the last
6
+ * check in a single call (one account rarely finishes more than 100 searches between
7
+ * checks).
8
+ */
9
+ export declare const MAX_LIST_LIMIT = 100;
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ // Shared constants for the Live Map Leads piece.
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.MAX_LIST_LIMIT = exports.PROD_BASE_URL = void 0;
5
+ /** The public Live Map Leads API base. Every action and trigger talks to this. */
6
+ exports.PROD_BASE_URL = 'https://livemapleads.com/api/v1';
7
+ /**
8
+ * Page size for listing completed searches. The API caps `limit` at 100; we ask for the
9
+ * max so the "New Search Completed" trigger picks up everything finished since the last
10
+ * check in a single call (one account rarely finishes more than 100 searches between
11
+ * checks).
12
+ */
13
+ exports.MAX_LIST_LIMIT = 100;
@@ -0,0 +1,19 @@
1
+ import { type Polling } from '@activepieces/pieces-common';
2
+ import { type AppConnectionValueForAuthProperty, TriggerStrategy } from '@activepieces/pieces-framework';
3
+ import { liveMapLeadsAuth } from '../common/auth';
4
+ type AuthValue = AppConnectionValueForAuthProperty<typeof liveMapLeadsAuth>;
5
+ /**
6
+ * Fetch completed searches and map them to the helper's poll-item shape (exported for unit
7
+ * tests). `lastFetchEpochMS` is supplied by the helper: `0` on the manual "test trigger"
8
+ * run (return the most recent searches), or the timestamp of the newest search seen so far
9
+ * on a live poll. We translate it into the API's `since` filter so the server only returns
10
+ * newer searches; each item carries its `completedAt` as `epochMilliSeconds` for dedupe.
11
+ */
12
+ export declare function pollCompletedSearches(auth: AuthValue, lastFetchEpochMS: number): Promise<{
13
+ epochMilliSeconds: number;
14
+ data: unknown;
15
+ }[]>;
16
+ /** The polling definition wired into the trigger hooks (exported for integration tests). */
17
+ export declare const polling: Polling<AuthValue, Record<string, never>>;
18
+ export declare const newSearchCompleted: import("@activepieces/pieces-framework").ITrigger<TriggerStrategy.WEBHOOK, import("@activepieces/pieces-framework").SecretTextProperty<true>, {}> | import("@activepieces/pieces-framework").ITrigger<TriggerStrategy.POLLING, import("@activepieces/pieces-framework").SecretTextProperty<true>, {}> | import("@activepieces/pieces-framework").ITrigger<TriggerStrategy.MANUAL, import("@activepieces/pieces-framework").SecretTextProperty<true>, {}> | import("@activepieces/pieces-framework").ITrigger<TriggerStrategy.APP_WEBHOOK, import("@activepieces/pieces-framework").SecretTextProperty<true>, {}>;
19
+ export {};
@@ -0,0 +1,100 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.newSearchCompleted = exports.polling = void 0;
4
+ exports.pollCompletedSearches = pollCompletedSearches;
5
+ // Trigger: "New Search Completed" — fires once per search as it finishes.
6
+ //
7
+ // Polling-first (decoupled from any push/webhook delivery): Activepieces checks the
8
+ // read-only list endpoint (GET /api/v1/jobs?status=completed&since=…) on its schedule and
9
+ // emits one item per newly-completed search. Dedupe is TIME-BASED on `completedAt`: the
10
+ // helper stores the newest timestamp it has seen and only emits searches that finished
11
+ // strictly after it — which is exactly the `since` cursor the API exposes. Listing spends
12
+ // no credits.
13
+ //
14
+ // An instant (webhook) variant is a future, drop-in upgrade: only this file's strategy
15
+ // changes — the action and the API client stay as-is (see README § Polling vs. instant).
16
+ const pieces_common_1 = require("@activepieces/pieces-common");
17
+ const pieces_framework_1 = require("@activepieces/pieces-framework");
18
+ const api_1 = require("../common/api");
19
+ const auth_1 = require("../common/auth");
20
+ const client_1 = require("../common/client");
21
+ const constants_1 = require("../common/constants");
22
+ /**
23
+ * Fetch completed searches and map them to the helper's poll-item shape (exported for unit
24
+ * tests). `lastFetchEpochMS` is supplied by the helper: `0` on the manual "test trigger"
25
+ * run (return the most recent searches), or the timestamp of the newest search seen so far
26
+ * on a live poll. We translate it into the API's `since` filter so the server only returns
27
+ * newer searches; each item carries its `completedAt` as `epochMilliSeconds` for dedupe.
28
+ */
29
+ async function pollCompletedSearches(auth, lastFetchEpochMS) {
30
+ const since = lastFetchEpochMS > 0 ? new Date(lastFetchEpochMS).toISOString() : undefined;
31
+ const page = await (0, api_1.listCompletedSearches)((0, client_1.resolveApiKey)(auth), { since, limit: constants_1.MAX_LIST_LIMIT });
32
+ // One page (the newest MAX_LIST_LIMIT, the API maximum) per poll. The API exposes no
33
+ // forward pagination — only `since` + `limit` (≤100) + `hasMore` — so if MORE than 100
34
+ // searches completed since the last poll, the page is truncated to the newest 100 and the
35
+ // older overflow is not emitted (the time cursor advances past them). This is the upstream
36
+ // API's documented, accepted polling residual ("rare for a single account") and is bounded
37
+ // to near-impossible here: an account runs one search at a time (each takes seconds to
38
+ // minutes), so >100 completing inside one poll interval cannot realistically occur. The
39
+ // instant (webhook) trigger upgrade removes the ceiling entirely. See README § Known limits.
40
+ return page.data.map((search) => ({
41
+ epochMilliSeconds: Date.parse(search.completedAt),
42
+ data: search,
43
+ }));
44
+ }
45
+ /** The polling definition wired into the trigger hooks (exported for integration tests). */
46
+ exports.polling = {
47
+ strategy: pieces_common_1.DedupeStrategy.TIMEBASED,
48
+ items: ({ auth, lastFetchEpochMS }) => pollCompletedSearches(auth, lastFetchEpochMS),
49
+ };
50
+ exports.newSearchCompleted = (0, pieces_framework_1.createTrigger)({
51
+ auth: auth_1.liveMapLeadsAuth,
52
+ name: 'new_search_completed',
53
+ displayName: 'New Search Completed',
54
+ description: 'Fires when one of your searches finishes, with its record count, the credits charged, and ready-to-use download links (CSV, XLSX, JSON).',
55
+ type: pieces_framework_1.TriggerStrategy.POLLING,
56
+ props: {},
57
+ sampleData: {
58
+ jobId: '8f1d6c2a-3b4e-4f7a-9c1d-2e3f4a5b6c7d',
59
+ status: 'completed',
60
+ recordCount: 142,
61
+ creditsCharged: 142,
62
+ completedAt: '2026-06-12T14:22:05Z',
63
+ downloads: {
64
+ csv: 'https://livemapleads.com/api/exports/download?token=eyJhbGciOi...',
65
+ xlsx: 'https://livemapleads.com/api/exports/download?token=eyJhbGciOi...',
66
+ json: 'https://livemapleads.com/api/exports/download?token=eyJhbGciOi...',
67
+ },
68
+ summary: 'Your search finished with 142 records.',
69
+ },
70
+ async onEnable(context) {
71
+ await pieces_common_1.pollingHelper.onEnable(exports.polling, {
72
+ store: context.store,
73
+ auth: context.auth,
74
+ propsValue: context.propsValue,
75
+ });
76
+ },
77
+ async onDisable(context) {
78
+ await pieces_common_1.pollingHelper.onDisable(exports.polling, {
79
+ store: context.store,
80
+ auth: context.auth,
81
+ propsValue: context.propsValue,
82
+ });
83
+ },
84
+ async run(context) {
85
+ return await pieces_common_1.pollingHelper.poll(exports.polling, {
86
+ store: context.store,
87
+ auth: context.auth,
88
+ propsValue: context.propsValue,
89
+ files: context.files,
90
+ });
91
+ },
92
+ async test(context) {
93
+ return await pieces_common_1.pollingHelper.test(exports.polling, {
94
+ store: context.store,
95
+ auth: context.auth,
96
+ propsValue: context.propsValue,
97
+ files: context.files,
98
+ });
99
+ },
100
+ });
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@livemapleads/piece-live-map-leads",
3
+ "version": "0.1.0",
4
+ "description": "Activepieces piece for Live Map Leads — start local-business searches and trigger when a search completes.",
5
+ "keywords": [
6
+ "activepieces",
7
+ "activepieces-piece",
8
+ "live-map-leads",
9
+ "leads",
10
+ "local-business",
11
+ "automation"
12
+ ],
13
+ "homepage": "https://livemapleads.com",
14
+ "bugs": {
15
+ "url": "https://github.com/LiveMapLeads/connectors/issues"
16
+ },
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/LiveMapLeads/connectors.git",
20
+ "directory": "activepieces"
21
+ },
22
+ "license": "MIT",
23
+ "type": "commonjs",
24
+ "main": "dist/index.js",
25
+ "types": "dist/index.d.ts",
26
+ "files": [
27
+ "dist"
28
+ ],
29
+ "scripts": {
30
+ "gen:types": "openapi-typescript openapi.yaml --default-non-nullable false -o src/generated/v1.ts",
31
+ "build": "tsc -p tsconfig.build.json",
32
+ "typecheck": "tsc -p tsconfig.json",
33
+ "lint": "eslint .",
34
+ "test": "vitest run",
35
+ "check": "npm run typecheck && npm run lint && npm run test && npm run build"
36
+ },
37
+ "dependencies": {
38
+ "@activepieces/pieces-common": "0.12.4",
39
+ "@activepieces/pieces-framework": "0.30.0",
40
+ "openapi-fetch": "0.17.0"
41
+ },
42
+ "devDependencies": {
43
+ "@activepieces/shared": "^0.92.0",
44
+ "@types/node": "^22.10.2",
45
+ "eslint": "^9.17.0",
46
+ "openapi-typescript": "7.13.0",
47
+ "typescript": "^5.7.2",
48
+ "typescript-eslint": "^8.18.1",
49
+ "vitest": "^2.1.8"
50
+ },
51
+ "engines": {
52
+ "node": ">=18"
53
+ }
54
+ }