@mediaviz/sdk 1.2.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/sdk.cjs CHANGED
@@ -91,8 +91,15 @@ function requireErrors () {
91
91
  * @returns {OAuthError}
92
92
  */
93
93
  static fromResponse(status, body) {
94
- if (body && typeof body === 'object' && typeof body.error === 'string') {
95
- return new OAuthError(body.error, body.error_description ?? '', status, body);
94
+ // FastAPI wraps HTTPException payloads in a `detail` envelope, so an OAuth
95
+ // error arrives as { detail: { error, error_description } }. Unwrap it
96
+ // before falling back, or every server-side OAuth error reads as server_error.
97
+ const payload =
98
+ body && typeof body === 'object' && typeof body.error !== 'string' && body.detail
99
+ ? body.detail
100
+ : body;
101
+ if (payload && typeof payload === 'object' && typeof payload.error === 'string') {
102
+ return new OAuthError(payload.error, payload.error_description ?? '', status, body);
96
103
  }
97
104
  return new OAuthError('server_error', 'Unexpected server response', status, body);
98
105
  }
@@ -173,6 +180,9 @@ function requireClient () {
173
180
  const { postForm, postJson } = requireHttp();
174
181
  const { OAuthError } = requireErrors();
175
182
 
183
+ // In-flight dedup key for client_credentials re-mints (no refresh_token to key on).
184
+ const CLIENT_CREDENTIALS_KEY = Symbol('client_credentials');
185
+
176
186
  class OAuthClient {
177
187
  /**
178
188
  * @param {import('./types').OAuthClientConfig} config
@@ -188,12 +198,22 @@ function requireClient () {
188
198
  */
189
199
  static async registerClient(params) {
190
200
  const baseUrl = params.baseUrl.replace(/\/+$/, '');
191
- return postJson(`${baseUrl}/oauth/clients`, {
201
+ const body = {
192
202
  client_name: params.clientName,
193
203
  client_type: params.clientType,
194
204
  redirect_uris: params.redirectUris,
195
205
  is_first_party: params.isFirstParty,
196
- });
206
+ };
207
+ // grant_types/company_id are required to provision a confidential
208
+ // client_credentials (machine-to-machine) client; omitted bodies keep the
209
+ // legacy public/authorization_code shape. The endpoint is admin-gated, so a
210
+ // bearer token must be supplied for the request to authenticate.
211
+ if (params.grantTypes !== undefined) body.grant_types = params.grantTypes;
212
+ if (params.companyId !== undefined) body.company_id = params.companyId;
213
+ const headers = params.accessToken
214
+ ? { Authorization: `Bearer ${params.accessToken}` }
215
+ : {};
216
+ return postJson(`${baseUrl}/oauth/clients`, body, headers);
197
217
  }
198
218
 
199
219
  /**
@@ -311,15 +331,27 @@ function requireClient () {
311
331
  return { data: json, updatedTokens: null };
312
332
  }
313
333
 
314
- // 401: attempt refresh — propagate OAuthError if refresh fails.
315
- // Concurrent request() callers with the same refresh_token share one in-flight
316
- // call so the server only sees one rotation (the second would get invalid_grant).
317
- let pending = this._inFlightRefreshes.get(refreshToken);
334
+ // 401: re-authenticate, then retry once.
335
+ // - User sessions (refresh_token present) use the refresh_token grant. The server
336
+ // rotates the token one-time, so concurrent callers share one in-flight call
337
+ // (a second rotation would get invalid_grant).
338
+ // - Machine-to-machine sessions (no refresh_token, client_secret configured) re-mint
339
+ // via client_credentials, which issues no refresh token to rotate (RFC 6749 §4.4).
340
+ // - With neither a refresh_token nor a client_secret there is nothing to retry with.
341
+ if (!refreshToken && !this._config.clientSecret) {
342
+ throw OAuthError.fromResponse(401, await firstResponse.json());
343
+ }
344
+
345
+ const reauthKey = refreshToken || CLIENT_CREDENTIALS_KEY;
346
+ let pending = this._inFlightRefreshes.get(reauthKey);
318
347
  if (!pending) {
319
- pending = this.refreshAccessToken(refreshToken).finally(() => {
320
- this._inFlightRefreshes.delete(refreshToken);
348
+ const reauth = refreshToken
349
+ ? this.refreshAccessToken(refreshToken)
350
+ : this.getClientCredentialsToken();
351
+ pending = reauth.finally(() => {
352
+ this._inFlightRefreshes.delete(reauthKey);
321
353
  });
322
- this._inFlightRefreshes.set(refreshToken, pending);
354
+ this._inFlightRefreshes.set(reauthKey, pending);
323
355
  }
324
356
  const newTokens = await pending;
325
357
 
@@ -401,6 +433,9 @@ function requireTypes () {
401
433
  * @property {string} clientType - "public" or "confidential"
402
434
  * @property {string[]} redirectUris - HTTPS redirect URIs
403
435
  * @property {boolean} isFirstParty - Whether this is a first-party client
436
+ * @property {string[]} [grantTypes] - Allowed grants, e.g. ["client_credentials"]; required for machine-to-machine clients
437
+ * @property {number} [companyId] - Company to bind the client to; required for the client_credentials grant
438
+ * @property {string} [accessToken] - Admin bearer token; the registration endpoint is authorization-gated
404
439
  */
405
440
 
406
441
  /**
@@ -458,6 +493,17 @@ class AiModelCredits {
458
493
  }
459
494
  }
460
495
 
496
+ class Admin {
497
+ constructor(ctx) { this._ctx = ctx; }
498
+
499
+ async getCategoryLabels(category) {
500
+ this._ctx.requireTokens();
501
+ const path = `/api/v1/admin/category_labels/${encodeURIComponent(category)}`;
502
+ const { data } = await this._ctx.client.request(path, 'GET', this._ctx.accessToken, this._ctx.refreshToken);
503
+ return data;
504
+ }
505
+ }
506
+
461
507
  class Company {
462
508
  constructor(ctx) { this._ctx = ctx; }
463
509
 
@@ -901,6 +947,17 @@ class Keywords {
901
947
  return data;
902
948
  }
903
949
 
950
+ async addCompanyToKeywordFilteringList(keywordListId, { companyId } = {}) {
951
+ this._ctx.requireTokens();
952
+ let path = `/api/v1/keyword/${encodeURIComponent(keywordListId)}/company`;
953
+ const query = new URLSearchParams();
954
+ if (companyId !== undefined) (Array.isArray(companyId) ? companyId : [companyId]).forEach(v => query.append('company_id', v));
955
+ const qs = query.toString();
956
+ if (qs) path += '?' + qs;
957
+ const { data } = await this._ctx.client.request(path, 'POST', this._ctx.accessToken, this._ctx.refreshToken);
958
+ return data;
959
+ }
960
+
904
961
  async requestKeywordListExport(keywordListId) {
905
962
  this._ctx.requireTokens();
906
963
  const path = `/api/v1/keyword/export/${encodeURIComponent(keywordListId)}`;
@@ -933,6 +990,17 @@ class Keywords {
933
990
  return data;
934
991
  }
935
992
 
993
+ async removeCompanyFromKeywordFilteringList(keywordListId, { companyId } = {}) {
994
+ this._ctx.requireTokens();
995
+ let path = `/api/v1/keyword/${encodeURIComponent(keywordListId)}/company`;
996
+ const query = new URLSearchParams();
997
+ if (companyId !== undefined) (Array.isArray(companyId) ? companyId : [companyId]).forEach(v => query.append('company_id', v));
998
+ const qs = query.toString();
999
+ if (qs) path += '?' + qs;
1000
+ const { data } = await this._ctx.client.request(path, 'DELETE', this._ctx.accessToken, this._ctx.refreshToken);
1001
+ return data;
1002
+ }
1003
+
936
1004
  async deleteKeywordFilteringListById(keywordListId) {
937
1005
  this._ctx.requireTokens();
938
1006
  const path = `/api/v1/keyword/${encodeURIComponent(keywordListId)}`;
@@ -1092,6 +1160,17 @@ class Person {
1092
1160
  return data;
1093
1161
  }
1094
1162
 
1163
+ async getPhotoIdsByPersons(projectTableName, { personId } = {}) {
1164
+ this._ctx.requireTokens();
1165
+ let path = `/api/v1/person/${encodeURIComponent(projectTableName)}/photos`;
1166
+ const query = new URLSearchParams();
1167
+ if (personId !== undefined) (Array.isArray(personId) ? personId : [personId]).forEach(v => query.append('person_id', v));
1168
+ const qs = query.toString();
1169
+ if (qs) path += '?' + qs;
1170
+ const { data } = await this._ctx.client.request(path, 'GET', this._ctx.accessToken, this._ctx.refreshToken);
1171
+ return data;
1172
+ }
1173
+
1095
1174
  async getAllPersonsFromPhoto(projectTableName, photoId) {
1096
1175
  this._ctx.requireTokens();
1097
1176
  const path = `/api/v1/person/${encodeURIComponent(projectTableName)}/photo/${encodeURIComponent(photoId)}`;
@@ -1392,13 +1471,6 @@ class Photos {
1392
1471
  return data;
1393
1472
  }
1394
1473
 
1395
- async getProjectThumbnail(tableName) {
1396
- this._ctx.requireTokens();
1397
- const path = `/api/v1/photos_project/${encodeURIComponent(tableName)}`;
1398
- const { data } = await this._ctx.client.request(path, 'GET', this._ctx.accessToken, this._ctx.refreshToken);
1399
- return data;
1400
- }
1401
-
1402
1474
  async updatePhotoInProject({ tableName, photoId, photoData } = {}) {
1403
1475
  this._ctx.requireTokens();
1404
1476
  let path = `/api/v1/photos_update`;
@@ -1461,50 +1533,56 @@ class Search {
1461
1533
  return data;
1462
1534
  }
1463
1535
 
1464
- async searchProjectPhotosText(projectTableName, searchText, size = undefined) {
1536
+ async searchProjectPhotosParametrized(projectTableName, { andSearchText, orSearchText, notSearchText, city, country, state, albums, dateMin, dateMax, dateNullAnd, dateNullOr, ascOrDesc, bestOfSimilarSetsOnly, splitByTier, lastId, limit } = {}) {
1465
1537
  this._ctx.requireTokens();
1466
- const path = `/api/v1/search/text/${encodeURIComponent(projectTableName)}/`;
1467
- const body = stripUndef$1({
1468
- search_text: searchText,
1469
- size: size,
1470
- });
1471
- const { data } = await this._ctx.client.request(path, 'POST', this._ctx.accessToken, this._ctx.refreshToken, body);
1472
- return data;
1473
- }
1474
-
1475
- async searchProjectPhotosNaturalLanguage(projectTableName, searchText, size = undefined) {
1476
- this._ctx.requireTokens();
1477
- const path = `/api/v1/search/nl/${encodeURIComponent(projectTableName)}/`;
1478
- const body = stripUndef$1({
1479
- search_text: searchText,
1480
- size: size,
1481
- });
1482
- const { data } = await this._ctx.client.request(path, 'POST', this._ctx.accessToken, this._ctx.refreshToken, body);
1538
+ let path = `/api/v1/search/parametrized/${encodeURIComponent(projectTableName)}/`;
1539
+ const query = new URLSearchParams();
1540
+ if (andSearchText !== undefined) (Array.isArray(andSearchText) ? andSearchText : [andSearchText]).forEach(v => query.append('and_search_text', v));
1541
+ if (orSearchText !== undefined) (Array.isArray(orSearchText) ? orSearchText : [orSearchText]).forEach(v => query.append('or_search_text', v));
1542
+ if (notSearchText !== undefined) (Array.isArray(notSearchText) ? notSearchText : [notSearchText]).forEach(v => query.append('not_search_text', v));
1543
+ if (city !== undefined) (Array.isArray(city) ? city : [city]).forEach(v => query.append('city', v));
1544
+ if (country !== undefined) (Array.isArray(country) ? country : [country]).forEach(v => query.append('country', v));
1545
+ if (state !== undefined) (Array.isArray(state) ? state : [state]).forEach(v => query.append('state', v));
1546
+ if (albums !== undefined) (Array.isArray(albums) ? albums : [albums]).forEach(v => query.append('albums', v));
1547
+ if (dateMin !== undefined) (Array.isArray(dateMin) ? dateMin : [dateMin]).forEach(v => query.append('date_min', v));
1548
+ if (dateMax !== undefined) (Array.isArray(dateMax) ? dateMax : [dateMax]).forEach(v => query.append('date_max', v));
1549
+ if (dateNullAnd !== undefined) (Array.isArray(dateNullAnd) ? dateNullAnd : [dateNullAnd]).forEach(v => query.append('date_null_and', v));
1550
+ if (dateNullOr !== undefined) (Array.isArray(dateNullOr) ? dateNullOr : [dateNullOr]).forEach(v => query.append('date_null_or', v));
1551
+ if (ascOrDesc !== undefined) (Array.isArray(ascOrDesc) ? ascOrDesc : [ascOrDesc]).forEach(v => query.append('asc_or_desc', v));
1552
+ if (bestOfSimilarSetsOnly !== undefined) (Array.isArray(bestOfSimilarSetsOnly) ? bestOfSimilarSetsOnly : [bestOfSimilarSetsOnly]).forEach(v => query.append('best_of_similar_sets_only', v));
1553
+ if (splitByTier !== undefined) (Array.isArray(splitByTier) ? splitByTier : [splitByTier]).forEach(v => query.append('split_by_tier', v));
1554
+ if (lastId !== undefined) (Array.isArray(lastId) ? lastId : [lastId]).forEach(v => query.append('last_id', v));
1555
+ if (limit !== undefined) (Array.isArray(limit) ? limit : [limit]).forEach(v => query.append('limit', v));
1556
+ const qs = query.toString();
1557
+ if (qs) path += '?' + qs;
1558
+ const { data } = await this._ctx.client.request(path, 'GET', this._ctx.accessToken, this._ctx.refreshToken);
1483
1559
  return data;
1484
1560
  }
1485
1561
 
1486
- async searchProjectPhotosNaturalLanguageHybrid(projectTableName, searchText, size = undefined, { blend, minCosine } = {}) {
1562
+ async recallProjectPhotoIds(projectTableName, { ascOrDesc, bestOfSimilarSetsOnly, lastId, limit } = {}) {
1487
1563
  this._ctx.requireTokens();
1488
- let path = `/api/v1/search/nl_hybrid/${encodeURIComponent(projectTableName)}/`;
1564
+ let path = `/api/v1/search/recall/${encodeURIComponent(projectTableName)}/`;
1489
1565
  const query = new URLSearchParams();
1490
- if (blend !== undefined) (Array.isArray(blend) ? blend : [blend]).forEach(v => query.append('blend', v));
1491
- if (minCosine !== undefined) (Array.isArray(minCosine) ? minCosine : [minCosine]).forEach(v => query.append('min_cosine', v));
1566
+ if (ascOrDesc !== undefined) (Array.isArray(ascOrDesc) ? ascOrDesc : [ascOrDesc]).forEach(v => query.append('asc_or_desc', v));
1567
+ if (bestOfSimilarSetsOnly !== undefined) (Array.isArray(bestOfSimilarSetsOnly) ? bestOfSimilarSetsOnly : [bestOfSimilarSetsOnly]).forEach(v => query.append('best_of_similar_sets_only', v));
1568
+ if (lastId !== undefined) (Array.isArray(lastId) ? lastId : [lastId]).forEach(v => query.append('last_id', v));
1569
+ if (limit !== undefined) (Array.isArray(limit) ? limit : [limit]).forEach(v => query.append('limit', v));
1492
1570
  const qs = query.toString();
1493
1571
  if (qs) path += '?' + qs;
1494
- const body = stripUndef$1({
1495
- search_text: searchText,
1496
- size: size,
1497
- });
1498
- const { data } = await this._ctx.client.request(path, 'POST', this._ctx.accessToken, this._ctx.refreshToken, body);
1572
+ const { data } = await this._ctx.client.request(path, 'GET', this._ctx.accessToken, this._ctx.refreshToken);
1499
1573
  return data;
1500
1574
  }
1501
1575
 
1502
- async searchProjectPhotosNaturalLanguageAuto(projectTableName, searchText, size = undefined, { blend, minCosine } = {}) {
1576
+ async searchProjectPhotosNaturalLanguage(projectTableName, searchText, size = undefined, { minCosine, labelMinCosine, labelTopK, labelDelta, bestOfSimilarSetsOnly, splitByTier } = {}) {
1503
1577
  this._ctx.requireTokens();
1504
- let path = `/api/v1/search/auto/${encodeURIComponent(projectTableName)}/`;
1578
+ let path = `/api/v1/search/natural_language/${encodeURIComponent(projectTableName)}/`;
1505
1579
  const query = new URLSearchParams();
1506
- if (blend !== undefined) (Array.isArray(blend) ? blend : [blend]).forEach(v => query.append('blend', v));
1507
1580
  if (minCosine !== undefined) (Array.isArray(minCosine) ? minCosine : [minCosine]).forEach(v => query.append('min_cosine', v));
1581
+ if (labelMinCosine !== undefined) (Array.isArray(labelMinCosine) ? labelMinCosine : [labelMinCosine]).forEach(v => query.append('label_min_cosine', v));
1582
+ if (labelTopK !== undefined) (Array.isArray(labelTopK) ? labelTopK : [labelTopK]).forEach(v => query.append('label_top_k', v));
1583
+ if (labelDelta !== undefined) (Array.isArray(labelDelta) ? labelDelta : [labelDelta]).forEach(v => query.append('label_delta', v));
1584
+ if (bestOfSimilarSetsOnly !== undefined) (Array.isArray(bestOfSimilarSetsOnly) ? bestOfSimilarSetsOnly : [bestOfSimilarSetsOnly]).forEach(v => query.append('best_of_similar_sets_only', v));
1585
+ if (splitByTier !== undefined) (Array.isArray(splitByTier) ? splitByTier : [splitByTier]).forEach(v => query.append('split_by_tier', v));
1508
1586
  const qs = query.toString();
1509
1587
  if (qs) path += '?' + qs;
1510
1588
  const body = stripUndef$1({
@@ -1656,6 +1734,17 @@ class Users {
1656
1734
  const { data } = await this._ctx.client.request(path, 'PUT', this._ctx.accessToken, this._ctx.refreshToken, body);
1657
1735
  return data;
1658
1736
  }
1737
+
1738
+ async deleteUser(userId, { newCompanyOwnerId } = {}) {
1739
+ this._ctx.requireTokens();
1740
+ let path = `/api/v1/users/delete/${encodeURIComponent(userId)}`;
1741
+ const query = new URLSearchParams();
1742
+ if (newCompanyOwnerId !== undefined) (Array.isArray(newCompanyOwnerId) ? newCompanyOwnerId : [newCompanyOwnerId]).forEach(v => query.append('new_company_owner_id', v));
1743
+ const qs = query.toString();
1744
+ if (qs) path += '?' + qs;
1745
+ const { data } = await this._ctx.client.request(path, 'DELETE', this._ctx.accessToken, this._ctx.refreshToken);
1746
+ return data;
1747
+ }
1659
1748
  }
1660
1749
 
1661
1750
  // Auto-generated — do not edit
@@ -1720,6 +1809,7 @@ class MediaViz {
1720
1809
 
1721
1810
  const _ctx = new _Context(this);
1722
1811
  this.aiModelCredits = new AiModelCredits(_ctx);
1812
+ this.admin = new Admin(_ctx);
1723
1813
  this.company = new Company(_ctx);
1724
1814
  this.curatedAlbums = new CuratedAlbums(_ctx);
1725
1815
  this.customAlbums = new CustomAlbums(_ctx);
@@ -1764,6 +1854,7 @@ class MediaViz {
1764
1854
  get refreshToken() { return this._refreshToken; }
1765
1855
  }
1766
1856
 
1857
+ exports.Admin = Admin;
1767
1858
  exports.AiModelCredits = AiModelCredits;
1768
1859
  exports.ApiError = ApiError;
1769
1860
  exports.Company = Company;