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.
Files changed (44) hide show
  1. package/README.md +5 -2
  2. package/dist/client.d.ts +31 -6
  3. package/dist/client.js +36 -8
  4. package/dist/client.js.map +1 -1
  5. package/dist/index.d.ts +3 -2
  6. package/dist/index.js +9 -5
  7. package/dist/index.js.map +1 -1
  8. package/dist/resources/audience/canned-replies.d.ts +95 -0
  9. package/dist/resources/audience/canned-replies.js +112 -0
  10. package/dist/resources/audience/canned-replies.js.map +1 -0
  11. package/dist/resources/audience/notifications.d.ts +14 -0
  12. package/dist/resources/audience/notifications.js +14 -0
  13. package/dist/resources/audience/notifications.js.map +1 -1
  14. package/dist/resources/campaign-messages.d.ts +153 -0
  15. package/dist/resources/campaign-messages.js +179 -0
  16. package/dist/resources/campaign-messages.js.map +1 -0
  17. package/dist/resources/data-stores.d.ts +469 -0
  18. package/dist/resources/data-stores.js +530 -0
  19. package/dist/resources/data-stores.js.map +1 -0
  20. package/dist/resources/paths.d.ts +6 -0
  21. package/dist/resources/paths.js +7 -1
  22. package/dist/resources/paths.js.map +1 -1
  23. package/dist/types/campaign.d.ts +137 -41
  24. package/dist/types/canned-reply.d.ts +124 -0
  25. package/dist/types/canned-reply.js +6 -0
  26. package/dist/types/canned-reply.js.map +1 -0
  27. package/dist/types/datastore.d.ts +260 -0
  28. package/dist/types/datastore.js +21 -0
  29. package/dist/types/datastore.js.map +1 -0
  30. package/dist/types/index.d.ts +2 -0
  31. package/dist/types/index.js +3 -0
  32. package/dist/types/index.js.map +1 -1
  33. package/docs/audience/canned-replies.md +302 -0
  34. package/docs/audience/notifications.md +99 -0
  35. package/docs/campaign/campaign-messages.md +282 -0
  36. package/docs/campaign/campaigns.md +8 -48
  37. package/docs/data-store.md +495 -0
  38. package/package.json +1 -1
  39. package/dist/resources/campaign-report-exports.d.ts +0 -14
  40. package/dist/resources/campaign-report-exports.js +0 -28
  41. package/dist/resources/campaign-report-exports.js.map +0 -1
  42. package/dist/resources/campaign-reports.d.ts +0 -15
  43. package/dist/resources/campaign-reports.js +0 -25
  44. 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
- # Campaign reports
437
+ # Reporting
438
438
 
439
- `crm.campaignReports` — `/v1/campaign-reports`
439
+ Campaign reporting lives on its own page: **[campaign messages](campaign-messages.md)**
440
+ — `crm.campaignMessages`.
440
441
 
441
- Delivery metrics for a campaign, rolled up by the platform.
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 report = await crm.campaignReports.getByCampaign(campaignId);
445
- report.metrics; // { queued, sent, delivered, failed, opened, clicked, unsubscribed }
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,6 +1,6 @@
1
1
  {
2
2
  "name": "ab-ecosystem-cpass",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
4
4
  "description": "JavaScript/TypeScript SDK for the CPaaS CRM gateway: audience and channels",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -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"}