Skip to content

Commit a7bd786

Browse files
committed
feat(pryv): access versioning support — v3.1.0 (Plan 66 Phase H)
New utilities, typed error, and Connection helpers for the composite access reference format introduced by open-pryv.io ≥ 2.0.0-pre.X (Plan 66). Utils - pryv.utils.parseAccessRef(ref) → { base, serial | null } Parses both bare cuid (never-updated access) and composite `<base>:<serial>` (versioned access). Throws on malformed input. - pryv.utils.serializeAccessRef({ base, serial }) → string Inverse helper. Typed error - pryv.StaleAccessIdError extends PryvError. Surfaced when the server responds with 409 stale-resource on accesses.update or accesses.delete. Carries { provided, currentSerial } in .data so callers can refetch + retry. Connection helpers - connection.updateAccess(id, changes) — wrapper around accesses.update. Translates the 409 stale-resource response into StaleAccessIdError automatically. - connection.getAccessWithHistory(id) — wrapper around accesses.getOne?includeHistory=true. Returns { access, current?, history?: [...] }. Wire-format compatibility - Older servers still return bare cuids; parseAccessRef yields { base, serial: null } in that case so callers can use it unconditionally. The new utilities are pure JS — no server dependency for the helpers themselves. Tests - 6 new utils.test.js cases: [UTLF]-[UTLK] covering parse, serialize, round-trip, validation, and the StaleAccessIdError class. All passing.
1 parent fe356e7 commit a7bd786

9 files changed

Lines changed: 297 additions & 102 deletions

File tree

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,20 @@
22

33
<!-- Format based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) -->
44

5+
## [3.1.0]
6+
7+
Plan 66 (open-pryv.io ≥ 2.0.0-pre.X): access versioning.
8+
9+
### Added
10+
- `pryv.utils.parseAccessRef(ref) → { base, serial | null }` — parses the wire-format access reference. Bare cuid → `{ base, serial: null }`, composite `<base>:<serial>``{ base, serial: <int> }`. Throws on malformed input.
11+
- `pryv.utils.serializeAccessRef({ base, serial }) → string` — inverse helper.
12+
- `pryv.StaleAccessIdError` — typed error (extends `PryvError`) surfaced when the server responds with `409 stale-resource` on `accesses.update` / `accesses.delete`. Carries `{ provided, currentSerial }` in `.data` so callers can refetch + retry.
13+
- `connection.updateAccess(id, changes)` — convenience wrapper around `accesses.update`. Automatically translates the server's 409 response into `StaleAccessIdError`.
14+
- `connection.getAccessWithHistory(id)` — convenience wrapper around `accesses.getOne?includeHistory=true`. Returns `{ access, current?, history?: [...] }`.
15+
16+
### Notes
17+
- Wire-format compatibility: every `access.id` / `access.createdBy` / `access.modifiedBy` returned by a Plan-66-capable server is parseable with `parseAccessRef`. Older servers still return bare cuids — `parseAccessRef` returns `{ base, serial: null }` for those, so callers can use the helper unconditionally.
18+
519
## [3.0.4](https://github.com/pryv/lib-js/compare/3.0.3...3.0.4)
620

721
### Added

components/pryv/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "pryv",
3-
"version": "3.0.4",
3+
"version": "3.1.0",
44
"description": "Pryv JavaScript library",
55
"keywords": [
66
"Pryv",

components/pryv/src/Connection.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const utils = require('./utils.js');
66
const jsonParser = require('./lib/json-parser');
77
const libGetEventStreamed = require('./lib/getEventStreamed');
88
const PryvError = require('./lib/PryvError');
9+
const StaleAccessIdError = require('./lib/StaleAccessIdError');
910
const buildSearchParams = require('./lib/buildSearchParams');
1011

1112
/**
@@ -416,6 +417,48 @@ class Connection {
416417
return utils.buildAPIEndpoint(this);
417418
}
418419

420+
/**
421+
* Plan 66 (open-pryv.io ≥ 2.0.0-pre.X): update an access by composite id.
422+
* Wraps `accesses.update` and translates the 409 `stale-resource` response
423+
* into a typed `StaleAccessIdError` so callers can `instanceof`-test and
424+
* refetch + retry without re-parsing the inner error.
425+
*
426+
* Pass `id` as the wire-format reference returned by the server — bare
427+
* cuid on a never-updated access, composite `<base>:<serial>` otherwise.
428+
* `changes` is the body of mutable fields (name, deviceName, permissions,
429+
* expireAfter, expires:null, clientData).
430+
*
431+
* @param {string} id
432+
* @param {Object} changes
433+
* @returns {Promise<Object>} the updated access (with new composite id)
434+
* @throws {StaleAccessIdError} if the server reports the id is stale
435+
*/
436+
async updateAccess (id, changes) {
437+
try {
438+
return await this.apiOne('accesses.update', { id, update: changes }, 'access');
439+
} catch (e) {
440+
if (e && e.innerObject && e.innerObject.id === 'stale-resource') {
441+
throw new StaleAccessIdError(e.message, e.innerObject.data || {});
442+
}
443+
throw e;
444+
}
445+
}
446+
447+
/**
448+
* Plan 66: fetch an access by composite id including its full version
449+
* history (oldest first). Server: `accesses.getOne ?includeHistory=true`.
450+
*
451+
* Useful for audit views. Pass the composite `<base>:<serial>` to
452+
* inspect a specific past version (the result's `current` field then
453+
* points at the live head's composite id).
454+
*
455+
* @param {string} id
456+
* @returns {Promise<{ access: Object, current?: string, history?: Object[] }>}
457+
*/
458+
async getAccessWithHistory (id) {
459+
return await this.apiOne('accesses.getOne', { id, includeHistory: true });
460+
}
461+
419462
// private method that handle meta data parsing
420463
_handleMeta (res, requestLocalTimestamp) {
421464
if (!res.meta) throw new Error('Cannot find .meta in response.');

components/pryv/src/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
* @property {pryv.Browser} Browser - Browser Tools - Access request helpers and visuals (button)
1111
* @property {pryv.utils} utils - Exposes some utils for HTTP calls and tools to manipulate Pryv's API endpoints
1212
* @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.
1315
* @property {Object} ERRORS - Catalogue of Pryv API error ids (mirrors open-pryv.io/components/errors)
1416
*/
1517
module.exports = {
@@ -20,6 +22,7 @@ module.exports = {
2022
utils: require('./utils'),
2123
PryvError: require('./lib/PryvError'),
2224
MfaRequiredError: require('./lib/MfaRequiredError'),
25+
StaleAccessIdError: require('./lib/StaleAccessIdError'),
2326
ERRORS: require('./lib/errorIds'),
2427
version: require('../package.json').version
2528
};
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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;

components/pryv/src/utils.js

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,56 @@ const utils = module.exports = {
155155
return url.replace(PRYV_REGEXP, '');
156156
},
157157

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+
158208
/**
159209
* Extract query parameters from a URL
160210
* @memberof pryv.utils

components/pryv/test/utils.test.js

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,4 +59,48 @@ describe('[UTLX] utils', function () {
5959
expect(apiEndpoint).to.equal(testData.apiEndpoint);
6060
done();
6161
});
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+
});
62106
});

0 commit comments

Comments
 (0)