2026-08-02 14:38:12 +00:00
|
|
|
# JavaScript sample
|
|
|
|
|
|
|
|
|
|
## What this does
|
|
|
|
|
|
|
|
|
|
The same flow as the [Python sample](python-sample.md), in JavaScript: obtain an access token,
|
Refactor comments for clarity and consistency across multiple screens and services
- Updated comments in LeadsListScreen, MoreScreen, TaskDetailScreen, TaskFormScreen, TasksListScreen, TicketAnalyticsScreen, TicketDetailScreen, TicketsListScreen, ApiService, AuthService, CrashReporting, DealCard, TicketCard, MultiSelectSheet, KanbanColumn, StageStepper, TicketApprovalPanel, TicketPropertiesCard, and TicketTimePanel to improve readability and maintain a consistent style.
- Adjusted punctuation and phrasing for better flow and understanding.
2026-08-03 04:06:48 +00:00
|
|
|
list the org's leads, and create one, using only the built-in `fetch`, no npm dependencies. It's
|
2026-08-02 14:38:12 +00:00
|
|
|
written for Node (tested for compatibility with Node 24), but the `fetch` calls themselves would
|
|
|
|
|
run unmodified in any modern browser's console against a CORS-enabled backend.
|
|
|
|
|
|
|
|
|
|
As with the Python sample, "obtains a token" means reading a personal access token (`bcrm_pat_…`)
|
Refactor comments for clarity and consistency across multiple screens and services
- Updated comments in LeadsListScreen, MoreScreen, TaskDetailScreen, TaskFormScreen, TasksListScreen, TicketAnalyticsScreen, TicketDetailScreen, TicketsListScreen, ApiService, AuthService, CrashReporting, DealCard, TicketCard, MultiSelectSheet, KanbanColumn, StageStepper, TicketApprovalPanel, TicketPropertiesCard, and TicketTimePanel to improve readability and maintain a consistent style.
- Adjusted punctuation and phrasing for better flow and understanding.
2026-08-03 04:06:48 +00:00
|
|
|
from the environment rather than driving an interactive sign-in. See
|
2026-08-02 14:38:12 +00:00
|
|
|
[Tokens and API keys](../api/tokens-and-api-keys.md#personal-access-tokens) for what a PAT
|
|
|
|
|
authenticates as, and the [Python sample](python-sample.md#what-this-does) for how to mint one
|
|
|
|
|
locally.
|
|
|
|
|
|
|
|
|
|
## The script
|
|
|
|
|
|
|
|
|
|
```javascript
|
|
|
|
|
#!/usr/bin/env node
|
|
|
|
|
// List an org's leads and create one, using a BottleCRM personal access token.
|
|
|
|
|
//
|
|
|
|
|
// Usage:
|
|
|
|
|
// BCRM_BASE_URL=http://localhost:8000 BCRM_TOKEN=bcrm_pat_... node leads-sample.mjs
|
|
|
|
|
//
|
|
|
|
|
// Requires Node 18+ for the built-in `fetch`. The flag history, precisely:
|
|
|
|
|
// --experimental-fetch was a Node 17.5 flag; Node 18 made fetch available
|
|
|
|
|
// with no flag at all (still labeled experimental, with a startup warning);
|
|
|
|
|
// Node 21 dropped that warning and made fetch stable. Tested for Node 24.
|
|
|
|
|
// No npm dependencies.
|
|
|
|
|
//
|
|
|
|
|
// See docs/api/tokens-and-api-keys.md for how to mint BCRM_TOKEN, and
|
|
|
|
|
// docs/api/leads.md for the shape of the /api/leads/ responses this reads.
|
|
|
|
|
|
|
|
|
|
const BASE_URL = (process.env.BCRM_BASE_URL || "http://localhost:8000").replace(/\/$/, "");
|
|
|
|
|
const TOKEN = process.env.BCRM_TOKEN;
|
|
|
|
|
|
|
|
|
|
if (!TOKEN) {
|
|
|
|
|
console.error(
|
|
|
|
|
"Set BCRM_TOKEN to a bcrm_pat_... personal access token " +
|
|
|
|
|
"(see docs/api/tokens-and-api-keys.md)."
|
|
|
|
|
);
|
|
|
|
|
process.exit(1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const headers = {
|
|
|
|
|
Authorization: `Bearer ${TOKEN}`,
|
|
|
|
|
Accept: "application/json",
|
|
|
|
|
"Content-Type": "application/json",
|
|
|
|
|
};
|
|
|
|
|
|
Refactor comments for clarity and consistency across multiple screens and services
- Updated comments in LeadsListScreen, MoreScreen, TaskDetailScreen, TaskFormScreen, TasksListScreen, TicketAnalyticsScreen, TicketDetailScreen, TicketsListScreen, ApiService, AuthService, CrashReporting, DealCard, TicketCard, MultiSelectSheet, KanbanColumn, StageStepper, TicketApprovalPanel, TicketPropertiesCard, and TicketTimePanel to improve readability and maintain a consistent style.
- Adjusted punctuation and phrasing for better flow and understanding.
2026-08-03 04:06:48 +00:00
|
|
|
// GET /api/leads/, the response splits leads into open/closed sections
|
2026-08-02 14:38:12 +00:00
|
|
|
// rather than one flat list; see docs/api/leads.md#list-leads.
|
|
|
|
|
async function listLeads() {
|
|
|
|
|
const res = await fetch(`${BASE_URL}/api/leads/`, { headers });
|
|
|
|
|
const data = await res.json();
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
throw new Error(`List failed (${res.status}): ${JSON.stringify(data)}`);
|
|
|
|
|
}
|
|
|
|
|
const openSection = data.open_leads;
|
|
|
|
|
const leads = openSection.open_leads;
|
|
|
|
|
console.log(
|
|
|
|
|
`${leads.length} open lead(s) on this page (of ${openSection.leads_count} total open):`
|
|
|
|
|
);
|
|
|
|
|
for (const lead of leads) {
|
|
|
|
|
const name = `${lead.first_name ?? ""} ${lead.last_name ?? ""}`.trim();
|
|
|
|
|
console.log(
|
|
|
|
|
` - ${name || "(no name)"} <${lead.email ?? "no email"}> [${lead.status}]`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
return data;
|
|
|
|
|
}
|
|
|
|
|
|
Refactor comments for clarity and consistency across multiple screens and services
- Updated comments in LeadsListScreen, MoreScreen, TaskDetailScreen, TaskFormScreen, TasksListScreen, TicketAnalyticsScreen, TicketDetailScreen, TicketsListScreen, ApiService, AuthService, CrashReporting, DealCard, TicketCard, MultiSelectSheet, KanbanColumn, StageStepper, TicketApprovalPanel, TicketPropertiesCard, and TicketTimePanel to improve readability and maintain a consistent style.
- Adjusted punctuation and phrasing for better flow and understanding.
2026-08-03 04:06:48 +00:00
|
|
|
// POST /api/leads/. A 200 with {"error": false, ...} is success; the
|
2026-08-02 14:38:12 +00:00
|
|
|
// response does not echo the created record, so we look it up afterward.
|
|
|
|
|
async function createLead() {
|
|
|
|
|
const email = `sample.lead+${Date.now()}@example.com`;
|
|
|
|
|
const res = await fetch(`${BASE_URL}/api/leads/`, {
|
|
|
|
|
method: "POST",
|
|
|
|
|
headers,
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
first_name: "Sample",
|
|
|
|
|
last_name: "Lead",
|
|
|
|
|
email,
|
|
|
|
|
status: "assigned",
|
|
|
|
|
source: "other",
|
|
|
|
|
}),
|
|
|
|
|
});
|
|
|
|
|
const body = await res.json();
|
|
|
|
|
if (!res.ok || body.error) {
|
|
|
|
|
throw new Error(`Create failed (${res.status}): ${JSON.stringify(body)}`);
|
|
|
|
|
}
|
|
|
|
|
console.log(`Create response: ${JSON.stringify(body)}`);
|
|
|
|
|
return email;
|
|
|
|
|
}
|
|
|
|
|
|
Refactor comments for clarity and consistency across multiple screens and services
- Updated comments in LeadsListScreen, MoreScreen, TaskDetailScreen, TaskFormScreen, TasksListScreen, TicketAnalyticsScreen, TicketDetailScreen, TicketsListScreen, ApiService, AuthService, CrashReporting, DealCard, TicketCard, MultiSelectSheet, KanbanColumn, StageStepper, TicketApprovalPanel, TicketPropertiesCard, and TicketTimePanel to improve readability and maintain a consistent style.
- Adjusted punctuation and phrasing for better flow and understanding.
2026-08-03 04:06:48 +00:00
|
|
|
// GET /api/leads/?email=...; `email` is a case-insensitive contains filter
|
2026-08-02 14:38:12 +00:00
|
|
|
// (docs/api/conventions.md#filtering-and-search), fine here since the address
|
|
|
|
|
// we just created is unique in this org.
|
|
|
|
|
async function findByEmail(email) {
|
|
|
|
|
const url = new URL(`${BASE_URL}/api/leads/`);
|
|
|
|
|
url.searchParams.set("email", email);
|
|
|
|
|
const res = await fetch(url, { headers });
|
|
|
|
|
const data = await res.json();
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
throw new Error(`Lookup failed (${res.status}): ${JSON.stringify(data)}`);
|
|
|
|
|
}
|
|
|
|
|
const matches = data.open_leads.open_leads;
|
|
|
|
|
if (matches.length) {
|
|
|
|
|
console.log(`Found the new lead: id=${matches[0].id}`);
|
|
|
|
|
} else {
|
|
|
|
|
console.log("New lead not on this page (increase limit/offset to find it).");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function main() {
|
|
|
|
|
console.log("Listing existing leads...");
|
|
|
|
|
await listLeads();
|
|
|
|
|
console.log("\nCreating a lead...");
|
|
|
|
|
const email = await createLead();
|
|
|
|
|
console.log("\nConfirming it was created...");
|
|
|
|
|
await findByEmail(email);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
main().catch((err) => {
|
|
|
|
|
console.error(err.message);
|
|
|
|
|
process.exit(1);
|
|
|
|
|
});
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Running it
|
|
|
|
|
|
|
|
|
|
Save the script as `leads-sample.mjs`. It uses no `import`/`export` and no top-level `await` (the
|
|
|
|
|
`await` calls are all inside `async function main()`, invoked with `.catch()` rather than awaited
|
|
|
|
|
at the top level), so it runs the same way whether Node treats the file as CommonJS or an ES
|
Refactor comments for clarity and consistency across multiple screens and services
- Updated comments in LeadsListScreen, MoreScreen, TaskDetailScreen, TaskFormScreen, TasksListScreen, TicketAnalyticsScreen, TicketDetailScreen, TicketsListScreen, ApiService, AuthService, CrashReporting, DealCard, TicketCard, MultiSelectSheet, KanbanColumn, StageStepper, TicketApprovalPanel, TicketPropertiesCard, and TicketTimePanel to improve readability and maintain a consistent style.
- Adjusted punctuation and phrasing for better flow and understanding.
2026-08-03 04:06:48 +00:00
|
|
|
module, the `.mjs` extension just removes any ambiguity from a `package.json` in the same
|
2026-08-02 14:38:12 +00:00
|
|
|
directory. You need a reachable
|
|
|
|
|
BottleCRM backend and a personal access token, minted as described in the
|
|
|
|
|
[Python sample](python-sample.md#what-this-does):
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
BCRM_BASE_URL=http://localhost:8000 \
|
|
|
|
|
BCRM_TOKEN=bcrm_pat_... \
|
|
|
|
|
node leads-sample.mjs
|
|
|
|
|
```
|
|
|
|
|
|
Refactor comments for clarity and consistency across multiple screens and services
- Updated comments in LeadsListScreen, MoreScreen, TaskDetailScreen, TaskFormScreen, TasksListScreen, TicketAnalyticsScreen, TicketDetailScreen, TicketsListScreen, ApiService, AuthService, CrashReporting, DealCard, TicketCard, MultiSelectSheet, KanbanColumn, StageStepper, TicketApprovalPanel, TicketPropertiesCard, and TicketTimePanel to improve readability and maintain a consistent style.
- Adjusted punctuation and phrasing for better flow and understanding.
2026-08-03 04:06:48 +00:00
|
|
|
**A note on verification:** like the Python sample, this script was checked by static means,
|
2026-08-02 14:38:12 +00:00
|
|
|
every URL and field against [Leads](../api/leads.md) and [Conventions](../api/conventions.md), and
|
Refactor comments for clarity and consistency across multiple screens and services
- Updated comments in LeadsListScreen, MoreScreen, TaskDetailScreen, TaskFormScreen, TasksListScreen, TicketAnalyticsScreen, TicketDetailScreen, TicketsListScreen, ApiService, AuthService, CrashReporting, DealCard, TicketCard, MultiSelectSheet, KanbanColumn, StageStepper, TicketApprovalPanel, TicketPropertiesCard, and TicketTimePanel to improve readability and maintain a consistent style.
- Adjusted punctuation and phrasing for better flow and understanding.
2026-08-03 04:06:48 +00:00
|
|
|
the whole file with `node --check`, rather than executed against a live database, to avoid
|
2026-08-02 14:38:12 +00:00
|
|
|
writing a row into a shared instance this documentation didn't provision. The example output is
|
|
|
|
|
illustrative, not a captured run, and is identical in shape to the
|
|
|
|
|
[Python sample's](python-sample.md#running-it) since both call the same endpoints:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
Listing existing leads...
|
|
|
|
|
3 open lead(s) on this page (of 3 total open):
|
|
|
|
|
- Jordan Blake <jordan.blake@example.com> [assigned]
|
|
|
|
|
- Priya Shah <priya.shah@example.com> [in process]
|
|
|
|
|
- (no name) <no email> [assigned]
|
|
|
|
|
|
|
|
|
|
Creating a lead...
|
|
|
|
|
Create response: {"error":false,"message":"Lead Created Successfully"}
|
|
|
|
|
|
|
|
|
|
Confirming it was created...
|
|
|
|
|
Found the new lead: id=3f2a1c9e-....
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
A validation failure comes back as a `400` and the script throws with the error body BottleCRM
|
|
|
|
|
returned, per [Errors](../api/errors.md#validation-errors):
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
Create failed (400): {"error":true,"errors":{"email":["Another lead in this organisation already uses that email address."]}}
|
|
|
|
|
```
|