pryv 3.0.4 → 3.1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pryv",
3
- "version": "3.0.4",
3
+ "version": "3.1.0",
4
4
  "description": "Pryv JavaScript library",
5
5
  "keywords": [
6
6
  "Pryv",
package/src/Connection.js CHANGED
@@ -6,6 +6,7 @@ const utils = require('./utils.js');
6
6
  const jsonParser = require('./lib/json-parser');
7
7
  const libGetEventStreamed = require('./lib/getEventStreamed');
8
8
  const PryvError = require('./lib/PryvError');
9
+ const StaleAccessIdError = require('./lib/StaleAccessIdError');
9
10
  const buildSearchParams = require('./lib/buildSearchParams');
10
11
 
11
12
  /**
@@ -71,11 +72,22 @@ class Connection {
71
72
 
72
73
  /**
73
74
  * Get access info for this connection.
74
- * It's async as it is fetched from the API.
75
+ *
76
+ * Memoized per-Connection: the first call fetches from the server and
77
+ * caches the result; subsequent calls return the cached copy in O(1).
78
+ * Pass `forceRefresh: true` to invalidate the cache and fetch a fresh
79
+ * copy from the server — used internally by `connection.socket` to
80
+ * react to Plan 66 `accessUpdated` server-push events. A failed
81
+ * server fetch leaves any prior cached value intact.
82
+ *
83
+ * @param {boolean} [forceRefresh=false] - bypass + refresh the cache
75
84
  * @returns {Promise<AccessInfo>} Promise resolving to the access info
76
85
  */
77
- async accessInfo () {
78
- return this.get('access-info', null);
86
+ async accessInfo (forceRefresh = false) {
87
+ if (!forceRefresh && this._accessInfoCache != null) return this._accessInfoCache;
88
+ const fresh = await this.get('access-info', null);
89
+ this._accessInfoCache = fresh;
90
+ return fresh;
79
91
  }
80
92
 
81
93
  /**
@@ -416,6 +428,48 @@ class Connection {
416
428
  return utils.buildAPIEndpoint(this);
417
429
  }
418
430
 
431
+ /**
432
+ * Plan 66 (open-pryv.io ≥ 2.0.0-pre.X): update an access by composite id.
433
+ * Wraps `accesses.update` and translates the 409 `stale-resource` response
434
+ * into a typed `StaleAccessIdError` so callers can `instanceof`-test and
435
+ * refetch + retry without re-parsing the inner error.
436
+ *
437
+ * Pass `id` as the wire-format reference returned by the server — bare
438
+ * cuid on a never-updated access, composite `<base>:<serial>` otherwise.
439
+ * `changes` is the body of mutable fields (name, deviceName, permissions,
440
+ * expireAfter, expires:null, clientData).
441
+ *
442
+ * @param {string} id
443
+ * @param {Object} changes
444
+ * @returns {Promise<Object>} the updated access (with new composite id)
445
+ * @throws {StaleAccessIdError} if the server reports the id is stale
446
+ */
447
+ async updateAccess (id, changes) {
448
+ try {
449
+ return await this.apiOne('accesses.update', { id, update: changes }, 'access');
450
+ } catch (e) {
451
+ if (e && e.innerObject && e.innerObject.id === 'stale-resource') {
452
+ throw new StaleAccessIdError(e.message, e.innerObject.data || {});
453
+ }
454
+ throw e;
455
+ }
456
+ }
457
+
458
+ /**
459
+ * Plan 66: fetch an access by composite id including its full version
460
+ * history (oldest first). Server: `accesses.getOne ?includeHistory=true`.
461
+ *
462
+ * Useful for audit views. Pass the composite `<base>:<serial>` to
463
+ * inspect a specific past version (the result's `current` field then
464
+ * points at the live head's composite id).
465
+ *
466
+ * @param {string} id
467
+ * @returns {Promise<{ access: Object, current?: string, history?: Object[] }>}
468
+ */
469
+ async getAccessWithHistory (id) {
470
+ return await this.apiOne('accesses.getOne', { id, includeHistory: true });
471
+ }
472
+
419
473
  // private method that handle meta data parsing
420
474
  _handleMeta (res, requestLocalTimestamp) {
421
475
  if (!res.meta) throw new Error('Cannot find .meta in response.');
package/src/index.d.ts CHANGED
@@ -723,7 +723,12 @@ declare module 'pryv' {
723
723
  fields: string[],
724
724
  points: Array<Array<number | string>>,
725
725
  ): Promise<HFSeriesAddResult>;
726
- accessInfo(): Promise<AccessInfo>;
726
+ /** Memoized; pass `forceRefresh: true` to bypass + refresh the cache. */
727
+ accessInfo(forceRefresh?: boolean): Promise<AccessInfo>;
728
+ /** Plan 66: update an access by composite id. */
729
+ updateAccess(id: string, changes: object): Promise<object>;
730
+ /** Plan 66: fetch an access including its full version history. */
731
+ getAccessWithHistory(id: string): Promise<{ access: object, current?: string, history?: object[] }>;
727
732
  revoke(throwOnFail?: boolean, usingConnection?: Connection): Promise<any>;
728
733
  readonly deltaTime: number;
729
734
  readonly apiEndpoint: string;
package/src/index.js CHANGED
@@ -10,6 +10,8 @@
10
10
  * @property {pryv.Browser} Browser - Browser Tools - Access request helpers and visuals (button)
11
11
  * @property {pryv.utils} utils - Exposes some utils for HTTP calls and tools to manipulate Pryv's API endpoints
12
12
  * @property {pryv.PryvError} PryvError - Custom error class with innerObject + structured API-error fields
13
+ * @property {pryv.MfaRequiredError} MfaRequiredError - Thrown by Service.login when the platform returns an mfaToken instead of a token. Carries `.mfaToken`.
14
+ * @property {pryv.StaleAccessIdError} StaleAccessIdError - Plan 66: thrown when a Pryv.io server rejects an `accesses.update` / `accesses.delete` with a 409 stale-resource. Refetch + retry.
13
15
  * @property {Object} ERRORS - Catalogue of Pryv API error ids (mirrors open-pryv.io/components/errors)
14
16
  */
15
17
  module.exports = {
@@ -20,6 +22,7 @@ module.exports = {
20
22
  utils: require('./utils'),
21
23
  PryvError: require('./lib/PryvError'),
22
24
  MfaRequiredError: require('./lib/MfaRequiredError'),
25
+ StaleAccessIdError: require('./lib/StaleAccessIdError'),
23
26
  ERRORS: require('./lib/errorIds'),
24
27
  version: require('../package.json').version
25
28
  };
@@ -0,0 +1,40 @@
1
+ /**
2
+ * @license
3
+ * [BSD-3-Clause](https://github.com/pryv/lib-js/blob/master/LICENSE)
4
+ */
5
+
6
+ const PryvError = require('./PryvError');
7
+
8
+ /**
9
+ * Plan 66: typed error surfaced when a Pryv.io server (≥ 2.0.0-pre.X)
10
+ * rejects an `accesses.update` or `accesses.delete` call with a
11
+ * 409 `stale-resource` response.
12
+ *
13
+ * The composite access id `<base>:<serial>` carries the version the
14
+ * caller last observed. If the access has since been updated, the
15
+ * server rejects the call so the caller refetches the current head
16
+ * (`connection.api('accesses.getOne', { id: base })`) and retries
17
+ * with the fresh composite id.
18
+ *
19
+ * Reach for `.data.provided` to see what the caller sent and
20
+ * `.data.currentSerial` to see what the server currently has.
21
+ *
22
+ * @extends PryvError
23
+ */
24
+ class StaleAccessIdError extends PryvError {
25
+ /**
26
+ * @param {string} message
27
+ * @param {{ provided?: string, currentSerial?: number | null }} data
28
+ */
29
+ constructor (message, data) {
30
+ super(message);
31
+ this.name = 'StaleAccessIdError';
32
+ /** @type {{ provided?: string, currentSerial?: number | null }} */
33
+ this.data = data || {};
34
+ if (Error.captureStackTrace) {
35
+ Error.captureStackTrace(this, StaleAccessIdError);
36
+ }
37
+ }
38
+ }
39
+
40
+ module.exports = StaleAccessIdError;
package/src/utils.js CHANGED
@@ -155,6 +155,56 @@ const utils = module.exports = {
155
155
  return url.replace(PRYV_REGEXP, '');
156
156
  },
157
157
 
158
+ /**
159
+ * Plan 66 (open-pryv.io ≥ 2.0.0-pre.X): parse a wire-format access
160
+ * reference into `{ base, serial }`. Accepts both bare cuid
161
+ * (`"abc123"` → `{ base: 'abc123', serial: null }`) and composite
162
+ * (`"abc123:3"` → `{ base: 'abc123', serial: 3 }`). Throws on
163
+ * malformed input. Apply this to `access.id`, `access.createdBy`,
164
+ * `access.modifiedBy`, and `streamIds` entries of the form
165
+ * `access-<base>:<serial>` from audit events.
166
+ * @memberof pryv.utils
167
+ * @param {string} ref - Access reference string
168
+ * @returns {{ base: string, serial: number | null }}
169
+ */
170
+ parseAccessRef: function (ref) {
171
+ if (typeof ref !== 'string' || ref.length === 0) {
172
+ throw new Error('parseAccessRef: expected a non-empty string, got ' + JSON.stringify(ref));
173
+ }
174
+ const colonIdx = ref.indexOf(':');
175
+ if (colonIdx === -1) return { base: ref, serial: null };
176
+ const base = ref.slice(0, colonIdx);
177
+ const tail = ref.slice(colonIdx + 1);
178
+ if (base.length === 0) {
179
+ throw new Error('parseAccessRef: empty base in ' + JSON.stringify(ref));
180
+ }
181
+ const serial = Number(tail);
182
+ if (!Number.isInteger(serial) || serial < 1) {
183
+ throw new Error('parseAccessRef: serial must be a positive integer, got ' + JSON.stringify(tail));
184
+ }
185
+ return { base, serial };
186
+ },
187
+
188
+ /**
189
+ * Plan 66: render an `{ base, serial }` pair back to the wire
190
+ * format. Bare cuid when serial is null/undefined; `<base>:<serial>`
191
+ * otherwise. Mostly used to construct the composite id when calling
192
+ * `connection.api()` for `accesses.update` / `accesses.delete`.
193
+ * @memberof pryv.utils
194
+ * @param {{ base: string, serial?: number | null }} ref
195
+ * @returns {string}
196
+ */
197
+ serializeAccessRef: function (ref) {
198
+ if (ref == null || typeof ref.base !== 'string' || ref.base.length === 0) {
199
+ throw new Error('serializeAccessRef: ref.base must be a non-empty string');
200
+ }
201
+ if (ref.serial == null) return ref.base;
202
+ if (!Number.isInteger(ref.serial) || ref.serial < 1) {
203
+ throw new Error('serializeAccessRef: serial must be a positive integer, got ' + JSON.stringify(ref.serial));
204
+ }
205
+ return ref.base + ':' + ref.serial;
206
+ },
207
+
158
208
  /**
159
209
  * Extract query parameters from a URL
160
210
  * @memberof pryv.utils
@@ -499,5 +499,31 @@ describe('[CONX] Connection', () => {
499
499
  expect(accessInfoUser.token).to.exist;
500
500
  expect(newUser.access.token).to.equal(accessInfoUser.token);
501
501
  });
502
+
503
+ // Plan 66 — accessInfo caching + forceRefresh.
504
+
505
+ it('[CAIC] accessInfo() memoizes — second call returns the same object reference', async () => {
506
+ const regexAPIandToken = /(.+):\/\/(.+)/gm;
507
+ const res = regexAPIandToken.exec(testData.apiEndpoint);
508
+ const apiEndpointWithToken = res[1] + '://' + newUser.access.token + '@' + res[2];
509
+ const cachingConn = new pryv.Connection(apiEndpointWithToken);
510
+ const first = await cachingConn.accessInfo();
511
+ const second = await cachingConn.accessInfo();
512
+ expect(second).to.equal(first); // same reference — served from cache
513
+ });
514
+
515
+ it('[CAID] accessInfo(true) forces a refresh and replaces the cached object', async () => {
516
+ const regexAPIandToken = /(.+):\/\/(.+)/gm;
517
+ const res = regexAPIandToken.exec(testData.apiEndpoint);
518
+ const apiEndpointWithToken = res[1] + '://' + newUser.access.token + '@' + res[2];
519
+ const cachingConn = new pryv.Connection(apiEndpointWithToken);
520
+ const first = await cachingConn.accessInfo();
521
+ const refreshed = await cachingConn.accessInfo(true);
522
+ expect(refreshed).to.not.equal(first); // distinct object — re-fetched
523
+ expect(refreshed.token).to.equal(first.token); // same content
524
+ // Next non-forced call returns the refreshed copy from cache.
525
+ const cached = await cachingConn.accessInfo();
526
+ expect(cached).to.equal(refreshed);
527
+ });
502
528
  });
503
529
  });
@@ -59,4 +59,48 @@ describe('[UTLX] utils', function () {
59
59
  expect(apiEndpoint).to.equal(testData.apiEndpoint);
60
60
  done();
61
61
  });
62
+
63
+ // Plan 66 — composite access references.
64
+
65
+ it('[UTLF] parseAccessRef on a bare cuid returns { base, serial: null }', function () {
66
+ const ref = pryv.utils.parseAccessRef('abc123def456');
67
+ expect(ref).to.eql({ base: 'abc123def456', serial: null });
68
+ });
69
+
70
+ it('[UTLG] parseAccessRef on a composite returns { base, serial }', function () {
71
+ const ref = pryv.utils.parseAccessRef('abc123:7');
72
+ expect(ref).to.eql({ base: 'abc123', serial: 7 });
73
+ });
74
+
75
+ it('[UTLH] parseAccessRef on garbage throws', function () {
76
+ expect(() => pryv.utils.parseAccessRef('')).to.throw();
77
+ expect(() => pryv.utils.parseAccessRef(':1')).to.throw();
78
+ expect(() => pryv.utils.parseAccessRef('abc:notanumber')).to.throw();
79
+ expect(() => pryv.utils.parseAccessRef('abc:0')).to.throw();
80
+ expect(() => pryv.utils.parseAccessRef('abc:-1')).to.throw();
81
+ expect(() => pryv.utils.parseAccessRef(null)).to.throw();
82
+ });
83
+
84
+ it('[UTLI] serializeAccessRef round-trips parseAccessRef', function () {
85
+ const samples = ['plainCuid', 'plainCuid:1', 'plainCuid:42'];
86
+ for (const s of samples) {
87
+ expect(pryv.utils.serializeAccessRef(pryv.utils.parseAccessRef(s))).to.equal(s);
88
+ }
89
+ });
90
+
91
+ it('[UTLJ] serializeAccessRef rejects bad inputs', function () {
92
+ expect(() => pryv.utils.serializeAccessRef(null)).to.throw();
93
+ expect(() => pryv.utils.serializeAccessRef({ base: '' })).to.throw();
94
+ expect(() => pryv.utils.serializeAccessRef({ base: 'abc', serial: 0 })).to.throw();
95
+ expect(() => pryv.utils.serializeAccessRef({ base: 'abc', serial: 1.5 })).to.throw();
96
+ });
97
+
98
+ it('[UTLK] StaleAccessIdError extends PryvError', function () {
99
+ const err = new pryv.StaleAccessIdError('stale!', { provided: 'abc:1', currentSerial: 2 });
100
+ expect(err).to.be.instanceOf(pryv.StaleAccessIdError);
101
+ expect(err).to.be.instanceOf(pryv.PryvError);
102
+ expect(err.data.provided).to.equal('abc:1');
103
+ expect(err.data.currentSerial).to.equal(2);
104
+ expect(err.name).to.equal('StaleAccessIdError');
105
+ });
62
106
  });