@nomad-media/public 1.1.15 → 2026.9.2-beta.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.
@@ -1,3 +1,175 @@
1
+ /**
2
+ * @typedef {Object} SearchFilter
3
+ * @property {string} fieldName - The content field to filter on
4
+ * @property {string} operator - Comparison operator (e.g. "Equals", "Contains", "GreaterThan")
5
+ * @property {string|string[]} values - The value or array of values to match
6
+ */
7
+
8
+ /**
9
+ * @typedef {Object} SortField
10
+ * @property {string} fieldName - The field to sort on
11
+ * @property {string} sortType - Sort direction: "Ascending" or "Descending"
12
+ */
13
+
14
+ /**
15
+ * @typedef {Object} SearchResultField
16
+ * @property {string} name - Field name to include in results
17
+ * @property {SearchResultField[]} [SearchResultFields] - Nested sub-fields for object fields
18
+ */
19
+
20
+ /**
21
+ * @typedef {Object} TagRef
22
+ * @property {string} id - Tag ID
23
+ * @property {string} [description] - Tag description
24
+ */
25
+
26
+ /**
27
+ * @typedef {Object} CollectionRef
28
+ * @property {string} id - Collection ID
29
+ * @property {string} [description] - Collection description
30
+ */
31
+
32
+ /**
33
+ * @typedef {Object} RelatedContentRef
34
+ * @property {string} id - Related content ID
35
+ * @property {string} [description] - Description
36
+ */
37
+
38
+ /**
39
+ * @typedef {Object} AudioTrack
40
+ * @property {string} id - Audio track ID
41
+ * @property {string} [name] - Track name
42
+ * @property {string} [language] - Language code
43
+ */
44
+
45
+ /**
46
+ * @typedef {Object} ExternalOutputProfile
47
+ * @property {string} id - Output profile ID
48
+ * @property {string} [name] - Profile name
49
+ * @property {string} [profileType] - Profile type identifier
50
+ */
51
+
52
+ /**
53
+ * @typedef {Object} LiveInputSource
54
+ * @property {string} url - Source stream URL
55
+ * @property {string} [id] - Source ID
56
+ */
57
+
58
+ /**
59
+ * @typedef {Object} LiveInputDestination
60
+ * @property {string} url - Destination stream URL
61
+ * @property {string} [id] - Destination ID
62
+ * @property {string} [name] - Destination name
63
+ */
64
+
65
+ /**
66
+ * @typedef {Object} LiveOutputProfile
67
+ * @property {string} id - Output profile ID
68
+ * @property {string} [name] - Profile name
69
+ * @property {string} [type] - Profile type
70
+ */
71
+
72
+ /**
73
+ * @typedef {Object} MediaSource
74
+ * @property {string} assetId - The asset ID to include in the composition
75
+ * @property {string} startTimeCode - Start timecode (HH:MM:SS:FF)
76
+ * @property {string} endTimeCode - End timecode (HH:MM:SS:FF)
77
+ * @property {string} [id] - Source segment ID
78
+ */
79
+
80
+ /**
81
+ * @typedef {Object} MediaBuilderItem
82
+ * @property {string} sourceAssetId - The asset ID to use as the source for this media builder item
83
+ * @property {string} startTimeCode - Start timecode for the segment (HH:MM:SS:FF)
84
+ * @property {string} endTimeCode - End timecode for the segment (HH:MM:SS:FF)
85
+ */
86
+
87
+ /**
88
+ * @typedef {Object} Annotation
89
+ * @property {string} [id] - Annotation ID
90
+ * @property {string} startTimeCode - Start timecode for the annotation (HH:MM:SS:FF)
91
+ * @property {string} [endTimeCode] - End timecode for the annotation (HH:MM:SS:FF)
92
+ * @property {Object} properties - Annotation properties (e.g. { title: "..." })
93
+ */
94
+
95
+ /**
96
+ * @typedef {Object} ContentRef
97
+ * @property {string} id - Content ID
98
+ */
99
+
100
+ /**
101
+ * @typedef {Object} NomadUser
102
+ * @property {string} [id] - User ID
103
+ */
104
+
105
+ /**
106
+ * @typedef {Object} ExternalUser
107
+ * @property {string} email - External user email address
108
+ * @property {string} [id] - External user ID
109
+ * @property {string} [externalUserType] - Type of external user
110
+ */
111
+
112
+ /**
113
+ * @typedef {Object} ContentField
114
+ * @property {string} contentFieldId - Unique identifier of the content field
115
+ * @property {Object} properties - Field property definitions (title, type, etc.)
116
+ */
117
+
118
+ /**
119
+ * @typedef {Object} Transfer
120
+ * @property {string} id - The transfer id (UUID), which cancel, requeue and progress calls address
121
+ * @property {integer} direction - Which way the asset moves: 1 = Upload, 2 = Download
122
+ * @property {string} transferStatus - Lifecycle state: one of "Queued", "Downloading", "Uploading", "Error", "Complete", "Cancelled"
123
+ * @property {string} [displayName] - A human readable label, normally the file name
124
+ * @property {number} [completePercentage] - Percent complete, 0 to 100, when known
125
+ * @property {integer} bytesTransferred - Bytes moved so far
126
+ * @property {integer} bytesTotal - Total size of the transfer, or 0 when not known up front
127
+ * @property {integer} attemptCount - How many times this transfer has been attempted
128
+ * @property {string} [errorMessage] - The failure reason. Only populated for a failed transfer
129
+ * @property {string} createdDate - When the record was created (ISO 8601)
130
+ * @property {string} [queueDate] - When the transfer was queued (ISO 8601)
131
+ * @property {string} [startTransferDate] - When bytes first started moving (ISO 8601)
132
+ * @property {string} [completeTransferDate] - When the transfer finished successfully (ISO 8601)
133
+ * @property {string} [cancelledDate] - When the transfer was cancelled (ISO 8601)
134
+ * @property {string} [errorDate] - When the transfer failed (ISO 8601)
135
+ * @property {string} [lastProgressDate] - When progress was last recorded (ISO 8601)
136
+ * @property {string} [assetId] - The asset this transfer concerns (UUID), in either direction
137
+ * @property {boolean} [downloadProxy] - Whether a download is taking the proxy rendition rather than the original. Download only
138
+ * @property {string} [relatedAssetId] - The asset this upload is a related file of (UUID). Upload only
139
+ * @property {string} [displayObjectKey] - The destination object key as shown to a user. Upload only
140
+ * @property {string} [assetName] - The display name of the asset. Download only
141
+ * @property {string} [assetUrl] - The internal URL of the asset in `{bucketName}::{objectKey}` format. Download only
142
+ * @property {string} [fullPreviewUrl] - A signed URL of the asset's thumbnail image, when one is available
143
+ */
144
+
145
+ /**
146
+ * @typedef {Object} UploadQueueItem
147
+ * @property {string} fileName - The file name, including its extension
148
+ * @property {integer} fileSize - The file size in bytes
149
+ * @property {string} [mimeType] - The MIME type, when known; otherwise inferred from the file name
150
+ * @property {string} [destinationFolderId] - The destination folder (UUID); omit for the configured default upload folder
151
+ * @property {integer} chunkSize - The multipart part size in bytes, e.g. 8388608
152
+ * @property {string} [uploadOverwriteOption] - How to treat an existing file: "continue", "replace" (default) or "cancel"
153
+ */
154
+
155
+ /**
156
+ * @typedef {Object} TransferList
157
+ * @property {Array<Transfer>} items - The page of transfers
158
+ * @property {integer} totalItemCount - The number of transfers matching the filters; approximate for very large histories
159
+ * @property {string} [nextPageOffset] - The opaque cursor to pass back as `cursor` for the next page. A null value means the end has been reached
160
+ * @property {boolean} hasItems - Whether the items collection contains any items
161
+ * @property {string} [message] - An optional message providing additional information about the result
162
+ */
163
+
164
+ /**
165
+ * @typedef {Object} TransferSummary
166
+ * @property {Object} [uploadCounts] - Per-status counts for uploads, keyed by status name (Queued, Downloading, Uploading, Error, Complete, Cancelled)
167
+ * @property {Object} [downloadCounts] - Per-status counts for downloads, keyed by status name
168
+ * @property {integer} activeCount - Number of transfers that have not reached a terminal state
169
+ * @property {integer} errorCount - Number of transfers that ended in failure
170
+ * @property {string} [changeToken] - A token that changes whenever any counted value changes; usable for If-None-Match polling
171
+ */
172
+
1
173
  // account
2
174
 
3
175
 
@@ -135,6 +307,10 @@
135
307
 
136
308
 
137
309
 
310
+
311
+
312
+
313
+
138
314
 
139
315
 
140
316
 
@@ -187,6 +363,20 @@
187
363
 
188
364
 
189
365
 
366
+
367
+
368
+
369
+
370
+
371
+
372
+
373
+
374
+
375
+
376
+
377
+
378
+
379
+
190
380
 
191
381
 
192
382
 
@@ -276,6 +466,10 @@
276
466
 
277
467
 
278
468
 
469
+ import axios from "axios";
470
+
471
+
472
+
279
473
  /**
280
474
  * @class NomadSDK
281
475
  * @classdesc This class is used to interact with the Nomad API.
@@ -283,11 +477,16 @@
283
477
 
284
478
  class NomadMediaSDK {
285
479
  constructor(config = {}) {
286
- if (NomadMediaSDK.instance)
480
+ if (NomadMediaSDK.instance)
287
481
  {
288
482
  return NomadMediaSDK.instance;
289
483
  }
290
484
 
485
+ if (!config.serviceApiUrl || typeof config.serviceApiUrl !== "string")
486
+ {
487
+ throw new MissingCredentialsException(["serviceApiUrl"]);
488
+ }
489
+
291
490
  this.config = config;
292
491
  this.token = null;
293
492
  this.refreshTokenVal = null;
@@ -296,42 +495,104 @@ class NomadMediaSDK {
296
495
  this.id = null
297
496
  this.debug = config.debugMode || false;
298
497
 
498
+ // SSO login params
499
+ this.ssoProvider = this.config.ssoProvider || null;
500
+ this.ssoCode = this.config.ssoCode || null;
501
+ this.ssoState = this.config.ssoState || null;
502
+ this.ssoSessionState = this.config.ssoSessionState || null;
503
+ this.ssoRedirectUrl = this.config.ssoRedirectUrl || null;
504
+
299
505
  if (this.config.serviceApiUrl[this.config.serviceApiUrl.length - 1] === "/")
300
506
  {
301
507
  this.config.serviceApiUrl = this.config.serviceApiUrl.slice(0, -1);
302
508
  }
303
509
 
510
+ if (this.ssoProvider || this.ssoCode || this.ssoState || this.ssoSessionState || this.ssoRedirectUrl)
511
+ {
512
+ const missingCredentials = [];
513
+ const hasRedirectUrl = !!this.ssoRedirectUrl;
514
+ const hasAllFields = this.ssoProvider && this.ssoCode && this.ssoState && this.ssoSessionState;
515
+ if (!hasRedirectUrl && !hasAllFields) {
516
+ if (!this.ssoProvider) missingCredentials.push("ssoProvider");
517
+ if (!this.ssoCode) missingCredentials.push("ssoCode");
518
+ if (!this.ssoState) missingCredentials.push("ssoState");
519
+ if (!this.ssoSessionState) missingCredentials.push("ssoSessionState");
520
+
521
+ throw new MissingCredentialsException(missingCredentials, "SSO login requires either ssoRedirectUrl or all of ssoProvider, ssoCode, ssoState, ssoSessionState");
522
+ }
523
+ }
524
+ else if (!this.config.apiKey)
525
+ {
526
+ const missingCredentials = [];
527
+ if (!this.config.username) missingCredentials.push("username");
528
+ if (!this.config.password && !this.config.apiKey) missingCredentials.push("password");
529
+
530
+ if (missingCredentials.length > 0) {
531
+ throw new MissingCredentialsException(missingCredentials);
532
+ }
533
+ }
534
+
304
535
  if (this.config.singleton) NomadMediaSDK.instance = this;
305
-
536
+
306
537
  return this;
307
- }
538
+ }
308
539
 
309
540
  /**
310
541
  * @function init
311
542
  * @async
312
543
  * @private
313
- * @description Initializes the SDK. Logs in using the username and password from the config.
544
+ * @description Initializes the SDK. Logs in using the username and password from the config.
314
545
  * Then schedules a token refresh for the duration set by the value returned in the login.
315
546
  * @returns {Promise<void>} - A promise that resolves when the SDK is initialized.
316
547
  * @throws {Error} - An error is thrown if the SDK fails to initialize.
317
548
  */
318
- async _init()
549
+ async _init()
550
+ {
551
+ // Calls made at the same time on a fresh SDK share one login instead of each starting their own.
552
+ if (!this._initPromise)
553
+ {
554
+ this._initPromise = this._runInit().finally(() => { this._initPromise = null; });
555
+ }
556
+ return this._initPromise;
557
+ }
558
+
559
+ async _runInit()
319
560
  {
320
561
  if (this.config.username && this.config.password)
321
562
  {
563
+ if (!this.config.serviceApiUrl)
564
+ {
565
+ throw new MissingCredentialsException(["serviceApiUrl"]);
566
+ }
322
567
  await this.login();
323
568
  }
569
+ else if (this.ssoProvider || this.ssoCode || this.ssoState || this.ssoSessionState || this.ssoRedirectUrl)
570
+ {
571
+ await this.authenticateWithSSO();
572
+ }
324
573
  }
325
-
574
+
326
575
  // account functions
327
576
  /**
328
577
  * @function login
329
578
  * @async
330
- * @description Logs in using the username and password from the config.
331
- * @returns {Promise<void>} - A promise that resolves when the login is successful.
332
- * Updates the token, refresh token, expiration seconds, and user session ID class variables.
333
- * @throws {Error} - An error is thrown if the login fails.
334
- */
579
+ * @description Authenticate a user and obtain a JWT access token.
580
+ * Validates the user's credentials and, if successful, creates a new session and returns a JWT
581
+ * access token with a refresh token. Accounts that are disabled, expired, pending invitation,
582
+ * or pending approval will be rejected. Guest users must have a valid profile and permitted
583
+ * application access.
584
+ * @returns {Promise<Object>} Authentication response. User-identity fields (from the backend `AccountLoginResponseModel`): `id`, `userName`, `firstName`, `lastName`, `loginStatus`, `cookies`. Session/token fields consumed by the SDK: `token`, `refreshToken`, `expirationSeconds`, `userSessionId`. Note: there is no `userId` or `expiresIn` field; use `id` and `expirationSeconds` respectively.
585
+ * @throws {Error} 400 - The request body is null or missing required fields (UserName, Password)
586
+ * @throws {Error} 404 - Authentication failed — invalid credentials or user not found
587
+ * @throws {Error} 403 - User account is Disabled, Expired, PendingInvitation, or PendingApproval
588
+ * @example
589
+ * const response = await nomadSDK.login();
590
+ * console.log('Access Token:', response.token);
591
+ * console.log('User ID:', response.id);
592
+ * console.log('User Name:', response.userName);
593
+ *
594
+ * @see {@link https://docs.nomad-cms.com/api/account/login Admin API} POST /account/login
595
+ */
335
596
  async login()
336
597
  {
337
598
 
@@ -339,17 +600,39 @@ class NomadMediaSDK {
339
600
 
340
601
  try
341
602
  {
342
- const LOGIN_INFO = await _login(this);
343
- if (!LOGIN_INFO) throw new Error("Login failed");
344
-
603
+ // _login()'s underlying _sendRequest() now throws on any
604
+ // failure (e.g. a 409 "Invalid username or password") rather
605
+ // than resolving to a falsy value — but this method's own
606
+ // documented contract is to throw the specific, public
607
+ // LoginException on any login failure, not whatever generic
608
+ // error the transport layer happens to throw. Any failure here
609
+ // is treated as a login failure, matching this method's
610
+ // original behavior when _login() resolved to a falsy value
611
+ // for the same case.
612
+ let LOGIN_INFO;
613
+ let LOGIN_ERROR;
614
+ try
615
+ {
616
+ LOGIN_INFO = await _login(this);
617
+ }
618
+ catch (error)
619
+ {
620
+ LOGIN_INFO = null;
621
+ LOGIN_ERROR = error;
622
+ }
623
+ // The original error (network failure, 403 disabled account, ...) is kept
624
+ // on `.cause`, with its HTTP status on `.status`.
625
+ if (!LOGIN_INFO) throw new LoginException(undefined, LOGIN_ERROR);
626
+
345
627
  this.token = LOGIN_INFO["token"];
346
628
  this.refreshTokenVal = LOGIN_INFO["refreshToken"];
347
629
  this.expirationSeconds = LOGIN_INFO["expirationSeconds"];
348
630
  this.userSessionId = LOGIN_INFO["userSessionId"];
349
631
  this.id = LOGIN_INFO["id"];
350
632
  _printDatetime(`Login successful`);
633
+ return LOGIN_INFO;
351
634
  }
352
- catch (error)
635
+ catch (error)
353
636
  {
354
637
  _printDatetime(`Login failed`);
355
638
  throw error;
@@ -359,9 +642,16 @@ class NomadMediaSDK {
359
642
  /**
360
643
  * @function logout
361
644
  * @async
362
- * @description Logs out the current user.
363
- * @returns {Promise<null>} - A promise that resolves when the logout is successful.
364
- * @throws {Error} - An error is thrown if the logout fails.
645
+ * @description Log out the currently authenticated user and end their session.
646
+ * Ends the user's active session and invalidates the authentication token. If a UserSessionId
647
+ * is provided, that specific session is terminated.
648
+ * @returns {Promise<Object>} Confirmation that the user was successfully logged out and the session was deleted
649
+ * @throws {Error} 401 - The caller is not authenticated
650
+ * @example
651
+ * await nomadSDK.logout();
652
+ * console.log('User logged out successfully');
653
+ *
654
+ * @see {@link https://docs.nomad-cms.com/api/account/logout Admin API} POST /account/logout
365
655
  */
366
656
  async logout()
367
657
  {
@@ -374,14 +664,15 @@ class NomadMediaSDK {
374
664
 
375
665
  try
376
666
  {
377
- await _logout(this);
667
+ const RESULT = await _logout(this);
378
668
  this.token = null;
379
669
  this.refreshTokenVal = null;
380
670
  this.expirationSeconds = null;
381
671
  this.userSessionId = null;
382
672
  this.id = null;
383
-
673
+
384
674
  _printDatetime(`Logout successful`);
675
+ return RESULT;
385
676
  }
386
677
  catch (error)
387
678
  {
@@ -393,11 +684,20 @@ class NomadMediaSDK {
393
684
  /**
394
685
  * @function refreshToken
395
686
  * @async
396
- * @description Refreshes the current token.
397
- * @returns {Promise<void>} - A promise that resolves when the token is refreshed.
398
- * Updates the token class variable.
399
- * @throws {Error} - An error is thrown if the token refresh fails.
400
- */
687
+ * @description Retrieve a new JWT access token using a refresh token.
688
+ * Exchanges a valid refresh token for a new JWT access token. Automatically updates the SDK
689
+ * instance's token property on success.
690
+ * @returns {Promise<boolean>} True if the token was successfully refreshed, false otherwise
691
+ * @throws {Error} 400 - The request body is null or RefreshToken is missing
692
+ * @throws {Error} 404 - The refresh token is invalid or has expired
693
+ * @example
694
+ * const success = await nomadSDK.refreshToken();
695
+ * if (success) {
696
+ * console.log('Token refreshed successfully');
697
+ * }
698
+ *
699
+ * @see {@link https://docs.nomad-cms.com/api/account/refresh-token Admin API} POST /account/refresh-token
700
+ */
401
701
  async refreshToken()
402
702
  {
403
703
  _printDatetime(`Refreshing token`);
@@ -411,6 +711,7 @@ class NomadMediaSDK {
411
711
  {
412
712
  const REFRESHED = await _refreshToken(this);
413
713
  if (REFRESHED) _printDatetime(`Token refresh successful`);
714
+ return REFRESHED;
414
715
  }
415
716
  catch (error)
416
717
  {
@@ -419,15 +720,59 @@ class NomadMediaSDK {
419
720
  }
420
721
  }
421
722
 
723
+ /**
724
+ * @function authenticateWithSSO
725
+ * @async
726
+ * @description Authenticates the user using SSO.
727
+ * @throws {LoginException} - If SSO authentication fails.
728
+ */
729
+ async authenticateWithSSO()
730
+ {
731
+ if (!this.ssoRedirectUrl)
732
+ {
733
+ this.ssoRedirectUrl = `${this.config.serviceApiUrl}/api/auth/${this.ssoProvider}/callback?code=${this.ssoCode}&state=${this.ssoState}&session_state=${this.ssoSessionState}`;
734
+ }
735
+
736
+ try
737
+ {
738
+ const response = await axios.get(this.ssoRedirectUrl, {
739
+ maxRedirects: 0,
740
+ timeout: 10000
741
+ });
742
+
743
+ throw new LoginException(`SSO authentication failed with status code: ${response.status}`);
744
+ }
745
+ catch (error)
746
+ {
747
+ if (error.response && error.response.status === 302) {
748
+ const location = error.response.headers['location'];
749
+ if (!location) {
750
+ throw new LoginException('SSO authentication failed: Missing location header in redirect.');
751
+ }
752
+ this.token = location.split("azurePortal_token=")[1]?.split("&")[0] || null;
753
+ this.refreshTokenVal = location.split("azurePortal_refresh_token=")[1]?.split("&")[0] || null;
754
+ this.expirationSeconds = location.split("expires=")[1]?.split("&")[0] || null;
755
+ return;
756
+ }
757
+ throw new LoginException(`SSO config parameters are invalid: ${error.message}`);
758
+ }
759
+ }
760
+
422
761
  /**
423
762
  * @function forgotPassword
424
763
  * @async
425
- * @description Sends a code to the email to the user to be used
426
- * be used to reset the password.
427
- * @returns {Promise<void>}
428
- * Sends email with code to reset password.
429
- * @throws {Error} - An error is thrown if the code fails to send.
430
- */
764
+ * @description Trigger a password reset code to be sent to the user's email address.
765
+ * Initiates the password reset flow for the specified user account. If the account is active, a
766
+ * 6-digit verification code is sent to the user's email address. The code should then be
767
+ * submitted to the reset-password endpoint along with the new password.
768
+ * @returns {Promise<Object>} Response indicating the reset code was sent, or a pending account state
769
+ * @throws {Error} 400 - The request is null or UserName is missing
770
+ * @example
771
+ * const result = await nomadSDK.forgotPassword();
772
+ * console.log('Password reset code sent to email');
773
+ *
774
+ * @see {@link https://docs.nomad-cms.com/api/account/forgot-password Admin API} POST /account/forgot-password
775
+ */
431
776
  async forgotPassword()
432
777
  {
433
778
  const USERNAME = this.config.username;
@@ -436,8 +781,9 @@ class NomadMediaSDK {
436
781
 
437
782
  try
438
783
  {
439
- await _forgotPassword(this);
784
+ const RESULT = await _forgotPassword(this);
440
785
  _printDatetime(`Code sent to ${USERNAME}`);
786
+ return RESULT;
441
787
  }
442
788
  catch (error)
443
789
  {
@@ -449,14 +795,20 @@ class NomadMediaSDK {
449
795
  /**
450
796
  * @function resetPassword
451
797
  * @async
452
- * @description Resets the password for the specified username.
453
- * @param {string} CODE - The code to use to reset the password. Sent to the user's email
454
- * by calling forgotPassword().
455
- * @param {string} NEW_PASSWORD - The new password to set.
456
- * @returns {Promise<void>}
457
- * Resets the password for the specified username.
458
- * @throws {Error} - An error is thrown if the password fails to reset.
459
- */
798
+ * @description Reset the user's password using a verification code from the forgot-password email.
799
+ * Completes the password reset flow by validating the verification code from the
800
+ * forgot-password email and setting the new password. For accounts pending approval, this also
801
+ * completes the email verification step.
802
+ * @param {string} CODE - The 6-digit verification code from the forgot-password email
803
+ * @param {string} NEW_PASSWORD - The new password to set for the user
804
+ * @returns {Promise<Object>} Response confirming the password was successfully reset
805
+ * @throws {Error} 400 - The request is null, UserName is missing, reset token is invalid/expired, or user is disabled
806
+ * @example
807
+ * const result = await nomadSDK.resetPassword('123456', 'newSecurePassword!');
808
+ * console.log('Password reset successfully');
809
+ *
810
+ * @see {@link https://docs.nomad-cms.com/api/account/reset-password Admin API} POST /account/reset-password
811
+ */
460
812
  async resetPassword(CODE, NEW_PASSWORD)
461
813
  {
462
814
  const USERNAME = this.config.username;
@@ -465,8 +817,9 @@ class NomadMediaSDK {
465
817
 
466
818
  try
467
819
  {
468
- await _resetPassword(this, CODE, NEW_PASSWORD);
820
+ const RESULT = await _resetPassword(this, CODE, NEW_PASSWORD);
469
821
  _printDatetime(`Password reset for ${USERNAME}`);
822
+ return RESULT;
470
823
  }
471
824
  catch (error)
472
825
  {
@@ -479,14 +832,16 @@ class NomadMediaSDK {
479
832
  /**
480
833
  * @function clearContinueWatching
481
834
  * @async
482
- * @description Delete continue watching markers.
483
- * @param {string | null} USER_ID - The user ID of the user to clear the continue watching list.
484
- * If not user Id is passed it clears the markers of the logged in user.
485
- * @param {string | null} ASSET_ID - The asset ID of the asset to clear the continue watching
486
- * list. If not asset Id is passed it clears the markers of all assets.
487
- * @returns {Promise<void>} - A promise that resolves when the continue watching list is cleared.
488
- * @throws {Error} - An error is thrown if the continue watching list fails to clear.
489
- * @throws {Error} - An error is thrown if the API type is not portal.
835
+ * @description Clear a "continue watching" entry for a user.
836
+ * Removes a specific asset from the user's continue watching list.
837
+ * @param {string} USER_ID - The unique identifier of the user
838
+ * @param {string} [ASSET_ID] - The asset ID to remove from continue watching
839
+ * @returns {Promise<Object>} Response confirming the clearing
840
+ * @throws {Error} 401 - The caller is not authenticated
841
+ * @example
842
+ * await nomadSDK.clearContinueWatching("user-123", "asset-456");
843
+ *
844
+ * @see {@link https://docs.nomad-cms.com/docs/api-guide/media-api/clear-continue-watching Portal API} POST /media/clear-watching
490
845
  */
491
846
  async clearContinueWatching(USER_ID, ASSET_ID)
492
847
  {
@@ -495,7 +850,7 @@ class NomadMediaSDK {
495
850
  await this._init();
496
851
  }
497
852
 
498
- if (this.config.apiType !== "portal")
853
+ if (this.config.apiType !== "portal")
499
854
  {
500
855
  throw new Error("This function is only available for portal API type.");
501
856
  }
@@ -504,8 +859,9 @@ class NomadMediaSDK {
504
859
 
505
860
  try
506
861
  {
507
- await _clearContinueWatching(this, USER_ID, ASSET_ID);
862
+ const RESULT = await _clearContinueWatching(this, USER_ID, ASSET_ID);
508
863
  _printDatetime(`Continue watching cleared`);
864
+ return RESULT;
509
865
  }
510
866
  catch (error)
511
867
  {
@@ -517,12 +873,15 @@ class NomadMediaSDK {
517
873
  /**
518
874
  * @function clearWatchlist
519
875
  * @async
520
- * @description Clears the watchlist.
521
- * @param {string | null} USER_ID - The user ID of the user to clear the watchlist.
522
- * If not user Id is passed it clears the watchlist of the logged in user.
523
- * @returns {Promise<void>} - A promise that resolves when the watchlist is cleared.
524
- * @throws {Error} - An error is thrown if the watchlist fails to clear.
525
- * @throws {Error} - An error is thrown if the API type is not portal.
876
+ * @description Clear the user's entire watchlist.
877
+ * Removes all items from the specified user's watchlist.
878
+ * @param {string} USER_ID - The unique identifier of the user whose watchlist to clear
879
+ * @returns {Promise<Object>} Response confirming the watchlist was cleared
880
+ * @throws {Error} 401 - The caller is not authenticated
881
+ * @example
882
+ * await nomadSDK.clearWatchlist("user-123");
883
+ *
884
+ * @see {@link https://docs.nomad-cms.com/docs/api-guide/media-api/clear-watchlist Portal API} POST /media/clear-watchlist
526
885
  */
527
886
  async clearWatchlist(USER_ID)
528
887
  {
@@ -532,7 +891,7 @@ class NomadMediaSDK {
532
891
  }
533
892
 
534
893
  if (this.config.apiType !== "portal")
535
- {
894
+ {
536
895
  throw new Error("This function is only available for portal API type.");
537
896
  }
538
897
 
@@ -542,8 +901,9 @@ class NomadMediaSDK {
542
901
  {
543
902
  USER_ID = USER_ID || this.id;
544
903
 
545
- await _clearWatchlist(this, USER_ID);
904
+ const RESULT = await _clearWatchlist(this, USER_ID);
546
905
  _printDatetime(`Watchlist cleared`);
906
+ return RESULT;
547
907
  }
548
908
  catch (error)
549
909
  {
@@ -555,16 +915,20 @@ class NomadMediaSDK {
555
915
  /**
556
916
  * @function createForm
557
917
  * @async
558
- * @description Gets media.
559
- * @param {string} CONTENT_DEFINITION_ID - The ID of the content definition the form
560
- * is going in.
561
- * @param {JSON} FORM_INFO - The information of the form.
562
- * JSON format: whatever key value pairs you want to add to the form.
563
- * @returns {Promise<JSON>} - A promise that resolves when the from is created.
564
- * Returns the id of the form.
565
- * @throws {Error} - An error is thrown if the media fails to get.
566
- * @throws {Error} - An error is thrown if the API type is not portal.
567
- */
918
+ * @description Create a form submission for a content definition.
919
+ * Submits form data to create a new entry for the specified content definition. Used for
920
+ * user-generated content submissions through portal forms.
921
+ * @param {string} CONTENT_DEFINITION_ID - The content definition ID for the form type
922
+ * @param {Object} FORM_INFO - The form data to submit
923
+ * @returns {Promise<Object>} The created content entry
924
+ * @throws {Error} 401 - The caller is not authenticated
925
+ * @example
926
+ * const entry = await nomadSDK.createForm("def-123", {
927
+ * title: "My Submission", description: "Description"
928
+ * });
929
+ *
930
+ * @see {@link https://docs.nomad-cms.com/docs/api-guide/media-api/create-form Portal API} POST /media/form/{contentDefinitionId}
931
+ */
568
932
  async createForm(CONTENT_DEFINITION_ID, FORM_INFO)
569
933
  {
570
934
  if (this.token === null && this.config.username)
@@ -595,14 +959,17 @@ class NomadMediaSDK {
595
959
  /**
596
960
  * @function getContentCookies
597
961
  * @async
598
- * @description Gets content cookies.
599
- * @param {string} CONTENT_ID - The Id of the content to retrieve the cookies for.
600
- * This can be the ID for the content definition of the LiveChannel, or a folder asset
601
- * ID or a specific Asset ID.
602
- * @returns {Promise<JSON>} - A promise that resolves when the content cookies are gotten.
603
- * Returns the information of the gotten content cookies.
604
- * @throws {Error} - An error is thrown if the content cookies fail to get.
605
- * @throws {Error} - An error is thrown if the API type is not portal.
962
+ * @description Retrieve secure streaming cookies for portal media content.
963
+ * Gets signed cookies required for secure streaming of media content through the portal API.
964
+ * The cookies authorize CDN access to the media files.
965
+ * @param {string} CONTENT_ID - The unique identifier of the media content
966
+ * @returns {Promise<Object>} Streaming cookies and manifest URL for the content
967
+ * @throws {Error} 401 - The caller is not authenticated
968
+ * @throws {Error} 404 - The content was not found
969
+ * @example
970
+ * const cookies = await nomadSDK.getContentCookies("media-123");
971
+ *
972
+ * @see {@link https://docs.nomad-cms.com/docs/api-guide/asset-api-1 Portal API} GET /media/set-cookies/{id}
606
973
  */
607
974
  async getContentCookies(CONTENT_ID)
608
975
  {
@@ -634,11 +1001,15 @@ class NomadMediaSDK {
634
1001
  /**
635
1002
  * @function getDefaultSiteConfig
636
1003
  * @async
637
- * @description Gets default site config.
638
- * @returns {Promise<Array<JSON>>} - A promise that resolves when the default site config is gotten.
639
- * Returns the information of the gotten dynamic content.
640
- * @throws {Error} - An error is thrown if the dynamic content fails to get.
641
- * @throws {Error} - An error is thrown if the API type is not portal.
1004
+ * @description Retrieve the default site configuration.
1005
+ * Gets the default portal site configuration used when no specific configuration ID is
1006
+ * provided.
1007
+ * @returns {Promise<Object>} The default site configuration object
1008
+ * @throws {Error} 401 - The caller is not authenticated
1009
+ * @example
1010
+ * const config = await nomadSDK.getDefaultSiteConfig();
1011
+ *
1012
+ * @see {@link https://docs.nomad-cms.com/docs/api-guide/media-api/get-default-site-config Portal API} GET /media/config
642
1013
  */
643
1014
  async getDefaultSiteConfig()
644
1015
  {
@@ -670,12 +1041,17 @@ class NomadMediaSDK {
670
1041
  /**
671
1042
  * @function getDynamicContent
672
1043
  * @async
673
- * @description Gets dynamic content.
674
- * @param {string} DYNAMIC_CONTENT_RECORD_ID - The dynamic content record ID.
675
- * @returns {Promise<JSON>} - A promise that resolves when the dynamic content is gotten.
676
- * Returns the information of the gotten dynamic content.
677
- * @throws {Error} - An error is thrown if the dynamic content fails to get.
678
- * @throws {Error} - An error is thrown if the API type is not portal.
1044
+ * @description Retrieve a dynamic content item by its unique identifier.
1045
+ * Gets a dynamically generated content item used for personalized or rule-based content display
1046
+ * on the portal.
1047
+ * @param {string} DYNAMIC_CONTENT_RECORD_ID - The unique identifier of the dynamic content
1048
+ * @returns {Promise<Object>} The dynamic content object with its configuration and items
1049
+ * @throws {Error} 401 - The caller is not authenticated
1050
+ * @throws {Error} 404 - The dynamic content was not found
1051
+ * @example
1052
+ * const content = await nomadSDK.getDynamicContent("dynamic-123");
1053
+ *
1054
+ * @see {@link https://docs.nomad-cms.com/docs/api-guide/media-api/get-dynamic-content Portal API} GET /media/content/{id}
679
1055
  */
680
1056
  async getDynamicContent(DYNAMIC_CONTENT_RECORD_ID)
681
1057
  {
@@ -702,11 +1078,15 @@ class NomadMediaSDK {
702
1078
  /**
703
1079
  * @function getDynamicContents
704
1080
  * @async
705
- * @description Gets dynamic contents.
706
- * @returns {Promise<Array<JSON>>} - A promise that resolves when the dynamic contents is gotten.
707
- * Returns the information of the gotten dynamic content.
708
- * @throws {Error} - An error is thrown if the dynamic content fails to get.
709
- * @throws {Error} - An error is thrown if the API type is not portal.
1081
+ * @description Retrieve all dynamic content items.
1082
+ * Gets the list of all dynamic content configurations available for the portal front-end.
1083
+ * @returns {Promise<Object>} Array of dynamic content objects
1084
+ * @throws {Error} 401 - The caller is not authenticated
1085
+ * @example
1086
+ * const contents = await nomadSDK.getDynamicContents();
1087
+ * contents.forEach(c => console.log(c.id, c.name));
1088
+ *
1089
+ * @see {@link https://docs.nomad-cms.com/docs/api-guide/media-api/get-dynamic-contents Portal API} GET /media/content
710
1090
  */
711
1091
  async getDynamicContents()
712
1092
  {
@@ -738,15 +1118,21 @@ class NomadMediaSDK {
738
1118
  /**
739
1119
  * @function getMediaGroup
740
1120
  * @async
741
- * @description Gets media group.
742
- * @param {string} MEDIA_GROUP_ID - The ID of the media group.
743
- * @param { Array<string> } FILTER_IDS - The IDs of the media group items to filter by.
744
- * @returns {Promise<JSON>} - A promise that resolves when the media group is gotten.
745
- * Returns the information of the gotten media group.
746
- * @throws {Error} - An error is thrown if the media group fails to get.
747
- * @throws {Error} - An error is thrown if the API type is not portal.
1121
+ * @description Retrieve a media group by its unique identifier.
1122
+ * Gets a media group and its contents with optional content filtering. Media groups organize
1123
+ * content items for portal display.
1124
+ * @param {string} MEDIA_GROUP_ID - The unique identifier of the media group
1125
+ * @param {Array<string>} [FILTER_IDS] - Optional array of content IDs to filter results
1126
+ * @param {string} [LANGUAGE_ID] - Optional language ID to localize the response
1127
+ * @returns {Promise<Object>} The media group object with its contents
1128
+ * @throws {Error} 401 - The caller is not authenticated
1129
+ * @throws {Error} 404 - The media group was not found
1130
+ * @example
1131
+ * const group = await nomadSDK.getMediaGroup("group-123", ["item-1", "item-2"]);
1132
+ *
1133
+ * @see {@link https://docs.nomad-cms.com/docs/api-guide/media-api/get-media-group Portal API} GET /media/group/{id}
748
1134
  */
749
- async getMediaGroup(MEDIA_GROUP_ID, FILTER_IDS)
1135
+ async getMediaGroup(MEDIA_GROUP_ID, FILTER_IDS, LANGUAGE_ID)
750
1136
  {
751
1137
  if (this.token === null && this.config.username)
752
1138
  {
@@ -762,7 +1148,7 @@ class NomadMediaSDK {
762
1148
 
763
1149
  try
764
1150
  {
765
- const GET_MEDIA_GROUP_INFO = await _getMediaGroup(this, MEDIA_GROUP_ID, FILTER_IDS);
1151
+ const GET_MEDIA_GROUP_INFO = await _getMediaGroup(this, MEDIA_GROUP_ID, FILTER_IDS, LANGUAGE_ID);
766
1152
  if (GET_MEDIA_GROUP_INFO) _printDatetime(`Media group gotten: ${MEDIA_GROUP_ID}`);
767
1153
  return GET_MEDIA_GROUP_INFO;
768
1154
  }
@@ -776,12 +1162,18 @@ class NomadMediaSDK {
776
1162
  /**
777
1163
  * @function getMediaItem
778
1164
  * @async
779
- * @description Gets media item.
780
- * @param {string} MEDIA_ITEM_ID - The ID of the media item.
781
- * @returns {Promise<JSON>} - A promise that resolves when the media item is gotten.
782
- * Returns the information of the gotten media item.
783
- * @throws {Error} - An error is thrown if the media item fails to get.
784
- * @throws {Error} - An error is thrown if the API type is not portal.
1165
+ * @description Retrieve a media item by its unique identifier.
1166
+ * Gets the full media item details including metadata, content definitions, and playback
1167
+ * information for the portal front-end.
1168
+ * @param {string} MEDIA_ITEM_ID - The unique identifier of the media item
1169
+ * @returns {Promise<Object>} The media item object with metadata and content details
1170
+ * @throws {Error} 401 - The caller is not authenticated
1171
+ * @throws {Error} 404 - The media item was not found
1172
+ * @example
1173
+ * const item = await nomadSDK.getMediaItem("item-123");
1174
+ * console.log(item.title);
1175
+ *
1176
+ * @see {@link https://docs.nomad-cms.com/docs/api-guide/media-api/get-media-item Portal API} GET /media/item/{id}
785
1177
  */
786
1178
  async getMediaItem(MEDIA_ITEM_ID)
787
1179
  {
@@ -813,13 +1205,14 @@ class NomadMediaSDK {
813
1205
  /**
814
1206
  * @function getMyContent
815
1207
  * @async
816
- * @description Gets favorites and continue watching lists of IDs for the logged in user.
817
- * @returns {Promise<JSON>} - A promise that resolves when the favorites and continue watching
818
- * lists of IDs are gotten.
819
- * Returns the information of the gotten favorites and continue watching lists of IDs.
820
- * @throws {Error} - An error is thrown if the favorites and continue watching lists of IDs fail
821
- * to get.
822
- * @throws {Error} - An error is thrown if the API type is not portal.
1208
+ * @description Retrieve the authenticated user's personal content.
1209
+ * Gets all content items owned by or assigned to the currently authenticated user.
1210
+ * @returns {Promise<Object>} Array of content items belonging to the user
1211
+ * @throws {Error} 401 - The caller is not authenticated
1212
+ * @example
1213
+ * const myContent = await nomadSDK.getMyContent();
1214
+ *
1215
+ * @see {@link https://docs.nomad-cms.com/docs/api-guide/media-api/get-my-content Portal API} GET /media/my-content
823
1216
  */
824
1217
  async getMyContent()
825
1218
  {
@@ -830,7 +1223,7 @@ class NomadMediaSDK {
830
1223
 
831
1224
  if (this.config.apiType !== "portal")
832
1225
  {
833
- throw new Error("This function is not available for portal API type.");
1226
+ throw new Error("This function is only available for portal API type.");
834
1227
  }
835
1228
 
836
1229
  _printDatetime(`Getting my content`);
@@ -851,25 +1244,36 @@ class NomadMediaSDK {
851
1244
  /**
852
1245
  * @function getMyGroup
853
1246
  * @async
854
- * @description Gets user's group.
855
- * @param {string} GROUP_ID - The ID of the group.
856
- * @returns {Promise<JSON>} - A promise that resolves when the user's group is gotten.
857
- * Returns the information of the gotten user's group.
858
- * @throws {Error} - An error is thrown if the user's group fails to get.
859
- * @throws {Error} - An error is thrown if the API type is not portal.
1247
+ * @description Retrieve a personal content group by its unique identifier.
1248
+ * Gets a content group from the user's personal groups, including its contents and
1249
+ * configuration.
1250
+ * @param {string} GROUP_ID - The unique identifier of the personal group
1251
+ * @param {Array<string>} [FILTER_IDS] - Optional array of content IDs to filter results
1252
+ * @returns {Promise<Object>} The personal group object with its contents
1253
+ * @throws {Error} 401 - The caller is not authenticated
1254
+ * @throws {Error} 404 - The group was not found
1255
+ * @example
1256
+ * const group = await nomadSDK.getMyGroup("group-123");
1257
+ *
1258
+ * @see {@link https://docs.nomad-cms.com/docs/api-guide/media-api/get-my-group Portal API} GET /media/my-group/{id}
860
1259
  */
861
- async getMyGroup(GROUP_ID)
1260
+ async getMyGroup(GROUP_ID, FILTER_IDS)
862
1261
  {
863
1262
  if (this.token === null && this.config.username)
864
1263
  {
865
1264
  await this._init();
866
1265
  }
867
1266
 
1267
+ if (this.config.apiType !== "portal")
1268
+ {
1269
+ throw new Error("This function is only available for portal API type.");
1270
+ }
1271
+
868
1272
  _printDatetime(`Getting my group: ${GROUP_ID}`);
869
1273
 
870
1274
  try
871
1275
  {
872
- const GET_MY_GROUP_INFO = await _getMyGroup(this, GROUP_ID);
1276
+ const GET_MY_GROUP_INFO = await _getMyGroup(this, GROUP_ID, FILTER_IDS);
873
1277
  if (GET_MY_GROUP_INFO) _printDatetime(`My group gotten: ${GROUP_ID}`);
874
1278
  return GET_MY_GROUP_INFO;
875
1279
  }
@@ -883,12 +1287,17 @@ class NomadMediaSDK {
883
1287
  /**
884
1288
  * @function getSiteConfig
885
1289
  * @async
886
- * @description Gets site config.
887
- * @param {string} SITE_CONFIG_RECORD_ID - The site config record ID.
888
- * @returns {Promise<JSON>} - A promise that resolves when the site config is gotten.
889
- * Returns the information of the gotten site config.
890
- * @throws {Error} - An error is thrown if the site config fails to get.
891
- * @throws {Error} - An error is thrown if the API type is not portal.
1290
+ * @description Retrieve a site configuration by its unique identifier.
1291
+ * Gets the portal site configuration including theme, layout, navigation, and content display
1292
+ * settings.
1293
+ * @param {string} SITE_CONFIG_RECORD_ID - The unique identifier of the site configuration
1294
+ * @returns {Promise<Object>} The site configuration object
1295
+ * @throws {Error} 401 - The caller is not authenticated
1296
+ * @throws {Error} 404 - The site configuration was not found
1297
+ * @example
1298
+ * const config = await nomadSDK.getSiteConfig("config-123");
1299
+ *
1300
+ * @see {@link https://docs.nomad-cms.com/docs/api-guide/media-api/get-site-config Portal API} GET /media/config/{id}
892
1301
  */
893
1302
  async getSiteConfig(SITE_CONFIG_RECORD_ID)
894
1303
  {
@@ -897,6 +1306,11 @@ class NomadMediaSDK {
897
1306
  await this._init();
898
1307
  }
899
1308
 
1309
+ if (this.config.apiType !== "portal")
1310
+ {
1311
+ throw new Error("This function is only available for portal API type.");
1312
+ }
1313
+
900
1314
  _printDatetime(`Getting site config: ${SITE_CONFIG_RECORD_ID}`);
901
1315
 
902
1316
  try
@@ -915,18 +1329,23 @@ class NomadMediaSDK {
915
1329
  /**
916
1330
  * @function mediaSearch
917
1331
  * @async
918
- * @description Searches for media.
919
- * @param {string | null} QUERY - The query of the search.
920
- * @param {Array<string> | null} IDS - The IDs of the media to be searched.
921
- * @param {Array<JSON> | null} SORT_FIELDS - The sort fields of the search.
922
- * JSON format: [{"fieldName": "string", "sortType": ("Ascending" | "Descending")} ...]
923
- * @param {number | null} OFFSET - The page offset of the search.
924
- * @param {number | null} SIZE - The page size of the search.
925
- * @returns {Promise<JSON>} - A promise that resolves when the media are searched.
926
- * Returns the information of the searched media.
927
- * @throws {Error} - An error is thrown if the media fail to search.
928
- * @throws {Error} - An error is thrown if the API type is not portal.
929
- */
1332
+ * @description Search for media content using a query string and optional filters.
1333
+ * Performs a search against the media index with support for pagination, sorting, and ID
1334
+ * filtering. Defaults to page offset 0 and page size 100.
1335
+ * @param {Object} QUERY - The search query object containing search criteria
1336
+ * @param {Array<string>} [IDS] - Optional array of specific content IDs to filter by
1337
+ * @param {Array<Object>} [SORT_FIELDS] - Optional array of sort field objects
1338
+ * @param {number} [OFFSET] - The page offset for pagination (defaults to 0)
1339
+ * @param {number} [SIZE] - The number of results per page (defaults to 100)
1340
+ * @returns {Promise<Object>} Search results containing items array and pagination info
1341
+ * @throws {Error} 401 - The caller is not authenticated
1342
+ * @example
1343
+ * const results = await nomadSDK.mediaSearch(
1344
+ * { query: "news" }, null, [{ fieldName: "date", order: "desc" }], 0, 25
1345
+ * );
1346
+ *
1347
+ * @see {@link https://docs.nomad-cms.com/docs/api-guide/media-api/media-search Portal API} POST /media/search
1348
+ */
930
1349
  async mediaSearch(QUERY, IDS, SORT_FIELDS, OFFSET, SIZE)
931
1350
  {
932
1351
  if (this.token === null && this.config.username)
@@ -1039,12 +1458,33 @@ export default NomadMediaSDK;
1039
1458
 
1040
1459
 
1041
1460
 
1042
- async function _forgotPassword(instance)
1461
+ /**
1462
+ * Trigger a password reset code to be sent to the user's email address.
1463
+ *
1464
+ * Initiates the password reset flow for the specified user account. If the account is active,
1465
+ * a 6-digit verification code is sent to the user's email address. The code should then be
1466
+ * submitted to the reset-password endpoint along with the new password.
1467
+ *
1468
+ * **Authorization:** Public endpoint — no authentication required.
1469
+ *
1470
+ * **Side Effects:** Sends a password reset email with a 6-digit verification code to the user.
1471
+ *
1472
+ * @param {Object} instance - The SDK instance containing configuration (username)
1473
+ * @returns {Promise<Object>} Response indicating the reset code was sent, or a pending account state
1474
+ * @throws {Error} 400 - The request is null or UserName is missing
1475
+ *
1476
+ * @example
1477
+ * const result = await nomadSDK.forgotPassword();
1478
+ * console.log('Password reset code sent to email');
1479
+ *
1480
+ * @see {@link https://docs.nomad-cms.com/api/account/forgot-password Admin API} POST /account/forgot-password
1481
+ */
1482
+ async function _forgotPassword(instance)
1043
1483
  {
1044
1484
  const API_URL = `${instance.config.serviceApiUrl}/api/account/forgot-password`;
1045
1485
 
1046
1486
  const BODY = {
1047
- username: instance.config.username
1487
+ userName: instance.config.username
1048
1488
  };
1049
1489
 
1050
1490
  return await _sendRequest(instance, "Forgot Password", API_URL, "POST", undefined, BODY);
@@ -1053,6 +1493,41 @@ async function _forgotPassword(instance)
1053
1493
 
1054
1494
 
1055
1495
 
1496
+ /**
1497
+ * Authenticate a user and obtain a JWT access token.
1498
+ *
1499
+ * Validates the user's credentials and, if successful, creates a new session and returns
1500
+ * a JWT access token with a refresh token. Accounts that are disabled, expired, pending invitation, or pending
1501
+ * approval will be rejected. Guest users must have a valid profile and permitted application access.
1502
+ *
1503
+ * **Authorization:** Public endpoint — no authentication required.
1504
+ *
1505
+ * **Side Effects:** Creates a new user session. The login event is recorded for auditing purposes.
1506
+ *
1507
+ * **Error Scenarios:**
1508
+ * - Returns 400 if the request body is null or missing required fields
1509
+ * - Returns 404 if authentication fails or the user cannot be found
1510
+ * - Returns 403 if the user account is Disabled, Expired, PendingInvitation, or PendingApproval
1511
+ * - LoginStatus on the response indicates the specific failure reason (e.g., IsDisabled, IsPendingEmailConfirmation, RequiresNewPassword)
1512
+ *
1513
+ * @param {Object} instance - The SDK instance containing configuration (username, password, serviceApiUrl, and optionally applicationId)
1514
+ * @returns {Promise<Object>} Authentication response. User-identity fields (from the backend
1515
+ * `AccountLoginResponseModel`): `id`, `userName`, `firstName`, `lastName`, `loginStatus`,
1516
+ * `cookies`. Session/token fields consumed by the SDK: `token`, `refreshToken`,
1517
+ * `expirationSeconds`, `userSessionId`. Note: there is no `userId` or `expiresIn` field;
1518
+ * use `id` and `expirationSeconds` respectively.
1519
+ * @throws {Error} 400 - The request body is null or missing required fields (UserName, Password)
1520
+ * @throws {Error} 404 - Authentication failed — invalid credentials or user not found
1521
+ * @throws {Error} 403 - User account is Disabled, Expired, PendingInvitation, or PendingApproval
1522
+ *
1523
+ * @example
1524
+ * const response = await nomadSDK.login();
1525
+ * console.log('Access Token:', response.token);
1526
+ * console.log('User ID:', response.id);
1527
+ * console.log('User Name:', response.userName);
1528
+ *
1529
+ * @see {@link https://docs.nomad-cms.com/api/account/login Admin API} POST /account/login
1530
+ */
1056
1531
  async function _login(instance)
1057
1532
  {
1058
1533
  const API_URL = `${instance.config.serviceApiUrl}/api/account/login`;
@@ -1062,6 +1537,9 @@ async function _login(instance)
1062
1537
  "password": instance.config.password
1063
1538
  };
1064
1539
 
1540
+ // Optional: ties the session to an application (config.applicationId), as ping and logout do.
1541
+ if (instance.config.applicationId) BODY.applicationId = instance.config.applicationId;
1542
+
1065
1543
  try
1066
1544
  {
1067
1545
  return await _sendRequest(instance, "Login", API_URL, "POST", undefined, BODY);
@@ -1083,11 +1561,38 @@ async function _login(instance)
1083
1561
 
1084
1562
 
1085
1563
 
1564
+ /**
1565
+ * Log out the currently authenticated user and end their session.
1566
+ *
1567
+ * Ends the user's active session and invalidates the authentication token.
1568
+ * If a UserSessionId is provided, that specific session is terminated.
1569
+ *
1570
+ * **Authorization:** Requires a valid Bearer token.
1571
+ *
1572
+ * **Side Effects:** Terminates the user session. The logout event is recorded for auditing purposes.
1573
+ *
1574
+ * @param {Object} instance - The SDK instance containing configuration and session information (the session ID and optional applicationId are sent in the request body)
1575
+ * @returns {Promise<Object>} Confirmation that the user was successfully logged out and the session was deleted
1576
+ * @throws {Error} 401 - The caller is not authenticated
1577
+ *
1578
+ * @example
1579
+ * await nomadSDK.logout();
1580
+ * console.log('User logged out successfully');
1581
+ *
1582
+ * @see {@link https://docs.nomad-cms.com/api/account/logout Admin API} POST /account/logout
1583
+ */
1086
1584
  async function _logout(instance)
1087
1585
  {
1088
1586
  const API_URL = `${instance.config.serviceApiUrl}/api/account/logout`;
1089
1587
 
1090
- return await _sendRequest(instance, "Logout", API_URL, "POST", undefined, undefined);
1588
+ // The API requires a request body; an empty body is rejected with 400.
1589
+ // Undefined fields are dropped by JSON serialisation, so this is at worst "{}".
1590
+ const BODY = {
1591
+ userSessionId: instance.userSessionId ?? undefined,
1592
+ applicationId: instance.config.applicationId ?? undefined
1593
+ };
1594
+
1595
+ return await _sendRequest(instance, "Logout", API_URL, "POST", undefined, BODY);
1091
1596
  }
1092
1597
 
1093
1598
 
@@ -1096,7 +1601,30 @@ async function _logout(instance)
1096
1601
 
1097
1602
 
1098
1603
 
1099
- async function _refreshToken(instance)
1604
+ /**
1605
+ * Retrieve a new JWT access token using a refresh token.
1606
+ *
1607
+ * Exchanges a valid refresh token for a new JWT access token. Automatically updates
1608
+ * the SDK instance's token property on success.
1609
+ *
1610
+ * **Authorization:** Public endpoint — no authentication required. The refresh token itself serves as the credential.
1611
+ *
1612
+ * **Side Effects:** Updates the SDK instance's token property with the new access token.
1613
+ *
1614
+ * @param {Object} instance - The SDK instance containing configuration and refreshTokenVal
1615
+ * @returns {Promise<boolean>} True if the token was successfully refreshed, false otherwise
1616
+ * @throws {Error} 400 - The request body is null or RefreshToken is missing
1617
+ * @throws {Error} 404 - The refresh token is invalid or has expired
1618
+ *
1619
+ * @example
1620
+ * const success = await nomadSDK.refreshToken();
1621
+ * if (success) {
1622
+ * console.log('Token refreshed successfully');
1623
+ * }
1624
+ *
1625
+ * @see {@link https://docs.nomad-cms.com/api/account/refresh-token Admin API} POST /account/refresh-token
1626
+ */
1627
+ async function _refreshToken(instance)
1100
1628
  {
1101
1629
  const API_URL = `${instance.config.serviceApiUrl}/api/account/refresh-token`;
1102
1630
 
@@ -1104,22 +1632,69 @@ async function _refreshToken(instance)
1104
1632
  refreshToken: instance.refreshTokenVal
1105
1633
  };
1106
1634
 
1107
- const TOKEN_INFO = await _sendRequest(instance, "Refreshing Token", API_URL, "POST", undefined, BODY);
1108
- instance.token = TOKEN_INFO.token;
1635
+ // _sendRequest() now throws on failure (api-exception-handler.js) rather
1636
+ // than resolving to a falsy value — but this function's own contract
1637
+ // ("returns false, never throws" — see its own doc comment/callers)
1638
+ // predates that and must be preserved regardless: _sendRequest()'s own
1639
+ // 403-retry logic calls this and explicitly checks the boolean result to
1640
+ // decide whether to fall back to a full re-login. Letting a failed
1641
+ // refresh (e.g. an expired/already-used refresh token, a 409) throw
1642
+ // uncaught here would crash that entire retry path instead of correctly
1643
+ // falling through to login().
1644
+ let TOKEN_INFO;
1645
+ try
1646
+ {
1647
+ TOKEN_INFO = await _sendRequest(instance, "Refreshing Token", API_URL, "POST", undefined, BODY);
1648
+ }
1649
+ catch (error)
1650
+ {
1651
+ return false;
1652
+ }
1653
+
1654
+ if (TOKEN_INFO && TOKEN_INFO.token)
1655
+ {
1656
+ instance.token = TOKEN_INFO.token;
1657
+ return true;
1658
+ }
1109
1659
 
1110
- return true;
1660
+ return false;
1111
1661
  }
1112
1662
 
1113
1663
 
1114
1664
 
1115
1665
 
1116
1666
 
1117
- async function _resetPassword(instance, CODE, NEW_PASSWORD)
1667
+ /**
1668
+ * Reset the user's password using a verification code from the forgot-password email.
1669
+ *
1670
+ * Completes the password reset flow by validating the verification code from the forgot-password
1671
+ * email and setting the new password. For accounts pending approval, this also completes the
1672
+ * email verification step.
1673
+ *
1674
+ * **Authorization:** Public endpoint — no authentication required. The reset token serves as the credential.
1675
+ *
1676
+ * **Side Effects:**
1677
+ * - Updates the user's password
1678
+ * - For pending-approval accounts, also verifies the email address
1679
+ *
1680
+ * @param {Object} instance - The SDK instance containing configuration (username)
1681
+ * @param {string} CODE - The 6-digit verification code from the forgot-password email
1682
+ * @param {string} NEW_PASSWORD - The new password to set for the user
1683
+ * @returns {Promise<Object>} Response confirming the password was successfully reset
1684
+ * @throws {Error} 400 - The request is null, UserName is missing, reset token is invalid/expired, or user is disabled
1685
+ *
1686
+ * @example
1687
+ * const result = await nomadSDK.resetPassword('123456', 'newSecurePassword!');
1688
+ * console.log('Password reset successfully');
1689
+ *
1690
+ * @see {@link https://docs.nomad-cms.com/api/account/reset-password Admin API} POST /account/reset-password
1691
+ */
1692
+ async function _resetPassword(instance, CODE, NEW_PASSWORD)
1118
1693
  {
1119
1694
  const API_URL = `${instance.config.serviceApiUrl}/api/account/reset-password`;
1120
1695
 
1121
1696
  const BODY = {
1122
- userName: instance.username,
1697
+ userName: instance.config.username,
1123
1698
  token: CODE,
1124
1699
  newPassword: NEW_PASSWORD
1125
1700
  };
@@ -1128,52 +1703,113 @@ async function _resetPassword(instance, CODE, NEW_PASSWORD)
1128
1703
  }
1129
1704
 
1130
1705
 
1131
- function _apiExceptionHandler(error, message)
1706
+ /**
1707
+ * Builds and throws a real Error for a failed API call, instead of logging
1708
+ * and swallowing it.
1709
+ *
1710
+ * This used to log via console.error and return null on every failure — a
1711
+ * design that made a genuinely failed request indistinguishable, to any
1712
+ * caller, from a real (if empty/falsy) success. Confirmed live to be the
1713
+ * root cause of several serious bugs: createContent() reporting "Content
1714
+ * created" for a record whose properties write had actually failed
1715
+ * (a rejected Lookup Multi-Select shape), updateAsset() silently no-opping
1716
+ * while an asset was still mid-upload, and getContent() returning `null`
1717
+ * for a genuine 404 rather than letting a caller distinguish "not found"
1718
+ * from "found, something else is wrong." Every one of those bugs required
1719
+ * tracing through live server logs to find, specifically because nothing
1720
+ * in this SDK's own return value or a thrown exception ever said so.
1721
+ *
1722
+ * @param {Error|Object|string} error - The underlying error (an Axios error,
1723
+ * a parsed error-response body, or a plain string message)
1724
+ * @param {string} message - A human-readable prefix describing what failed
1725
+ * (e.g. "Update Content failed")
1726
+ * @param {number} [status] - The HTTP status code, when known, attached to
1727
+ * the thrown error as `.status` so callers can distinguish e.g. a 404
1728
+ * from a 403 without parsing the message text.
1729
+ * @throws {Error} Always — with `.message` built from `message` + whatever
1730
+ * detail could be extracted from `error`, `.cause` set to the original
1731
+ * `error` value, and `.status` set when provided.
1732
+ */
1733
+ function _apiExceptionHandler(error, message, status)
1132
1734
  {
1133
- // Check if we have a response object and error message
1134
- if (!error)
1735
+ let detail;
1736
+
1737
+ if (!error)
1135
1738
  {
1136
- console.error(message);
1739
+ detail = undefined;
1137
1740
  }
1138
-
1139
- // Check if error is a string
1140
- if (typeof error === "string")
1741
+ else if (typeof error === "string")
1141
1742
  {
1142
- console.error(`${message}: ${error}`);
1743
+ detail = error;
1143
1744
  }
1144
1745
  else if (error.errors)
1145
1746
  {
1146
- if (Array.isArray(error.errors))
1747
+ if (Array.isArray(error.errors))
1147
1748
  {
1148
1749
  let errorString = "";
1149
- for (let errorIdx = 0; errorIdx < error.errors.length; errorIdx++)
1750
+ for (let errorIdx = 0; errorIdx < error.errors.length; errorIdx++)
1150
1751
  {
1151
- errorString += `${error.errors[errorIdx].code}: ${error.errors[errorIdx].field}: ${error.errors[errorIdx].message}\n`;
1752
+ errorString += `${error.errors[errorIdx].code}: ${error.errors[errorIdx].field}: ${error.errors[errorIdx].message}\n`;
1152
1753
  }
1153
- console.error(`${message}: ${errorString}`);
1154
- }
1155
- else if (typeof error.errors === "object")
1754
+ detail = errorString;
1755
+ }
1756
+ else if (typeof error.errors === "object")
1156
1757
  {
1157
1758
  let errorString = "";
1158
- for (let key in error.errors)
1759
+ for (let key in error.errors)
1159
1760
  {
1160
- if (Object.prototype.hasOwnProperty.call(error.errors, key))
1761
+ if (Object.prototype.hasOwnProperty.call(error.errors, key))
1161
1762
  {
1162
1763
  errorString += `${key}: ${error.errors[key]}\n`;
1163
1764
  }
1164
1765
  }
1165
- console.error(`${message}: ${errorString}`);
1766
+ detail = errorString;
1166
1767
  }
1167
1768
  }
1168
1769
  else
1169
1770
  {
1170
- console.error(`${message}: ${error.message}`);
1771
+ detail = error.message;
1772
+ }
1773
+
1774
+ const thrown = new Error(detail ? `${message}: ${detail}` : message);
1775
+ thrown.cause = error;
1776
+ if (status !== undefined && status !== null) thrown.status = status;
1777
+ throw thrown;
1778
+ }
1779
+
1780
+
1781
+ class InvalidAPITypeException extends Error {
1782
+ constructor(message) {
1783
+ super(message);
1784
+ this.name = "InvalidAPITypeException";
1171
1785
  }
1786
+ }
1172
1787
 
1173
- return null;
1788
+ class MissingCredentialsException extends Error {
1789
+ constructor(credentials = null, message = "Missing login credentials") {
1790
+ if (credentials) {
1791
+ message = `${message}: ${Array.isArray(credentials) ? credentials.join(", ") : credentials}`;
1792
+ }
1793
+ super(message);
1794
+ this.name = "MissingCredentialsException";
1795
+ this.credentials = credentials;
1796
+ }
1797
+ }
1798
+
1799
+ class LoginException extends Error {
1800
+ constructor(message = "Login info incorrect", cause = undefined) {
1801
+ super(message);
1802
+ this.name = "LoginException";
1803
+ if (cause !== undefined)
1804
+ {
1805
+ this.cause = cause;
1806
+ if (cause && cause.status !== undefined) this.status = cause.status;
1807
+ }
1808
+ }
1174
1809
  }
1175
1810
 
1176
1811
 
1812
+
1177
1813
  import { getRandomValues } from 'crypto';
1178
1814
 
1179
1815
  function _newGuid()
@@ -1196,51 +1832,116 @@ function _printDatetime(msg)
1196
1832
  import { promises as fs } from 'fs';
1197
1833
  import path from 'path';
1198
1834
 
1835
+ /**
1836
+ * Normalizes the file argument of an upload into the properties the upload
1837
+ * needs, without reading the file's contents into memory.
1838
+ *
1839
+ * Accepts:
1840
+ * - a file path (Node.js only), read part by part from disk
1841
+ * - a browser `File`/`Blob`, read part by part with `slice()`
1842
+ * - an object that already carries its bytes in `buffer` (for example a
1843
+ * multer file), plus `size` and a name
1844
+ *
1845
+ * @param {string|File|Blob|Object} FILE - The file to upload
1846
+ * @returns {Promise<{originalName: string, size: number, path?: string, blob?: Blob, buffer?: Buffer|ArrayBuffer|Uint8Array}>}
1847
+ * The file's name and size, and the single source its parts are read from
1848
+ * @throws {Error} When the path does not exist, or a path is given in a browser
1849
+ */
1199
1850
  async function _processFile(FILE)
1200
1851
  {
1201
- let FILE_PATH = null;
1202
1852
  if (typeof FILE === "string")
1203
1853
  {
1204
- // if running in node
1205
- if (typeof window === "undefined")
1854
+ if (typeof window !== "undefined")
1206
1855
  {
1207
- FILE_PATH = FILE;
1208
- FILE = await fs.stat(FILE);
1209
- FILE.originalName = path.basename(FILE_PATH);
1210
- if (!FILE.buffer)
1211
- {
1212
- FILE.buffer = await fs.readFile(FILE_PATH);
1213
- }
1856
+ throw new Error("File path is not supported in browser");
1214
1857
  }
1215
- else
1858
+
1859
+ let stats;
1860
+ try
1216
1861
  {
1217
- throw new Error("File path is not supported in browser");
1862
+ stats = await fs.stat(FILE);
1218
1863
  }
1219
- }
1220
- else
1221
- {
1222
- FILE.originalName = FILE.name;
1223
- // if running in browser
1224
- if (!FILE.buffer)
1864
+ catch (error)
1225
1865
  {
1226
- FILE.buffer = await _readFileAsArrayBuffer(FILE);
1866
+ const notFound = new Error(`File not found: ${FILE}`);
1867
+ notFound.code = error.code;
1868
+ notFound.cause = error;
1869
+ throw notFound;
1227
1870
  }
1871
+
1872
+ if (!stats.isFile())
1873
+ {
1874
+ throw new Error(`Not a file: ${FILE}`);
1875
+ }
1876
+
1877
+ return { originalName: path.basename(FILE), size: stats.size, path: FILE };
1228
1878
  }
1229
1879
 
1230
- return FILE;
1880
+ const originalName = FILE.originalName || FILE.name || FILE.originalname;
1881
+
1882
+ if (FILE.buffer)
1883
+ {
1884
+ const size = FILE.size ?? FILE.buffer.byteLength;
1885
+ return { originalName, size, buffer: FILE.buffer };
1886
+ }
1887
+
1888
+ if (typeof FILE.slice === "function")
1889
+ {
1890
+ return { originalName, size: FILE.size, blob: FILE };
1891
+ }
1892
+
1893
+ throw new Error("Unsupported file: pass a file path, a File/Blob, or an object with a buffer");
1231
1894
  }
1232
1895
 
1233
- async function _readFileAsArrayBuffer(file)
1896
+ /**
1897
+ * Opens a reader that returns the bytes of one part at a time.
1898
+ *
1899
+ * In Node.js a path is read with positional reads on a single file handle, so
1900
+ * concurrent parts never share or move a file position. Blobs are sliced
1901
+ * lazily and buffers are viewed without copying.
1902
+ *
1903
+ * @param {Object} FILE_PROPS - The result of `_processFile`
1904
+ * @returns {Promise<{read: function(number, number): Promise<Buffer|Uint8Array|Blob>, close: function(): Promise<void>}>}
1905
+ * `read(start, end)` returns the bytes from `start` to `end` inclusive
1906
+ */
1907
+ async function _openPartReader(FILE_PROPS)
1234
1908
  {
1235
- return new Promise((resolve, reject) =>
1909
+ if (FILE_PROPS.path)
1236
1910
  {
1237
- /* eslint-disable no-undef */
1238
- const reader = new FileReader();
1239
- /* eslint-enable no-undef */
1240
- reader.onload = () => resolve(reader.result);
1241
- reader.onerror = reject;
1242
- reader.readAsArrayBuffer(file);
1243
- });
1911
+ const HANDLE = await fs.open(FILE_PROPS.path, "r");
1912
+ return {
1913
+ async read(START, END)
1914
+ {
1915
+ const LENGTH = END + 1 - START;
1916
+ const PART = Buffer.alloc(LENGTH);
1917
+ let offset = 0;
1918
+ while (offset < LENGTH)
1919
+ {
1920
+ const { bytesRead } = await HANDLE.read(PART, offset, LENGTH - offset, START + offset);
1921
+ if (bytesRead === 0) throw new Error(`Unexpected end of file reading bytes ${START}-${END}`);
1922
+ offset += bytesRead;
1923
+ }
1924
+ return PART;
1925
+ },
1926
+ close: () => HANDLE.close()
1927
+ };
1928
+ }
1929
+
1930
+ if (FILE_PROPS.blob)
1931
+ {
1932
+ return {
1933
+ read: async (START, END) => FILE_PROPS.blob.slice(START, END + 1),
1934
+ close: async () => {}
1935
+ };
1936
+ }
1937
+
1938
+ const BYTES = FILE_PROPS.buffer instanceof ArrayBuffer
1939
+ ? new Uint8Array(FILE_PROPS.buffer)
1940
+ : FILE_PROPS.buffer;
1941
+ return {
1942
+ read: async (START, END) => BYTES.subarray(START, END + 1),
1943
+ close: async () => {}
1944
+ };
1244
1945
  }
1245
1946
 
1246
1947
 
@@ -1248,7 +1949,10 @@ async function _readFileAsArrayBuffer(file)
1248
1949
 
1249
1950
 
1250
1951
 
1251
- import axios from 'axios';
1952
+
1953
+
1954
+
1955
+
1252
1956
 
1253
1957
  const MAX_RETRIES = 3;
1254
1958
  const TIMEOUT = 10000;
@@ -1263,8 +1967,22 @@ async function _sendRequest(instance, FUNCTION_NAME, URL, METHOD_TYPE, PARAMS, B
1263
1967
  if (instance.token) HEADERS["Authorization"] = `Bearer ${instance.token}`;
1264
1968
  else if (instance.config.apiKey) HEADERS["Authorization"] = `ApiKey ${instance.config.apiKey}`;
1265
1969
 
1266
- if (instance.debug && FUNCTION_NAME !== "Login") console.log(`URL: ${URL}\nMETHOD: ${METHOD_TYPE}` + (PARAMS && PARAMS.size === 0 ? `\nPARAMS: ${JSON.stringify(PARAMS, null, 4)}` : "") + (BODY ? `\nBODY: ${JSON.stringify(BODY, null, 4)}` : ""));
1970
+ // A missing required id argument would otherwise be sent to the server as ".../undefined".
1971
+ // String(URL) so a caller that built a URL object does not crash the check.
1972
+ const URL_PATH = String(URL).split("?")[0];
1973
+ if (/\/(undefined|null)(\/|\?|$)/.test(URL_PATH))
1974
+ {
1975
+ _apiExceptionHandler(`a required id argument was not provided (${URL_PATH})`, `${FUNCTION_NAME} failed`);
1976
+ }
1977
+
1978
+ const HAS_PARAMS = PARAMS && Object.keys(PARAMS).length > 0;
1979
+ if (instance.debug && FUNCTION_NAME !== "Login") console.log(`URL: ${URL}\nMETHOD: ${METHOD_TYPE}` + (HAS_PARAMS ? `\nPARAMS: ${JSON.stringify(PARAMS, null, 4)}` : "") + (BODY ? `\nBODY: ${JSON.stringify(BODY, null, 4)}` : ""));
1980
+
1981
+ // Only requests that are safe to repeat are retried after a timeout. A timed-out
1982
+ // POST/PUT/DELETE may already have been applied by the server.
1983
+ const IS_IDEMPOTENT = ["GET", "HEAD", "OPTIONS"].includes(String(METHOD_TYPE).toUpperCase());
1267
1984
 
1985
+ let lastError = null;
1268
1986
  let retries = 0;
1269
1987
  let RESPONSE = null;
1270
1988
  let refreshedToken = false;
@@ -1288,32 +2006,58 @@ async function _sendRequest(instance, FUNCTION_NAME, URL, METHOD_TYPE, PARAMS, B
1288
2006
 
1289
2007
  throw new Error();
1290
2008
  }
1291
- catch (error)
2009
+ catch (error)
1292
2010
  {
2011
+ lastError = error;
2012
+ const ERROR_MESSAGE = error.response?.data?.message ? error.response.data.message : error.message;
2013
+
1293
2014
  if (retries >= MAX_RETRIES)
1294
2015
  {
1295
- return _apiExceptionHandler(error.response?.data.message ? error.response.data.message : error.message, `${FUNCTION_NAME} failed`);
2016
+ _apiExceptionHandler(ERROR_MESSAGE, `${FUNCTION_NAME} failed`, error.response?.status);
1296
2017
  }
1297
2018
 
1298
2019
  if (error?.response?.status === 403 && FUNCTION_NAME !== "Refresh Token" && !refreshedToken)
1299
2020
  {
1300
- await instance.refreshToken();
1301
- HEADERS["Authorization"] = `Bearer ${instance.token}`;
1302
- refreshedToken = true;
2021
+ if (!instance.refreshTokenVal)
2022
+ {
2023
+ throw new LoginException();
2024
+ }
2025
+
2026
+ const refreshSuccess = await instance.refreshToken();
2027
+ if (refreshSuccess)
2028
+ {
2029
+ HEADERS["Authorization"] = `Bearer ${instance.token}`;
2030
+ refreshedToken = true;
2031
+ }
2032
+ else
2033
+ {
2034
+ // If refresh token is invalid, fall back to login
2035
+ await instance.login();
2036
+ HEADERS["Authorization"] = `Bearer ${instance.token}`;
2037
+ refreshedToken = false;
2038
+ }
1303
2039
  }
1304
- else if (error.code === 'ECONNABORTED')
2040
+
2041
+ else if (error.code === 'ECONNABORTED' && IS_IDEMPOTENT)
1305
2042
  {
1306
2043
  await _sleep(SLEEP_TIME * 2 ** (retries + 1));
1307
2044
  }
1308
- else
2045
+ else
1309
2046
  {
1310
- return _apiExceptionHandler(error.response?.data.message ? error.response.data.message : error.message, `${FUNCTION_NAME} failed`);
2047
+ _apiExceptionHandler(ERROR_MESSAGE, `${FUNCTION_NAME} failed`, error.response?.status);
1311
2048
  }
1312
2049
  ++retries;
1313
2050
  }
1314
2051
  } while ((!RESPONSE || RESPONSE.status < 200 || RESPONSE.status >= 300) && retries < MAX_RETRIES);
1315
- try { return RESPONSE.data; }
1316
- catch (error) { return; }
2052
+
2053
+ // Retries ran out without a successful response: fail loudly instead of returning undefined.
2054
+ if (!RESPONSE || RESPONSE.status < 200 || RESPONSE.status >= 300)
2055
+ {
2056
+ _apiExceptionHandler(lastError?.response?.data?.message ?? lastError?.message ?? "no response received",
2057
+ `${FUNCTION_NAME} failed`, lastError?.response?.status);
2058
+ }
2059
+
2060
+ return RESPONSE.data;
1317
2061
  }
1318
2062
 
1319
2063
 
@@ -1352,9 +2096,91 @@ function _slugify(text)
1352
2096
 
1353
2097
 
1354
2098
 
2099
+ /**
2100
+ * Strips read-only metadata the server adds when *reading back* a
2101
+ * reference-type field, so a value read from `getContent()`/`getAsset()`/
2102
+ * similar and resubmitted unchanged on a later update keeps the same shape
2103
+ * it was originally written with.
2104
+ *
2105
+ * Confirmed live (this project's own Media Contributor app, dev-05): a
2106
+ * Content Reference or Asset Reference field's read-back shape includes a
2107
+ * `type` key (disambiguating what kind of thing is referenced — e.g. an
2108
+ * Asset vs. another Content record) that no caller ever supplies on write.
2109
+ * A read-modify-write update that resubmits a field's read-back value
2110
+ * verbatim (because the caller didn't intend to change it) sends that
2111
+ * `type` key back too — and on `Upload Batch.FolderId`, this triggered an
2112
+ * OpenSearch mapping-type rejection ("failed to parse field
2113
+ * [identifiers.folderId] of type [text]") that destroyed the *entire*
2114
+ * document's searchability, not just that one field, with no error ever
2115
+ * surfaced to the caller (see api-exception-handler.js's own history).
2116
+ *
2117
+ * `id` and `description`/`title` are left untouched — both appear in real,
2118
+ * confirmed-working write shapes (e.g. a Lookup Multi-Select's
2119
+ * `{id, description}`) — only `type` is confirmed read-only/write-unsafe.
2120
+ *
2121
+ * @param {*} value - A property value that may be a reference object
2122
+ * (`{id, type, ...}`), an array of them, or anything else (passed through
2123
+ * unchanged)
2124
+ * @returns {*} The same shape with `type` removed from any object that has
2125
+ * both `id` and `type`
2126
+ */
2127
+ function _stripReadOnlyReferenceMetadata(value)
2128
+ {
2129
+ if (Array.isArray(value)) return value.map(_stripReadOnlyReferenceMetadata);
2130
+ if (value && typeof value === 'object' && 'id' in value && 'type' in value)
2131
+ {
2132
+ const { type, ...rest } = value;
2133
+ return rest;
2134
+ }
2135
+ return value;
2136
+ }
2137
+
2138
+
2139
+
2140
+ /**
2141
+ * Convert a list of IDs into the lookup objects the Nomad API expects.
2142
+ *
2143
+ * Fields such as tags, collections and related content are bound by the API as
2144
+ * lookup objects and read each item's `id`; a bare string cannot be bound. Items
2145
+ * that are already objects are passed through unchanged.
2146
+ *
2147
+ * @param {Array<string|Object>|null|undefined} VALUES - IDs, or lookup objects
2148
+ * @returns {Array<Object>|null|undefined} `VALUES` unchanged when it is null or
2149
+ * undefined (the field is left as it is); otherwise one lookup object per item
2150
+ */
2151
+ function _toLookupModels(VALUES)
2152
+ {
2153
+ if (VALUES === null || VALUES === undefined) return VALUES;
2154
+
2155
+ return VALUES.map(VALUE => typeof VALUE === "string" ? { id: VALUE } : VALUE);
2156
+ }
2157
+
2158
+
1355
2159
 
1356
2160
 
1357
- async function _clearContinueWatching(instance, USER_ID, ASSET_ID)
2161
+
2162
+ /**
2163
+ * Clear a "continue watching" entry for a user.
2164
+ *
2165
+ * Removes a specific asset from the user's continue watching list.
2166
+ *
2167
+ * **Authorization:** Requires a valid Bearer token.
2168
+ *
2169
+ * **Side Effects:**
2170
+ * - Removes the continue watching tracking data for the user/asset
2171
+ *
2172
+ * @param {Object} instance - The SDK instance containing configuration and auth token
2173
+ * @param {string} USER_ID - The unique identifier of the user
2174
+ * @param {string} [ASSET_ID] - The asset ID to remove from continue watching
2175
+ * @returns {Promise<Object>} Response confirming the clearing
2176
+ * @throws {Error} 401 - The caller is not authenticated
2177
+ *
2178
+ * @example
2179
+ * await nomadSDK.clearContinueWatching("user-123", "asset-456");
2180
+ *
2181
+ * @see {@link https://docs.nomad-cms.com/docs/api-guide/media-api/clear-continue-watching Portal API} POST /media/clear-watching
2182
+ */
2183
+ async function _clearContinueWatching(instance, USER_ID, ASSET_ID)
1358
2184
  {
1359
2185
  const API_URL = `${instance.config.serviceApiUrl}/api/media/clear-watching`;
1360
2186
 
@@ -1369,7 +2195,27 @@ async function _clearContinueWatching(instance, USER_ID, ASSET_ID)
1369
2195
 
1370
2196
 
1371
2197
 
1372
- async function _clearWatchlist(instance, USER_ID)
2198
+ /**
2199
+ * Clear the user's entire watchlist.
2200
+ *
2201
+ * Removes all items from the specified user's watchlist.
2202
+ *
2203
+ * **Authorization:** Requires a valid Bearer token.
2204
+ *
2205
+ * **Side Effects:**
2206
+ * - Removes all watchlist entries for the user
2207
+ *
2208
+ * @param {Object} instance - The SDK instance containing configuration and auth token
2209
+ * @param {string} USER_ID - The unique identifier of the user whose watchlist to clear
2210
+ * @returns {Promise<Object>} Response confirming the watchlist was cleared
2211
+ * @throws {Error} 401 - The caller is not authenticated
2212
+ *
2213
+ * @example
2214
+ * await nomadSDK.clearWatchlist("user-123");
2215
+ *
2216
+ * @see {@link https://docs.nomad-cms.com/docs/api-guide/media-api/clear-watchlist Portal API} POST /media/clear-watchlist
2217
+ */
2218
+ async function _clearWatchlist(instance, USER_ID)
1373
2219
  {
1374
2220
  const API_URL = `${instance.config.serviceApiUrl}/api/media/clear-watchlist?userId=${USER_ID}`;
1375
2221
 
@@ -1379,7 +2225,32 @@ async function _clearWatchlist(instance, USER_ID)
1379
2225
 
1380
2226
 
1381
2227
 
1382
- async function _createForm(instance, CONTENT_DEFINITION_ID, FORM_INFO)
2228
+ /**
2229
+ * Create a form submission for a content definition.
2230
+ *
2231
+ * Submits form data to create a new entry for the specified content
2232
+ * definition. Used for user-generated content submissions through
2233
+ * portal forms.
2234
+ *
2235
+ * **Authorization:** Requires a valid Bearer token.
2236
+ *
2237
+ * **Side Effects:**
2238
+ * - Creates a new content entry from the form submission
2239
+ *
2240
+ * @param {Object} instance - The SDK instance containing configuration and auth token
2241
+ * @param {string} CONTENT_DEFINITION_ID - The content definition ID for the form type
2242
+ * @param {Object} FORM_INFO - The form data to submit
2243
+ * @returns {Promise<Object>} The created content entry
2244
+ * @throws {Error} 401 - The caller is not authenticated
2245
+ *
2246
+ * @example
2247
+ * const entry = await nomadSDK.createForm("def-123", {
2248
+ * title: "My Submission", description: "Description"
2249
+ * });
2250
+ *
2251
+ * @see {@link https://docs.nomad-cms.com/docs/api-guide/media-api/create-form Portal API} POST /media/form/{contentDefinitionId}
2252
+ */
2253
+ async function _createForm(instance, CONTENT_DEFINITION_ID, FORM_INFO)
1383
2254
  {
1384
2255
  const API_URL = `${instance.config.serviceApiUrl}/api/media/form/${CONTENT_DEFINITION_ID}`;
1385
2256
 
@@ -1391,9 +2262,28 @@ async function _createForm(instance, CONTENT_DEFINITION_ID, FORM_INFO)
1391
2262
 
1392
2263
 
1393
2264
 
1394
- async function _getContentCookies(instance, ID)
2265
+ /**
2266
+ * Retrieve secure streaming cookies for portal media content.
2267
+ *
2268
+ * Gets signed cookies required for secure streaming of media content through the
2269
+ * portal API. The cookies authorize CDN access to the media files.
2270
+ *
2271
+ * **Authorization:** Requires a valid Bearer token.
2272
+ *
2273
+ * @param {Object} instance - The SDK instance containing configuration and auth token
2274
+ * @param {string} CONTENT_ID - The unique identifier of the media content
2275
+ * @returns {Promise<Object>} Streaming cookies and manifest URL for the content
2276
+ * @throws {Error} 401 - The caller is not authenticated
2277
+ * @throws {Error} 404 - The content was not found
2278
+ *
2279
+ * @example
2280
+ * const cookies = await nomadSDK.getContentCookies("media-123");
2281
+ *
2282
+ * @see {@link https://docs.nomad-cms.com/docs/api-guide/asset-api-1 Portal API} GET /media/set-cookies/{id}
2283
+ */
2284
+ async function _getContentCookies(instance, CONTENT_ID)
1395
2285
  {
1396
- const API_URL = `${instance.config.serviceApiUrl}/api/media/set-cookies/${ID}`;
2286
+ const API_URL = `${instance.config.serviceApiUrl}/api/media/set-cookies/${CONTENT_ID}`;
1397
2287
 
1398
2288
  return await _sendRequest(instance, "Get Content Cookies", API_URL, "GET", undefined, undefined);
1399
2289
  }
@@ -1401,7 +2291,24 @@ async function _getContentCookies(instance, ID)
1401
2291
 
1402
2292
 
1403
2293
 
1404
- async function _getDefaultSiteConfig(instance)
2294
+ /**
2295
+ * Retrieve the default site configuration.
2296
+ *
2297
+ * Gets the default portal site configuration used when no specific
2298
+ * configuration ID is provided.
2299
+ *
2300
+ * **Authorization:** Requires a valid Bearer token.
2301
+ *
2302
+ * @param {Object} instance - The SDK instance containing configuration and auth token
2303
+ * @returns {Promise<Object>} The default site configuration object
2304
+ * @throws {Error} 401 - The caller is not authenticated
2305
+ *
2306
+ * @example
2307
+ * const config = await nomadSDK.getDefaultSiteConfig();
2308
+ *
2309
+ * @see {@link https://docs.nomad-cms.com/docs/api-guide/media-api/get-default-site-config Portal API} GET /media/config
2310
+ */
2311
+ async function _getDefaultSiteConfig(instance)
1405
2312
  {
1406
2313
  const API_URL = `${instance.config.serviceApiUrl}/api/media/config`;
1407
2314
 
@@ -1411,9 +2318,28 @@ async function _getDefaultSiteConfig(instance)
1411
2318
 
1412
2319
 
1413
2320
 
1414
- async function _getDynamicContent(instance, ID)
2321
+ /**
2322
+ * Retrieve a dynamic content item by its unique identifier.
2323
+ *
2324
+ * Gets a dynamically generated content item used for personalized or
2325
+ * rule-based content display on the portal.
2326
+ *
2327
+ * **Authorization:** Requires a valid Bearer token.
2328
+ *
2329
+ * @param {Object} instance - The SDK instance containing configuration and auth token
2330
+ * @param {string} DYNAMIC_CONTENT_RECORD_ID - The unique identifier of the dynamic content
2331
+ * @returns {Promise<Object>} The dynamic content object with its configuration and items
2332
+ * @throws {Error} 401 - The caller is not authenticated
2333
+ * @throws {Error} 404 - The dynamic content was not found
2334
+ *
2335
+ * @example
2336
+ * const content = await nomadSDK.getDynamicContent("dynamic-123");
2337
+ *
2338
+ * @see {@link https://docs.nomad-cms.com/docs/api-guide/media-api/get-dynamic-content Portal API} GET /media/content/{id}
2339
+ */
2340
+ async function _getDynamicContent(instance, DYNAMIC_CONTENT_RECORD_ID)
1415
2341
  {
1416
- const API_URL = `${instance.config.serviceApiUrl}/api/media/content/${ID}`;
2342
+ const API_URL = `${instance.config.serviceApiUrl}/api/media/content/${DYNAMIC_CONTENT_RECORD_ID}`;
1417
2343
 
1418
2344
  return await _sendRequest(instance, "Get Dynamic Content", API_URL, "GET", undefined, undefined);
1419
2345
  }
@@ -1421,7 +2347,25 @@ async function _getDynamicContent(instance, ID)
1421
2347
 
1422
2348
 
1423
2349
 
1424
- async function _getDynamicContents(instance)
2350
+ /**
2351
+ * Retrieve all dynamic content items.
2352
+ *
2353
+ * Gets the list of all dynamic content configurations available for
2354
+ * the portal front-end.
2355
+ *
2356
+ * **Authorization:** Requires a valid Bearer token.
2357
+ *
2358
+ * @param {Object} instance - The SDK instance containing configuration and auth token
2359
+ * @returns {Promise<Object>} Array of dynamic content objects
2360
+ * @throws {Error} 401 - The caller is not authenticated
2361
+ *
2362
+ * @example
2363
+ * const contents = await nomadSDK.getDynamicContents();
2364
+ * contents.forEach(c => console.log(c.id, c.name));
2365
+ *
2366
+ * @see {@link https://docs.nomad-cms.com/docs/api-guide/media-api/get-dynamic-contents Portal API} GET /media/content
2367
+ */
2368
+ async function _getDynamicContents(instance)
1425
2369
  {
1426
2370
  const API_URL = `${instance.config.serviceApiUrl}/api/media/content`;
1427
2371
 
@@ -1431,12 +2375,38 @@ async function _getDynamicContents(instance)
1431
2375
 
1432
2376
 
1433
2377
 
1434
- async function _getMediaGroup(instance, ID, FILTER_IDS)
2378
+ /**
2379
+ * Retrieve a media group by its unique identifier.
2380
+ *
2381
+ * Gets a media group and its contents with optional content filtering.
2382
+ * Media groups organize content items for portal display.
2383
+ *
2384
+ * **Authorization:** Requires a valid Bearer token.
2385
+ *
2386
+ * @param {Object} instance - The SDK instance containing configuration and auth token
2387
+ * @param {string} MEDIA_GROUP_ID - The unique identifier of the media group
2388
+ * @param {Array<string>} [FILTER_IDS] - Optional array of content IDs to filter results
2389
+ * @param {string} [LANGUAGE_ID] - Optional language ID to localize the response
2390
+ * @returns {Promise<Object>} The media group object with its contents
2391
+ * @throws {Error} 401 - The caller is not authenticated
2392
+ * @throws {Error} 404 - The media group was not found
2393
+ *
2394
+ * @example
2395
+ * const group = await nomadSDK.getMediaGroup("group-123", ["item-1", "item-2"]);
2396
+ *
2397
+ * @see {@link https://docs.nomad-cms.com/docs/api-guide/media-api/get-media-group Portal API} GET /media/group/{id}
2398
+ */
2399
+ async function _getMediaGroup(instance, MEDIA_GROUP_ID, FILTER_IDS, LANGUAGE_ID)
1435
2400
  {
1436
- let apiUrl = `${instance.config.serviceApiUrl}/api/media/group/${ID}`;
2401
+ let apiUrl = `${instance.config.serviceApiUrl}/api/media/group/${MEDIA_GROUP_ID}`;
2402
+
2403
+ // filterIds is sent as repeated query keys (filterIds=a&filterIds=b)
2404
+ const QUERY = [];
1437
2405
  if (FILTER_IDS && FILTER_IDS.length > 0) {
1438
- apiUrl += '?' + FILTER_IDS.map(id => `filterIds=${id}`).join('&');
2406
+ QUERY.push(...FILTER_IDS.map(id => `filterIds=${encodeURIComponent(id)}`));
1439
2407
  }
2408
+ if (LANGUAGE_ID) QUERY.push(`languageId=${encodeURIComponent(LANGUAGE_ID)}`);
2409
+ if (QUERY.length > 0) apiUrl += '?' + QUERY.join('&');
1440
2410
 
1441
2411
  return await _sendRequest(instance, "Get Media Group", apiUrl, "GET", undefined, undefined);
1442
2412
  }
@@ -1444,9 +2414,29 @@ async function _getMediaGroup(instance, ID, FILTER_IDS)
1444
2414
 
1445
2415
 
1446
2416
 
1447
- async function _getMediaItem(instance, ID)
2417
+ /**
2418
+ * Retrieve a media item by its unique identifier.
2419
+ *
2420
+ * Gets the full media item details including metadata, content definitions,
2421
+ * and playback information for the portal front-end.
2422
+ *
2423
+ * **Authorization:** Requires a valid Bearer token.
2424
+ *
2425
+ * @param {Object} instance - The SDK instance containing configuration and auth token
2426
+ * @param {string} MEDIA_ITEM_ID - The unique identifier of the media item
2427
+ * @returns {Promise<Object>} The media item object with metadata and content details
2428
+ * @throws {Error} 401 - The caller is not authenticated
2429
+ * @throws {Error} 404 - The media item was not found
2430
+ *
2431
+ * @example
2432
+ * const item = await nomadSDK.getMediaItem("item-123");
2433
+ * console.log(item.title);
2434
+ *
2435
+ * @see {@link https://docs.nomad-cms.com/docs/api-guide/media-api/get-media-item Portal API} GET /media/item/{id}
2436
+ */
2437
+ async function _getMediaItem(instance, MEDIA_ITEM_ID)
1448
2438
  {
1449
- const API_URL = `${instance.config.serviceApiUrl}/api/media/item/${ID}`;
2439
+ const API_URL = `${instance.config.serviceApiUrl}/api/media/item/${MEDIA_ITEM_ID}`;
1450
2440
 
1451
2441
  return await _sendRequest(instance, "Get Media Item", API_URL, "GET", undefined, undefined);
1452
2442
  }
@@ -1454,7 +2444,24 @@ async function _getMediaItem(instance, ID)
1454
2444
 
1455
2445
 
1456
2446
 
1457
- async function _getMyContent(instance)
2447
+ /**
2448
+ * Retrieve the authenticated user's personal content.
2449
+ *
2450
+ * Gets all content items owned by or assigned to the currently
2451
+ * authenticated user.
2452
+ *
2453
+ * **Authorization:** Requires a valid Bearer token.
2454
+ *
2455
+ * @param {Object} instance - The SDK instance containing configuration and auth token
2456
+ * @returns {Promise<Object>} Array of content items belonging to the user
2457
+ * @throws {Error} 401 - The caller is not authenticated
2458
+ *
2459
+ * @example
2460
+ * const myContent = await nomadSDK.getMyContent();
2461
+ *
2462
+ * @see {@link https://docs.nomad-cms.com/docs/api-guide/media-api/get-my-content Portal API} GET /media/my-content
2463
+ */
2464
+ async function _getMyContent(instance)
1458
2465
  {
1459
2466
  const API_URL = `${instance.config.serviceApiUrl}/api/media/my-content`;
1460
2467
 
@@ -1464,9 +2471,34 @@ async function _getMyContent(instance)
1464
2471
 
1465
2472
 
1466
2473
 
1467
- async function _getMyGroup(instance, ID)
2474
+ /**
2475
+ * Retrieve a personal content group by its unique identifier.
2476
+ *
2477
+ * Gets a content group from the user's personal groups, including its
2478
+ * contents and configuration.
2479
+ *
2480
+ * **Authorization:** Requires a valid Bearer token.
2481
+ *
2482
+ * @param {Object} instance - The SDK instance containing configuration and auth token
2483
+ * @param {string} GROUP_ID - The unique identifier of the personal group
2484
+ * @param {Array<string>} [FILTER_IDS] - Optional array of content IDs to filter results
2485
+ * @returns {Promise<Object>} The personal group object with its contents
2486
+ * @throws {Error} 401 - The caller is not authenticated
2487
+ * @throws {Error} 404 - The group was not found
2488
+ *
2489
+ * @example
2490
+ * const group = await nomadSDK.getMyGroup("group-123");
2491
+ *
2492
+ * @see {@link https://docs.nomad-cms.com/docs/api-guide/media-api/get-my-group Portal API} GET /media/my-group/{id}
2493
+ */
2494
+ async function _getMyGroup(instance, GROUP_ID, FILTER_IDS)
1468
2495
  {
1469
- const API_URL = `${instance.config.serviceApiUrl}/api/media/my-group/${ID}`;
2496
+ let API_URL = `${instance.config.serviceApiUrl}/api/media/my-group/${GROUP_ID}`;
2497
+
2498
+ // filterIds is sent as repeated query keys (filterIds=a&filterIds=b)
2499
+ if (FILTER_IDS && FILTER_IDS.length > 0) {
2500
+ API_URL += '?' + FILTER_IDS.map(id => `filterIds=${encodeURIComponent(id)}`).join('&');
2501
+ }
1470
2502
 
1471
2503
  return await _sendRequest(instance, "Get My Group", API_URL, "GET", undefined, undefined);
1472
2504
  }
@@ -1474,9 +2506,28 @@ async function _getMyGroup(instance, ID)
1474
2506
 
1475
2507
 
1476
2508
 
1477
- async function _getSiteConfig(instance, ID)
2509
+ /**
2510
+ * Retrieve a site configuration by its unique identifier.
2511
+ *
2512
+ * Gets the portal site configuration including theme, layout, navigation,
2513
+ * and content display settings.
2514
+ *
2515
+ * **Authorization:** Requires a valid Bearer token.
2516
+ *
2517
+ * @param {Object} instance - The SDK instance containing configuration and auth token
2518
+ * @param {string} SITE_CONFIG_RECORD_ID - The unique identifier of the site configuration
2519
+ * @returns {Promise<Object>} The site configuration object
2520
+ * @throws {Error} 401 - The caller is not authenticated
2521
+ * @throws {Error} 404 - The site configuration was not found
2522
+ *
2523
+ * @example
2524
+ * const config = await nomadSDK.getSiteConfig("config-123");
2525
+ *
2526
+ * @see {@link https://docs.nomad-cms.com/docs/api-guide/media-api/get-site-config Portal API} GET /media/config/{id}
2527
+ */
2528
+ async function _getSiteConfig(instance, SITE_CONFIG_RECORD_ID)
1478
2529
  {
1479
- const API_URL = `${instance.config.serviceApiUrl}/api/media/config/${ID}`;
2530
+ const API_URL = `${instance.config.serviceApiUrl}/api/media/config/${SITE_CONFIG_RECORD_ID}`;
1480
2531
 
1481
2532
  return await _sendRequest(instance, "Get Site Config", API_URL, "GET", undefined, undefined);
1482
2533
  }
@@ -1484,19 +2535,43 @@ async function _getSiteConfig(instance, ID)
1484
2535
 
1485
2536
 
1486
2537
 
1487
- async function _mediaSearch(instance, SEARCH_QUERY, IDS, SORT_FIELDS,
1488
- PAGE_OFFSET, PAGE_SIZE)
2538
+ /**
2539
+ * Search for media content using a query string and optional filters.
2540
+ *
2541
+ * Performs a search against the media index with support for pagination,
2542
+ * sorting, and ID filtering. Defaults to page offset 0 and page size 100.
2543
+ *
2544
+ * **Authorization:** Requires a valid Bearer token.
2545
+ *
2546
+ * @param {Object} instance - The SDK instance containing configuration and auth token
2547
+ * @param {Object} QUERY - The search query object containing search criteria
2548
+ * @param {Array<string>} [IDS] - Optional array of specific content IDs to filter by
2549
+ * @param {Array<Object>} [SORT_FIELDS] - Optional array of sort field objects
2550
+ * @param {number} [OFFSET=0] - The page offset for pagination (defaults to 0)
2551
+ * @param {number} [SIZE=100] - The number of results per page (defaults to 100)
2552
+ * @returns {Promise<Object>} Search results containing items array and pagination info
2553
+ * @throws {Error} 401 - The caller is not authenticated
2554
+ *
2555
+ * @example
2556
+ * const results = await nomadSDK.mediaSearch(
2557
+ * { query: "news" }, null, [{ fieldName: "date", order: "desc" }], 0, 25
2558
+ * );
2559
+ *
2560
+ * @see {@link https://docs.nomad-cms.com/docs/api-guide/media-api/media-search Portal API} POST /media/search
2561
+ */
2562
+ async function _mediaSearch(instance, QUERY, IDS, SORT_FIELDS,
2563
+ OFFSET, SIZE)
1489
2564
  {
1490
2565
  const API_URL = `${instance.config.serviceApiUrl}/api/media/search`;
1491
2566
 
1492
2567
  const BODY = {
1493
- searchQuery: SEARCH_QUERY,
2568
+ searchQuery: QUERY,
1494
2569
  ids: IDS,
1495
2570
  sortFields: SORT_FIELDS
1496
2571
  };
1497
2572
 
1498
- PAGE_OFFSET ? BODY["pageOffset"] = PAGE_OFFSET : BODY["pageOffset"] = 0;
1499
- PAGE_SIZE ? BODY["pageSize"] = PAGE_SIZE : BODY["pageSize"] = 100;
2573
+ OFFSET ? BODY["pageOffset"] = OFFSET : BODY["pageOffset"] = 0;
2574
+ SIZE ? BODY["pageSize"] = SIZE : BODY["pageSize"] = 100;
1500
2575
 
1501
2576
  return await _sendRequest(instance, "Media Search", API_URL, "POST", undefined, BODY);
1502
2577
  }