@mediaviz/sdk 1.0.78 → 1.0.80

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
@@ -173,6 +173,9 @@ function requireClient () {
173
173
  const { postForm, postJson } = requireHttp();
174
174
  const { OAuthError } = requireErrors();
175
175
 
176
+ // In-flight dedup key for client_credentials re-mints (no refresh_token to key on).
177
+ const CLIENT_CREDENTIALS_KEY = Symbol('client_credentials');
178
+
176
179
  class OAuthClient {
177
180
  /**
178
181
  * @param {import('./types').OAuthClientConfig} config
@@ -311,15 +314,27 @@ function requireClient () {
311
314
  return { data: json, updatedTokens: null };
312
315
  }
313
316
 
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);
317
+ // 401: re-authenticate, then retry once.
318
+ // - User sessions (refresh_token present) use the refresh_token grant. The server
319
+ // rotates the token one-time, so concurrent callers share one in-flight call
320
+ // (a second rotation would get invalid_grant).
321
+ // - Machine-to-machine sessions (no refresh_token, client_secret configured) re-mint
322
+ // via client_credentials, which issues no refresh token to rotate (RFC 6749 §4.4).
323
+ // - With neither a refresh_token nor a client_secret there is nothing to retry with.
324
+ if (!refreshToken && !this._config.clientSecret) {
325
+ throw OAuthError.fromResponse(401, await firstResponse.json());
326
+ }
327
+
328
+ const reauthKey = refreshToken || CLIENT_CREDENTIALS_KEY;
329
+ let pending = this._inFlightRefreshes.get(reauthKey);
318
330
  if (!pending) {
319
- pending = this.refreshAccessToken(refreshToken).finally(() => {
320
- this._inFlightRefreshes.delete(refreshToken);
331
+ const reauth = refreshToken
332
+ ? this.refreshAccessToken(refreshToken)
333
+ : this.getClientCredentialsToken();
334
+ pending = reauth.finally(() => {
335
+ this._inFlightRefreshes.delete(reauthKey);
321
336
  });
322
- this._inFlightRefreshes.set(refreshToken, pending);
337
+ this._inFlightRefreshes.set(reauthKey, pending);
323
338
  }
324
339
  const newTokens = await pending;
325
340
 
@@ -1125,6 +1140,17 @@ class Person {
1125
1140
  return data;
1126
1141
  }
1127
1142
 
1143
+ async getPhotoIdsByPersons(projectTableName, { personId } = {}) {
1144
+ this._ctx.requireTokens();
1145
+ let path = `/api/v1/person/${encodeURIComponent(projectTableName)}/photos`;
1146
+ const query = new URLSearchParams();
1147
+ if (personId !== undefined) (Array.isArray(personId) ? personId : [personId]).forEach(v => query.append('person_id', v));
1148
+ const qs = query.toString();
1149
+ if (qs) path += '?' + qs;
1150
+ const { data } = await this._ctx.client.request(path, 'GET', this._ctx.accessToken, this._ctx.refreshToken);
1151
+ return data;
1152
+ }
1153
+
1128
1154
  async getAllPersonsFromPhoto(projectTableName, photoId) {
1129
1155
  this._ctx.requireTokens();
1130
1156
  const path = `/api/v1/person/${encodeURIComponent(projectTableName)}/photo/${encodeURIComponent(photoId)}`;
@@ -1487,7 +1513,7 @@ class Search {
1487
1513
  return data;
1488
1514
  }
1489
1515
 
1490
- async searchProjectPhotosParametrized(projectTableName, { andSearchText, orSearchText, notSearchText, city, country, state, albums, dateMin, dateMax, dateNullAnd, dateNullOr, dateOrder, bestOfSimilarSetsOnly, splitByTier, size } = {}) {
1516
+ async searchProjectPhotosParametrized(projectTableName, { andSearchText, orSearchText, notSearchText, city, country, state, albums, dateMin, dateMax, dateNullAnd, dateNullOr, ascOrDesc, bestOfSimilarSetsOnly, splitByTier, lastId, limit } = {}) {
1491
1517
  this._ctx.requireTokens();
1492
1518
  let path = `/api/v1/search/parametrized/${encodeURIComponent(projectTableName)}/`;
1493
1519
  const query = new URLSearchParams();
@@ -1502,10 +1528,25 @@ class Search {
1502
1528
  if (dateMax !== undefined) (Array.isArray(dateMax) ? dateMax : [dateMax]).forEach(v => query.append('date_max', v));
1503
1529
  if (dateNullAnd !== undefined) (Array.isArray(dateNullAnd) ? dateNullAnd : [dateNullAnd]).forEach(v => query.append('date_null_and', v));
1504
1530
  if (dateNullOr !== undefined) (Array.isArray(dateNullOr) ? dateNullOr : [dateNullOr]).forEach(v => query.append('date_null_or', v));
1505
- if (dateOrder !== undefined) (Array.isArray(dateOrder) ? dateOrder : [dateOrder]).forEach(v => query.append('date_order', v));
1531
+ if (ascOrDesc !== undefined) (Array.isArray(ascOrDesc) ? ascOrDesc : [ascOrDesc]).forEach(v => query.append('asc_or_desc', v));
1506
1532
  if (bestOfSimilarSetsOnly !== undefined) (Array.isArray(bestOfSimilarSetsOnly) ? bestOfSimilarSetsOnly : [bestOfSimilarSetsOnly]).forEach(v => query.append('best_of_similar_sets_only', v));
1507
1533
  if (splitByTier !== undefined) (Array.isArray(splitByTier) ? splitByTier : [splitByTier]).forEach(v => query.append('split_by_tier', v));
1508
- if (size !== undefined) (Array.isArray(size) ? size : [size]).forEach(v => query.append('size', v));
1534
+ if (lastId !== undefined) (Array.isArray(lastId) ? lastId : [lastId]).forEach(v => query.append('last_id', v));
1535
+ if (limit !== undefined) (Array.isArray(limit) ? limit : [limit]).forEach(v => query.append('limit', v));
1536
+ const qs = query.toString();
1537
+ if (qs) path += '?' + qs;
1538
+ const { data } = await this._ctx.client.request(path, 'GET', this._ctx.accessToken, this._ctx.refreshToken);
1539
+ return data;
1540
+ }
1541
+
1542
+ async recallProjectPhotoIds(projectTableName, { ascOrDesc, bestOfSimilarSetsOnly, lastId, limit } = {}) {
1543
+ this._ctx.requireTokens();
1544
+ let path = `/api/v1/search/recall/${encodeURIComponent(projectTableName)}/`;
1545
+ const query = new URLSearchParams();
1546
+ if (ascOrDesc !== undefined) (Array.isArray(ascOrDesc) ? ascOrDesc : [ascOrDesc]).forEach(v => query.append('asc_or_desc', v));
1547
+ if (bestOfSimilarSetsOnly !== undefined) (Array.isArray(bestOfSimilarSetsOnly) ? bestOfSimilarSetsOnly : [bestOfSimilarSetsOnly]).forEach(v => query.append('best_of_similar_sets_only', v));
1548
+ if (lastId !== undefined) (Array.isArray(lastId) ? lastId : [lastId]).forEach(v => query.append('last_id', v));
1549
+ if (limit !== undefined) (Array.isArray(limit) ? limit : [limit]).forEach(v => query.append('limit', v));
1509
1550
  const qs = query.toString();
1510
1551
  if (qs) path += '?' + qs;
1511
1552
  const { data } = await this._ctx.client.request(path, 'GET', this._ctx.accessToken, this._ctx.refreshToken);
package/dist/sdk.d.cts CHANGED
@@ -411,6 +411,7 @@ export class Person {
411
411
  splitPersons(projectTableName: string, id: number, options?: { newName?: string | string[], destinationPersonId?: string | string[] }): Promise<PhotoPersonLight>;
412
412
  getAllPersonsFromProject(projectTableName: string): Promise<PhotoPersonWithS3Link[]>;
413
413
  getAllPersonNamesFromProject(projectTableName: string): Promise<any>;
414
+ getPhotoIdsByPersons(projectTableName: string, options?: { personId?: string[] | string[][] }): Promise<any>;
414
415
  getAllPersonsFromPhoto(projectTableName: string, photoId: number): Promise<PhotoPersonLight[]>;
415
416
  }
416
417
 
@@ -454,7 +455,8 @@ export class Projects {
454
455
 
455
456
  export class Search {
456
457
  searchProjectPhotos(projectTableName: string, options?: { andParams?: any, andStringParams?: any, orParams?: any, orStringParams?: any, notParams?: any, notStringParams?: any, dateMin?: string | string[], dateMax?: string | string[], dateNullAnd?: boolean | boolean[], dateNullOr?: boolean | boolean[], dateOrder?: string | string[], customAlbumId?: number | number[], bestOfSimilarSetsOnly?: boolean | boolean[], curatedAlbumId?: number | number[], splitByTier?: boolean | boolean[] }): Promise<Record<string, any>>;
457
- searchProjectPhotosParametrized(projectTableName: string, options?: { andSearchText?: any, orSearchText?: any, notSearchText?: any, city?: any, country?: any, state?: any, albums?: any, dateMin?: string | string[], dateMax?: string | string[], dateNullAnd?: boolean | boolean[], dateNullOr?: boolean | boolean[], dateOrder?: string | string[], bestOfSimilarSetsOnly?: boolean | boolean[], splitByTier?: boolean | boolean[], size?: number | number[] }): Promise<Record<string, any>>;
458
+ searchProjectPhotosParametrized(projectTableName: string, options?: { andSearchText?: any, orSearchText?: any, notSearchText?: any, city?: any, country?: any, state?: any, albums?: any, dateMin?: string | string[], dateMax?: string | string[], dateNullAnd?: boolean | boolean[], dateNullOr?: boolean | boolean[], ascOrDesc?: string | string[], bestOfSimilarSetsOnly?: boolean | boolean[], splitByTier?: boolean | boolean[], lastId?: number | number[], limit?: number | number[] }): Promise<Record<string, any>>;
459
+ recallProjectPhotoIds(projectTableName: string, options?: { ascOrDesc?: string | string[], bestOfSimilarSetsOnly?: boolean | boolean[], lastId?: number | number[], limit?: number | number[] }): Promise<any>;
458
460
  searchProjectPhotosNaturalLanguage(projectTableName: string, searchText: string, size?: number, options?: { minCosine?: number | number[], labelMinCosine?: number | number[], labelTopK?: number | number[], labelDelta?: number | number[], bestOfSimilarSetsOnly?: boolean | boolean[], splitByTier?: boolean | boolean[] }): Promise<Record<string, any>>;
459
461
  getProjectSavedSearches(projectTableName: string): Promise<any>;
460
462
  getSavedSearchById(searchId: number): Promise<SearchDisplay>;
package/dist/sdk.d.ts CHANGED
@@ -411,6 +411,7 @@ export class Person {
411
411
  splitPersons(projectTableName: string, id: number, options?: { newName?: string | string[], destinationPersonId?: string | string[] }): Promise<PhotoPersonLight>;
412
412
  getAllPersonsFromProject(projectTableName: string): Promise<PhotoPersonWithS3Link[]>;
413
413
  getAllPersonNamesFromProject(projectTableName: string): Promise<any>;
414
+ getPhotoIdsByPersons(projectTableName: string, options?: { personId?: string[] | string[][] }): Promise<any>;
414
415
  getAllPersonsFromPhoto(projectTableName: string, photoId: number): Promise<PhotoPersonLight[]>;
415
416
  }
416
417
 
@@ -454,7 +455,8 @@ export class Projects {
454
455
 
455
456
  export class Search {
456
457
  searchProjectPhotos(projectTableName: string, options?: { andParams?: any, andStringParams?: any, orParams?: any, orStringParams?: any, notParams?: any, notStringParams?: any, dateMin?: string | string[], dateMax?: string | string[], dateNullAnd?: boolean | boolean[], dateNullOr?: boolean | boolean[], dateOrder?: string | string[], customAlbumId?: number | number[], bestOfSimilarSetsOnly?: boolean | boolean[], curatedAlbumId?: number | number[], splitByTier?: boolean | boolean[] }): Promise<Record<string, any>>;
457
- searchProjectPhotosParametrized(projectTableName: string, options?: { andSearchText?: any, orSearchText?: any, notSearchText?: any, city?: any, country?: any, state?: any, albums?: any, dateMin?: string | string[], dateMax?: string | string[], dateNullAnd?: boolean | boolean[], dateNullOr?: boolean | boolean[], dateOrder?: string | string[], bestOfSimilarSetsOnly?: boolean | boolean[], splitByTier?: boolean | boolean[], size?: number | number[] }): Promise<Record<string, any>>;
458
+ searchProjectPhotosParametrized(projectTableName: string, options?: { andSearchText?: any, orSearchText?: any, notSearchText?: any, city?: any, country?: any, state?: any, albums?: any, dateMin?: string | string[], dateMax?: string | string[], dateNullAnd?: boolean | boolean[], dateNullOr?: boolean | boolean[], ascOrDesc?: string | string[], bestOfSimilarSetsOnly?: boolean | boolean[], splitByTier?: boolean | boolean[], lastId?: number | number[], limit?: number | number[] }): Promise<Record<string, any>>;
459
+ recallProjectPhotoIds(projectTableName: string, options?: { ascOrDesc?: string | string[], bestOfSimilarSetsOnly?: boolean | boolean[], lastId?: number | number[], limit?: number | number[] }): Promise<any>;
458
460
  searchProjectPhotosNaturalLanguage(projectTableName: string, searchText: string, size?: number, options?: { minCosine?: number | number[], labelMinCosine?: number | number[], labelTopK?: number | number[], labelDelta?: number | number[], bestOfSimilarSetsOnly?: boolean | boolean[], splitByTier?: boolean | boolean[] }): Promise<Record<string, any>>;
459
461
  getProjectSavedSearches(projectTableName: string): Promise<any>;
460
462
  getSavedSearchById(searchId: number): Promise<SearchDisplay>;
package/dist/sdk.esm.d.ts CHANGED
@@ -411,6 +411,7 @@ export class Person {
411
411
  splitPersons(projectTableName: string, id: number, options?: { newName?: string | string[], destinationPersonId?: string | string[] }): Promise<PhotoPersonLight>;
412
412
  getAllPersonsFromProject(projectTableName: string): Promise<PhotoPersonWithS3Link[]>;
413
413
  getAllPersonNamesFromProject(projectTableName: string): Promise<any>;
414
+ getPhotoIdsByPersons(projectTableName: string, options?: { personId?: string[] | string[][] }): Promise<any>;
414
415
  getAllPersonsFromPhoto(projectTableName: string, photoId: number): Promise<PhotoPersonLight[]>;
415
416
  }
416
417
 
@@ -454,7 +455,8 @@ export class Projects {
454
455
 
455
456
  export class Search {
456
457
  searchProjectPhotos(projectTableName: string, options?: { andParams?: any, andStringParams?: any, orParams?: any, orStringParams?: any, notParams?: any, notStringParams?: any, dateMin?: string | string[], dateMax?: string | string[], dateNullAnd?: boolean | boolean[], dateNullOr?: boolean | boolean[], dateOrder?: string | string[], customAlbumId?: number | number[], bestOfSimilarSetsOnly?: boolean | boolean[], curatedAlbumId?: number | number[], splitByTier?: boolean | boolean[] }): Promise<Record<string, any>>;
457
- searchProjectPhotosParametrized(projectTableName: string, options?: { andSearchText?: any, orSearchText?: any, notSearchText?: any, city?: any, country?: any, state?: any, albums?: any, dateMin?: string | string[], dateMax?: string | string[], dateNullAnd?: boolean | boolean[], dateNullOr?: boolean | boolean[], dateOrder?: string | string[], bestOfSimilarSetsOnly?: boolean | boolean[], splitByTier?: boolean | boolean[], size?: number | number[] }): Promise<Record<string, any>>;
458
+ searchProjectPhotosParametrized(projectTableName: string, options?: { andSearchText?: any, orSearchText?: any, notSearchText?: any, city?: any, country?: any, state?: any, albums?: any, dateMin?: string | string[], dateMax?: string | string[], dateNullAnd?: boolean | boolean[], dateNullOr?: boolean | boolean[], ascOrDesc?: string | string[], bestOfSimilarSetsOnly?: boolean | boolean[], splitByTier?: boolean | boolean[], lastId?: number | number[], limit?: number | number[] }): Promise<Record<string, any>>;
459
+ recallProjectPhotoIds(projectTableName: string, options?: { ascOrDesc?: string | string[], bestOfSimilarSetsOnly?: boolean | boolean[], lastId?: number | number[], limit?: number | number[] }): Promise<any>;
458
460
  searchProjectPhotosNaturalLanguage(projectTableName: string, searchText: string, size?: number, options?: { minCosine?: number | number[], labelMinCosine?: number | number[], labelTopK?: number | number[], labelDelta?: number | number[], bestOfSimilarSetsOnly?: boolean | boolean[], splitByTier?: boolean | boolean[] }): Promise<Record<string, any>>;
459
461
  getProjectSavedSearches(projectTableName: string): Promise<any>;
460
462
  getSavedSearchById(searchId: number): Promise<SearchDisplay>;
package/dist/sdk.esm.js CHANGED
@@ -171,6 +171,9 @@ function requireClient () {
171
171
  const { postForm, postJson } = requireHttp();
172
172
  const { OAuthError } = requireErrors();
173
173
 
174
+ // In-flight dedup key for client_credentials re-mints (no refresh_token to key on).
175
+ const CLIENT_CREDENTIALS_KEY = Symbol('client_credentials');
176
+
174
177
  class OAuthClient {
175
178
  /**
176
179
  * @param {import('./types').OAuthClientConfig} config
@@ -309,15 +312,27 @@ function requireClient () {
309
312
  return { data: json, updatedTokens: null };
310
313
  }
311
314
 
312
- // 401: attempt refresh — propagate OAuthError if refresh fails.
313
- // Concurrent request() callers with the same refresh_token share one in-flight
314
- // call so the server only sees one rotation (the second would get invalid_grant).
315
- let pending = this._inFlightRefreshes.get(refreshToken);
315
+ // 401: re-authenticate, then retry once.
316
+ // - User sessions (refresh_token present) use the refresh_token grant. The server
317
+ // rotates the token one-time, so concurrent callers share one in-flight call
318
+ // (a second rotation would get invalid_grant).
319
+ // - Machine-to-machine sessions (no refresh_token, client_secret configured) re-mint
320
+ // via client_credentials, which issues no refresh token to rotate (RFC 6749 §4.4).
321
+ // - With neither a refresh_token nor a client_secret there is nothing to retry with.
322
+ if (!refreshToken && !this._config.clientSecret) {
323
+ throw OAuthError.fromResponse(401, await firstResponse.json());
324
+ }
325
+
326
+ const reauthKey = refreshToken || CLIENT_CREDENTIALS_KEY;
327
+ let pending = this._inFlightRefreshes.get(reauthKey);
316
328
  if (!pending) {
317
- pending = this.refreshAccessToken(refreshToken).finally(() => {
318
- this._inFlightRefreshes.delete(refreshToken);
329
+ const reauth = refreshToken
330
+ ? this.refreshAccessToken(refreshToken)
331
+ : this.getClientCredentialsToken();
332
+ pending = reauth.finally(() => {
333
+ this._inFlightRefreshes.delete(reauthKey);
319
334
  });
320
- this._inFlightRefreshes.set(refreshToken, pending);
335
+ this._inFlightRefreshes.set(reauthKey, pending);
321
336
  }
322
337
  const newTokens = await pending;
323
338
 
@@ -1123,6 +1138,17 @@ class Person {
1123
1138
  return data;
1124
1139
  }
1125
1140
 
1141
+ async getPhotoIdsByPersons(projectTableName, { personId } = {}) {
1142
+ this._ctx.requireTokens();
1143
+ let path = `/api/v1/person/${encodeURIComponent(projectTableName)}/photos`;
1144
+ const query = new URLSearchParams();
1145
+ if (personId !== undefined) (Array.isArray(personId) ? personId : [personId]).forEach(v => query.append('person_id', v));
1146
+ const qs = query.toString();
1147
+ if (qs) path += '?' + qs;
1148
+ const { data } = await this._ctx.client.request(path, 'GET', this._ctx.accessToken, this._ctx.refreshToken);
1149
+ return data;
1150
+ }
1151
+
1126
1152
  async getAllPersonsFromPhoto(projectTableName, photoId) {
1127
1153
  this._ctx.requireTokens();
1128
1154
  const path = `/api/v1/person/${encodeURIComponent(projectTableName)}/photo/${encodeURIComponent(photoId)}`;
@@ -1485,7 +1511,7 @@ class Search {
1485
1511
  return data;
1486
1512
  }
1487
1513
 
1488
- async searchProjectPhotosParametrized(projectTableName, { andSearchText, orSearchText, notSearchText, city, country, state, albums, dateMin, dateMax, dateNullAnd, dateNullOr, dateOrder, bestOfSimilarSetsOnly, splitByTier, size } = {}) {
1514
+ async searchProjectPhotosParametrized(projectTableName, { andSearchText, orSearchText, notSearchText, city, country, state, albums, dateMin, dateMax, dateNullAnd, dateNullOr, ascOrDesc, bestOfSimilarSetsOnly, splitByTier, lastId, limit } = {}) {
1489
1515
  this._ctx.requireTokens();
1490
1516
  let path = `/api/v1/search/parametrized/${encodeURIComponent(projectTableName)}/`;
1491
1517
  const query = new URLSearchParams();
@@ -1500,10 +1526,25 @@ class Search {
1500
1526
  if (dateMax !== undefined) (Array.isArray(dateMax) ? dateMax : [dateMax]).forEach(v => query.append('date_max', v));
1501
1527
  if (dateNullAnd !== undefined) (Array.isArray(dateNullAnd) ? dateNullAnd : [dateNullAnd]).forEach(v => query.append('date_null_and', v));
1502
1528
  if (dateNullOr !== undefined) (Array.isArray(dateNullOr) ? dateNullOr : [dateNullOr]).forEach(v => query.append('date_null_or', v));
1503
- if (dateOrder !== undefined) (Array.isArray(dateOrder) ? dateOrder : [dateOrder]).forEach(v => query.append('date_order', v));
1529
+ if (ascOrDesc !== undefined) (Array.isArray(ascOrDesc) ? ascOrDesc : [ascOrDesc]).forEach(v => query.append('asc_or_desc', v));
1504
1530
  if (bestOfSimilarSetsOnly !== undefined) (Array.isArray(bestOfSimilarSetsOnly) ? bestOfSimilarSetsOnly : [bestOfSimilarSetsOnly]).forEach(v => query.append('best_of_similar_sets_only', v));
1505
1531
  if (splitByTier !== undefined) (Array.isArray(splitByTier) ? splitByTier : [splitByTier]).forEach(v => query.append('split_by_tier', v));
1506
- if (size !== undefined) (Array.isArray(size) ? size : [size]).forEach(v => query.append('size', v));
1532
+ if (lastId !== undefined) (Array.isArray(lastId) ? lastId : [lastId]).forEach(v => query.append('last_id', v));
1533
+ if (limit !== undefined) (Array.isArray(limit) ? limit : [limit]).forEach(v => query.append('limit', v));
1534
+ const qs = query.toString();
1535
+ if (qs) path += '?' + qs;
1536
+ const { data } = await this._ctx.client.request(path, 'GET', this._ctx.accessToken, this._ctx.refreshToken);
1537
+ return data;
1538
+ }
1539
+
1540
+ async recallProjectPhotoIds(projectTableName, { ascOrDesc, bestOfSimilarSetsOnly, lastId, limit } = {}) {
1541
+ this._ctx.requireTokens();
1542
+ let path = `/api/v1/search/recall/${encodeURIComponent(projectTableName)}/`;
1543
+ const query = new URLSearchParams();
1544
+ if (ascOrDesc !== undefined) (Array.isArray(ascOrDesc) ? ascOrDesc : [ascOrDesc]).forEach(v => query.append('asc_or_desc', v));
1545
+ if (bestOfSimilarSetsOnly !== undefined) (Array.isArray(bestOfSimilarSetsOnly) ? bestOfSimilarSetsOnly : [bestOfSimilarSetsOnly]).forEach(v => query.append('best_of_similar_sets_only', v));
1546
+ if (lastId !== undefined) (Array.isArray(lastId) ? lastId : [lastId]).forEach(v => query.append('last_id', v));
1547
+ if (limit !== undefined) (Array.isArray(limit) ? limit : [limit]).forEach(v => query.append('limit', v));
1507
1548
  const qs = query.toString();
1508
1549
  if (qs) path += '?' + qs;
1509
1550
  const { data } = await this._ctx.client.request(path, 'GET', this._ctx.accessToken, this._ctx.refreshToken);
package/dist/sdk.umd.js CHANGED
@@ -177,6 +177,9 @@
177
177
  const { postForm, postJson } = requireHttp();
178
178
  const { OAuthError } = requireErrors();
179
179
 
180
+ // In-flight dedup key for client_credentials re-mints (no refresh_token to key on).
181
+ const CLIENT_CREDENTIALS_KEY = Symbol('client_credentials');
182
+
180
183
  class OAuthClient {
181
184
  /**
182
185
  * @param {import('./types').OAuthClientConfig} config
@@ -315,15 +318,27 @@
315
318
  return { data: json, updatedTokens: null };
316
319
  }
317
320
 
318
- // 401: attempt refresh — propagate OAuthError if refresh fails.
319
- // Concurrent request() callers with the same refresh_token share one in-flight
320
- // call so the server only sees one rotation (the second would get invalid_grant).
321
- let pending = this._inFlightRefreshes.get(refreshToken);
321
+ // 401: re-authenticate, then retry once.
322
+ // - User sessions (refresh_token present) use the refresh_token grant. The server
323
+ // rotates the token one-time, so concurrent callers share one in-flight call
324
+ // (a second rotation would get invalid_grant).
325
+ // - Machine-to-machine sessions (no refresh_token, client_secret configured) re-mint
326
+ // via client_credentials, which issues no refresh token to rotate (RFC 6749 §4.4).
327
+ // - With neither a refresh_token nor a client_secret there is nothing to retry with.
328
+ if (!refreshToken && !this._config.clientSecret) {
329
+ throw OAuthError.fromResponse(401, await firstResponse.json());
330
+ }
331
+
332
+ const reauthKey = refreshToken || CLIENT_CREDENTIALS_KEY;
333
+ let pending = this._inFlightRefreshes.get(reauthKey);
322
334
  if (!pending) {
323
- pending = this.refreshAccessToken(refreshToken).finally(() => {
324
- this._inFlightRefreshes.delete(refreshToken);
335
+ const reauth = refreshToken
336
+ ? this.refreshAccessToken(refreshToken)
337
+ : this.getClientCredentialsToken();
338
+ pending = reauth.finally(() => {
339
+ this._inFlightRefreshes.delete(reauthKey);
325
340
  });
326
- this._inFlightRefreshes.set(refreshToken, pending);
341
+ this._inFlightRefreshes.set(reauthKey, pending);
327
342
  }
328
343
  const newTokens = await pending;
329
344
 
@@ -1129,6 +1144,17 @@
1129
1144
  return data;
1130
1145
  }
1131
1146
 
1147
+ async getPhotoIdsByPersons(projectTableName, { personId } = {}) {
1148
+ this._ctx.requireTokens();
1149
+ let path = `/api/v1/person/${encodeURIComponent(projectTableName)}/photos`;
1150
+ const query = new URLSearchParams();
1151
+ if (personId !== undefined) (Array.isArray(personId) ? personId : [personId]).forEach(v => query.append('person_id', v));
1152
+ const qs = query.toString();
1153
+ if (qs) path += '?' + qs;
1154
+ const { data } = await this._ctx.client.request(path, 'GET', this._ctx.accessToken, this._ctx.refreshToken);
1155
+ return data;
1156
+ }
1157
+
1132
1158
  async getAllPersonsFromPhoto(projectTableName, photoId) {
1133
1159
  this._ctx.requireTokens();
1134
1160
  const path = `/api/v1/person/${encodeURIComponent(projectTableName)}/photo/${encodeURIComponent(photoId)}`;
@@ -1491,7 +1517,7 @@
1491
1517
  return data;
1492
1518
  }
1493
1519
 
1494
- async searchProjectPhotosParametrized(projectTableName, { andSearchText, orSearchText, notSearchText, city, country, state, albums, dateMin, dateMax, dateNullAnd, dateNullOr, dateOrder, bestOfSimilarSetsOnly, splitByTier, size } = {}) {
1520
+ async searchProjectPhotosParametrized(projectTableName, { andSearchText, orSearchText, notSearchText, city, country, state, albums, dateMin, dateMax, dateNullAnd, dateNullOr, ascOrDesc, bestOfSimilarSetsOnly, splitByTier, lastId, limit } = {}) {
1495
1521
  this._ctx.requireTokens();
1496
1522
  let path = `/api/v1/search/parametrized/${encodeURIComponent(projectTableName)}/`;
1497
1523
  const query = new URLSearchParams();
@@ -1506,10 +1532,25 @@
1506
1532
  if (dateMax !== undefined) (Array.isArray(dateMax) ? dateMax : [dateMax]).forEach(v => query.append('date_max', v));
1507
1533
  if (dateNullAnd !== undefined) (Array.isArray(dateNullAnd) ? dateNullAnd : [dateNullAnd]).forEach(v => query.append('date_null_and', v));
1508
1534
  if (dateNullOr !== undefined) (Array.isArray(dateNullOr) ? dateNullOr : [dateNullOr]).forEach(v => query.append('date_null_or', v));
1509
- if (dateOrder !== undefined) (Array.isArray(dateOrder) ? dateOrder : [dateOrder]).forEach(v => query.append('date_order', v));
1535
+ if (ascOrDesc !== undefined) (Array.isArray(ascOrDesc) ? ascOrDesc : [ascOrDesc]).forEach(v => query.append('asc_or_desc', v));
1510
1536
  if (bestOfSimilarSetsOnly !== undefined) (Array.isArray(bestOfSimilarSetsOnly) ? bestOfSimilarSetsOnly : [bestOfSimilarSetsOnly]).forEach(v => query.append('best_of_similar_sets_only', v));
1511
1537
  if (splitByTier !== undefined) (Array.isArray(splitByTier) ? splitByTier : [splitByTier]).forEach(v => query.append('split_by_tier', v));
1512
- if (size !== undefined) (Array.isArray(size) ? size : [size]).forEach(v => query.append('size', v));
1538
+ if (lastId !== undefined) (Array.isArray(lastId) ? lastId : [lastId]).forEach(v => query.append('last_id', v));
1539
+ if (limit !== undefined) (Array.isArray(limit) ? limit : [limit]).forEach(v => query.append('limit', v));
1540
+ const qs = query.toString();
1541
+ if (qs) path += '?' + qs;
1542
+ const { data } = await this._ctx.client.request(path, 'GET', this._ctx.accessToken, this._ctx.refreshToken);
1543
+ return data;
1544
+ }
1545
+
1546
+ async recallProjectPhotoIds(projectTableName, { ascOrDesc, bestOfSimilarSetsOnly, lastId, limit } = {}) {
1547
+ this._ctx.requireTokens();
1548
+ let path = `/api/v1/search/recall/${encodeURIComponent(projectTableName)}/`;
1549
+ const query = new URLSearchParams();
1550
+ if (ascOrDesc !== undefined) (Array.isArray(ascOrDesc) ? ascOrDesc : [ascOrDesc]).forEach(v => query.append('asc_or_desc', v));
1551
+ if (bestOfSimilarSetsOnly !== undefined) (Array.isArray(bestOfSimilarSetsOnly) ? bestOfSimilarSetsOnly : [bestOfSimilarSetsOnly]).forEach(v => query.append('best_of_similar_sets_only', v));
1552
+ if (lastId !== undefined) (Array.isArray(lastId) ? lastId : [lastId]).forEach(v => query.append('last_id', v));
1553
+ if (limit !== undefined) (Array.isArray(limit) ? limit : [limit]).forEach(v => query.append('limit', v));
1513
1554
  const qs = query.toString();
1514
1555
  if (qs) path += '?' + qs;
1515
1556
  const { data } = await this._ctx.client.request(path, 'GET', this._ctx.accessToken, this._ctx.refreshToken);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mediaviz/sdk",
3
- "version": "1.0.78",
3
+ "version": "1.0.80",
4
4
  "description": "MediaViz JavaScript SDK \u2014 auto-generated public endpoint client.",
5
5
  "license": "MIT",
6
6
  "repository": {