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 +1 -1
- package/src/Connection.js +57 -3
- package/src/index.d.ts +6 -1
- package/src/index.js +3 -0
- package/src/lib/StaleAccessIdError.js +40 -0
- package/src/utils.js +50 -0
- package/test/Connection.test.js +26 -0
- package/test/utils.test.js +44 -0
package/package.json
CHANGED
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
package/test/Connection.test.js
CHANGED
|
@@ -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
|
});
|
package/test/utils.test.js
CHANGED
|
@@ -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
|
});
|