@qikdev/sdk 1.0.12 → 1.0.13

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.
@@ -5,9 +5,15 @@ import { isBrowser, isNode } from "browser-or-node";
5
5
  ///////////////////////////////////////////////////////////////////////////////
6
6
 
7
7
  /**
8
- * @name utils
9
- * @classdesc Utility helper functions — id, ids, hashing, parsing, cleaning, and other general-purpose helpers
8
+ * @classdesc General helpers that run locally: ids (id, ids, isValidID, getTypeFromID), parsing and cleaning
9
+ * values (parseDate, parseURL, parseEmail, parseNumber, parseInt, parseBoolean, cleanValue, isValidValue),
10
+ * working with arrays (hash, extractFromArray, matchInArray, comma), money (formatCurrency, currencySymbol),
11
+ * field lists (mapFields, getAllFields), error messages, unique ids and loading external scripts or styles.
12
+ * @alias utils
10
13
  * @class
14
+ * @example
15
+ * const ids = sdk.utils.ids(records); // ['61eca...', ...]
16
+ * const byID = sdk.utils.hash(records, '_id');
11
17
  */
12
18
 
13
19
  const service = {};
@@ -17,14 +23,13 @@ const service = {};
17
23
  const loadedExternalScripts = {};
18
24
 
19
25
  /**
20
- * A function that dynamically include an external javascript resource
21
- * ensuring that it will only be included once
26
+ * Adds a <script> tag for an external JavaScript file, once per URL: later calls with the same URL return the
27
+ * same promise. Browser only.
22
28
  * @alias utils.loadExternalScript
23
- * @param {String} url The url of the external script
24
- * @return {Promise} A promise that will be resolved once the script has been loaded
29
+ * @param {String} url The script's URL.
30
+ * @returns {Promise<String>} Resolves with the URL once the script has loaded. It never settles if the script fails to load.
25
31
  * @example
26
- *
27
- * await sdk.utils.loadExternalScript('https://cdn.javascript.com/external/script.js');
32
+ * await sdk.utils.loadExternalScript('https://cdn.example.com/library.js');
28
33
  */
29
34
 
30
35
  service.loadExternalScript = function (url) {
@@ -64,14 +69,12 @@ service.loadExternalScript = function (url) {
64
69
  const loadedExternalStyles = {};
65
70
 
66
71
  /**
67
- * A function that dynamically include an external css resource
68
- * ensuring that it will only be included once
72
+ * Adds a <link rel="stylesheet"> tag for an external CSS file, once per URL. Browser only.
69
73
  * @alias utils.loadExternalStyle
70
- * @param {String} url The url of the external css
71
- * @return {Promise} A promise that will be resolved once the css has been loaded
74
+ * @param {String} url The stylesheet's URL.
75
+ * @returns {Promise<String>} Resolves with the URL once loaded (straight away if it was added before). It never settles if the stylesheet fails to load.
72
76
  * @example
73
- *
74
- * await sdk.utils.loadExternalStyle('https://cdn.css.com/external/style.css');
77
+ * await sdk.utils.loadExternalStyle('https://cdn.example.com/theme.css');
75
78
  */
76
79
  service.loadExternalStyle = function (url) {
77
80
  const promise = new Promise(createNewScript);
@@ -109,16 +112,17 @@ service.loadExternalStyle = function (url) {
109
112
  ///////////////////////////////////////////////////////////////////////////////
110
113
 
111
114
  /**
112
- * A helper function for checking whether a value is truthy/falsy
115
+ * Checks whether a value counts as "provided": false for undefined, null, '' and the strings 'undefined'
116
+ * and 'null' (in any case); true for everything else, including 0, false and empty arrays.
113
117
  * @alias utils.exists
114
- * @param {Anything} value The value to check
115
- * @return {Boolean} whether the value is truthy
118
+ * @param {*} value The value to check.
119
+ * @returns {Boolean} Whether the value is provided.
116
120
  * @example
117
- *
118
121
  * sdk.utils.exists('undefined'); // false
119
- * sdk.utils.exists([]); // true
120
122
  * sdk.utils.exists(''); // false
121
123
  * sdk.utils.exists(undefined); // false
124
+ * sdk.utils.exists([]); // true
125
+ * sdk.utils.exists(0); // true
122
126
  */
123
127
  service.exists = function (value) {
124
128
  var isUndefinedOrNull;
@@ -160,14 +164,13 @@ service.exists = function (value) {
160
164
  //////////////////////////////////
161
165
 
162
166
  /**
163
- * A helper function for getting a javascript date object from some input
164
- * returns undefined if it can not be parsed
167
+ * Converts a value to a JavaScript Date.
165
168
  * @alias utils.parseDate
166
- * @param {String|Number} input The value to parse as a date
167
- * @return {Date} the javascript date object
169
+ * @param {String|Number|Date} input An ISO string, timestamp or Date.
170
+ * @returns {Date|undefined} The Date, or undefined when the input is empty or not a valid date.
168
171
  * @example
169
- *
170
- * sdk.utils.parseDate(input);
172
+ * sdk.utils.parseDate('2026-09-29T10:00:00Z'); // Date
173
+ * sdk.utils.parseDate('not a date'); // undefined
171
174
  */
172
175
  service.parseDate = function (input) {
173
176
  if (!input) {
@@ -190,16 +193,18 @@ service.parseDate = function (input) {
190
193
  };
191
194
 
192
195
  /**
193
- * A helper function for getting a full url string from some input
196
+ * Turns user input into a usable link: adds http:// when no scheme is given, turns a bare email address into
197
+ * a mailto: link, and leaves relative paths ('/about'), '://', mailto:, tel: and sms: links as they are.
198
+ * Spaces are removed. Returns false when the host isn't a valid domain name or IPv4 address.
194
199
  * @alias utils.parseURL
195
- * @param {String} input The input value
196
- * @return {String} the fully parsed URL
200
+ * @param {String} string The input.
201
+ * @returns {String|Boolean} The link, or false when it can't be made into one.
197
202
  * @example
198
- *
199
- * sdk.utils.parseURL('google.com'); // Returns https://google.com
200
- * sdk.utils.parseURL('mailto:hello@qik.dev'); // Returns 'mailto:hello@qik.dev'
201
- * sdk.utils.parseURL('://hello.com'); // Returns '://hello.com'
202
- * sdk.utils.parseURL('hello@email.com'); // Returns 'mailto:hello@email.com'
203
+ * sdk.utils.parseURL('example.com'); // 'http://example.com'
204
+ * sdk.utils.parseURL('https://example.com/a?b=c'); // 'https://example.com/a?b=c'
205
+ * sdk.utils.parseURL('hello@example.com'); // 'mailto:hello@example.com'
206
+ * sdk.utils.parseURL('/about'); // '/about'
207
+ * sdk.utils.parseURL('not a url'); // false
203
208
  */
204
209
  service.parseURL = function (string) {
205
210
  if (!string) {
@@ -294,18 +299,16 @@ service.parseURL = function (string) {
294
299
  ///////////////////////////////////////////////
295
300
 
296
301
  /**
297
- * A helper function for getting a number from some input
302
+ * Converts a value to a number, returning 0 when it isn't numeric.
298
303
  * @alias utils.parseNumber
299
- * @param {String} value The input value
300
- * @param {Number} decimalPoints The number of decimal points to round to
301
- * @return {Number} the parsed number value
304
+ * @param {String|Number} input The value.
305
+ * @param {Number} [decimalPoints] Round to this many decimal places.
306
+ * @returns {Number} The number, or 0.
302
307
  * @example
303
- *
304
- * sdk.utils.parseNumber('123'); // Returns 123
305
- * sdk.utils.parseNumber('75.501', 2); // Returns 75.5
306
- * sdk.utils.parseNumber(''); // Returns 0
307
- * sdk.utils.parseNumber(null); // Returns 0
308
- * sdk.utils.parseNumber(); // Returns 0
308
+ * sdk.utils.parseNumber('123'); // 123
309
+ * sdk.utils.parseNumber('75.501', 2); // 75.5
310
+ * sdk.utils.parseNumber('abc'); // 0
311
+ * sdk.utils.parseNumber(null); // 0
309
312
  */
310
313
  service.parseNumber = function (input, decimalPoints) {
311
314
  if (!input) {
@@ -326,15 +329,13 @@ service.parseNumber = function (input, decimalPoints) {
326
329
  };
327
330
 
328
331
  /**
329
- * A helper function for getting a number from some input
332
+ * Checks whether a string is a valid email address.
330
333
  * @alias utils.isValidEmailAddress
331
- * @param {String} emailAddress The email to validate
332
- * @return {Boolean} whether or not the input is a valid email address
334
+ * @param {String} email The text to check.
335
+ * @returns {Boolean} Whether it is a valid email address.
333
336
  * @example
334
- *
335
- * sdk.utils.isValidEmailAddress('123.com'); // Returns false
336
- * sdk.utils.isValidEmailAddress('hello@world.com'); // Returns true
337
- * sdk.utils.isValidEmailAddress('something@special.io'); // Returns true
337
+ * sdk.utils.isValidEmailAddress('123.com'); // false
338
+ * sdk.utils.isValidEmailAddress('hello@example.com'); // true
338
339
  */
339
340
  service.isValidEmailAddress = function (email) {
340
341
  var tester =
@@ -366,14 +367,13 @@ service.isValidEmailAddress = function (email) {
366
367
  };
367
368
 
368
369
  /**
369
- * A helper function for getting a formatted email address
370
+ * Lowercases an email address and checks it's valid.
370
371
  * @alias utils.parseEmail
371
- * @param {String} emailAddress The input to parse as an email
372
- * @return {String|Boolean} a valid email address in lowercase, or false if parse is not possible
372
+ * @param {String} input The text to parse.
373
+ * @returns {String|Boolean} The lowercased address, or false when it isn't a valid email address.
373
374
  * @example
374
- *
375
- * sdk.utils.parseEmail('ToMack@gmail.com'); // returns 'tomack@gmail.com'
376
- * sdk.utils.isValidEmailAddress('hello.world.com'); // Returns false
375
+ * sdk.utils.parseEmail('ToMack@Example.com'); // 'tomack@example.com'
376
+ * sdk.utils.parseEmail('hello.world.com'); // false
377
377
  */
378
378
  service.parseEmail = function (input) {
379
379
  if (!input) {
@@ -393,15 +393,14 @@ service.parseEmail = function (input) {
393
393
  //////////////////////////////////
394
394
 
395
395
  /**
396
- * A helper function for getting an integer
396
+ * Converts a value to a whole number (dropping any decimals), returning 0 when it isn't numeric.
397
397
  * @alias utils.parseInt
398
- * @param {String|Number} input The input to parse as an integer
399
- * @return {Integer} the resulting integer or 0
398
+ * @param {String|Number} input The value.
399
+ * @returns {Number} The integer, or 0.
400
400
  * @example
401
- *
402
- * sdk.utils.parseInt('134'); // returns 134
403
- * sdk.utils.parseInt('cows'); // returns 0
404
- * sdk.utils.parseInt(); // returns 0
401
+ * sdk.utils.parseInt('134'); // 134
402
+ * sdk.utils.parseInt('12.9'); // 12
403
+ * sdk.utils.parseInt('cows'); // 0
405
404
  */
406
405
  service.parseInt = function (input) {
407
406
  if (!input) {
@@ -417,19 +416,20 @@ service.parseInt = function (input) {
417
416
  };
418
417
 
419
418
  /**
420
- * A helper function for cleaning an input value to match
421
- * a required type
419
+ * Converts a value to a field data type: 'reference' gives the id, 'boolean' reads 'yes'/'true'/'1' style
420
+ * strings, 'url' and 'email' are parsed as by parseURL / parseEmail, 'key' gives a camelCase key, 'date' a Date,
421
+ * number types a number, 'object' only accepts plain objects, 'string' a string. Other types become strings.
422
422
  * @alias utils.cleanValue
423
- * @param {*} data The input to clean
424
- * @param {String} type The data type to parse
425
- * @param {Object} options Additional options for parsing
426
- * @return {*} the resulting cleaned value
423
+ * @param {*} data The value.
424
+ * @param {String} type The data type.
425
+ * @param {Object} [options] Options.
426
+ * @param {Boolean} [options.strict] Return any provided value unchanged (booleans are still converted).
427
+ * @returns {*} The converted value; undefined (or false, for url and email) when it can't be converted.
427
428
  * @example
428
- *
429
- * sdk.utils.cleanValue({_id:'1234', title:'Item'...}, 'reference'); // returns '1234'
430
- * sdk.utils.cleanValue('true', 'boolean'); // returns true
431
- * sdk.utils.cleanValue('Mr Rogers House', 'key'); // returns 'mrRogersHouse';
432
- * sdk.utils.cleanValue('Hello.World@email.COM', 'email'); // returns 'hello.world@email.com';
429
+ * sdk.utils.cleanValue({ _id: '61eca4746971e75c1fc670cf', title: 'Item' }, 'reference'); // '61eca4746971e75c1fc670cf'
430
+ * sdk.utils.cleanValue('true', 'boolean'); // true
431
+ * sdk.utils.cleanValue('Mr Rogers House', 'key'); // 'mrRogersHouse'
432
+ * sdk.utils.cleanValue('Hello.World@Example.COM', 'email'); // 'hello.world@example.com'
433
433
  */
434
434
  service.cleanValue = function (data, type, options) {
435
435
  if (!options) {
@@ -529,6 +529,20 @@ service.cleanValue = function (data, type, options) {
529
529
  }
530
530
  };
531
531
 
532
+ /**
533
+ * Checks whether a value is valid for a field data type ('url', 'key', 'date', 'email', 'number', 'decimal',
534
+ * 'float', 'integer', 'boolean', 'reference', 'string', 'object', 'array'). Values that aren't provided (see
535
+ * exists) are never valid; unknown types are never valid.
536
+ * @alias utils.isValidValue
537
+ * @param {*} value The value.
538
+ * @param {String} dataType The data type.
539
+ * @param {Boolean} [strict] Require the right JavaScript type (a real number, Date, boolean or string) rather than a convertible value.
540
+ * @returns {Boolean} Whether the value is valid.
541
+ * @example
542
+ * sdk.utils.isValidValue('42', 'integer'); // true
543
+ * sdk.utils.isValidValue('42', 'integer', true); // false: not a number
544
+ * sdk.utils.isValidValue('yes', 'boolean'); // true
545
+ */
532
546
  service.isValidValue = function (value, dataType, strict) {
533
547
  var isValue = service.exists(value);
534
548
  var valueIsNumber = typeof value == "number";
@@ -649,23 +663,15 @@ service.isValidValue = function (value, dataType, strict) {
649
663
  };
650
664
 
651
665
  /**
652
- * A helper function for parsing input as boolean values
666
+ * Converts a value to true or false. 'true', 'y', 'yes', '1' and 't' (any case) are true; 'false', 'n', 'no',
667
+ * '0', 'f', '-1', 'null', 'undefined' and '' are false; anything else is judged by JavaScript truthiness.
653
668
  * @alias utils.parseBoolean
654
- * @param {*} value The input to parse
655
- * @return {Boolean} the resulting true/false value
669
+ * @param {*} value The value.
670
+ * @returns {Boolean} The boolean.
656
671
  * @example
657
- *
658
- * sdk.utils.parseBoolean('true'); // returns true
659
- * sdk.utils.parseBoolean('y'); // returns true
660
- * sdk.utils.parseBoolean('YES'); // returns true
661
- * sdk.utils.parseBoolean('1'); // returns true
662
- * sdk.utils.parseBoolean('t'); // returns true
663
- * sdk.utils.parseBoolean(''); // returns false
664
- * sdk.utils.parseBoolean('0'); // returns false
665
- * sdk.utils.parseBoolean('n'); // returns false
666
- * sdk.utils.parseBoolean('no'); // returns false
667
- * sdk.utils.parseBoolean('f'); // returns false
668
- * sdk.utils.parseBoolean('null'); // returns false
672
+ * sdk.utils.parseBoolean('YES'); // true
673
+ * sdk.utils.parseBoolean('0'); // false
674
+ * sdk.utils.parseBoolean('no'); // false
669
675
  */
670
676
  service.parseBoolean = function (value) {
671
677
  switch (String(value).toLowerCase()) {
@@ -694,12 +700,32 @@ service.parseBoolean = function (value) {
694
700
 
695
701
  ///////////////////////////////////////////////////////////////////////////////
696
702
 
703
+ /**
704
+ * Deep-copies plain data through JSON. Dates become strings, and functions and undefined values are dropped.
705
+ * @alias utils.clone
706
+ * @param {*} input JSON-safe data.
707
+ * @returns {*} The copy.
708
+ * @example
709
+ * const copy = sdk.utils.clone(record);
710
+ */
697
711
  service.clone = function (input) {
698
712
  return JSON.parse(JSON.stringify(input));
699
713
  };
700
714
 
701
715
  ///////////////////////////////////////////////////////////////////////////////
702
716
 
717
+ /**
718
+ * Lists every field of a content type or definition as a flat, filterable list sorted by title: its own
719
+ * fields, its definition fields (under `data`, or `formData` and `data` for form submissions), and for profiles
720
+ * the computed '_age' and '_dob'. Each entry is a field with `path` (e.g. 'data.shirtSize'), `trail` and a `title`
721
+ * that includes its parent groups ('Address › City'). Single-value object groups themselves are left out.
722
+ * @alias utils.getAllFields
723
+ * @param {Object} actualDefinition A glossary entry (see sdk.content.glossary) with `fields` and optionally `definedFields`.
724
+ * @returns {Array<Object>} The fields.
725
+ * @example
726
+ * const { profile } = await sdk.content.glossary({ hash: true });
727
+ * const paths = sdk.utils.getAllFields(profile).map((field) => field.path);
728
+ */
703
729
  service.getAllFields = function (actualDefinition) {
704
730
  const self = this;
705
731
  const isProfile =
@@ -800,6 +826,23 @@ service.getAllFields = function (actualDefinition) {
800
826
  };
801
827
  ///////////////////////////////////////////////////////////////////////////////
802
828
 
829
+ /**
830
+ * Flattens a nested field list, adding to each field its `trail` (array of keys), `path` (dot path) and
831
+ * `titles` (titles of the field and its parent groups). Layout-only groups (groups without asObject) are
832
+ * walked through but not listed themselves.
833
+ * @alias utils.mapFields
834
+ * @param {Array<Object>} fields The fields.
835
+ * @param {Object} [options] Options.
836
+ * @param {Number} [options.depth] Stop descending after this many levels.
837
+ * @param {Boolean} [options.includeLayout] Also list layout-only groups.
838
+ * @param {Boolean} [options.includeArrayDelimeter] Add '[]' after the key of multi-value groups in paths, e.g. 'items[].title'.
839
+ * @param {String} [options.arrayDelimeter] The marker to use instead of '[]'.
840
+ * @param {Boolean} [options.original] Annotate and return the original field objects instead of copies.
841
+ * @returns {Array<Object>} The flattened fields.
842
+ * @example
843
+ * const flat = sdk.utils.mapFields(definition.fields, { includeArrayDelimeter: true });
844
+ * const paths = flat.map((field) => field.path);
845
+ */
803
846
  service.mapFields = function (fields, options) {
804
847
  if (!options) {
805
848
  options = {};
@@ -896,13 +939,14 @@ service.mapFields = function (fields, options) {
896
939
  ///////////////////////////////////////////////////////////////////////////////
897
940
 
898
941
  /**
899
- * A helpful function that can take a keyed object literal and map it to url query string parameters
942
+ * Builds a URL query string from an object. Entries whose value is undefined, null, false, 0 or '' are left
943
+ * out; arrays become repeated keys. No leading '?' or '&'.
900
944
  * @alias utils.mapParameters
901
- * @param {Object} parameters The object you want to transalte
902
- * @return {String} The query string
945
+ * @param {Object} parameters The values.
946
+ * @returns {String} The query string.
903
947
  * @example
904
- * //Returns &this=that&hello=world
905
- * sdk.utils.mapParameters({"this":"that", "hello":"world"})
948
+ * sdk.utils.mapParameters({ this: 'that', hello: 'world', tags: ['a', 'b'] });
949
+ * // 'this=that&hello=world&tags=a&tags=b'
906
950
  */
907
951
  service.mapParameters = function (parameters) {
908
952
  parameters = parameters || {};
@@ -929,19 +973,15 @@ service.mapParameters = function (parameters) {
929
973
  ///////////////////////////////////////////////////////////////////////////////
930
974
 
931
975
  /**
932
- * A function that will take an integer and a currency string and return a formatted numeric amount rounded to 2 decimal places
976
+ * Formats an amount in cents as money with a currency symbol ('£' for gbp, '€' for eur, '$' for everything else).
933
977
  * @alias utils.formatCurrency
934
- * @param {Integer} value The amount in cents
935
- * @param {String} currency The currency to format
936
- * @return {String} The formatted value
978
+ * @param {Number} value The amount in cents (1000 = 10.00).
979
+ * @param {String} [currency] Currency code, e.g. 'aud', 'gbp'.
980
+ * @param {Number} [decimalPoints] Decimal places, default 2.
981
+ * @returns {String} The formatted amount.
937
982
  * @example
938
- *
939
- * //Returns £10.00
940
- * sdk.utils.formatCurrency(1000, 'gbp');
941
- *
942
- * //Returns $10.00
943
- * sdk.utils.formatCurrency(1000, 'usd');
944
- *
983
+ * sdk.utils.formatCurrency(1000, 'gbp'); // '£10.00'
984
+ * sdk.utils.formatCurrency(1999, 'aud'); // '$19.99'
945
985
  */
946
986
  service.formatCurrency = function (value, currency, decimalPoints) {
947
987
  if (!value || isNaN(value)) {
@@ -955,18 +995,13 @@ service.formatCurrency = function (value, currency, decimalPoints) {
955
995
  };
956
996
 
957
997
  /**
958
- * A function that will take an id and return the type key
998
+ * Works out the base type of a record from its id (characters 9-10 of every Qik id encode the type).
959
999
  * @alias utils.getTypeFromID
960
- * @param {String} id The id of an object
961
- * @return {String} The key
1000
+ * @param {String|Object} id The id, or an object with an _id.
1001
+ * @returns {String|undefined} The type key, e.g. 'profile', or undefined for an invalid or unknown id. Organisation definitions share their base type's id prefix.
962
1002
  * @example
963
- *
964
- * // Returns 'user'
965
- * sdk.utils.getTypeFromID('52b523f775beea960013f6cd');
966
- *
967
- * // Returns 'role'
968
- * sdk.utils.getTypeFromID('62b59cb572fb4772e7b5fa93');
969
- *
1003
+ * sdk.utils.getTypeFromID('52b523f775beea960013f6cd'); // 'user'
1004
+ * sdk.utils.getTypeFromID('62b59cb572fb4772e7b5fa93'); // 'role'
970
1005
  */
971
1006
  service.getTypeFromID = function (id) {
972
1007
  id = service.id(id);
@@ -1038,18 +1073,13 @@ service.getTypeFromID = function (id) {
1038
1073
  };
1039
1074
 
1040
1075
  /**
1041
- * A function that will take a currency string and return the symbol
1076
+ * Returns the symbol for a currency code: '£' for gbp, '€' for eur, '$' for anything else.
1042
1077
  * @alias utils.currencySymbol
1043
- * @param {String} currency The currency
1044
- * @return {String} The symbol
1078
+ * @param {String} currency The currency code (any case).
1079
+ * @returns {String} The symbol.
1045
1080
  * @example
1046
- *
1047
- * //Returns £
1048
- * sdk.utils.currencySymbol('gbp');
1049
- *
1050
- * //Returns $
1051
- * sdk.utils.currencySymbol('usd');
1052
- *
1081
+ * sdk.utils.currencySymbol('gbp'); // '£'
1082
+ * sdk.utils.currencySymbol('usd'); // '$'
1053
1083
  */
1054
1084
  const CURRENCY_SYMBOLS = { gbp: "\u00A3", eur: "\u20AC" };
1055
1085
 
@@ -1066,6 +1096,15 @@ const SUPPORTED_CURRENCIES = [
1066
1096
  { code: "sgd", countries: ["SG"] },
1067
1097
  ];
1068
1098
 
1099
+ /**
1100
+ * Lists the currencies Qik supports (usd, gbp, cad, aud, nzd, sgd) as picker options, optionally with the one
1101
+ * for a country first.
1102
+ * @alias utils.getAvailableCurrencies
1103
+ * @param {String} [defaultCountryID] A two-letter country code (e.g. 'AU') whose currency should come first.
1104
+ * @returns {Array<Object>} Options { name, value, countryCode }, e.g. { name: 'AUD ($)', value: 'aud', countryCode: { AU: true } }.
1105
+ * @example
1106
+ * const currencies = sdk.utils.getAvailableCurrencies('AU'); // AUD first
1107
+ */
1069
1108
  service.getAvailableCurrencies = function (defaultCountryID) {
1070
1109
  let currencies = SUPPORTED_CURRENCIES.map(({ code, countries }) => ({
1071
1110
  name: `${code.toUpperCase()} (${service.currencySymbol(code)})`,
@@ -1087,15 +1126,17 @@ service.getAvailableCurrencies = function (defaultCountryID) {
1087
1126
  ///////////////////////////////////////////////////////////////////////////////
1088
1127
 
1089
1128
  /**
1090
- * Creates a fast keyed hash object from an array of items
1129
+ * Turns an array into an object for quick lookups, keyed by a property of each item (later items win when keys
1130
+ * repeat). Without a key the items themselves are the keys, e.g. for a set of ids.
1091
1131
  * @alias utils.hash
1092
- * @param {Array} array The array of items to convert into a hash
1093
- * @param {String} key The key or path to the property on each item to use as the hashed key
1094
- * @return {Object} A key/value paired object
1132
+ * @param {Array} items The items. Anything that isn't an array is treated as empty.
1133
+ * @param {String} [key] The property (or dot path) to key by.
1134
+ * @returns {Object} The lookup.
1095
1135
  * @example
1096
- * //Returns { jimbo:{id:'jimbo', title:'Jim Jones'}, {id:'roger', title:'Roger Fellow'} }
1097
- * sdk.utils.hash([{id:'jimbo', title:'Jim Jones'}, {id:'roger', title:'Roger Fellow'}], 'id');
1136
+ * sdk.utils.hash([{ id: 'jim', title: 'Jim Jones' }, { id: 'roger', title: 'Roger Fellow' }], 'id');
1137
+ * // { jim: { id: 'jim', title: 'Jim Jones' }, roger: { id: 'roger', title: 'Roger Fellow' } }
1098
1138
  *
1139
+ * sdk.utils.hash(['a', 'b']); // { a: 'a', b: 'b' }
1099
1140
  */
1100
1141
  service.hash = function (items, key) {
1101
1142
  items = !Array.isArray(items) ? [] : items;
@@ -1109,23 +1150,20 @@ service.hash = function (items, key) {
1109
1150
  //////////////////////////////////////////////////
1110
1151
 
1111
1152
  /**
1112
- * Returns a subset of values in an array that match a provided rule
1153
+ * Pulls one property (dot path) out of every item of an array, optionally flattening, de-duplicating,
1154
+ * dropping empty values or adding them up.
1113
1155
  * @alias utils.extractFromArray
1114
- * @param {Array} array The array you want to extract values from
1115
- * @param {String} key The path to the child property you want to extract
1116
- * @param {Boolean} sum Whether to sum the extracted values together in total
1117
- * @param {Boolean} flatten Whether to flatten nested child arrays
1118
- * @param {Boolean} unique Whether to only return unique values
1119
- * @param {Boolean} exclude Whether to exclude null or undefined values
1120
- * @param {Object} options Pass through extra options for how to extract the values
1121
- * @return {Array} An array of all values retrieved from the array, unless provided arguments require otherwise
1156
+ * @param {Array} array The items.
1157
+ * @param {String} key The property or dot path to read from each item.
1158
+ * @param {Boolean} [sum] Add the values together and return the total.
1159
+ * @param {Boolean} [flatten] Flatten values that are arrays.
1160
+ * @param {Boolean} [unique] Remove duplicate values.
1161
+ * @param {Boolean} [exclude] Drop null, undefined and other empty values (0 and false are kept).
1162
+ * @param {Object} [options] The same settings as an object: { sum, flatten, unique, excludeNull }.
1163
+ * @returns {Array|Number} The values, or their total when summing.
1122
1164
  * @example
1123
- * //Returns [12, 45] as all the values
1124
- * sdk.utils.extractFromArray([{name:'Wendy', age:12}, {name:'Roger', age:45}], 'age');
1125
- *
1126
- * //Returns 32
1127
- * sdk.utils.extractFromArray([{name:'Wendy', age:12}, {name:'Roger', age:20}], 'age', {sum:true});
1128
- *
1165
+ * sdk.utils.extractFromArray([{ name: 'Wendy', age: 12 }, { name: 'Roger', age: 45 }], 'age'); // [12, 45]
1166
+ * sdk.utils.extractFromArray([{ name: 'Wendy', age: 12 }, { name: 'Roger', age: 20 }], 'age', true); // 32
1129
1167
  */
1130
1168
  service.extractFromArray = function (
1131
1169
  array,
@@ -1191,18 +1229,17 @@ service.extractFromArray = function (
1191
1229
  //////////////////////////////////////////////////////
1192
1230
 
1193
1231
  /**
1194
- * A function that can return a selection of values that were found in an array that match a specific rule,
1195
- * This is often used to evaluate expressions within form fields
1232
+ * Returns the items of an array whose property (dot path) matches a value, for example when evaluating
1233
+ * form field conditions.
1196
1234
  * @alias utils.matchInArray
1197
- * @param {Array} array The array to check
1198
- * @param {String} key The javascript dot notation path to extract
1199
- * @param {String} value The value to compare against
1200
- * @param {String} comparator The logical operator to use to compare the extracted value with the provided value ('>', '<', '>=', '<=', 'in', '==') Defaults to '==' (Is equal to)
1201
- * @return {Array} Returns an array of matching values
1235
+ * @param {Array} array The items.
1236
+ * @param {String} key The property or dot path to compare.
1237
+ * @param {*} v The value to compare with. With the default comparator and no value, items whose property is truthy match.
1238
+ * @param {String} [comparator] '==' (default, loose equality), '>', '<', '>=', '<=', or 'in' (the property, an array or string, includes the value).
1239
+ * @returns {Array} The matching items.
1202
1240
  * @example
1203
- * //Returns [{name:'Michael', age:45}] as that is only item in the array that matches the criteria
1204
- * sdk.utils.matchInArray([{name:'Wendy', age:12}, {name:'Michael', age:45}], 'age', 45, '>=');
1205
- *
1241
+ * sdk.utils.matchInArray([{ name: 'Wendy', age: 12 }, { name: 'Michael', age: 45 }], 'age', 45, '>=');
1242
+ * // [{ name: 'Michael', age: 45 }]
1206
1243
  */
1207
1244
  service.matchInArray = function (array, key, v, comparator) {
1208
1245
  //Filter the array options by a certain v and comparator
@@ -1246,18 +1283,16 @@ service.matchInArray = function (array, key, v, comparator) {
1246
1283
  ///////////////////////////////////////////////////////////////////////////////
1247
1284
 
1248
1285
  /**
1249
- * A helpful class that can take an array of values and return them as a comma seperated
1250
- * string, If the values are objects, then a property to use as the string representation can be specified
1286
+ * Joins values into a comma-separated string, skipping null, undefined and empty strings. For objects, give a
1287
+ * property to use.
1251
1288
  * @alias utils.comma
1252
- * @param {Array} array The array of values to translate
1253
- * @param {String} path An optional property key to use for each value
1254
- * @return {String} The resulting comma seperated string
1289
+ * @param {Array} array The values.
1290
+ * @param {String} [path] The property (dot path) to use for each value.
1291
+ * @param {Number} [limit] Only use the first this many values.
1292
+ * @returns {String} e.g. 'cat, dog, bird'.
1255
1293
  * @example
1256
- * //Returns 'cat, dog, bird'
1257
- * sdk.utils.comma(['cat', 'dog', 'bird']);
1258
- *
1259
- * //Returns 'cat, dog, bird'
1260
- * sdk.utils.comma([{title:'cat'}, {title:'dog'}, {title:'bird'}], 'title');
1294
+ * sdk.utils.comma(['cat', 'dog', 'bird']); // 'cat, dog, bird'
1295
+ * sdk.utils.comma([{ title: 'cat' }, { title: 'dog' }], 'title'); // 'cat, dog'
1261
1296
  */
1262
1297
  service.comma = function (array, path, limit) {
1263
1298
  if (limit) {
@@ -1283,22 +1318,14 @@ service.comma = function (array, path, limit) {
1283
1318
  //Helper function to get an id of an object
1284
1319
 
1285
1320
  /**
1286
- * Returns a specified _id for an object
1321
+ * Gets a record id from either an id string or an object with an _id, and checks it's a valid 24-character id.
1287
1322
  * @alias utils.id
1288
- * @param {Object} input An object that is or has an _id property
1289
- * @param {Boolean} asObjectID Whether to convert to a Mongo ObjectId
1290
- * @return {String} Will return either a string or a Mongo ObjectId
1291
- *
1323
+ * @param {String|Object} source An id, or an object with an _id.
1324
+ * @returns {String|undefined} The id, or undefined when there isn't a valid one.
1292
1325
  * @example
1293
- *
1294
- * //Returns '5cb3d8b3a2219970e6f86927'
1295
- * sdk.utils.id('5cb3d8b3a2219970e6f86927')
1296
- *
1297
- * //Returns true
1298
- * typeof service.id({_id:'5cb3d8b3a2219970e6f86927', title, ...}) == 'string';
1299
-
1300
- * //Returns true
1301
- * typeof service.id({_id:'5cb3d8b3a2219970e6f86927'}, true) == 'object';
1326
+ * sdk.utils.id('5cb3d8b3a2219970e6f86927'); // '5cb3d8b3a2219970e6f86927'
1327
+ * sdk.utils.id({ _id: '5cb3d8b3a2219970e6f86927', title: 'Item' }); // '5cb3d8b3a2219970e6f86927'
1328
+ * sdk.utils.id('not an id'); // undefined
1302
1329
  */
1303
1330
  service.id = function (source) {
1304
1331
  if (!source) {
@@ -1326,15 +1353,13 @@ service.id = function (source) {
1326
1353
  ///////////////////////////////////////////////////////////////////////////////
1327
1354
 
1328
1355
  /**
1329
- * Cleans and maps an array of objects to an array of IDs
1356
+ * Maps an array of ids and/or objects with an _id to a list of unique valid ids; invalid and empty entries are dropped.
1330
1357
  * @alias utils.ids
1331
- * @param {Array} array An array of objects or object ids
1332
- * @param {Boolean} asObjectID Whether or not to map the ids as Mongo ObjectIds
1333
- * @return {Array} An array of Ids
1334
- *
1358
+ * @param {Array} array Ids and/or objects with an _id.
1359
+ * @returns {Array<String>} The unique ids (an empty array for no input).
1335
1360
  * @example
1336
- * //Returns ['5cb3d8b3a2219970e6f86927', '5cb3d8b3a2219970e6f86927', '5cb3d8b3a2219970e6f86927']
1337
- * sdk.utils.ids([{_id:'5cb3d8b3a2219970e6f86927'}, {_id:'5cb3d8b3a2219970e6f86927'}, null, '5cb3d8b3a2219970e6f86927'])
1361
+ * sdk.utils.ids([{ _id: '5cb3d8b3a2219970e6f86927' }, null, '5cb3d8b3a2219970e6f86927', '5cb3d8b3a2219970e6f86928']);
1362
+ * // ['5cb3d8b3a2219970e6f86927', '5cb3d8b3a2219970e6f86928']
1338
1363
  */
1339
1364
 
1340
1365
  service.ids = function (array) {
@@ -1367,6 +1392,15 @@ service.ids = function (array) {
1367
1392
 
1368
1393
  ///////////////////////////////////////////////////////////////////////////////
1369
1394
 
1395
+ /**
1396
+ * Checks whether a value is a 24-character hexadecimal record id.
1397
+ * @alias utils.isValidID
1398
+ * @param {*} input The value.
1399
+ * @returns {Boolean} Whether it's a valid id.
1400
+ * @example
1401
+ * sdk.utils.isValidID('5cb3d8b3a2219970e6f86927'); // true
1402
+ * sdk.utils.isValidID('hello'); // false
1403
+ */
1370
1404
  service.isValidID = function (input) {
1371
1405
  var checkForHexRegExp = new RegExp("^[0-9a-fA-F]{24}$");
1372
1406
  return checkForHexRegExp.test(String(input));
@@ -1375,10 +1409,17 @@ service.isValidID = function (input) {
1375
1409
  ///////////////////////////////////////////////////////////////////////////////
1376
1410
 
1377
1411
  /**
1378
- * Helper function for retrieving a human readable error message from server error response objects
1412
+ * Gets a readable message from an error, such as a failed sdk.api request: the server's message when there is
1413
+ * one, otherwise the error's own message, otherwise the error as JSON.
1379
1414
  * @alias utils.errorMessage
1380
- * @param {Object} error The error object to translate
1381
- * @return {String} The resulting human readable error message
1415
+ * @param {Object|Array} err The error (for an array, its first entry).
1416
+ * @returns {String|undefined} The message, or undefined when no error is given.
1417
+ * @example
1418
+ * try {
1419
+ * await sdk.content.get(id);
1420
+ * } catch (err) {
1421
+ * alert(sdk.utils.errorMessage(err));
1422
+ * }
1382
1423
  */
1383
1424
  service.errorMessage = function (err) {
1384
1425
  if (!err) {
@@ -1406,12 +1447,11 @@ service.errorMessage = function (err) {
1406
1447
  ////////////////////////////////////
1407
1448
 
1408
1449
  /**
1409
- * Generates a globally unique ID, helpful for adding unique keys for iterable loops
1450
+ * Generates a random unique id (a UUID), e.g. for keys in rendered lists.
1410
1451
  * @alias utils.guid
1411
- * @return {String} The new globally unique identifier
1452
+ * @returns {String} e.g. '3b241101-e2bb-4255-8caf-4136c566a962'.
1412
1453
  * @example
1413
- * //Returns 4323a78br-z16h-289j-zwl1-938lda334asd
1414
- * sdk.utils.guid()
1454
+ * const key = sdk.utils.guid();
1415
1455
  */
1416
1456
  service.guid = function () {
1417
1457
  if (typeof crypto !== "undefined" && crypto.randomUUID) {
@@ -1429,10 +1469,13 @@ service.guid = function () {
1429
1469
  ////////////////////////////////////
1430
1470
 
1431
1471
  /**
1432
- * Helper function for cleaning strings to use as database ids
1472
+ * Turns text into a key: non-alphanumeric characters become word breaks and each part between underscores is
1473
+ * camelCased.
1433
1474
  * @alias utils.machineName
1434
- * @param {String} string The string to clean eg. (Awesome Event!)
1435
- * @return {String} A cleaned and formatted string eg. (awesomeEvent)
1475
+ * @param {String} string The text, e.g. 'Awesome Event!'.
1476
+ * @returns {String|undefined} The key, e.g. 'awesomeEvent'; undefined for empty input.
1477
+ * @example
1478
+ * sdk.utils.machineName('Awesome Event!'); // 'awesomeEvent'
1436
1479
  */
1437
1480
  service.machineName = function (string) {
1438
1481
  if (!string || !string.length) {
@@ -1458,16 +1501,18 @@ export default service;
1458
1501
  /**
1459
1502
  * A lightweight event emitter that can be attached to any service object
1460
1503
  * to provide pub/sub event capabilities. Can be called with or without `new`.
1504
+ * dispatch(event, ...details) passes every argument after the event name on to each listener.
1461
1505
  */
1462
1506
  export function EventDispatcher() {
1463
1507
  const handlers = new Map();
1464
1508
 
1465
1509
  const emitter = {
1466
- dispatch(event, details) {
1510
+ dispatch(event, ...details) {
1467
1511
  const fns = handlers.get(event);
1468
1512
  if (fns) {
1469
1513
  for (const fn of fns) {
1470
- fn(details);
1514
+ // Every argument after the event name is passed on to the listener
1515
+ fn(...details);
1471
1516
  }
1472
1517
  }
1473
1518
  },