@wokuapp/sdk 0.4.0 → 0.5.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/README.md CHANGED
@@ -313,9 +313,127 @@ available if re-enabled later.
313
313
  Existing definitions retain their contract; create a new v2 journey to adopt these
314
314
  rules. Current participations retain their original definition version.
315
315
 
316
+ ### Flows
317
+
318
+ A flow is an ordered sequence of steps, each a woku or an NPS, CSAT or CES tool,
319
+ that a respondent answers one after the other. Flows live outside folders.
320
+
321
+ ```ts
322
+ const flow = await woku.flows.create({
323
+ name: 'Post-purchase',
324
+ steps: [
325
+ { type: 'nps', toolId: 'nps_tool_id' },
326
+ { type: 'csat', toolId: 'csat_tool_id' },
327
+ { type: 'woku', toolId: 'woku_id' },
328
+ ],
329
+ });
330
+ await woku.flows.update(flow._id, {
331
+ steps: [{ type: 'csat', toolId: 'csat_tool_id' }],
332
+ });
333
+ for await (const f of await woku.flows.list())
334
+ console.log(f.name, f.steps.length);
335
+ await woku.flows.delete(flow._id); // its tools and responses stay
336
+ ```
337
+
338
+ Use 1 to 20 steps, each tool once and all of your company. For an `nps` step,
339
+ your company id as `toolId` means the company-level NPS. `flows.get(id)` returns
340
+ the flow data (steps with their tools, wokus and branding).
341
+
342
+ ### Folders and tools
343
+
344
+ One folder tree holds wokus and NPS, CSAT and CES tools together. Create folders,
345
+ find any tool, read figures per type and move things in batches:
346
+
347
+ ```ts
348
+ const folder = await woku.folders.create({ name: 'Post-sale' });
349
+
350
+ // Create a tool already inside a folder.
351
+ await woku.csatTools.create({
352
+ name: 'Delivery',
353
+ question: 'How satisfied are you with your delivery?',
354
+ folderId: folder.id,
355
+ });
356
+
357
+ // One list for every tool type. Iterate across pages with the cursor.
358
+ const page = await woku.tools.list({
359
+ folderId: folder.id,
360
+ types: ['csat', 'nps'],
361
+ });
362
+ for await (const tool of page) {
363
+ console.log(tool.type, tool.name, tool.reviewCount, tool.headline.value);
364
+ }
365
+ const found = await woku.tools.list({ q: 'delivery', scope: 'company' });
366
+ // Tracker chips (`name:value`): a tool matches when it has every one. Company-wide.
367
+ const tagged = await woku.tools.list({ trackers: ['TRR:00482'] });
368
+
369
+ // "See more" for folders: foldersLimit pages the folders of the level (omit it
370
+ // to receive all of them). The tools keep their own cursor.
371
+ const first = await woku.tools.list({ foldersLimit: 12 });
372
+ if (first.hasMoreFolders()) {
373
+ const more = await first.getMoreFolders(); // only the next folders, no tools
374
+ console.log(more.folders.length);
375
+ }
376
+
377
+ // Headline figures per type (native scales, never mixed), company or folder.
378
+ const stats = await woku.tools.stats({ folderId: folder.id });
379
+
380
+ // Move items of mixed types, or send them to the root with folderId: null.
381
+ await woku.tools.move({
382
+ items: [
383
+ { type: 'csat', id: 'csat_tool_id' },
384
+ { type: 'woku', id: 'woku_id' },
385
+ ],
386
+ folderId: folder.id,
387
+ });
388
+ await woku.npsTools.move('nps_tool_id', { folderId: null });
389
+
390
+ // AI report and CSV over every tool of the folder and its subfolders.
391
+ const report = await woku.folders.report(folder.id, { lang: 'en' });
392
+ const csv: string = await woku.folders.csv(folder.id);
393
+ // PDF of that report with a breakdown per type (bytes, e.g. write them to a file).
394
+ const pdf: Uint8Array = await woku.folders.pdf(folder.id, { lang: 'en' });
395
+
396
+ // Per-tool figures for a period against the one before it (7, 30 or 90 days, or
397
+ // 0 for the whole history), with the daily sums and the histogram of scores.
398
+ // Every type keeps its own scale: NPS -100..100, CSAT %, CES and stars 1-5.
399
+ const metrics = await woku.folders.metrics(folder.id, { days: 30 });
400
+ for (const tool of metrics.tools) {
401
+ console.log(
402
+ tool.name,
403
+ tool.current.headline.value,
404
+ tool.previous.headline.value,
405
+ );
406
+ }
407
+
408
+ // WhatsApp numbers alerted when a comment on any tool of the folder is an
409
+ // improvement (wokus, NPS, CSAT and CES). The list replaces the previous one.
410
+ await woku.folders.update(folder.id, {
411
+ whatsappAlertRecipients: [{ whatsappId: '56912345678', name: 'Ana' }],
412
+ });
413
+
414
+ // Deleting a folder never deletes its content: it moves up to the parent.
415
+ await woku.folders.delete(folder.id);
416
+ ```
417
+
418
+ ```ts
419
+ // Keep customer journeys in folders: Viajes > journey > moment.
420
+ const status = await woku.journeyFolders.status(); // { enabled, organizing, pending: { journeys, tools }, ... }
421
+ if (!status.enabled) await woku.journeyFolders.organize(); // 202; rejects with 409 while a pass is running
422
+
423
+ const journey = await woku.journeys.get('journey_id');
424
+ console.log(journey.folderId, journey.momentFolders); // moment key -> folder id
425
+ ```
426
+
427
+ Folders created by woku (journeys, moments, integrations) report their `origin`
428
+ (`'journeys-root' | 'journey' | 'journey-moment' | 'integration'`); your own folders have `origin: null`.
429
+
430
+ `woku.folders` also has `list`, `get`, `update` and `move`. NPS, CSAT and CES
431
+ tools (`npsTools`, `csatTools`, `cesTools`) accept `folderId` on `create`, expose
432
+ `folderId` on every tool and have `move(id, { folderId })`.
433
+
316
434
  ## Pagination
317
435
 
318
- Paginated resource methods return a `Page` (journey enrollments use the cursor envelope described above). Iterate every item across pages, or walk pages:
436
+ Paginated resource methods return a `Page` (journey enrollments and `tools.list` use a cursor envelope). Iterate every item across pages, or walk pages:
319
437
 
320
438
  ```ts
321
439
  for await (const ticket of await woku.tickets.list({ severity: 'high' })) {
@@ -376,7 +494,7 @@ await woku.npsTools.create(body, { idempotencyKey: 'my-key' });
376
494
  `trackers`, `npsTools` / `csatTools` / `cesTools`, `nps` / `csat` / `ces`,
377
495
  `wokus`, `forms`, `flows`, `actionPlans`, `actionPlanGroups`, `tickets`,
378
496
  `ticketDestinations`, `dispatches`, `reports`, `company`, `quarantines`,
379
- `journeys`.
497
+ `journeys`, `folders`, `tools`, `journeyFolders`.
380
498
 
381
499
  ## License
382
500
 
package/dist/index.cjs CHANGED
@@ -38,7 +38,7 @@ var FollowUps = class {
38
38
  };
39
39
 
40
40
  // package.json
41
- var version = "0.4.0";
41
+ var version = "0.5.0";
42
42
 
43
43
  // src/core/errors.ts
44
44
  var WokuError = class extends Error {
@@ -385,7 +385,8 @@ var WokuClient = class {
385
385
  headers,
386
386
  bodyText,
387
387
  timeout,
388
- args.signal
388
+ args.signal,
389
+ args.binary
389
390
  );
390
391
  if (status >= 200 && status < 300) {
391
392
  return body;
@@ -502,7 +503,7 @@ var WokuClient = class {
502
503
  }
503
504
  return headers;
504
505
  }
505
- async send(url, method, headers, body, timeout, signal) {
506
+ async send(url, method, headers, body, timeout, signal, binary) {
506
507
  if (signal?.aborted)
507
508
  throw new WokuConnectionError("Request aborted", { code: "aborted" });
508
509
  const controller = new AbortController();
@@ -523,6 +524,20 @@ var WokuClient = class {
523
524
  body,
524
525
  signal: controller.signal
525
526
  });
527
+ if (binary && response.status >= 200 && response.status < 300) {
528
+ if (!response.arrayBuffer) {
529
+ throw new WokuError(
530
+ "The fetch implementation cannot read binary responses.",
531
+ { code: "config_error" }
532
+ );
533
+ }
534
+ return {
535
+ status: response.status,
536
+ body: new Uint8Array(await response.arrayBuffer()),
537
+ requestId: requestIdFrom(response.headers),
538
+ retryAfterSeconds: retryAfterFromHeaders(response.headers)
539
+ };
540
+ }
526
541
  const text = await response.text();
527
542
  return {
528
543
  status: response.status,
@@ -721,6 +736,14 @@ var NpsTools = class {
721
736
  }
722
737
  );
723
738
  }
739
+ /** Move the tool into a folder, or to the root with `folderId: null`. */
740
+ move(id, body, opts) {
741
+ return this.client.request(
742
+ "patch",
743
+ `/v1/nps-tools/${encodeURIComponent(id)}/move`,
744
+ { ...opts, body }
745
+ );
746
+ }
724
747
  delete(id, opts) {
725
748
  return this.client.request(
726
749
  "delete",
@@ -760,6 +783,14 @@ var CsatTools = class {
760
783
  }
761
784
  );
762
785
  }
786
+ /** Move the tool into a folder, or to the root with `folderId: null`. */
787
+ move(id, body, opts) {
788
+ return this.client.request(
789
+ "patch",
790
+ `/v1/csat-tools/${encodeURIComponent(id)}/move`,
791
+ { ...opts, body }
792
+ );
793
+ }
763
794
  delete(id, opts) {
764
795
  return this.client.request(
765
796
  "delete",
@@ -799,6 +830,14 @@ var CesTools = class {
799
830
  }
800
831
  );
801
832
  }
833
+ /** Move the tool into a folder, or to the root with `folderId: null`. */
834
+ move(id, body, opts) {
835
+ return this.client.request(
836
+ "patch",
837
+ `/v1/ces-tools/${encodeURIComponent(id)}/move`,
838
+ { ...opts, body }
839
+ );
840
+ }
802
841
  delete(id, opts) {
803
842
  return this.client.request(
804
843
  "delete",
@@ -1016,9 +1055,11 @@ var Flows = class {
1016
1055
  constructor(client) {
1017
1056
  this.client = client;
1018
1057
  }
1058
+ /** List the flows of the company, with their ordered `steps`. */
1019
1059
  list(params, opts) {
1020
1060
  return this.client.getPage("/v1/flows", params, opts);
1021
1061
  }
1062
+ /** The data of a flow: ordered steps with their tools, wokus and branding. */
1022
1063
  get(id, opts) {
1023
1064
  return this.client.request(
1024
1065
  "get",
@@ -1026,6 +1067,33 @@ var Flows = class {
1026
1067
  opts
1027
1068
  );
1028
1069
  }
1070
+ /**
1071
+ * Create a flow from 1 to 20 ordered steps (each tool once, all of your
1072
+ * company). Flows live outside folders.
1073
+ */
1074
+ create(body, opts) {
1075
+ return this.client.request("post", "/v1/flows", {
1076
+ ...opts,
1077
+ body,
1078
+ idempotent: true
1079
+ });
1080
+ }
1081
+ /** Rename a flow or replace its full ordered list of steps. */
1082
+ update(id, body, opts) {
1083
+ return this.client.request(
1084
+ "patch",
1085
+ `/v1/flows/${encodeURIComponent(id)}`,
1086
+ { ...opts, body }
1087
+ );
1088
+ }
1089
+ /** Delete a flow. Its tools and their responses stay. */
1090
+ delete(id, opts) {
1091
+ return this.client.request(
1092
+ "delete",
1093
+ `/v1/flows/${encodeURIComponent(id)}`,
1094
+ opts
1095
+ );
1096
+ }
1029
1097
  };
1030
1098
 
1031
1099
  // src/resources/action-plans.ts
@@ -1523,6 +1591,240 @@ var Quarantines = class {
1523
1591
  }
1524
1592
  };
1525
1593
 
1594
+ // src/resources/folders.ts
1595
+ var Folders = class {
1596
+ constructor(client) {
1597
+ this.client = client;
1598
+ }
1599
+ /** Every folder of the company, or the direct subfolders of `parentFolderId`. */
1600
+ list(params, opts) {
1601
+ return this.client.request("get", "/v1/folders", {
1602
+ ...opts,
1603
+ query: { ...params, ...opts?.query }
1604
+ });
1605
+ }
1606
+ get(id, opts) {
1607
+ return this.client.request(
1608
+ "get",
1609
+ `/v1/folders/${encodeURIComponent(id)}`,
1610
+ opts
1611
+ );
1612
+ }
1613
+ create(body, opts) {
1614
+ return this.client.request("post", "/v1/folders", {
1615
+ ...opts,
1616
+ body,
1617
+ idempotent: true
1618
+ });
1619
+ }
1620
+ /**
1621
+ * Rename or recolor a folder (`color: null` clears the color) and/or replace
1622
+ * its WhatsApp alert recipients (`whatsappAlertRecipients`, `[]` clears them).
1623
+ */
1624
+ update(id, body, opts) {
1625
+ return this.client.request(
1626
+ "patch",
1627
+ `/v1/folders/${encodeURIComponent(id)}`,
1628
+ { ...opts, body }
1629
+ );
1630
+ }
1631
+ /**
1632
+ * Move a folder under another folder, or to the root with
1633
+ * `parentFolderId: null`. A folder cannot move into its own subtree.
1634
+ */
1635
+ move(id, body, opts) {
1636
+ return this.client.request(
1637
+ "patch",
1638
+ `/v1/folders/${encodeURIComponent(id)}/move`,
1639
+ { ...opts, body }
1640
+ );
1641
+ }
1642
+ /**
1643
+ * Delete a folder. Nothing inside is deleted: subfolders, wokus and tools
1644
+ * move up to the parent folder (or the root).
1645
+ */
1646
+ delete(id, opts) {
1647
+ return this.client.request(
1648
+ "delete",
1649
+ `/v1/folders/${encodeURIComponent(id)}`,
1650
+ opts
1651
+ );
1652
+ }
1653
+ /**
1654
+ * AI report over every woku, NPS, CSAT and CES tool of the folder and its
1655
+ * subfolders. Scales differ per type, so there is no rating distribution.
1656
+ */
1657
+ report(id, params, opts) {
1658
+ return this.client.request(
1659
+ "get",
1660
+ `/v1/folders/${encodeURIComponent(id)}/report`,
1661
+ { ...opts, query: { ...params, ...opts?.query } }
1662
+ );
1663
+ }
1664
+ /**
1665
+ * Per-tool figures of the woku, NPS, CSAT and CES tools of the folder and its
1666
+ * subfolders for a period against the one before it, with the daily sums and
1667
+ * the histogram of scores. `days` is 7, 30 (default) or 90, or 0 for the whole
1668
+ * history (every `previous` window is then empty). The period is the last
1669
+ * `days` calendar days in UTC, today included. Scales are never mixed across
1670
+ * types.
1671
+ */
1672
+ metrics(id, params, opts) {
1673
+ return this.client.request(
1674
+ "get",
1675
+ `/v1/folders/${encodeURIComponent(id)}/metrics`,
1676
+ { ...opts, query: { ...params, ...opts?.query } }
1677
+ );
1678
+ }
1679
+ /** CSV text with one row per response of every tool of the folder subtree. */
1680
+ csv(id, opts) {
1681
+ return this.client.request(
1682
+ "get",
1683
+ `/v1/folders/${encodeURIComponent(id)}/csv`,
1684
+ opts
1685
+ );
1686
+ }
1687
+ /**
1688
+ * PDF of the folder report (every woku, NPS, CSAT and CES tool of the folder
1689
+ * and its subfolders) with a breakdown per type on each native scale.
1690
+ * Returns the file bytes: write them to disk or send them as is.
1691
+ */
1692
+ pdf(id, params, opts) {
1693
+ return this.client.request(
1694
+ "get",
1695
+ `/v1/folders/${encodeURIComponent(id)}/pdf`,
1696
+ {
1697
+ ...opts,
1698
+ headers: { Accept: "application/pdf", ...opts?.headers },
1699
+ query: { ...params, ...opts?.query },
1700
+ binary: true
1701
+ }
1702
+ );
1703
+ }
1704
+ };
1705
+
1706
+ // src/resources/tools.ts
1707
+ var ToolsPage = class {
1708
+ constructor(response, fetchNext, fetchMoreFolders = () => Promise.reject(new RangeError("No more folders"))) {
1709
+ this.fetchNext = fetchNext;
1710
+ this.fetchMoreFolders = fetchMoreFolders;
1711
+ this.folders = response.folders;
1712
+ this.tools = response.tools;
1713
+ this.nextCursor = response.nextCursor;
1714
+ this.foldersNextCursor = response.foldersNextCursor ?? null;
1715
+ this.facets = response.facets;
1716
+ }
1717
+ hasMoreFolders() {
1718
+ return this.foldersNextCursor !== null;
1719
+ }
1720
+ /** The next page of folders ("see more"): a page with those folders and no tools. */
1721
+ getMoreFolders() {
1722
+ if (this.foldersNextCursor === null)
1723
+ throw new RangeError("No more folders");
1724
+ return this.fetchMoreFolders(this.foldersNextCursor);
1725
+ }
1726
+ hasNextPage() {
1727
+ return this.nextCursor !== null;
1728
+ }
1729
+ getNextPage() {
1730
+ if (this.nextCursor === null) throw new RangeError("No next page");
1731
+ return this.fetchNext(this.nextCursor);
1732
+ }
1733
+ async *iterPages() {
1734
+ yield this;
1735
+ if (this.hasNextPage()) yield* (await this.getNextPage()).iterPages();
1736
+ }
1737
+ async *[Symbol.asyncIterator]() {
1738
+ for await (const page of this.iterPages()) {
1739
+ for (const tool of page.tools) yield tool;
1740
+ }
1741
+ }
1742
+ };
1743
+ var Tools = class {
1744
+ constructor(client) {
1745
+ this.client = client;
1746
+ }
1747
+ /**
1748
+ * List one folder level (or search the company). Folders and tools of every
1749
+ * type come together; the first page also carries the folders and facets.
1750
+ */
1751
+ async list(params, opts) {
1752
+ const fetchPage = async (cursor, foldersCursor) => {
1753
+ const response = await this.client.request(
1754
+ "get",
1755
+ "/v1/tools",
1756
+ {
1757
+ ...opts,
1758
+ query: {
1759
+ ...params,
1760
+ types: params?.types?.join(","),
1761
+ ...cursor ? { cursor } : {},
1762
+ ...foldersCursor ? { foldersCursor } : {},
1763
+ ...opts?.query
1764
+ }
1765
+ }
1766
+ );
1767
+ return new ToolsPage(
1768
+ response,
1769
+ fetchPage,
1770
+ (next) => fetchPage(void 0, next)
1771
+ );
1772
+ };
1773
+ return fetchPage();
1774
+ }
1775
+ /**
1776
+ * Headline figures per type (woku stars, NPS score, CSAT satisfied %, CES
1777
+ * mean) for the company or one folder and its subfolders, in a period.
1778
+ * A type that fails reports `{ error: true }`; the others stay valid.
1779
+ */
1780
+ stats(params, opts) {
1781
+ return this.client.request("get", "/v1/tools/stats", {
1782
+ ...opts,
1783
+ query: { ...params, ...opts?.query }
1784
+ });
1785
+ }
1786
+ /**
1787
+ * Move up to 200 items of mixed types (`woku`, `nps`, `csat`, `ces`,
1788
+ * `folder`) into a folder, or to the root with `folderId: null`. Idempotent;
1789
+ * nothing moves if one item or the destination is not found.
1790
+ */
1791
+ move(body, opts) {
1792
+ return this.client.request("post", "/v1/tools/move", {
1793
+ ...opts,
1794
+ body
1795
+ });
1796
+ }
1797
+ };
1798
+
1799
+ // src/resources/journey-folders.ts
1800
+ var JourneyFolders = class {
1801
+ constructor(client) {
1802
+ this.client = client;
1803
+ }
1804
+ /** Whether the organization is on, running, and how much is still without a folder. */
1805
+ status(opts) {
1806
+ return this.client.request(
1807
+ "get",
1808
+ "/v1/journey-folders",
1809
+ opts
1810
+ );
1811
+ }
1812
+ /**
1813
+ * Switch the organization on and run the one-off pass in the background: tools
1814
+ * of existing journeys without a folder move into their moment folder, and
1815
+ * tools journeys create from now on are placed there. Tools that already have
1816
+ * a folder are never touched. Rejects with a conflict (409) while a pass is
1817
+ * already running; poll {@link status} for progress.
1818
+ */
1819
+ organize(opts) {
1820
+ return this.client.request(
1821
+ "post",
1822
+ "/v1/journey-folders/organize",
1823
+ opts
1824
+ );
1825
+ }
1826
+ };
1827
+
1526
1828
  // src/woku.ts
1527
1829
  var Woku = class {
1528
1830
  constructor(options) {
@@ -1548,6 +1850,9 @@ var Woku = class {
1548
1850
  this.reports = new Reports(this.client);
1549
1851
  this.company = new Company(this.client);
1550
1852
  this.journeys = new Journeys(this.client);
1853
+ this.folders = new Folders(this.client);
1854
+ this.tools = new Tools(this.client);
1855
+ this.journeyFolders = new JourneyFolders(this.client);
1551
1856
  this.quarantines = new Quarantines(this.client);
1552
1857
  }
1553
1858
  };
@@ -1555,8 +1860,10 @@ var Woku = class {
1555
1860
  exports.AuthenticationError = AuthenticationError;
1556
1861
  exports.BadRequestError = BadRequestError;
1557
1862
  exports.ConflictError = ConflictError;
1863
+ exports.Folders = Folders;
1558
1864
  exports.FollowUps = FollowUps;
1559
1865
  exports.InternalServerError = InternalServerError;
1866
+ exports.JourneyFolders = JourneyFolders;
1560
1867
  exports.Journeys = Journeys;
1561
1868
  exports.Media = Media;
1562
1869
  exports.NotFoundError = NotFoundError;
@@ -1564,6 +1871,8 @@ exports.Page = Page;
1564
1871
  exports.PayloadTooLargeError = PayloadTooLargeError;
1565
1872
  exports.PermissionDeniedError = PermissionDeniedError;
1566
1873
  exports.RateLimitError = RateLimitError;
1874
+ exports.Tools = Tools;
1875
+ exports.ToolsPage = ToolsPage;
1567
1876
  exports.UnprocessableEntityError = UnprocessableEntityError;
1568
1877
  exports.Woku = Woku;
1569
1878
  exports.WokuAPIError = WokuAPIError;