Skip to content

Commit eddfb06

Browse files
committed
Factorized Apps
1 parent e5607c1 commit eddfb06

5 files changed

Lines changed: 161 additions & 135 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,9 @@ const itemKeys = [
8888

8989
### AppTemplates
9090

91+
App templates based on HDS Model, provide frameworks to build application for HDS.
9192

93+
Some of the functionnalities will be moved from the lib
9294

9395

9496

src/appTemplates/AppClientAccount.js

Lines changed: 10 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,91 +1,26 @@
1-
const pryv = require('../patchedPryv');
2-
1+
const Application = require('./Application');
32
/**
43
* - applications
54
* - [baseStreamId] "Root" stream from this app
65
*/
7-
class AppClientAccount {
8-
conection;
9-
baseStreamId;
10-
appName;
11-
#cache;
12-
13-
get streamData () {
14-
return this.#cache.streamData;
15-
}
16-
17-
/**
18-
* Create with an apiEnpoint
19-
* @param {string} apiEndpoint
20-
* @param {string} baseStreamId - application base Strem ID
21-
* @param {string} appName
22-
* @returns {AppClientAccount}
23-
*/
24-
static async newFromApiEndpoint (apiEndpoint, baseStreamId, appName) {
25-
const connection = new pryv.Connection(apiEndpoint);
26-
return await this.newWithConnection(connection, baseStreamId, appName);
6+
class AppClientAccount extends Application {
7+
get appSettings () {
8+
return {
9+
canBePersonnal: true,
10+
mustBeMaster: true
11+
};
2712
}
2813

29-
/**
30-
* Create with an apiEnpoint
31-
* @param {Pryv.connection} connection - must be a connection with personnalToken or masterToken
32-
* @param {string} baseStreamId - application base Strem ID
33-
* @param {string} appName
34-
* @returns {AppClientAccount}
35-
*/
36-
static async newWithConnection (connection, baseStreamId, appName) {
37-
const appClientAccount = new AppClientAccount(connection, baseStreamId, appName);
38-
await appClientAccount.init();
39-
return appClientAccount;
40-
}
41-
42-
/**
43-
* @private
44-
* use one of AppClientAccount.createWith..()
45-
* @param {string} baseStreamId - application base Strem ID
46-
* @param {Pryv.connection} connection - must be a connection with personnalToken or masterToken
47-
*/
48-
constructor (connection, baseStreamId, appName) {
49-
if (!baseStreamId || baseStreamId.length < 2) throw new Error('Missing or too short baseStreamId');
50-
this.connection = connection;
51-
this.baseStreamId = baseStreamId;
52-
this.appName = appName;
53-
this.#cache = {};
14+
get streamData () {
15+
return this.cache.streamData;
5416
}
5517

5618
/**
5719
* - Check connection validity
5820
* - Make sure stream structure exists
5921
*/
6022
async init () {
61-
// check that connection has a master token or is a personnal token
62-
const infos = await this.connection.accessInfo();
63-
if (infos.type !== 'personal') {
64-
if (infos.type !== 'app') throw new Error('AppClientAccount requires a "personal" or "app" type of access');
65-
const masterFound = infos.permissions.find(p => (p.streamId === '*' && p.level === 'manage'));
66-
if (!masterFound) throw new Error('AppClientAccount with "app" type of access requires "master" token (streamId = "*", level = "manage")');
67-
}
68-
// get streamStructure
69-
let found = false;
70-
try {
71-
const streams = await this.connection.apiOne('streams.get', { id: this.baseStreamId }, 'streams');
72-
if (streams[0]) this.#cache.streamData = streams[0];
73-
found = true;
74-
} catch (e) {
75-
if (e.innerObject?.id !== 'unknown-referenced-resource' || e.innerObject?.data?.id !== 'test-app-template-client') {
76-
throw e;
77-
}
78-
}
79-
// not found create streams
80-
if (!found) {
81-
const apiCalls = [
82-
{ method: 'streams.create', params: { id: 'applications', name: 'Applications' } },
83-
{ method: 'streams.create', params: { id: this.baseStreamId, name: this.appName, parentId: 'applications' } }
84-
];
85-
const streamCreateResult = await this.connection.api(apiCalls);
86-
const stream = streamCreateResult[1].stream;
87-
this.#cache.streamData = stream;
88-
}
23+
return super.init();
8924
}
9025
}
9126

Lines changed: 14 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
const ShortUniqueId = require('short-unique-id');
22
const collectorIdGenerator = new ShortUniqueId({ dictionary: 'alphanum_lower', length: 7 });
3+
const Application = require('./Application');
34
const Collector = require('./Collector');
45

56
/**
@@ -20,58 +21,34 @@ const Collector = require('./Collector');
2021
* - [baseStreamId]-[collectorsId]-active Contains events with "active" users
2122
* - [baseStreamId]-[scollectorsId]-errors Contains events with "revoked" or "erroneous" users
2223
*/
23-
class AppManagingAccount {
24-
/** @type {Pryv.Connection} */
25-
connection;
26-
/** @type {string} */
27-
baseStreamId;
28-
/** @type {string} */
29-
appName;
30-
31-
#cache;
32-
33-
/**
34-
* @private
35-
* use AppManagingAccount.newFromConnection() to create new AppManagingAccount
36-
* @param {string} appName
37-
* @param {string} baseStreamId
38-
* @param {Pryv.Connection} connection
39-
*/
40-
constructor (appName, baseStreamId, connection) {
41-
this.baseStreamId = baseStreamId;
42-
this.appName = appName;
43-
this.connection = connection;
44-
this.#cache = { };
45-
}
46-
47-
/**
48-
* Return an initialized AppManagingAccount instance
49-
* @param {Pryv.Connection} connection - should be personalConnection, a connection with "manage" right on `baseStreamId` or "manage" on "*"
50-
* @returns {AppManagingAccount}
51-
*/
52-
static async newFromConnection (connection, baseStreamId) {
53-
const appManagingAccount = await newAppManagingAccountFromConnection(connection, baseStreamId);
54-
await appManagingAccount.init();
55-
return appManagingAccount;
24+
class AppManagingAccount extends Application {
25+
// used by Application.init();
26+
get appSettings () {
27+
return {
28+
canBePersonnal: true,
29+
mustBeMaster: true,
30+
appNameFromAccessInfo: true // application name will be taken from Access-Info Name
31+
};
5632
}
5733

5834
async init () {
35+
await super.init();
5936
// -- check if stream structure exists
6037
await this.getCollectors();
6138
return this;
6239
}
6340

6441
async getCollectors (forceRefresh) {
65-
if (!forceRefresh && this.#cache.collectorsMap) return Object.values(this.#cache.collectorsMap);
42+
if (!forceRefresh && this.cache.collectorsMap) return Object.values(this.cache.collectorsMap);
6643
// Collectors are materialized by streams
6744
const streams = await this.connection.apiOne('streams.get', { parentId: this.baseStreamId }, 'streams');
6845
const collectorsMap = {};
6946
for (const stream of streams) {
7047
const collector = new Collector(this, stream);
7148
collectorsMap[collector.streamId] = collector;
7249
}
73-
this.#cache.collectorsMap = collectorsMap;
74-
return Object.values(this.#cache.collectorsMap);
50+
this.cache.collectorsMap = collectorsMap;
51+
return Object.values(this.cache.collectorsMap);
7552
}
7653

7754
/**
@@ -99,27 +76,9 @@ class AppManagingAccount {
9976
};
10077
const stream = await this.connection.apiOne('streams.create', params, 'stream');
10178
const collector = new Collector(this, stream);
102-
this.#cache.collectorsMap[collector.streamId] = collector;
79+
this.cache.collectorsMap[collector.streamId] = collector;
10380
return collector;
10481
}
10582
}
10683

10784
module.exports = AppManagingAccount;
108-
109-
async function newAppManagingAccountFromConnection (connection, baseStreamId) {
110-
const accessInfo = await connection.apiOne('getAccessInfo');
111-
if (!accessInfo.type === 'personal') {
112-
if (!accessInfo.type === 'app') throw new Error('Failed creating new AppManagingAccount, "app" authorization required: ' + JSON.stringify(accessInfo));
113-
// check if baseStreamId is in pemission set
114-
const found = accessInfo.permissions.find(p => (p.streamId === baseStreamId || p.streamId === '*'));
115-
if (found && found.level !== 'manage') { // check if level 'manage'
116-
throw new Error(`Failed creating new AppManagingAccount, Not sufficient permissions on stream: ${baseStreamId}`);
117-
}
118-
if (!found) {
119-
// here we may check if we can create or manage baseStreamId as it might be covered by permissions
120-
throw new Error(`Failed creating new AppManagingAccount, cannot find "${baseStreamId}" in permission list`);
121-
}
122-
}
123-
const appManagingAccount = new AppManagingAccount(accessInfo.name, baseStreamId, connection);
124-
return appManagingAccount;
125-
}

src/appTemplates/Application.js

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
const pryv = require('../patchedPryv');
2+
class Application {
3+
/** @type {Pryv.Connection} */
4+
connection;
5+
/** @type {string} */
6+
baseStreamId;
7+
/** @type {string} */
8+
appName;
9+
10+
cache;
11+
12+
get appSettings () {
13+
throw new Error('appSettings must be implemneted');
14+
// possible return values:
15+
/**
16+
* return {
17+
* canBePersonnal: true,
18+
* mustBeMaster: true
19+
* appNameFromAccessInfo: true // application name will be taken from Access-Info Name
20+
* };
21+
*/
22+
}
23+
24+
/**
25+
* Create with an apiEnpoint
26+
* @param {string} apiEndpoint
27+
* @param {string} baseStreamId - application base Strem ID
28+
* @param {string} [appName] - optional if appSettings.appNameFromAccessInfo is set to true
29+
* @returns {AppClientAccount}
30+
*/
31+
static async newFromApiEndpoint (baseStreamId, apiEndpoint, appName) {
32+
const connection = new pryv.Connection(apiEndpoint);
33+
// in a static method "this" is the Class (here the extending class)
34+
return await this.newFromConnection(baseStreamId, connection, appName);
35+
}
36+
37+
/**
38+
* Create with an apiEnpoint
39+
* @param {Pryv.connection} connection - must be a connection with personnalToken or masterToken
40+
* @param {string} baseStreamId - application base Strem ID
41+
* @param {string} [appName] - optional if appSettings.appNameFromAccessInfo is set to true
42+
* @returns {AppClientAccount}
43+
*/
44+
static async newFromConnection (baseStreamId, connection, appName) {
45+
// in a static method "this" is the Class (here the extending class)
46+
const app = new this(baseStreamId, connection, appName);
47+
await app.init();
48+
return app;
49+
}
50+
51+
/**
52+
* @private
53+
* use .newFrom...() to create new AppManagingAccount
54+
* @param {string} baseStreamId
55+
* @param {Pryv.Connection} connection
56+
* @param {string} [appName] - optional if appSettings.appNameFromAccessInfo is set to true
57+
*/
58+
constructor (baseStreamId, connection, appName) {
59+
if (!baseStreamId || baseStreamId.length < 2) throw new Error('Missing or too short baseStreamId');
60+
this.baseStreamId = baseStreamId;
61+
if (appName == null && !this.appSettings.appNameFromAccessInfo) {
62+
throw new Error('appName must be given unless appSettings.appNameFromAccessInfo = true');
63+
}
64+
this.appName = appName;
65+
this.connection = connection;
66+
this.cache = { };
67+
}
68+
69+
async init () {
70+
await createAppStreams(this);
71+
}
72+
}
73+
74+
module.exports = Application;
75+
76+
// create app Streams
77+
78+
async function createAppStreams (app) {
79+
// check that connection has a personal or master token or has "manage" rights on baseStream
80+
const infos = await app.connection.accessInfo();
81+
if (app.appSettings.appNameFromAccessInfo) {
82+
app.appName = infos.name;
83+
}
84+
let isPersonalOrMaster = infos.type === 'personal';
85+
if (!app.appSettings.canBePersonnal && infos.type === 'personal') {
86+
throw new Error('Application should not use a personal token');
87+
}
88+
if (!isPersonalOrMaster) {
89+
const allowPersonalStr = app.appSettings.canBePersonnal ? '"personal" or ' : '';
90+
91+
if (infos.type !== 'app') throw new Error(`Application requires a ${allowPersonalStr} "app" type of access`);
92+
const masterFound = infos.permissions.find(p => (p.streamId === '*' && p.level === 'manage'));
93+
isPersonalOrMaster = true;
94+
if (app.appSettings.mustBemaster && !masterFound) {
95+
throw new Error('Application with "app" type of access requires "master" token (streamId = "*", level = "manage")');
96+
} else { // check that app has "manage" level on baseStreamId
97+
const baseStreamFound = infos.permissions.find(p => (p.streamId === app.baseStreamId && p.level === 'manage'));
98+
if (!baseStreamFound) throw new Error(`Application with "app" type of access requires (streamId = '${app.baseStreamId}', level = "manage") or master access`);
99+
}
100+
}
101+
// get streamStructure
102+
let found = false;
103+
try {
104+
const stream = (await app.connection.apiOne('streams.get', { id: app.baseStreamId }, 'streams'))[0];
105+
if (stream) {
106+
app.cache.streamData = stream;
107+
}
108+
found = true;
109+
} catch (e) {
110+
if (e.innerObject?.id !== 'unknown-referenced-resource' || e.innerObject?.data?.id !== 'test-app-template-client') {
111+
throw e;
112+
}
113+
}
114+
// not found create streams
115+
if (!found) {
116+
if (app.appName == null) {
117+
throw new Error('Cannot create app stream if not "appName" has been given');
118+
}
119+
if (!isPersonalOrMaster) {
120+
throw new Error('Token has not sufficient right to create App streams. Create them upfront');
121+
}
122+
const apiCalls = [
123+
{ method: 'streams.create', params: { id: 'applications', name: 'Applications' } },
124+
{ method: 'streams.create', params: { id: app.baseStreamId, name: app.appName, parentId: 'applications' } }
125+
];
126+
const streamCreateResult = await app.connection.api(apiCalls);
127+
const stream = streamCreateResult[1].stream;
128+
app.cache.streamData = stream;
129+
}
130+
}

tests/apptemplates.test.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ describe('[APTX] appTemplates', function () {
1818
const permissionsManager = [{ streamId: baseStreamIdManager, level: 'manage' }];
1919
managingUser = await createUserAndPermissions(null, permissionsManager, initialStreams, appName);
2020
const connection = new pryv.Connection(managingUser.appApiEndpoint);
21-
appManaging = await AppManagingAccount.newFromConnection(connection, baseStreamIdManager);
21+
appManaging = await AppManagingAccount.newFromConnection(baseStreamIdManager, connection);
2222
// -- user
2323
user = await createUser();
2424
});
@@ -93,7 +93,7 @@ describe('[APTX] appTemplates', function () {
9393

9494
// creating a new Manager with same connection should load the structure
9595
const connection2 = new pryv.Connection(appManaging.connection.apiEndpoint);
96-
const appManaging2 = await AppManagingAccount.newFromConnection(connection2, baseStreamIdManager);
96+
const appManaging2 = await AppManagingAccount.newFromConnection(baseStreamIdManager, connection2);
9797
// check if collector is in the list
9898
const collectors2 = await appManaging2.getCollectors();
9999
const collector2 = collectors2.find(c => c.name === collectorName);
@@ -131,13 +131,13 @@ describe('[APTX] appTemplates', function () {
131131
const clientUserNonMaster = await createUserPermissions(user, permissionsManager, [], appName);
132132
// non master app
133133
try {
134-
await AppClientAccount.newFromApiEndpoint(clientUserNonMaster.appApiEndpoint, baseStreamIdClient, appClientName);
134+
await AppClientAccount.newFromApiEndpoint(baseStreamIdClient, clientUserNonMaster.appApiEndpoint, appClientName);
135135
throw new Error('Should throw error');
136136
} catch (e) {
137-
assert.equal(e.message, 'AppClientAccount with "app" type of access requires "master" token (streamId = "*", level = "manage")');
137+
assert.equal(e.message, `Application with "app" type of access requires (streamId = '${baseStreamIdClient}', level = "manage") or master access`);
138138
}
139139
// personal
140-
const appClient = await AppClientAccount.newFromApiEndpoint(user.apiEndpoint, baseStreamIdClient, appClientName);
140+
const appClient = await AppClientAccount.newFromApiEndpoint(baseStreamIdClient, user.apiEndpoint, appClientName);
141141
assert.equal(appClient.streamData.id, baseStreamIdClient);
142142
assert.equal(appClient.streamData.name, appClientName);
143143
});

0 commit comments

Comments
 (0)