Skip to content

Commit b44ec68

Browse files
committed
Finalizing model refactor
1 parent 4173947 commit b44ec68

4 files changed

Lines changed: 191 additions & 174 deletions

File tree

src/HDSModel-Authorizations.js

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
/**
2+
* Authorizations - Extension of HDSModel
3+
*/
4+
class HDSModelAuthorizations {
5+
/**
6+
* @type {HDSModel}
7+
*/
8+
#model;
9+
10+
constructor (model) {
11+
this.#model = model;
12+
}
13+
14+
/**
15+
* Get minimal Authorization set for itemKeys
16+
* /!\ Does not handle requests with streamId = "*"
17+
* @param {Array<itemKeys>} itemKeys
18+
* @param {Object} [options]
19+
* @param {string} [options.defaultLevel] (default = write) one of 'read', 'write', 'contribute', 'writeOnly'
20+
* @param {boolean} [options.includeDefaultName] (default = true) defaultNames are needed for permission requests but not for access creation
21+
* @param {Array<AuthorizationRequestItem>} [options.preRequest]
22+
* @return {Array<AuthorizationRequestItem>}
23+
*/
24+
forItemKeys (itemKeys, options = {}) {
25+
const opts = {
26+
defaultLevel: 'read',
27+
preRequest: [],
28+
includeDefaultName: true
29+
};
30+
Object.assign(opts, options);
31+
const streamsRequested = {};
32+
for (const pre of opts.preRequest) {
33+
if (!pre.streamId) throw new Error(`Missing streamId in options.preRequest item: ${JSON.stringify(pre)}`);
34+
// complete pre with defaultName if missing
35+
if (opts.includeDefaultName && !pre.defaultName) {
36+
// try to get it from streams Data
37+
const stream = this.#model.streams.getDataById(pre.streamId, false);
38+
if (stream) {
39+
pre.defaultName = stream.name;
40+
} else {
41+
throw new Error(`No "defaultName" in options.preRequest item: ${JSON.stringify(pre)} and cannot find matching streams in default list`);
42+
}
43+
}
44+
// check there is no defaultName if not required
45+
if (!opts.includeDefaultName) {
46+
if (pre.defaultName) throw new Error(`Do not include defaultName when not included explicitely on ${JSON.stringify(pre)}`);
47+
}
48+
// add default level
49+
if (!pre.level) {
50+
pre.level = opts.defaultLevel;
51+
}
52+
streamsRequested[pre.streamId] = pre;
53+
}
54+
// add streamId not already in
55+
for (const itemKey of itemKeys) {
56+
const itemDef = this.#model.itemsDefs.forKey(itemKey);
57+
const streamId = itemDef.data.streamId;
58+
if (!streamsRequested[streamId]) { // new streamId
59+
const auth = { streamId, level: opts.defaultLevel };
60+
if (opts.includeDefaultName) {
61+
const stream = this.#model.streams.getDataById(streamId);
62+
auth.defaultName = stream.name;
63+
}
64+
streamsRequested[streamId] = auth;
65+
} else { // existing just adapt level
66+
streamsRequested[streamId].level = mixAuthorizationLevels(streamsRequested[streamId].level, opts.defaultLevel);
67+
}
68+
}
69+
// remove all permissions with a parent having identical or higher level
70+
for (const auth of Object.values(streamsRequested)) {
71+
const parents = this.#model.streams.getParentsIds(auth.streamId, false);
72+
for (const parent of parents) {
73+
const found = streamsRequested[parent];
74+
if (found && authorizationOverride(found.level, auth.level)) {
75+
// delete entry
76+
delete streamsRequested[auth.streamId];
77+
// break loop
78+
continue;
79+
}
80+
}
81+
}
82+
return Object.values(streamsRequested);
83+
}
84+
}
85+
86+
/**
87+
* @typedef {Object} AuthorizationRequestItem
88+
* @property {string} streamId
89+
* @property {string} level
90+
* @property {string} defaultName
91+
*/
92+
93+
/**
94+
* Authorization level1 (parent) does override level2
95+
* Return "true" if identical or level1 == "manage"
96+
*/
97+
function authorizationOverride (level1, level2) {
98+
if (level1 === level2) return true;
99+
if (level1 === 'manage') return true;
100+
if (level1 === 'contribute' && level2 !== 'manage') return true;
101+
return false;
102+
}
103+
104+
/**
105+
* Given two authorization level, give the resulting one
106+
* @param {string} level1
107+
* @param {string} level1
108+
* @returns {string} level
109+
*/
110+
function mixAuthorizationLevels (level1, level2) {
111+
if (level1 === level2) return level1;
112+
// sort level in orders [ 'contribute', 'manage', 'read', 'writeOnly' ]
113+
const levels = [level1, level2].sort();
114+
if (levels.includes('manage')) return 'manage'; // any & manage
115+
if (levels[0] === 'contribute') return 'contribute'; // read ore writeOnly & contribute
116+
if (levels[1] === 'writeOnly') return 'contribute'; // mix read & writeOnly
117+
/* c8 ignore next */ // error if there .. 'read' & 'read' should have already be found
118+
throw new Error(`Invalid level found level1: ${level1}, level2 ${level2}`);
119+
}
120+
121+
module.exports = HDSModelAuthorizations;

src/HDSModel.js

Lines changed: 33 additions & 165 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,41 @@
1-
const HDSModelItemsDefs = require('./HDSModel-ItemsDefs');
2-
const HDSModelStreams = require('./HDSModel-Streams');
1+
const { deepFreeze } = require('./utils');
2+
3+
const LAZILY_LOADED = {
4+
streams: require('./HDSModel-Streams'),
5+
authorizations: require('./HDSModel-Authorizations'),
6+
itemsDefs: require('./HDSModel-ItemsDefs')
7+
};
8+
9+
/**
10+
* @class {HDSModel}
11+
* @property {object} modelData - Raw ModelData
12+
* @property {HDSModelItemsDefs} itemsDefs
13+
* @property {HDSModelStreams} streams
14+
* @property {HDSModelAuthorizations} authorizations
15+
*/
316
class HDSModel {
417
/**
5-
* JSON definition file
18+
* JSON definition file URL.
619
* Should come from service/info assets.hds-model
720
* @type {string}
821
*/
922
#modelUrl;
1023

11-
/**
12-
* Content on model definitions
13-
* @type {object}
14-
*/
24+
/** @type {object} RAW content of model definitions */
1525
#modelData;
1626

1727
/**
18-
* @type {HDSModelItemsDefs}
28+
* @private
29+
* Map of properties loaded "on demand"
1930
*/
20-
#modelItemsDefs;
31+
laziliyLoadedMap;
2132

2233
/**
23-
* @type {HDSModelStreams}
24-
*/
25-
#modelStreams;
26-
27-
/**
28-
* JSON definition file
29-
* Should come from service/info assets.hds-model
30-
* @type {string}
34+
* @param {string} modelUrl - JSON definition file URL. Should come from service/info assets.hds-model
3135
*/
3236
constructor (modelUrl) {
3337
this.#modelUrl = modelUrl;
38+
this.laziliyLoadedMap = { };
3439
}
3540

3641
/**
@@ -45,162 +50,25 @@ class HDSModel {
4550
for (const [key, item] of Object.entries(this.#modelData.items)) {
4651
item.key = key;
4752
}
48-
49-
deepFreeze(this.#modelData); // make sure it cannot be modified
53+
// make sure it cannot be modified
54+
deepFreeze(this.#modelData);
5055
}
5156

52-
/**
53-
* RAW model data
54-
*/
57+
/** RAW model data */
5558
get modelData () {
5659
if (!this.#modelData) throw new Error('Model not loaded call `await model.load()` first.');
5760
return this.#modelData;
5861
}
62+
}
5963

60-
/**
61-
* @type HDSModelItemsDefs
62-
*/
63-
get itemsDefs () {
64-
if (!this.#modelItemsDefs) this.#modelItemsDefs = new HDSModelItemsDefs(this);
65-
return this.#modelItemsDefs;
66-
}
67-
68-
/**
69-
* @type HDSModelStreams
70-
*/
71-
get streams () {
72-
if (!this.#modelStreams) this.#modelStreams = new HDSModelStreams(this);
73-
return this.#modelStreams;
74-
}
75-
76-
// --------- authorizations builder ------ //
77-
78-
/**
79-
* @typedef {Object} AuthorizationRequestItem
80-
* @property {string} streamId
81-
* @property {string} level
82-
* @property {string} defaultName
83-
*/
84-
85-
/**
86-
* Get minimal Authorization set for itemKeys
87-
* /!\ Does not handle requests with streamId = "*"
88-
* @param {Array<itemKeys>} itemKeys
89-
* @param {Object} [options]
90-
* @param {string} [options.defaultLevel] (default = write) one of 'read', 'write', 'contribute', 'writeOnly'
91-
* @param {boolean} [options.includeDefaultName] (default = true) defaultNames are needed for permission requests but not for access creation
92-
* @param {Array<AuthorizationRequestItem>} [options.preRequest]
93-
* @return {Array<AuthorizationRequestItem>}
94-
*/
95-
authorizationForItemKeys (itemKeys, options = {}) {
96-
const opts = {
97-
defaultLevel: 'read',
98-
preRequest: [],
99-
includeDefaultName: true
100-
};
101-
Object.assign(opts, options);
102-
const streamsRequested = {};
103-
for (const pre of opts.preRequest) {
104-
if (!pre.streamId) throw new Error(`Missing streamId in options.preRequest item: ${JSON.stringify(pre)}`);
105-
// complete pre with defaultName if missing
106-
if (opts.includeDefaultName && !pre.defaultName) {
107-
// try to get it from streams Data
108-
const stream = this.streams.getDataById(pre.streamId, false);
109-
if (stream) {
110-
pre.defaultName = stream.name;
111-
} else {
112-
throw new Error(`No "defaultName" in options.preRequest item: ${JSON.stringify(pre)} and cannot find matching streams in default list`);
113-
}
114-
}
115-
// check there is no defaultName if not required
116-
if (!opts.includeDefaultName) {
117-
if (pre.defaultName) throw new Error(`Do not include defaultName when not included explicitely on ${JSON.stringify(pre)}`);
118-
}
119-
// add default level
120-
if (!pre.level) {
121-
pre.level = opts.defaultLevel;
122-
}
123-
streamsRequested[pre.streamId] = pre;
124-
}
125-
// add streamId not already in
126-
for (const itemKey of itemKeys) {
127-
const itemDef = this.itemsDefs.forKey(itemKey);
128-
const streamId = itemDef.data.streamId;
129-
if (!streamsRequested[streamId]) { // new streamId
130-
const auth = { streamId, level: opts.defaultLevel };
131-
if (opts.includeDefaultName) {
132-
const stream = this.streams.getDataById(streamId);
133-
auth.defaultName = stream.name;
134-
}
135-
streamsRequested[streamId] = auth;
136-
} else { // existing just adapt level
137-
streamsRequested[streamId].level = mixAuthorizationLevels(streamsRequested[streamId].level, opts.defaultLevel);
138-
}
139-
}
140-
// remove all permissions with a parent having identical or higher level
141-
for (const auth of Object.values(streamsRequested)) {
142-
const parents = this.streams.getParentsIds(auth.streamId, false);
143-
for (const parent of parents) {
144-
const found = streamsRequested[parent];
145-
if (found && authorizationOverride(found.level, auth.level)) {
146-
// delete entry
147-
delete streamsRequested[auth.streamId];
148-
// break loop
149-
continue;
150-
}
151-
}
64+
// add properties to be lazily loaded
65+
for (const [prop, Obj] of Object.entries(LAZILY_LOADED)) {
66+
Object.defineProperty(HDSModel.prototype, prop, {
67+
get: function () {
68+
if (!this.laziliyLoadedMap[prop]) this.laziliyLoadedMap[prop] = new Obj(this);
69+
return this.laziliyLoadedMap[prop];
15270
}
153-
return Object.values(streamsRequested);
154-
}
71+
});
15572
}
15673

15774
module.exports = HDSModel;
158-
159-
/**
160-
* Authorization level1 (parent) does override level2
161-
* Return "true" if identical or level1 == "manage"
162-
*/
163-
function authorizationOverride (level1, level2) {
164-
if (level1 === level2) return true;
165-
if (level1 === 'manage') return true;
166-
if (level1 === 'contribute' && level2 !== 'manage') return true;
167-
return false;
168-
}
169-
170-
/**
171-
* Given two authorization level, give the resulting one
172-
* @param {string} level1
173-
* @param {string} level1
174-
* @returns {string} level
175-
*/
176-
function mixAuthorizationLevels (level1, level2) {
177-
if (level1 === level2) return level1;
178-
// sort level in orders [ 'contribute', 'manage', 'read', 'writeOnly' ]
179-
const levels = [level1, level2].sort();
180-
if (levels.includes('manage')) return 'manage'; // any & manage
181-
if (levels[0] === 'contribute') return 'contribute'; // read ore writeOnly & contribute
182-
if (levels[1] === 'writeOnly') return 'contribute'; // mix read & writeOnly
183-
/* c8 ignore next */ // error if there .. 'read' & 'read' should have already be found
184-
throw new Error(`Invalid level found level1: ${level1}, level2 ${level2}`);
185-
}
186-
187-
/**
188-
* Recursively make immutable an object
189-
* @param {*} object
190-
* @returns {*}
191-
*/
192-
function deepFreeze (object) {
193-
// Retrieve the property names defined on object
194-
const propNames = Reflect.ownKeys(object);
195-
196-
// Freeze properties before freezing self
197-
for (const name of propNames) {
198-
const value = object[name];
199-
200-
if ((value && typeof value === 'object') || typeof value === 'function') {
201-
deepFreeze(value);
202-
}
203-
}
204-
205-
return Object.freeze(object);
206-
}

src/utils.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/**
2+
* Set of Misc utilities
3+
*/
4+
5+
module.exports = {
6+
deepFreeze
7+
};
8+
9+
/**
10+
* Recursively make immutable an object
11+
* @param {*} object
12+
* @returns {*}
13+
*/
14+
function deepFreeze (object) {
15+
// Retrieve the property names defined on object
16+
const propNames = Reflect.ownKeys(object);
17+
18+
// Freeze properties before freezing self
19+
for (const name of propNames) {
20+
const value = object[name];
21+
22+
if ((value && typeof value === 'object') || typeof value === 'function') {
23+
deepFreeze(value);
24+
}
25+
}
26+
27+
return Object.freeze(object);
28+
}

0 commit comments

Comments
 (0)