@qikdev/sdk 1.0.10 → 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.
@@ -1,8 +1,10 @@
1
1
  ///////////////////////////////////////////////////
2
2
 
3
3
  /**
4
- * Creates a new QikContent instance.
5
- * This module provides a number of helper functions for creating and modifying content via the REST API
4
+ * Reading and writing records (content) through the REST API: list, get, create, update, patch,
5
+ * delete and restore, plus the glossary of content types, filter comparators and field validation helpers.
6
+ * Every call runs as the signed-in user (or the application token) and is subject to their permissions:
7
+ * records they can't see aren't returned, and fields they can't see are removed from the records that are.
6
8
  * @alias content
7
9
  * @constructor
8
10
  * @hideconstructor
@@ -26,14 +28,16 @@ export default function (qik) {
26
28
  let inflightVariablesRequest;
27
29
 
28
30
  /**
29
- *
30
- * Retrieves all global variables for the current user. This is often used when running custom code in an action.
31
+ * Retrieves the organisation's variables (stored key/value settings such as API keys) that the current user
32
+ * is allowed to see, as one object keyed by variable key (POST /variables, up to 1000 variables).
33
+ * Often used by action code to read credentials. The result is kept in memory after the first call;
34
+ * later calls return that copy unless `options.reload` is set.
31
35
  * @alias content.variables
32
- * @param {Array} keys Provide specific keys of variables you want to retrieve
33
- * @param {Object} options Additional options when making the request
34
- * @param {Boolean} options.reload Force variables to reload and not be cached. If false will retrieve any variables that are already known from the in memory cache.
36
+ * @param {Array<String>} [keys] Sent to the server, but currently ignored there: every visible variable is always returned. Pick the ones you need from the result.
37
+ * @param {Object} [options] Request options.
38
+ * @param {Boolean} [options.reload] Fetch again instead of returning the in-memory copy (`options.refresh` works too).
39
+ * @returns {Promise<Object>} An object mapping each variable key to its value, e.g. { OAUTH_CLIENT_ID: 'abc', OAUTH_KEY: 'xyz' }.
35
40
  * @example
36
- *
37
41
  * const { OAUTH_CLIENT_ID, OAUTH_KEY } = await sdk.content.variables();
38
42
  */
39
43
 
@@ -117,22 +121,23 @@ export default function (qik) {
117
121
  }
118
122
 
119
123
  /**
120
- *
121
- * Retrieves the glossary of all content types visible to the requesting user.
122
- By default this will include all fields, validation, expressions and other configuration
123
- * @alias content.glossary
124
- * @param {Object} options Additional options
125
- * @param {Boolean} options.hash Whether to return the data as a keyed object
126
- allowing for fast selection of specific content types, by default will return as an array
127
- * @param {Boolean} options.reload Force glossary to reload and not be cached.
128
- If false will retrieve content type data from the in memory cache
129
- * @param {Boolean} options.uncompressed Use the uncompressed endpoint
130
- * @example
131
- *
132
- * const { article, profile } = await sdk.content.glossary({hash:true});
133
- * // Use compressed endpoint for faster loading
134
- * const glossary = await sdk.content.glossary({compressed:true});
135
- */
124
+ * Retrieves the glossary: every content type the current user can know about, built-in types and the
125
+ * organisation's own definitions alike, each with its key, title, plural, fields (including definedFields
126
+ * for definitions), validation and other configuration. Use it to find the real type key for
127
+ * content.list / content.create; keys can't be inferred from titles.
128
+ * Loaded from GET /glossary/compressed (or GET /glossary with `uncompressed`) and kept in the SDK cache
129
+ * until `options.reload` or sdk.cache.reset() (which logout and organisation switches call).
130
+ * @alias content.glossary
131
+ * @param {Object} [options] Options.
132
+ * @param {Boolean} [options.hash] Return an object keyed by type key ({ profile: {...}, article: {...} }) instead of an array.
133
+ * @param {Boolean} [options.hex] Return an object keyed by each type's two-character id prefix instead of an array.
134
+ * @param {Boolean} [options.reload] Fetch again instead of using the cached copy (`options.refresh` works too).
135
+ * @param {Boolean} [options.uncompressed] Load from the uncompressed endpoint. The default compressed endpoint returns the same data, smaller.
136
+ * @returns {Promise<Array|Object>} An array of content type definitions, or an object keyed by type key when `hash` is set (by id prefix when `hex` is set).
137
+ * @example
138
+ * const { article, profile } = await sdk.content.glossary({ hash: true });
139
+ * const fieldKeys = profile.fields.map((field) => field.key);
140
+ */
136
141
  service.glossary = async function (options) {
137
142
  options = options || {};
138
143
 
@@ -190,18 +195,17 @@ export default function (qik) {
190
195
  let inflightScopeGlossaryRequest;
191
196
 
192
197
  /**
193
- *
194
- * Retrieves the scope glossary of all scopes the user can know about. This helps to convert a scope id into a human readable title.
195
- * @alias content.scopeGlossary
196
- * @param {Object} options Additional options
197
- * @param {Boolean} options.hash Whether to return the data as a keyed object with each scopes _id as the key
198
- allowing for fast selection of specific scopes, by default will return a structured tree
199
- * @param {Boolean} options.reload Force the glossary to reload and not be cached.
200
- If false will retrieve content type data from the in memory cache
201
- * @example
202
- *
203
- * const scopes = await sdk.content.scopeGlossary();
204
- */
198
+ * Retrieves every scope the current user holds a permission in (GET /scope/glossary), handy for turning
199
+ * a scope id into a readable title. The result is kept in memory after the first call.
200
+ * @alias content.scopeGlossary
201
+ * @param {Object} [options] Options.
202
+ * @param {Boolean} [options.hash] Return an object keyed by scope _id instead of an array.
203
+ * @param {Boolean} [options.reload] Fetch again instead of returning the in-memory copy (`options.refresh` works too).
204
+ * @returns {Promise<Array|Object>} A flat array of scopes, each { _id, title, bgColor, color, meta: { definition } }, or an object keyed by _id when `hash` is set.
205
+ * @example
206
+ * const scopes = await sdk.content.scopeGlossary({ hash: true });
207
+ * const scopeTitle = scopes['61eca4746971e75c1fc670cf']?.title;
208
+ */
205
209
 
206
210
  service.scopeGlossary = async function (options) {
207
211
  options = options || {};
@@ -234,15 +238,19 @@ export default function (qik) {
234
238
  ///////////////////////////////////////////////////
235
239
 
236
240
  /**
237
- *
238
- * Retrieves all available filter comparators for each data type
241
+ * Retrieves every filter comparator the server understands (GET /system/comparators), for building the
242
+ * `filter` of content.list. Each comparator has a `title`, an `operator` (the name to use as a filter's
243
+ * `comparator`), optional `aliases` (e.g. '>' for 'greater') and the data types it applies to.
244
+ * The result is kept in memory after the first call.
239
245
  * @alias content.comparators
240
- * @param {Object} options Additional options
241
- * @param {Boolean} options.reload Ignore any locally cached data
246
+ * @param {Object} [options] Options.
247
+ * @param {Boolean} [options.reload] Fetch again instead of returning the in-memory copy (`options.refresh` works too).
248
+ * @returns {Promise<Object>} { hash, types, available }: `hash` maps each operator name and alias to its comparator; `types` maps each data type (string, number, date, boolean, reference...) to the operator names that apply to it; `available` maps each data type to the comparator objects themselves.
242
249
  * @example
243
- * const {hash, available, types} = await sdk.content.comparators();
244
- * console.log(available) // {boolean:[{title:'Is equal to', operator:'equal'...}]}
245
- * console.log(hash) // {equal:[{title:'Is equal to', operator:'equal'...}]}
250
+ * const { hash, available, types } = await sdk.content.comparators();
251
+ * console.log(types.date); // includes 'datebefore', 'dateafter', 'datebetween' ...
252
+ * console.log(available.boolean[0]); // { title: 'Is equal to', operator: 'equal', ... }
253
+ * console.log(hash['>'].operator); // 'greater'
246
254
  */
247
255
 
248
256
  const comparators = {};
@@ -295,6 +303,22 @@ export default function (qik) {
295
303
  return { minimum, maximum };
296
304
  }
297
305
 
306
+ /**
307
+ * Checks a single value against a field's extra length and range rules. Used by validateField; it does
308
+ * not check the data type or how many values a field needs.
309
+ * @alias content.meetsValidationRequirements
310
+ * @param {*} input The value to check.
311
+ * @param {String} fieldType The field's data type (currently not used by the check).
312
+ * @param {Object} validationCriteria The rules to apply. Each is skipped when missing or 0.
313
+ * @param {Number} [validationCriteria.minLength] Minimum number of characters.
314
+ * @param {Number} [validationCriteria.maxLength] Maximum number of characters.
315
+ * @param {Number} [validationCriteria.minValue] Smallest number allowed.
316
+ * @param {Number} [validationCriteria.maxValue] Largest number allowed.
317
+ * @returns {String|undefined} A message describing the first rule the value breaks, or undefined when it passes.
318
+ * @example
319
+ * sdk.content.meetsValidationRequirements('Jo', 'string', { minLength: 3 });
320
+ * // 'Must be at least 3 characters'
321
+ */
298
322
  service.meetsValidationRequirements = function (
299
323
  input,
300
324
  fieldType,
@@ -334,19 +358,25 @@ export default function (qik) {
334
358
  };
335
359
 
336
360
  /**
337
- * Checks if a certain input validates against a field definition
361
+ * Checks a value against a field definition the way forms do before submitting: required (minimum),
362
+ * how many values are allowed (maximum, 0 = unlimited; a field with maximum other than 1 expects an array),
363
+ * the data type, and minLength / maxLength / minValue / maxValue rules. Runs locally, no request is made.
364
+ * A required boolean field only passes when the value is true.
338
365
  * @alias content.validateField
339
- * @param {Any} input The input to validate
340
- * @param {Object} fieldDefinition The field to validate against
341
- * @param {Object} options Additional options when calling the function
366
+ * @param {*} input The value to check (an array for multi-value fields).
367
+ * @param {Object} fieldDefinition The field from a definition or glossary entry: { title, key, type, minimum, maximum, minLength, maxLength, minValue, maxValue, validation, widget, asObject }.
368
+ * @param {Object} [options] Options.
369
+ * @param {Boolean} [options.strict] Require values to already be the right JavaScript type (e.g. a real number or Date) instead of accepting values that can be converted.
370
+ * @returns {Object} { valid: true } when the value passes, otherwise { valid: false, status: 400, message } with a readable message.
342
371
  * @example
343
- * const validationResult = await sdk.content.validateField('Johnny Bobbins', {title:'Name', key:'firstName', type:'string', minimum:1, maximum:1, ...});
344
- * console.log(validationResult)
345
- * // Results in { valid:true }
372
+ * sdk.content.validateField('Johnny', { title: 'Name', key: 'firstName', type: 'string', minimum: 1, maximum: 1 });
373
+ * // { valid: true }
346
374
  *
347
- * const validationResult = await sdk.content.validateField('Johnny Bobbins', {title:'Number', key:'number', type:'integer', minimum:1, maximum:1, ...});
348
- * console.log(validationResult)
349
- * // Results in { valid:false, status:400, message:'Invalid number input for field' }
375
+ * sdk.content.validateField('Johnny', { title: 'Number', key: 'number', type: 'integer', minimum: 1, maximum: 1 });
376
+ * // { valid: false, status: 400, message: "Single value 'Johnny' is not a valid integer for Number", criteria: {...} }
377
+ *
378
+ * sdk.content.validateField(undefined, { title: 'Email', key: 'email', type: 'email', minimum: 1, maximum: 1 });
379
+ * // { valid: false, status: 400, message: 'Email is a required field' }
350
380
  */
351
381
 
352
382
  service.validateField = function (input, fieldDefinition, options) {
@@ -615,7 +645,24 @@ export default function (qik) {
615
645
  };
616
646
  };
617
647
 
648
+ /**
649
+ * Converts a raw value to a data type the way validateField does before checking it: numbers are parsed,
650
+ * booleans read from 'yes' / 'true' / '1' style strings, emails lowercased, references reduced to an id,
651
+ * dates parsed. Runs locally.
652
+ * @alias content.getCleanedValue
653
+ * @param {*} input The raw value.
654
+ * @param {String} dataType The data type: 'number' | 'decimal' | 'float' | 'integer' | 'boolean' | 'email' | 'reference' | 'date' | 'url' | 'key' | 'string' | 'object' ...
655
+ * @param {Object} [options] Options.
656
+ * @param {Boolean} [options.strict] Return the value unchanged (numbers, integers and booleans are still converted).
657
+ * @returns {*} The converted value. Missing or unconvertible values come back as undefined (false for a url that can't be parsed).
658
+ * @example
659
+ * sdk.content.getCleanedValue('42', 'integer', {}); // 42
660
+ * sdk.content.getCleanedValue('Yes', 'boolean', {}); // true
661
+ * sdk.content.getCleanedValue('Hi@Example.COM', 'email', {}); // 'hi@example.com'
662
+ */
618
663
  service.getCleanedValue = function (input, dataType, options) {
664
+ options = options || {};
665
+
619
666
  switch (dataType) {
620
667
  case "number":
621
668
  case "float":
@@ -658,43 +705,67 @@ export default function (qik) {
658
705
  ///////////////////////////////////////////////////
659
706
 
660
707
  /**
708
+ * Lists the records of one content type that match a query (POST /content/:type/list). This is the way to
709
+ * read many records at once: filter, search, sort and page on the server, and ask for exactly the fields you need.
661
710
  *
662
- * Retrieves a list of records matching the provided criteria
711
+ * ALWAYS pass `select` listing the fields you want. Without it each row contains only `_id` and `title`,
712
+ * and reading anything else would mean fetching every record again with content.get.
713
+ * Rows are filtered by the viewer's field permissions: a selected field the viewer isn't allowed to see is
714
+ * simply left out of the row (no error), and records they can't see aren't returned at all.
715
+ * By default only active, non-deleted records are listed.
663
716
  * @alias content.list
664
- * @param {String} type The type or definition of records we want to retrieve
665
- * @param {Object} options The options for our query
666
- * @param {String} options.search Freeform text keywords
667
- * @param {Object} options.sort How to sort the results
668
- * @param {String} options.sort.key Which key to sort on
669
- * @param {String} options.sort.direction Which direction to sort on
670
- * @param {Object} options.sort.type What type of data is being sorted
671
- * @param {Object} options.page Page configuration
672
- * @param {Number} options.page.size Page size
673
- * @param {Number} options.page.index Page index
674
- * @param {Object} options.filter How to filter the results
717
+ * @param {String} type The type key or definition key to list, e.g. 'profile', 'event', or an organisation's own definition key. Look keys up with content.glossary(); they can't be inferred from titles.
718
+ * @param {Object} [options] The query. Every property is optional.
719
+ * @param {Array<String>} [options.select] Fields to include on each row, as dot paths: 'firstName', 'data.shirtSize', 'meta.created', 'meta.scopes'. Must be an array (a string is rejected with a 400). A parent key returns everything under it ('data' returns all custom fields). `_id` is always included. Reference fields named here come back populated with the referenced records. Computed keys are accepted too: '_age' and '_dob' on profiles, and 'join_...' keys for related data. Without select, rows contain only `_id` and `title`.
720
+ * @param {String} [options.search] Keywords. Matches whole words and the start of words (not the middle of words) in the record's indexed terms such as names, titles, emails and phone numbers. On profiles every word must match; on other types a record also matches when any one word matches a whole term. A 24-character record id finds that record.
721
+ * @param {Object} [options.filter] A filter group: { operator, filters }. `operator` is 'and' (default), 'or' or 'nor'. `filters` is an array of conditions or nested groups. A condition is { key, comparator, value, value2, values }: `key` is a field path ('data.shirtSize', 'meta.created', '_age'); `comparator` is a comparator name such as 'equal', 'notequal', 'in', 'notin', 'contains', 'startswith', 'greater', 'lesser', 'between', 'empty', 'notempty', 'datebefore', 'dateafter', 'datebetween', 'datenext', 'datepast' (content.comparators() lists them all, with aliases such as '>' and '=='); `value` is the value to compare with; `value2` the upper bound for between-style comparators; `values` the array for 'in' / 'notin'. A condition with no key, or no value for a comparator that needs one, is ignored. A key that isn't a real field matches nothing (see options.validateFilterKeys).
722
+ * @param {Object} [options.sort] How to order the results. When omitted: most recently updated first ('meta.updated', newest first); emails and SMS newest created first; events and assignments by start date, soonest first.
723
+ * @param {String} options.sort.key The field path to sort on, e.g. 'title', 'meta.created', 'data.rank'.
724
+ * @param {String} [options.sort.direction] 'asc' (the default when a sort is given) or 'desc' ('dsc' also works).
725
+ * @param {String} [options.sort.type] How values are compared: 'string' (default), 'number' ('integer', 'decimal' and 'float' also work), 'date', 'boolean', 'natural' (so 'Item 2' comes before 'Item 10') or 'dateproximity' (closest to now first).
726
+ * @param {Object} [options.page] Paging.
727
+ * @param {Number} [options.page.size] Rows per page. Default 50, maximum 5000.
728
+ * @param {Number} [options.page.index] Which page, starting at 1. Default 1.
729
+ * @param {Object} [options.date] A date range, { startDate, endDate } (dates or ISO strings; either may be left out). For most types it applies to meta.created. For events it returns events that overlap the range; for assignments and check-ins, those whose event overlaps it. Log queries default to the last 12 hours. (The property is `date`; `dates` is rejected with a 400.)
730
+ * @param {Array<String>} [options.ids] Only return records with these ids (inactive and deleted ones included). An empty array does NOT restrict the list: it is ignored and every matching record is returned.
731
+ * @param {Boolean} [options.includeInactive] Include records whose meta.status isn't 'active'. Events, runsheets, rosters, assignments and check-ins are never limited by status, and filtering on meta.status includes inactive records automatically.
732
+ * @param {Boolean} [options.includeTrash] Include deleted records alongside live ones.
733
+ * @param {Boolean} [options.trash] Return only deleted records (the trash).
734
+ * @param {Boolean} [options.includeAll] Also return `all`: the ids of every matching record across all pages, in sorted order.
735
+ * @param {Array<Object>} [options.math] Totals over every matching record, not just the page: [{ key: 'data.amount' }, ...]. Returned as `math`, keyed by field path: { total, count, mean, small, large, unique }.
736
+ * @param {Number} [options.sample] Return this many matching records picked at random.
737
+ * @param {Boolean} [options.validateFilterKeys] Fail with a 400 and "did you mean" suggestions when a filter key isn't a field of the type, instead of quietly matching nothing.
738
+ * @param {Boolean} [options.includeThreads] Attach each record's comment thread to its row.
739
+ * @param {String} [options.rolodexPrimary] Only records whose alphabetical index letter (meta.rolodexPrimary) is this letter.
740
+ * @param {String} [options.rolodexSecondary] Only records whose second index letter (meta.rolodexSecondary) is this letter.
741
+ * @param {Object} [advanced] SDK settings for the request itself; nothing here is sent as part of the query.
742
+ * @param {Boolean} [advanced.cancellable] Return { promise, cancel(message) } straight away instead of waiting. `promise` resolves to the full axios response, so read its `.data`; a cancelled request rejects with an error that sdk.api.wasCancelled(err) recognises.
743
+ * @param {Object} [advanced.config] Extra axios request config for the call (headers, timeout...).
744
+ * @param {String} [advanced.remoteURL] Send the query to this endpoint instead of /content/:type/list.
745
+ * @returns {Promise<Object>} { items, total, page: { index, total }, benchmarks } plus, when relevant, `all` (with includeAll), `math` (with math), `warnings` (problems found in the query) and `suggestions` (similar names, for a profile search that found nothing). `items` holds the rows of the requested page, `total` counts every matching record and `page.total` is the number of pages. With advanced.cancellable: { promise, cancel } instead.
675
746
  * @example
676
- *
677
- *
678
- * sdk.content.list('profile', {
679
- * search:'Jim',
680
- * page:{
681
- * size:50,
682
- * index:2,
683
- * },
684
- * sort:{
685
- * key:'age',
686
- * direction:'asc',
687
- * type:'integer',
747
+ * const { items, total, page } = await sdk.content.list('profile', {
748
+ * select: ['firstName', 'lastName', 'emails', 'data.shirtSize', 'meta.created'],
749
+ * search: 'jim',
750
+ * filter: {
751
+ * operator: 'and',
752
+ * filters: [
753
+ * { key: '_age', comparator: 'greater', value: 17 },
754
+ * { key: 'meta.created', comparator: 'datebetween', value: '2026-01-01', value2: '2026-06-30' },
755
+ * ],
688
756
  * },
689
- * filter:{
690
- * operator:'and',
691
- * filters:[{
692
- * key:'age',
693
- * comparator:'>',
694
- * value:5,
695
- * }],
696
- * },
697
- * })
757
+ * sort: { key: 'lastName', direction: 'asc', type: 'string' },
758
+ * page: { size: 100, index: 1 },
759
+ * });
760
+ *
761
+ * // Walk every page
762
+ * let index = 1, rows = [];
763
+ * while (true) {
764
+ * const result = await sdk.content.list('event', { select: ['title', 'startDate'], page: { size: 500, index } });
765
+ * rows.push(...result.items);
766
+ * if (index >= result.page.total) break;
767
+ * index++;
768
+ * }
698
769
  */
699
770
 
700
771
  service.list = async function (type, options, advanced) {
@@ -730,30 +801,44 @@ export default function (qik) {
730
801
  ///////////////////////////////////////////////////
731
802
 
732
803
  /**
733
- *
734
- * Create an item
804
+ * Creates a record (POST /content/:type/create). The input is validated against the type's fields;
805
+ * a missing required field or a wrong value rejects with a 400 explaining which.
806
+ * Built-in fields go at the top level, an organisation definition's own fields go under `data`,
807
+ * and `meta.scopes` says which scopes (permission groups) the record belongs to — the user needs
808
+ * create permission in them. Objects in meta.scopes are reduced to their ids in the request; the input you pass in is left unchanged.
809
+ * To use the endpoint's query options (e.g. ?skipGetContent=true for a smaller response), call sdk.api.post directly.
735
810
  * @alias content.create
736
- * @param {String} type The type or definition of the record we want to create
737
- * @param {Object} input The data for our new record
811
+ * @param {String} type The type key or definition key of the new record, e.g. 'profile', 'article' or an organisation's definition key (see content.glossary()).
812
+ * @param {Object} input The new record's fields.
813
+ * @param {Object} [input.data] Values for the definition's own fields, keyed by field key.
814
+ * @param {Object} [input.meta] Record settings. The ones most often set: scopes (array of scope ids) and status ('active', the default, or 'inactive').
815
+ * @param {Array<String>} [input.tags] Tag names to attach; tags that don't exist yet are created.
816
+ * @returns {Promise<Object>} The created record, including its new `_id`, as the creator is allowed to see it.
738
817
  * @example
818
+ * const profile = await sdk.content.create('profile', {
819
+ * firstName: 'Mickey',
820
+ * lastName: 'Mouse',
821
+ * emails: ['mickey@example.com'],
822
+ * meta: { scopes: ['61eca4746971e75c1fc670cf'] },
823
+ * });
739
824
  *
740
- *
741
- * const result = await sdk.content.create('profile', {
742
- * firstName:'Mickey',
743
- * lastName:'Mouse',
744
- * gender:'male',
745
- * meta:{
746
- * scopes:['61eca4746971e75c1fc670cf'],
747
- * }
748
- * })
825
+ * const application = await sdk.content.create('jobApplication', {
826
+ * title: 'Mickey Mouse',
827
+ * data: { role: 'Receptionist', startDate: '2026-10-01' },
828
+ * meta: { scopes: ['61eca4746971e75c1fc670cf'] },
829
+ * });
749
830
  */
750
831
 
751
832
  service.create = async function (type, input) {
752
833
  const dataModel = { ...input };
753
834
 
754
- // Sanitize down to just ids before we send
835
+ // Sanitize down to just ids before we send. Copy meta rather than
836
+ // changing it in place, so the caller's own object keeps its scopes.
755
837
  if (dataModel.meta?.scopes) {
756
- dataModel.meta.scopes = qik.utils.ids(dataModel.meta.scopes);
838
+ dataModel.meta = {
839
+ ...dataModel.meta,
840
+ scopes: qik.utils.ids(dataModel.meta.scopes),
841
+ };
757
842
  }
758
843
 
759
844
  const { data } = await qik.api.post(`/content/${type}/create`, dataModel);
@@ -761,30 +846,35 @@ export default function (qik) {
761
846
  };
762
847
 
763
848
  /**
764
- *
765
- * Update an item, Only fields the user has permission to view will be returned
849
+ * Replaces a record with the input (PUT /content/:id). This is a full replacement, not a merge:
850
+ * top-level fields left out of the input are cleared and `data` is replaced as a whole, so send the
851
+ * complete record (for example what content.get returned, with your changes applied). Use content.patch
852
+ * to change only some fields. If the user isn't allowed to edit some of the record's fields, the server
853
+ * switches to a merge so those fields are kept. Objects in meta.scopes are reduced to their ids in the request; the input you pass in is left unchanged.
854
+ * To use the endpoint's query options (e.g. ?expectedUhash=... to reject the write with a 409 if someone else
855
+ * changed the record first), call sdk.api.put directly.
766
856
  * @alias content.update
767
- * @param {String} id The id of the record we want to update
768
- * @param {Object} input The data to update
857
+ * @param {String|Object} id The record's id, or an object with an _id.
858
+ * @param {Object} input The complete record.
859
+ * @returns {Promise<Object>} The updated record, showing only the fields the user is allowed to see.
769
860
  * @example
770
- *
771
- * const result = await sdk.content.update('61eca4746971e75c1fc670cd', {
772
- * firstName:'Minnie',
773
- * lastName:'Mouse',
774
- * gender:'female',
775
- * meta:{
776
- * scopes:['61eca4746971e75c1fc670cd'],
777
- * }
778
- * })
861
+ * const profile = await sdk.content.get('61eca4746971e75c1fc670cd');
862
+ * profile.firstName = 'Minnie';
863
+ * profile.data = { ...profile.data, shirtSize: 'M' };
864
+ * const updated = await sdk.content.update(profile._id, profile);
779
865
  */
780
866
  service.update = async function (id, input) {
781
867
  id = qik.utils.id(id);
782
868
 
783
869
  const dataModel = { ...input };
784
870
 
785
- // Sanitize down to just ids before we send
871
+ // Sanitize down to just ids before we send. Copy meta rather than
872
+ // changing it in place, so the caller's own object keeps its scopes.
786
873
  if (dataModel.meta?.scopes) {
787
- dataModel.meta.scopes = qik.utils.ids(dataModel.meta.scopes);
874
+ dataModel.meta = {
875
+ ...dataModel.meta,
876
+ scopes: qik.utils.ids(dataModel.meta.scopes),
877
+ };
788
878
  }
789
879
 
790
880
  const { data } = await qik.api.put(`/content/${id}`, dataModel);
@@ -792,26 +882,35 @@ export default function (qik) {
792
882
  };
793
883
 
794
884
  /**
795
- *
796
- * Partially update and patch an item, Only fields the user has permission to edit will be updated
885
+ * Changes only the fields you send (PATCH /content/:id); everything else is kept. How values combine:
886
+ * a top-level field you send replaces the current value, arrays included (sending `emails` replaces the whole list);
887
+ * `data` is merged key by key, deeply; an array inside `data` is COMBINED with the existing one (new items
888
+ * are added, nothing is removed; an object with the same _id, id, title or name as an existing one replaces it). To remove items from a data array, either send the whole record with
889
+ * content.update, or call sdk.api.patch(`/content/${id}?mergeReplaceArrays=true`, input) so arrays are replaced.
890
+ * Within `meta` only some settings can be changed this way (scopes, status, security, slug, keywords, owners and a few more).
891
+ * Fields the user isn't allowed to edit are left unchanged. Objects in meta.scopes are reduced to their ids in the request; the input you pass in is left unchanged.
797
892
  * @alias content.patch
798
- * @param {String} id The id of the record we want to update
799
- * @param {Object} input The data to update, this will be merged with existing data
893
+ * @param {String|Object} id The record's id, or an object with an _id.
894
+ * @param {Object} input The fields to change.
895
+ * @returns {Promise<Object>} The updated record, showing only the fields the user is allowed to see.
800
896
  * @example
801
- *
802
- * const result = await sdk.content.patch('61eca4746971e75c1fc670cd', {
803
- * firstName:'Mickey',
804
- * gender:'male',
805
- * })
897
+ * const updated = await sdk.content.patch('61eca4746971e75c1fc670cd', {
898
+ * firstName: 'Mickey',
899
+ * data: { shirtSize: 'L', tags: ['mentor'] }, // 'mentor' is added to data.tags, other data fields are kept
900
+ * });
806
901
  */
807
902
  service.patch = async function (id, input) {
808
903
  id = qik.utils.id(id);
809
904
 
810
905
  const dataModel = { ...input };
811
906
 
812
- // Sanitize down to just ids before we send
907
+ // Sanitize down to just ids before we send. Copy meta rather than
908
+ // changing it in place, so the caller's own object keeps its scopes.
813
909
  if (dataModel.meta?.scopes) {
814
- dataModel.meta.scopes = qik.utils.ids(dataModel.meta.scopes);
910
+ dataModel.meta = {
911
+ ...dataModel.meta,
912
+ scopes: qik.utils.ids(dataModel.meta.scopes),
913
+ };
815
914
  }
816
915
 
817
916
  const { data } = await qik.api.patch(`/content/${id}`, dataModel);
@@ -819,13 +918,26 @@ export default function (qik) {
819
918
  };
820
919
 
821
920
  /**
822
- *
823
- * Get an item from the database, Only fields the user has permission to view will be returned
921
+ * Fetches one record by id (GET /content/:id). The type is worked out from the id, so no type is needed.
922
+ * Returns every field the user is allowed to see (the rest are removed), with reference fields populated.
923
+ * Rejects with 403 when the user may not view the record, 404 when it doesn't exist and 400 for an invalid id.
924
+ * To fetch many records use content.list with `select` rather than calling this in a loop.
824
925
  * @alias content.get
825
- * @param {String} id The id of the record we want to update
926
+ * @param {String|Object} id The record's id, or an object with an _id.
927
+ * @param {Object} [params] axios request config, passed unchanged to sdk.api.get. Query options for the server go inside its `params` property (see below).
928
+ * @param {Object} [params.params] Query string options for the server.
929
+ * @param {Array<String>} [params.params.select] Only return these fields (dot paths) plus _id. Give at least two keys: a single key is sent as a plain string, which the server doesn't read as a list.
930
+ * @param {String} [params.params.version] Id of an entry from the record's history (log) to return that earlier version instead of the current one. Needs edit access to the record (or the log.viewany permission).
931
+ * @param {String} [params.params.context] 'edit' to require edit permission instead of view permission.
932
+ * @param {Boolean} [params.params.unpopulated] Leave reference fields as ids instead of populating them.
933
+ * @returns {Promise<Object>} The record.
826
934
  * @example
935
+ * const profile = await sdk.content.get('61eca4746971e75c1fc670cd');
827
936
  *
828
- * const result = await sdk.content.get('61eca4746971e75c1fc670cd')
937
+ * // Only some fields
938
+ * const partial = await sdk.content.get('61eca4746971e75c1fc670cd', {
939
+ * params: { select: ['firstName', 'lastName', 'data.shirtSize'] },
940
+ * });
829
941
  */
830
942
  service.get = async function (id, params) {
831
943
  id = qik.utils.id(id);
@@ -835,17 +947,33 @@ export default function (qik) {
835
947
  return data;
836
948
  };
837
949
 
950
+ /**
951
+ * Same as content.get(id) with no options.
952
+ * @alias content.getFromID
953
+ * @param {String|Object} id The record's id, or an object with an _id.
954
+ * @returns {Promise<Object>} The record.
955
+ * @example
956
+ * const record = await sdk.content.getFromID('61eca4746971e75c1fc670cd');
957
+ */
838
958
  service.getFromID = async function (id) {
839
959
  return service.get(id);
840
960
  };
841
961
 
842
962
  /**
843
- *
844
- * Delete an item from the database
963
+ * Deletes a record (DELETE /content/:id). By default this moves it to the trash (meta.deleted = true),
964
+ * from where content.restore brings it back; trashed records are hidden from content.list unless
965
+ * `trash` or `includeTrash` is set. Needs delete permission on the record.
845
966
  * @alias content.delete
846
- * @param {String} id The id of the record we want to delete
967
+ * @param {String|Object} id The record's id, or an object with an _id.
968
+ * @param {Object} [input] axios request config, passed unchanged to sdk.api.delete. A request body goes in its `data` property.
969
+ * @param {Object} [input.data] Request body.
970
+ * @param {Boolean} [input.data.erase] Permanently erase the record instead of trashing it. Can't be undone; needs erase permission.
971
+ * @returns {Promise<Object>} For a normal delete: { _id, meta: { type, definition, deleted: true } }.
847
972
  * @example
848
- * const result = await sdk.content.delete('61eca4746971e75c1fc670cd')
973
+ * await sdk.content.delete('61eca4746971e75c1fc670cd');
974
+ *
975
+ * // Permanently erase
976
+ * await sdk.content.delete('61eca4746971e75c1fc670cd', { data: { erase: true } });
849
977
  */
850
978
  service.delete = async function (id, input) {
851
979
  id = qik.utils.id(id);
@@ -854,12 +982,14 @@ export default function (qik) {
854
982
  };
855
983
 
856
984
  /**
857
- *
858
- * Restore a deleted item from the database
985
+ * Brings a trashed record back (GET /content/:id/restore). Needs restore permission on the record.
986
+ * Erased records can't be restored.
859
987
  * @alias content.restore
860
- * @param {String} id The id of the record we want to restore
988
+ * @param {String|Object} id The record's id, or an object with an _id.
989
+ * @param {Object} [input] axios request config, passed unchanged to sdk.api.get.
990
+ * @returns {Promise<Object>} The restored record.
861
991
  * @example
862
- * const result = await sdk.content.restore('61eca4746971e75c1fc670cd')
992
+ * const restored = await sdk.content.restore('61eca4746971e75c1fc670cd');
863
993
  */
864
994
  service.restore = async function (id, input) {
865
995
  id = qik.utils.id(id);
@@ -868,16 +998,14 @@ export default function (qik) {
868
998
  };
869
999
 
870
1000
  /**
871
- *
872
- * Retrieve an item from the database by providing it's 'slug'
1001
+ * Fetches one record by its slug (GET /content/slug/:slug) instead of its id, with the same permission
1002
+ * rules as content.get. The slug is written as '<type or definition key>:<slug>', e.g. 'article:how-to-get-started';
1003
+ * a record's `meta.abs` holds this form.
873
1004
  * @alias content.getFromSlug
874
- * @param {String} slug The slug id of the record we want to retrieve
875
- * it must be provided as either `(type):(slug)`` or `(definition):(slug)`
876
- * (if unsure use the `meta.abs` absolute slug property of the item you are wanting to retrieve).
1005
+ * @param {String} slug '<type or definition key>:<the record's meta.slug>'.
1006
+ * @returns {Promise<Object>} The record.
877
1007
  * @example
878
- * const result = await sdk.content.getFromSlug('article:how-to-get-started')
879
- * const result = await sdk.content.getFromSlug('car:toyota-landcruiser')
880
- * const result = await sdk.content.getFromSlug('article:toyota-landcruiser')
1008
+ * const article = await sdk.content.getFromSlug('article:how-to-get-started');
881
1009
  */
882
1010
  service.getFromSlug = async function (slug) {
883
1011
  const { data } = await qik.api.get(`/content/slug/${slug}`);