ab-ecosystem-cpass 1.0.6 → 1.0.8
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/README.md +5 -2
- package/dist/client.d.ts +31 -6
- package/dist/client.js +36 -8
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +9 -5
- package/dist/index.js.map +1 -1
- package/dist/resources/audience/canned-replies.d.ts +95 -0
- package/dist/resources/audience/canned-replies.js +112 -0
- package/dist/resources/audience/canned-replies.js.map +1 -0
- package/dist/resources/audience/notifications.d.ts +14 -0
- package/dist/resources/audience/notifications.js +14 -0
- package/dist/resources/audience/notifications.js.map +1 -1
- package/dist/resources/campaign-messages.d.ts +153 -0
- package/dist/resources/campaign-messages.js +179 -0
- package/dist/resources/campaign-messages.js.map +1 -0
- package/dist/resources/data-stores.d.ts +469 -0
- package/dist/resources/data-stores.js +530 -0
- package/dist/resources/data-stores.js.map +1 -0
- package/dist/resources/paths.d.ts +6 -0
- package/dist/resources/paths.js +7 -1
- package/dist/resources/paths.js.map +1 -1
- package/dist/types/campaign.d.ts +137 -41
- package/dist/types/canned-reply.d.ts +124 -0
- package/dist/types/canned-reply.js +6 -0
- package/dist/types/canned-reply.js.map +1 -0
- package/dist/types/datastore.d.ts +260 -0
- package/dist/types/datastore.js +21 -0
- package/dist/types/datastore.js.map +1 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.js +3 -0
- package/dist/types/index.js.map +1 -1
- package/docs/audience/canned-replies.md +302 -0
- package/docs/audience/notifications.md +99 -0
- package/docs/campaign/campaign-messages.md +282 -0
- package/docs/campaign/campaigns.md +8 -48
- package/docs/data-store.md +495 -0
- package/package.json +1 -1
- package/dist/resources/campaign-report-exports.d.ts +0 -14
- package/dist/resources/campaign-report-exports.js +0 -28
- package/dist/resources/campaign-report-exports.js.map +0 -1
- package/dist/resources/campaign-reports.d.ts +0 -15
- package/dist/resources/campaign-reports.js +0 -25
- package/dist/resources/campaign-reports.js.map +0 -1
|
@@ -434,56 +434,16 @@ await crm.campaigns.delete(id);
|
|
|
434
434
|
|
|
435
435
|
---
|
|
436
436
|
|
|
437
|
-
#
|
|
437
|
+
# Reporting
|
|
438
438
|
|
|
439
|
-
|
|
439
|
+
Campaign reporting lives on its own page: **[campaign messages](campaign-messages.md)**
|
|
440
|
+
— `crm.campaignMessages`.
|
|
440
441
|
|
|
441
|
-
|
|
442
|
+
There is no report collection and no export job. One row is written per attempted
|
|
443
|
+
send, its status is advanced by the channel's delivery webhooks, and every number
|
|
444
|
+
is counted off those rows when you ask for it:
|
|
442
445
|
|
|
443
446
|
```ts
|
|
444
|
-
const
|
|
445
|
-
|
|
447
|
+
const stats = await crm.campaignMessages.stats(id);
|
|
448
|
+
// { total, sent, delivered, read, replied, failed }
|
|
446
449
|
```
|
|
447
|
-
|
|
448
|
-
| Action | Route |
|
|
449
|
-
| --- | --- |
|
|
450
|
-
| `list` | `GET /v1/campaign-reports` — cursor-paged, `campaignId` optional |
|
|
451
|
-
| `getByCampaign` | `GET /v1/campaign-reports/:campaignId` |
|
|
452
|
-
|
|
453
|
-
`getByCampaign` takes an optional window; without it the report covers the
|
|
454
|
-
campaign's whole life:
|
|
455
|
-
|
|
456
|
-
```ts
|
|
457
|
-
await crm.campaignReports.getByCampaign(campaignId, {
|
|
458
|
-
from: '2026-10-01T00:00:00.000Z',
|
|
459
|
-
to: '2026-10-31T23:59:59.000Z',
|
|
460
|
-
});
|
|
461
|
-
```
|
|
462
|
-
|
|
463
|
-
`generatedAt` is when the metrics were last rolled up, which may lag the
|
|
464
|
-
campaign — a report read seconds after a send will under-count.
|
|
465
|
-
|
|
466
|
-
## Report exports
|
|
467
|
-
|
|
468
|
-
`crm.campaignReportExports` — `/v1/campaign-report-exports`
|
|
469
|
-
|
|
470
|
-
A per-recipient extract, generated asynchronously because it is a file rather
|
|
471
|
-
than a few counters.
|
|
472
|
-
|
|
473
|
-
```ts
|
|
474
|
-
const job = await crm.campaignReportExports.create({ campaignId, format: 'xlsx' });
|
|
475
|
-
|
|
476
|
-
// Poll until it finishes, then download.
|
|
477
|
-
const done = await crm.campaignReportExports.get(job.id);
|
|
478
|
-
if (done.status === 'completed') window.location.href = done.fileUrl!;
|
|
479
|
-
```
|
|
480
|
-
|
|
481
|
-
| Field | Type | Notes |
|
|
482
|
-
| --- | --- | --- |
|
|
483
|
-
| `campaignId` | `string` | required |
|
|
484
|
-
| `format` | `csv` `xlsx` | default `csv` |
|
|
485
|
-
| `from` / `to` | ISO datetime | narrows the rows by period |
|
|
486
|
-
|
|
487
|
-
`status` moves `queued` → `processing` → `completed` or `failed`; `rowCount` is
|
|
488
|
-
filled in as it goes, and `errorMessage` explains a failure. **`fileUrl` expires**
|
|
489
|
-
— download it rather than storing the link.
|
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
# Data Store
|
|
2
|
+
|
|
3
|
+
`crm.dataStores`, `crm.dataStoreCollections`, `crm.dataStoreRecords` — `/v1/datastores`
|
|
4
|
+
|
|
5
|
+
A MongoDB **you** own, registered with the platform. You give it a connection,
|
|
6
|
+
declare the collections and fields you want, and read and write records through
|
|
7
|
+
the API — from a chatbot flow, an automation, or your own code.
|
|
8
|
+
|
|
9
|
+
This runs as **its own service**, with its own database and its own deployment.
|
|
10
|
+
Nothing else in the platform depends on it, and it depends on nothing else: a
|
|
11
|
+
data store outage leaves contacts, the inbox and campaigns untouched, and the
|
|
12
|
+
connections it holds to customer clusters cannot crowd out anything but itself.
|
|
13
|
+
|
|
14
|
+
| Resource | Route | Scope |
|
|
15
|
+
| --- | --- | --- |
|
|
16
|
+
| [`dataStores`](#data-stores) | `/v1/datastores` | `datastores:read` / `datastores:write` |
|
|
17
|
+
| [`dataStoreCollections`](#collections) | `…/:store/collections` | `datastores:read` / `datastores:write` |
|
|
18
|
+
| [`dataStoreRecords`](#records) | `…/:store/collections/:collection/records` | `datastore-records:read` / `datastore-records:write` |
|
|
19
|
+
|
|
20
|
+
## The model
|
|
21
|
+
|
|
22
|
+
Three things, nested:
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
data store a connection to a database you own
|
|
26
|
+
└── collection a schema: one collection name, and its fields
|
|
27
|
+
└── records the documents, in your database
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
The platform holds **the credential and the schemas, and nothing else**. There
|
|
31
|
+
is no copy of your data here, no mirror and no sync: a record read is a live
|
|
32
|
+
query against your cluster, and a record write lands in it before the response
|
|
33
|
+
comes back.
|
|
34
|
+
|
|
35
|
+
Two consequences worth holding on to:
|
|
36
|
+
|
|
37
|
+
- **The platform is not the system of record.** Anything else holding those
|
|
38
|
+
credentials — your own application, a colleague with Compass — writes the same
|
|
39
|
+
documents. So a schema here is a *declaration*, not a guarantee: a document
|
|
40
|
+
may carry fields it never mentioned, or lack ones it does.
|
|
41
|
+
- **Every record call costs a round trip to your database.** The connection
|
|
42
|
+
stays open between calls, so that is one query's latency rather than a fresh
|
|
43
|
+
TLS handshake each time — but a loop of a thousand single inserts is a
|
|
44
|
+
thousand round trips. Send one `insert` with `records`.
|
|
45
|
+
|
|
46
|
+
## Deleting never deletes your data
|
|
47
|
+
|
|
48
|
+
Worth stating before anything else, because the opposite assumption is the
|
|
49
|
+
expensive one:
|
|
50
|
+
|
|
51
|
+
| Call | What it removes |
|
|
52
|
+
| --- | --- |
|
|
53
|
+
| `dataStores.remove()` | The stored credential, and every schema on it. **Your database is untouched.** |
|
|
54
|
+
| `dataStoreCollections.remove()` | The schema. **Your collection and its documents remain.** |
|
|
55
|
+
| `dataStoreRecords.remove()` / `bulkRemove()` | Documents, from your database, irreversibly. |
|
|
56
|
+
|
|
57
|
+
Register the same credential again, declare the same collection name, and all of
|
|
58
|
+
it reads straight back. Only the record calls delete anything of yours.
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Data stores
|
|
63
|
+
|
|
64
|
+
### Registering one
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
const store = await crm.dataStores.create({
|
|
68
|
+
title: 'Orders DB',
|
|
69
|
+
credentials: {
|
|
70
|
+
type: 'field',
|
|
71
|
+
host: 'cluster0.ab12c.mongodb.net',
|
|
72
|
+
username: 'crm_writer',
|
|
73
|
+
password: process.env.ORDERS_DB_PASSWORD!,
|
|
74
|
+
database: 'shop',
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Or with a connection string, for anything the four fields cannot express — a
|
|
80
|
+
replica-set seed list, a non-standard port, TLS options:
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
await crm.dataStores.create({
|
|
84
|
+
title: 'Orders DB',
|
|
85
|
+
credentials: { type: 'string', uri: process.env.ORDERS_DB_URI! },
|
|
86
|
+
});
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The URI **must name a database** (`mongodb+srv://user:pass@host/shop`). A URI
|
|
90
|
+
without one is legal Mongo and puts every document in a database called `test`,
|
|
91
|
+
which is a surprising place to find your records a month later, so it is refused
|
|
92
|
+
here instead.
|
|
93
|
+
|
|
94
|
+
### What happens before the row is written
|
|
95
|
+
|
|
96
|
+
1. The credential is checked for shape.
|
|
97
|
+
2. The host is resolved, and refused if it resolves somewhere private.
|
|
98
|
+
3. A connection is opened and pinged.
|
|
99
|
+
4. **Only then** is anything stored, with the secret encrypted.
|
|
100
|
+
|
|
101
|
+
So a `201` means your database really did accept those credentials. A store you
|
|
102
|
+
can see in the list is one that worked at least once.
|
|
103
|
+
|
|
104
|
+
| Status | Code | Meaning |
|
|
105
|
+
| --- | --- | --- |
|
|
106
|
+
| 422 | `validation_failed` | Malformed URI, a `host` with a scheme in it, no database named. |
|
|
107
|
+
| 422 | `invalid_datastore_host` | The host does not resolve, or resolves to a private address. |
|
|
108
|
+
| 502 | `datastore_auth_failed` | The database refused the username or password. |
|
|
109
|
+
| 502 | `datastore_unreachable` | No answer in time — usually an IP access list. |
|
|
110
|
+
|
|
111
|
+
### The host has to be reachable from the internet
|
|
112
|
+
|
|
113
|
+
A credential pointing at a private or loopback address is refused. The platform
|
|
114
|
+
would be connecting to *its own* network rather than to yours, so
|
|
115
|
+
`mongodb://localhost:27017` and `10.x`, `172.16–31.x`, `192.168.x`, `169.254.x`
|
|
116
|
+
are all rejected — including when a hostname you control resolves to one, since
|
|
117
|
+
the check is on the resolved address rather than on the string.
|
|
118
|
+
|
|
119
|
+
For `mongodb+srv` the SRV record is resolved first and its targets are checked,
|
|
120
|
+
which is also why a missing `_mongodb._tcp` record is reported as a bad host
|
|
121
|
+
rather than as a timeout thirty seconds later.
|
|
122
|
+
|
|
123
|
+
On Atlas you will usually also need to allow the platform's egress addresses in
|
|
124
|
+
your project's **IP access list**. Without that the symptom is
|
|
125
|
+
`502 datastore_unreachable` with a server-selection timeout, which looks like a
|
|
126
|
+
wrong host but is not.
|
|
127
|
+
|
|
128
|
+
### No credential ever comes back
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
store.summary; // 'mongodb+srv://crm_writer:***@cluster0.ab12c.mongodb.net/shop'
|
|
132
|
+
store.host; // 'cluster0.ab12c.mongodb.net'
|
|
133
|
+
store.username; // 'crm_writer'
|
|
134
|
+
store.database; // 'shop'
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
The password is encrypted at rest and is not returned in any form. `summary` is
|
|
138
|
+
the connection with the password replaced, which is what an edit screen needs in
|
|
139
|
+
order to show which database it is looking at. Host, user and database come back
|
|
140
|
+
in the clear because an edit form has to render them, and none of them opens a
|
|
141
|
+
connection on its own.
|
|
142
|
+
|
|
143
|
+
### Re-pointing one
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
await crm.dataStores.update(store.id, { title: 'Orders (production)' });
|
|
147
|
+
|
|
148
|
+
await crm.dataStores.update(store.id, {
|
|
149
|
+
credentials: { type: 'string', uri: rotatedUri },
|
|
150
|
+
});
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Credentials are replaced **whole or not at all**. There is no patching one field
|
|
154
|
+
of them: a host without its matching password is not a weaker credential, it is
|
|
155
|
+
a broken one. New credentials go through the same four steps as a create, and
|
|
156
|
+
the old connection is dropped — so the next record call reads from the new
|
|
157
|
+
cluster rather than from a connection that happens to still be open.
|
|
158
|
+
|
|
159
|
+
### Testing one
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
const { ok, database, collections, latencyMs } = await crm.dataStores.test(store.id);
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
The answer to "is it us or them". A credential that worked last month may have
|
|
166
|
+
been rotated, an Atlas project may be paused, an access list may have been
|
|
167
|
+
tightened — and each of those comes back as a different status code rather than
|
|
168
|
+
being inferred from a failing record call.
|
|
169
|
+
|
|
170
|
+
`collections` is also what you need to point a new schema at a collection that
|
|
171
|
+
already exists. It is **empty when the database user has no `listCollections`
|
|
172
|
+
privilege** — a least-privilege credential granted only `readWrite` legitimately
|
|
173
|
+
does not have it — so an empty list does not mean an empty database.
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## Collections
|
|
178
|
+
|
|
179
|
+
A collection is a schema: one collection name inside your database, and the
|
|
180
|
+
fields it holds.
|
|
181
|
+
|
|
182
|
+
```ts
|
|
183
|
+
const customers = await crm.dataStoreCollections.create(store.id, {
|
|
184
|
+
title: 'Customers',
|
|
185
|
+
fields: [
|
|
186
|
+
{ name: 'Email', type: 'String' },
|
|
187
|
+
{ name: 'Signed Up', type: 'Date' },
|
|
188
|
+
{ name: 'Credits', type: 'Number', default: 0 },
|
|
189
|
+
{ name: 'Tags', type: 'Array' },
|
|
190
|
+
],
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
customers.collection; // '68f1c2a39b4e5d6f70812345_customers'
|
|
194
|
+
customers.fields.map((f) => f.name); // ['email','signed_up','credits','tags']
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### Field names are slugged, once
|
|
198
|
+
|
|
199
|
+
Punctuation is dropped, runs of spaces and hyphens become one underscore, and
|
|
200
|
+
the result is lowercased. `"First Name"` becomes `first_name`.
|
|
201
|
+
|
|
202
|
+
**Use the names the response gives you as your record keys.** That is the key
|
|
203
|
+
the document actually has. The legacy panel slugged the name when building its
|
|
204
|
+
schema but stored the original, so a field called `First Name` was stored under
|
|
205
|
+
`first_name` while the schema said `First Name` — and a record written against
|
|
206
|
+
the schema's spelling was silently dropped. Here only the slug is ever
|
|
207
|
+
persisted, so the two cannot disagree.
|
|
208
|
+
|
|
209
|
+
Two names that slug to the same thing (`"First Name"` and `"first_name"`) are
|
|
210
|
+
refused rather than letting the later one quietly win.
|
|
211
|
+
|
|
212
|
+
### The field types
|
|
213
|
+
|
|
214
|
+
| Type | Accepts | Notes |
|
|
215
|
+
| --- | --- | --- |
|
|
216
|
+
| `String` | a string; numbers and booleans are stringified | |
|
|
217
|
+
| `Number` | a number, or a numeric string | `NaN` and `Infinity` are refused: nothing compares equal to `NaN`, so a document holding one cannot be found by its own value |
|
|
218
|
+
| `Boolean` | `true`/`false`, `"true"`/`"false"`, `1`/`0` | |
|
|
219
|
+
| `Date` | an ISO string, or a unix timestamp in seconds or milliseconds | Read by magnitude, so both units work |
|
|
220
|
+
| `Array` | an array of anything | Untyped, because the legacy declaration was |
|
|
221
|
+
|
|
222
|
+
`default` applies when a record omits the field, and only on `String`, `Number`
|
|
223
|
+
and `Boolean`: a `Date` default would have to mean "now", which is a different
|
|
224
|
+
feature, and an `Array` default shared between documents is a trap.
|
|
225
|
+
|
|
226
|
+
### Pointing at a collection you already have
|
|
227
|
+
|
|
228
|
+
```ts
|
|
229
|
+
const existing = await crm.dataStoreCollections.create(store.id, {
|
|
230
|
+
title: 'Orders',
|
|
231
|
+
collection: 'orders', // the collection already in your database
|
|
232
|
+
fields: [{ name: 'order_no', type: 'String' }, { name: 'total', type: 'Number' }],
|
|
233
|
+
});
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Omit `collection` and the server generates `<schema id>_<slugged title>`, which
|
|
237
|
+
cannot collide with anything already there. Supply it and the schema reads and
|
|
238
|
+
writes a collection you already have — which is how a database with data in it
|
|
239
|
+
gets used rather than duplicated. `dataStores.test()` lists the names.
|
|
240
|
+
|
|
241
|
+
It is **immutable afterwards**, and `update()` does not accept it. A schema
|
|
242
|
+
points at one collection for its whole life: every record ever written through
|
|
243
|
+
it lives there, and re-pointing it would orphan all of them. Point a *new*
|
|
244
|
+
schema at the other collection instead.
|
|
245
|
+
|
|
246
|
+
One schema per collection name per store, enforced by a unique index — two
|
|
247
|
+
schemas over one collection with different field declarations would mean a
|
|
248
|
+
record validating or not depending on which one it was written through.
|
|
249
|
+
|
|
250
|
+
### Declaring a collection does not create one
|
|
251
|
+
|
|
252
|
+
Mongo creates a collection on its first write. So a brand new schema addresses a
|
|
253
|
+
collection that is not there yet, and reads against it correctly return an empty
|
|
254
|
+
page rather than failing.
|
|
255
|
+
|
|
256
|
+
### Editing fields rewrites nothing
|
|
257
|
+
|
|
258
|
+
```ts
|
|
259
|
+
await crm.dataStoreCollections.update(store.id, customers.id, {
|
|
260
|
+
fields: [...customers.fields, { name: 'plan', type: 'String', default: 'free' }],
|
|
261
|
+
});
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
`fields` replaces the whole list, and takes effect from the next call onward. It
|
|
265
|
+
does not migrate anything:
|
|
266
|
+
|
|
267
|
+
- **Dropping a field** stops it being writable and does not delete the data
|
|
268
|
+
under that key. Re-declare the field and the values are there again.
|
|
269
|
+
- **Changing a type** leaves existing documents holding the old type. A
|
|
270
|
+
`Number` field that used to be a `String` reads back strings for old records
|
|
271
|
+
and numbers for new ones.
|
|
272
|
+
- **Adding a default** does not backfill. It applies to records inserted after.
|
|
273
|
+
|
|
274
|
+
---
|
|
275
|
+
|
|
276
|
+
## Records
|
|
277
|
+
|
|
278
|
+
Documents in your database.
|
|
279
|
+
|
|
280
|
+
```ts
|
|
281
|
+
await crm.dataStoreRecords.insert(store.id, customers.id, {
|
|
282
|
+
record: { email: 'a@example.com', signed_up: '2026-10-14T09:00:00Z', tags: ['vip'] },
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
await crm.dataStoreRecords.insert(store.id, customers.id, {
|
|
286
|
+
records: rows, // up to 1000
|
|
287
|
+
});
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### Writes are validated against the declaration
|
|
291
|
+
|
|
292
|
+
Keys must be declared field names, and values are coerced to the declared type —
|
|
293
|
+
`"42"` into a `Number` field is stored as `42`, an ISO string into a `Date` field
|
|
294
|
+
as a real date.
|
|
295
|
+
|
|
296
|
+
An **undeclared key is a 422 that names it**:
|
|
297
|
+
|
|
298
|
+
```json
|
|
299
|
+
{
|
|
300
|
+
"error": {
|
|
301
|
+
"code": "validation_failed",
|
|
302
|
+
"details": {
|
|
303
|
+
"fieldErrors": {
|
|
304
|
+
"unknown_fields": ["Not declared on this collection: emial. Declared fields are: email, signed_up, credits, tags"]
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
This is the one deliberate break from the legacy endpoint. There, a mongoose
|
|
312
|
+
schema with `strict: true` *dropped* unknown keys, so a record posted with
|
|
313
|
+
`emial` returned success having stored nothing under either spelling — and
|
|
314
|
+
nobody noticed for as long as nobody went looking.
|
|
315
|
+
|
|
316
|
+
| Key | On the way in |
|
|
317
|
+
| --- | --- |
|
|
318
|
+
| a declared field | coerced to its type |
|
|
319
|
+
| `null` | clears the field |
|
|
320
|
+
| `created_on`, `updated_on` | ignored — the server stamps them |
|
|
321
|
+
| `id`, `_id` | refused: the database assigns the id |
|
|
322
|
+
| anything else | **422**, naming it |
|
|
323
|
+
|
|
324
|
+
`created_on` and `updated_on` are unix **seconds**, or real `Date`s if the
|
|
325
|
+
collection declared them with `type: 'Date'`. `created_on` is written once;
|
|
326
|
+
`updated_on` moves on every update, and an update cannot move `created_on`.
|
|
327
|
+
|
|
328
|
+
### Inserting many
|
|
329
|
+
|
|
330
|
+
```ts
|
|
331
|
+
const { inserted, ids } = await crm.dataStoreRecords.insert(store.id, orders.id, {
|
|
332
|
+
records: rows,
|
|
333
|
+
});
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
A batch is **unordered**: one document rejected by a unique index of yours does
|
|
337
|
+
not abandon the rest, so `inserted` can be lower than what you sent. `ids` come
|
|
338
|
+
back in the order the documents went out.
|
|
339
|
+
|
|
340
|
+
### Reading
|
|
341
|
+
|
|
342
|
+
```ts
|
|
343
|
+
const { data, pagination } = await crm.dataStoreRecords.list(store.id, orders.id, {
|
|
344
|
+
rows: 50,
|
|
345
|
+
sort: 'total',
|
|
346
|
+
order: 'desc',
|
|
347
|
+
});
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
| Query | Default | Notes |
|
|
351
|
+
| --- | --- | --- |
|
|
352
|
+
| `page` | `1` | 1-based. |
|
|
353
|
+
| `rows` | `25` | 1–100. |
|
|
354
|
+
| `sort` | `created_on` | Any field name. |
|
|
355
|
+
| `order` | `desc` | |
|
|
356
|
+
|
|
357
|
+
`_id` is appended to the sort as a tiebreaker, so paging is reproducible even
|
|
358
|
+
when the sort field repeats — or does not exist at all, which it will not on an
|
|
359
|
+
adopted collection something else wrote. Without that, offset paging over tied
|
|
360
|
+
values can return the same document on two pages and skip another entirely.
|
|
361
|
+
|
|
362
|
+
**Reads return every stored key**, declared or not. The documents are yours and
|
|
363
|
+
may have been written by something else; hiding your own data because our
|
|
364
|
+
declaration is incomplete would be worse than surprising. `_id` reads back as
|
|
365
|
+
`id`, and dates as ISO strings.
|
|
366
|
+
|
|
367
|
+
### Searching
|
|
368
|
+
|
|
369
|
+
```ts
|
|
370
|
+
const { data } = await crm.dataStoreRecords.search(store.id, orders.id, {
|
|
371
|
+
filter: {
|
|
372
|
+
$or: [{ status: 'open' }, { total: { $gte: 100 } }],
|
|
373
|
+
created_on: { $gte: '2026-10-01' },
|
|
374
|
+
tags: 'vip',
|
|
375
|
+
},
|
|
376
|
+
sort: 'total',
|
|
377
|
+
order: 'desc',
|
|
378
|
+
});
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
A real Mongo filter — `$in`, `$gt`, `$or`, `$nor`, `$not`, `$regex`, `$exists`,
|
|
382
|
+
`$elemMatch`, all of it. It is your database: there is nothing in it you do not
|
|
383
|
+
already own, so narrowing the query language would only cost you expressiveness.
|
|
384
|
+
|
|
385
|
+
A POST rather than a GET because a Mongo filter is a nested object, and encoding
|
|
386
|
+
one into a query string is a different argument in every HTTP client. It reads
|
|
387
|
+
and changes nothing.
|
|
388
|
+
|
|
389
|
+
**Operands are coerced per declared field** before the query is sent, which is
|
|
390
|
+
the difference between a filter that works and one that silently matches
|
|
391
|
+
nothing:
|
|
392
|
+
|
|
393
|
+
| Filter | Against | What is actually sent |
|
|
394
|
+
| --- | --- | --- |
|
|
395
|
+
| `{ created_on: { $gte: '2026-10-01' } }` | a `Date` field | a real `Date` |
|
|
396
|
+
| `{ total: { $gt: '100' } }` | a `Number` field | `100` |
|
|
397
|
+
| `{ id: '68f1c2…' }` | — | `{ _id: ObjectId('68f1c2…') }` |
|
|
398
|
+
| `{ tags: 'vip' }` | an `Array` field | unchanged — "contains" |
|
|
399
|
+
|
|
400
|
+
`id` works as an alias for `_id` and takes the string form, because `id` is the
|
|
401
|
+
name every record reads back with.
|
|
402
|
+
|
|
403
|
+
Three operators are refused wherever they appear, at any depth:
|
|
404
|
+
|
|
405
|
+
| Status | Code | Meaning |
|
|
406
|
+
| --- | --- | --- |
|
|
407
|
+
| 422 | `filter_operator_not_allowed` | `$where`, `$function` or `$accumulator` — each runs JavaScript on your cluster. |
|
|
408
|
+
| 422 | `filter_too_complex` | Nested deeper than 10 levels, or more than 500 keys. |
|
|
409
|
+
| 422 | `validation_failed` | An operand that cannot be coerced — `{ total: { $gt: 'abc' } }` on a `Number` field. |
|
|
410
|
+
|
|
411
|
+
A value that cannot be coerced is reported rather than quietly matching nothing.
|
|
412
|
+
|
|
413
|
+
### Updating
|
|
414
|
+
|
|
415
|
+
```ts
|
|
416
|
+
await crm.dataStoreRecords.update(store.id, orders.id, record.id, {
|
|
417
|
+
record: { status: 'shipped', tracking: 'XY123' },
|
|
418
|
+
});
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
A `$set` of those fields and nothing else. Keys you leave out are untouched,
|
|
422
|
+
`null` clears a value, and the response is the record as it is after the write.
|
|
423
|
+
Validation is identical to an insert.
|
|
424
|
+
|
|
425
|
+
There is no `$inc`, `$push` or `$rename`: an update here is "these fields now
|
|
426
|
+
have these values", which is what every caller of the legacy endpoint was doing
|
|
427
|
+
— it wrapped the body in `$set` too — and it keeps an update subject to the same
|
|
428
|
+
field validation as an insert.
|
|
429
|
+
|
|
430
|
+
### Bulk update and delete
|
|
431
|
+
|
|
432
|
+
```ts
|
|
433
|
+
const { matched, modified } = await crm.dataStoreRecords.bulkUpdate(store.id, orders.id, {
|
|
434
|
+
filter: { status: 'pending', created_on: { $lt: cutoff } },
|
|
435
|
+
update: { status: 'expired' },
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
const { deleted } = await crm.dataStoreRecords.bulkRemove(store.id, orders.id, {
|
|
439
|
+
filter: { status: 'expired' },
|
|
440
|
+
});
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
**The filter cannot be empty.** `{}` matches the whole collection, and a
|
|
444
|
+
one-character slip that rewrites or deletes every record is not something to
|
|
445
|
+
make convenient — emptying a collection deliberately means paging through it.
|
|
446
|
+
|
|
447
|
+
`modified` is lower than `matched` where a record already held the values being
|
|
448
|
+
written, which is worth seeing rather than collapsing into one number.
|
|
449
|
+
|
|
450
|
+
---
|
|
451
|
+
|
|
452
|
+
## Isolation
|
|
453
|
+
|
|
454
|
+
Records carry no tenant field. They are not ours to stamp — they are documents
|
|
455
|
+
in your database, often ones that existed before you registered it.
|
|
456
|
+
|
|
457
|
+
So isolation is the **resolution order**, and it is the same on every record
|
|
458
|
+
call:
|
|
459
|
+
|
|
460
|
+
1. The store is resolved inside the calling account.
|
|
461
|
+
2. The collection is resolved inside that store.
|
|
462
|
+
3. Only then is a connection acquired and a query sent.
|
|
463
|
+
|
|
464
|
+
A store id from another account is a `404`. A collection id from another store —
|
|
465
|
+
including another of your own — is a `404`. Never a `403`: that would confirm
|
|
466
|
+
the id exists.
|
|
467
|
+
|
|
468
|
+
## Connections
|
|
469
|
+
|
|
470
|
+
One connection per distinct credential, shared by every request using it, with a
|
|
471
|
+
small driver pool inside it. Idle connections close after ten minutes and the
|
|
472
|
+
least recently used one closes when the cache is full; both are invisible to
|
|
473
|
+
callers, since the next call reconnects.
|
|
474
|
+
|
|
475
|
+
This is worth knowing for one reason: the platform holds open connections to
|
|
476
|
+
your cluster while you are using it. They count against your Atlas connection
|
|
477
|
+
limit, alongside your own applications. The per-credential pool is deliberately
|
|
478
|
+
small for that reason.
|
|
479
|
+
|
|
480
|
+
The legacy implementation opened a **new connection for every single
|
|
481
|
+
operation** and closed it afterwards — a TCP handshake, a TLS handshake, a SCRAM
|
|
482
|
+
exchange and two DNS lookups before each query, and nothing bounding how many
|
|
483
|
+
existed at once. A busy flow could exhaust a customer's own connection limit.
|
|
484
|
+
|
|
485
|
+
## Not here
|
|
486
|
+
|
|
487
|
+
- **No aggregation.** A pipeline can read other collections with `$lookup` and
|
|
488
|
+
write them with `$merge`, which makes a schema's boundary meaningless, and
|
|
489
|
+
`$function` runs JavaScript on your cluster. Use your own credentials and your
|
|
490
|
+
own driver for that — it is your database.
|
|
491
|
+
- **No index management.** Create indexes on your own cluster. Any unique index
|
|
492
|
+
you create is enforced on writes through this API, and a violation comes back
|
|
493
|
+
as a `422` quoting the database's own message.
|
|
494
|
+
- **No bulk import from a file.** `insert` with `records` is the batch path,
|
|
495
|
+
1000 at a time.
|
package/package.json
CHANGED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import type { CampaignReportExport, CreateCampaignReportExportInput } from '../types/campaign';
|
|
2
|
-
import type { Page, PageQuery } from '../types/common';
|
|
3
|
-
import type { HttpClient } from '../http';
|
|
4
|
-
export declare class CampaignReportExports {
|
|
5
|
-
private readonly http;
|
|
6
|
-
constructor(http: HttpClient);
|
|
7
|
-
list(query?: Partial<PageQuery>): Promise<Page<CampaignReportExport>>;
|
|
8
|
-
/**
|
|
9
|
-
* One export. Poll this until `status` is `completed`, then read `fileUrl` --
|
|
10
|
-
* which expires, so download rather than storing the link.
|
|
11
|
-
*/
|
|
12
|
-
get(id: string): Promise<CampaignReportExport>;
|
|
13
|
-
create(input: CreateCampaignReportExportInput): Promise<CampaignReportExport>;
|
|
14
|
-
}
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.CampaignReportExports = void 0;
|
|
4
|
-
class CampaignReportExports {
|
|
5
|
-
http;
|
|
6
|
-
constructor(http) {
|
|
7
|
-
this.http = http;
|
|
8
|
-
}
|
|
9
|
-
list(query = {}) {
|
|
10
|
-
return this.http.request('GET', '/v1/campaign-report-exports', {
|
|
11
|
-
query,
|
|
12
|
-
});
|
|
13
|
-
}
|
|
14
|
-
/**
|
|
15
|
-
* One export. Poll this until `status` is `completed`, then read `fileUrl` --
|
|
16
|
-
* which expires, so download rather than storing the link.
|
|
17
|
-
*/
|
|
18
|
-
get(id) {
|
|
19
|
-
return this.http.request('GET', `/v1/campaign-report-exports/${encodeURIComponent(id)}`);
|
|
20
|
-
}
|
|
21
|
-
create(input) {
|
|
22
|
-
return this.http.request('POST', '/v1/campaign-report-exports', {
|
|
23
|
-
body: input,
|
|
24
|
-
});
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
exports.CampaignReportExports = CampaignReportExports;
|
|
28
|
-
//# sourceMappingURL=campaign-report-exports.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"campaign-report-exports.js","sourceRoot":"","sources":["../../src/resources/campaign-report-exports.ts"],"names":[],"mappings":";;;AAIA,MAAa,qBAAqB;IACH;IAA7B,YAA6B,IAAgB;QAAhB,SAAI,GAAJ,IAAI,CAAY;IAAG,CAAC;IAEjD,IAAI,CAAC,QAA4B,EAAE;QACjC,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,CAA6B,KAAK,EAAE,6BAA6B,EAAE;YACzF,KAAK;SACN,CAAC,CAAC;IACL,CAAC;IAED;;;OAGG;IACH,GAAG,CAAC,EAAU;QACZ,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,CACtB,KAAK,EACL,+BAA+B,kBAAkB,CAAC,EAAE,CAAC,EAAE,CACxD,CAAC;IACJ,CAAC;IAED,MAAM,CAAC,KAAsC;QAC3C,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,CAAuB,MAAM,EAAE,6BAA6B,EAAE;YACpF,IAAI,EAAE,KAAK;SACZ,CAAC,CAAC;IACL,CAAC;CACF;AAzBD,sDAyBC"}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import type { CampaignReport, GetCampaignReportQuery } from '../types/campaign';
|
|
2
|
-
import type { Page, PageQuery } from '../types/common';
|
|
3
|
-
import type { HttpClient } from '../http';
|
|
4
|
-
export declare class CampaignReports {
|
|
5
|
-
private readonly http;
|
|
6
|
-
constructor(http: HttpClient);
|
|
7
|
-
list(query?: Partial<PageQuery>): Promise<Page<CampaignReport>>;
|
|
8
|
-
/**
|
|
9
|
-
* The report for one campaign.
|
|
10
|
-
*
|
|
11
|
-
* `from`/`to` narrow the window the metrics are rolled up over; without them
|
|
12
|
-
* the report covers the campaign's whole life.
|
|
13
|
-
*/
|
|
14
|
-
getByCampaign(campaignId: string, query?: GetCampaignReportQuery): Promise<CampaignReport>;
|
|
15
|
-
}
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.CampaignReports = void 0;
|
|
4
|
-
class CampaignReports {
|
|
5
|
-
http;
|
|
6
|
-
constructor(http) {
|
|
7
|
-
this.http = http;
|
|
8
|
-
}
|
|
9
|
-
list(query = {}) {
|
|
10
|
-
return this.http.request('GET', '/v1/campaign-reports', { query });
|
|
11
|
-
}
|
|
12
|
-
/**
|
|
13
|
-
* The report for one campaign.
|
|
14
|
-
*
|
|
15
|
-
* `from`/`to` narrow the window the metrics are rolled up over; without them
|
|
16
|
-
* the report covers the campaign's whole life.
|
|
17
|
-
*/
|
|
18
|
-
getByCampaign(campaignId, query = {}) {
|
|
19
|
-
return this.http.request('GET', `/v1/campaign-reports/${encodeURIComponent(campaignId)}`, {
|
|
20
|
-
query,
|
|
21
|
-
});
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
exports.CampaignReports = CampaignReports;
|
|
25
|
-
//# sourceMappingURL=campaign-reports.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"campaign-reports.js","sourceRoot":"","sources":["../../src/resources/campaign-reports.ts"],"names":[],"mappings":";;;AAIA,MAAa,eAAe;IACG;IAA7B,YAA6B,IAAgB;QAAhB,SAAI,GAAJ,IAAI,CAAY;IAAG,CAAC;IAEjD,IAAI,CAAC,QAA4B,EAAE;QACjC,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,CAAuB,KAAK,EAAE,sBAAsB,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;IAC3F,CAAC;IAED;;;;;OAKG;IACH,aAAa,CAAC,UAAkB,EAAE,QAAgC,EAAE;QAClE,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,CAAiB,KAAK,EAAE,wBAAwB,kBAAkB,CAAC,UAAU,CAAC,EAAE,EAAE;YACxG,KAAK;SACN,CAAC,CAAC;IACL,CAAC;CACF;AAlBD,0CAkBC"}
|