@hitchy/plugin-odem-socket.io 0.4.0 → 0.5.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/api/services/odem/websocket-provider.js +85 -53
- package/package.json +15 -9
- package/readme.md +7 -3
|
@@ -31,7 +31,7 @@ export default function() {
|
|
|
31
31
|
const txPerModel = {};
|
|
32
32
|
|
|
33
33
|
for ( const [ modelName, model ] of Object.entries( api.models ) ) {
|
|
34
|
-
if ( api.service.OdemSchema.mayBeExposed(
|
|
34
|
+
if ( api.service.OdemSchema.mayBeExposed( undefined, model ) ) {
|
|
35
35
|
txPerModel[modelName] = this.broadcastModelNotifications( notifications, namespace, model );
|
|
36
36
|
}
|
|
37
37
|
}
|
|
@@ -84,7 +84,7 @@ export default function() {
|
|
|
84
84
|
const handlers = {
|
|
85
85
|
created: ( uuid, newRecord, asyncGeneratorFn ) => {
|
|
86
86
|
asyncGeneratorFn()
|
|
87
|
-
.then( item => this.namespaceEmit( namespace,
|
|
87
|
+
.then( item => this.namespaceEmit( namespace, item, {
|
|
88
88
|
change: "created",
|
|
89
89
|
model: Model.name,
|
|
90
90
|
properties: this.serializeItem( item, false ),
|
|
@@ -95,8 +95,8 @@ export default function() {
|
|
|
95
95
|
},
|
|
96
96
|
changed: ( uuid, newRecord, oldRecord, asyncGeneratorFn ) => {
|
|
97
97
|
asyncGeneratorFn()
|
|
98
|
-
.then( item => this.namespaceEmit( namespace,
|
|
99
|
-
change: "
|
|
98
|
+
.then( item => this.namespaceEmit( namespace, item, {
|
|
99
|
+
change: "changed",
|
|
100
100
|
model: Model.name,
|
|
101
101
|
properties: this.serializeItem( item, false ),
|
|
102
102
|
}, true ) )
|
|
@@ -104,12 +104,13 @@ export default function() {
|
|
|
104
104
|
logError( "broadcasting notification on having updated item %s of %s has failed:", uuid, Model.name, cause.stack );
|
|
105
105
|
} );
|
|
106
106
|
},
|
|
107
|
-
removed: uuid => {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
107
|
+
removed: ( uuid, lastRecord, asyncGeneratorFn ) => {
|
|
108
|
+
asyncGeneratorFn()
|
|
109
|
+
.then( item => this.namespaceEmit( namespace, item, {
|
|
110
|
+
change: "removed",
|
|
111
|
+
model: Model.name,
|
|
112
|
+
properties: { uuid: String( uuid ) },
|
|
113
|
+
}, false ) )
|
|
113
114
|
.catch( cause => {
|
|
114
115
|
logError( "broadcasting notification on having removed item %s of %s has failed:", uuid, Model.name, cause.stack );
|
|
115
116
|
} );
|
|
@@ -180,6 +181,8 @@ export default function() {
|
|
|
180
181
|
}
|
|
181
182
|
}
|
|
182
183
|
|
|
184
|
+
// TODO track the whole request instead of just its user to enable the plugin to assign $context to processed model instances
|
|
185
|
+
|
|
183
186
|
// pick user eventually injected into mocked request
|
|
184
187
|
return request.user || undefined;
|
|
185
188
|
}
|
|
@@ -197,14 +200,16 @@ export default function() {
|
|
|
197
200
|
* accessing the changed model and/or its properties.
|
|
198
201
|
*
|
|
199
202
|
* @param {Server} namespace namespace managing a pool of sockets that have joined the namespace
|
|
200
|
-
* @param {
|
|
201
|
-
* @param {{change:
|
|
203
|
+
* @param {Hitchy.Plugin.Odem.Model} item instance of model notification is related to
|
|
204
|
+
* @param {{change: "created"|"changed"|"removed", model: string, properties: Object}} payload raw payload of model-specific notification
|
|
202
205
|
* @param {boolean} filterProperties if true, properties need to be filtered per peer based on its authorization
|
|
203
206
|
* @returns {Promise<void>} promise resolved when all authorized peers have been notified
|
|
204
207
|
*/
|
|
205
|
-
static async namespaceEmit( namespace,
|
|
208
|
+
static async namespaceEmit( namespace, item, payload, filterProperties ) {
|
|
206
209
|
const hasPluginAuth = Boolean( api.plugins.authentication );
|
|
207
210
|
const sockets = await namespace.fetchSockets();
|
|
211
|
+
const Model = item.constructor;
|
|
212
|
+
const userCache = {};
|
|
208
213
|
|
|
209
214
|
logDebug( "forwarding notification to %d socket(s)", sockets.length );
|
|
210
215
|
|
|
@@ -219,46 +224,73 @@ export default function() {
|
|
|
219
224
|
return;
|
|
220
225
|
}
|
|
221
226
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
return;
|
|
225
|
-
}
|
|
227
|
+
const userId = user?.uuid ?? "(null)";
|
|
228
|
+
let cached = userCache[userId];
|
|
226
229
|
|
|
227
|
-
|
|
228
|
-
|
|
230
|
+
if ( cached == null ) {
|
|
231
|
+
cached = userCache[userId] = false;
|
|
229
232
|
|
|
230
|
-
|
|
231
|
-
|
|
233
|
+
// check connected user's common authorization to access the
|
|
234
|
+
// notification's model or the instance properties included with
|
|
235
|
+
// the notification
|
|
236
|
+
if ( !OdemSchema.mayBeExposed( user, Model ) ) {
|
|
237
|
+
logDebug( `omit notification to user ${userId} (${user?.name}) at peer ${socket.id} with access on ${Model.name} forbidden by model` );
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
232
240
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
241
|
+
let mayRead = OdemSchema.isAdmin( user );
|
|
242
|
+
let scope = "read";
|
|
243
|
+
|
|
244
|
+
if ( !mayRead && hasPluginAuth ) {
|
|
245
|
+
mayRead = await Authorization.mayAccess( user, `@hitchy.odem.model.${Model.name}.read` );
|
|
246
|
+
|
|
247
|
+
if ( !mayRead ) {
|
|
248
|
+
mayRead = await Authorization.mayAccess( user, `@hitchy.odem.model.${Model.name}.list` );
|
|
249
|
+
scope = "list";
|
|
250
|
+
}
|
|
236
251
|
}
|
|
237
|
-
}
|
|
238
252
|
|
|
239
|
-
|
|
253
|
+
// check model's life cycle hook individually controlling
|
|
254
|
+
// if current socket's user may receive the notification
|
|
255
|
+
const mayBeNotified = await item.beforeNotify( user, payload.change );
|
|
240
256
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
} else if ( filterProperties ) {
|
|
246
|
-
const copy = { ...payload };
|
|
257
|
+
if ( !mayBeNotified ) {
|
|
258
|
+
logDebug( `notification to user ${userId} (${user?.name}) at peer ${socket.id} prevented by beforeNotify() of model ${Model.name}` );
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
247
261
|
|
|
248
|
-
|
|
262
|
+
// eventually send the notification ...
|
|
263
|
+
if ( !mayRead ) {
|
|
264
|
+
// user must not read items of this model -> do not
|
|
265
|
+
// provide any identifier, but basically notify on some
|
|
266
|
+
// (unspecified) change to the model
|
|
267
|
+
cached = userCache[userId] = { ...payload, properties: {} };
|
|
268
|
+
} else if ( filterProperties ) {
|
|
269
|
+
// user may read items of this model, but access to some
|
|
270
|
+
// properties may be restricted by the model's schema
|
|
271
|
+
cached = userCache[userId] = {
|
|
272
|
+
...payload,
|
|
273
|
+
properties: OdemSchema.filterItem( payload.properties, user, Model, scope ),
|
|
274
|
+
};
|
|
275
|
+
} else {
|
|
276
|
+
// user gets notification with all properties of item
|
|
277
|
+
cached = userCache[userId] = payload;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
249
280
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
socket.emit(
|
|
281
|
+
if ( cached ) {
|
|
282
|
+
// user may receive notification -> emit it eventually
|
|
283
|
+
const prefix = api.utility.case.pascalToKebab( Model.name );
|
|
284
|
+
|
|
285
|
+
socket.emit( `${prefix}:changed`, cached );
|
|
286
|
+
socket.emit( "*:changed", cached );
|
|
255
287
|
}
|
|
256
288
|
};
|
|
257
289
|
|
|
258
290
|
for ( const socket of sockets ) {
|
|
259
291
|
notifyPeer( socket ).catch( cause => {
|
|
260
292
|
logError(
|
|
261
|
-
`notifying peer ${socket.id} on having ${payload.change} item of ${Model.name} has failed:`,
|
|
293
|
+
`notifying peer ${socket.id} on having ${payload.change} item ${item.uuid} of ${Model.name} has failed:`,
|
|
262
294
|
cause.stack
|
|
263
295
|
);
|
|
264
296
|
} );
|
|
@@ -308,7 +340,7 @@ export default function() {
|
|
|
308
340
|
}[action];
|
|
309
341
|
|
|
310
342
|
if ( !handler ) {
|
|
311
|
-
logDebug( "ignoring socket-based request for
|
|
343
|
+
logDebug( "ignoring socket-based request for action %s on model %s due to lack of handler", action, Model.name );
|
|
312
344
|
return;
|
|
313
345
|
}
|
|
314
346
|
|
|
@@ -343,7 +375,7 @@ export default function() {
|
|
|
343
375
|
}
|
|
344
376
|
}
|
|
345
377
|
|
|
346
|
-
if ( !api.service.OdemSchema.mayBeExposed(
|
|
378
|
+
if ( !api.service.OdemSchema.mayBeExposed( user, Model ) ) {
|
|
347
379
|
throw new Error( "access forbidden by model" );
|
|
348
380
|
}
|
|
349
381
|
|
|
@@ -352,7 +384,7 @@ export default function() {
|
|
|
352
384
|
let filter;
|
|
353
385
|
|
|
354
386
|
if ( loadRecords ) {
|
|
355
|
-
filter = item => api.service.OdemSchema.filterItem( this.serializeItem( item, false ),
|
|
387
|
+
filter = item => api.service.OdemSchema.filterItem( this.serializeItem( item, false ), user, Model, "list" );
|
|
356
388
|
} else {
|
|
357
389
|
filter = item => ( { uuid: item.uuid } );
|
|
358
390
|
}
|
|
@@ -395,7 +427,7 @@ export default function() {
|
|
|
395
427
|
}
|
|
396
428
|
}
|
|
397
429
|
|
|
398
|
-
if ( !api.service.OdemSchema.mayBeExposed(
|
|
430
|
+
if ( !api.service.OdemSchema.mayBeExposed( user, Model ) ) {
|
|
399
431
|
throw new Error( "access forbidden by model" );
|
|
400
432
|
}
|
|
401
433
|
|
|
@@ -404,7 +436,7 @@ export default function() {
|
|
|
404
436
|
let filter;
|
|
405
437
|
|
|
406
438
|
if ( loadRecords ) {
|
|
407
|
-
filter = item => api.service.OdemSchema.filterItem( this.serializeItem( item, false ),
|
|
439
|
+
filter = item => api.service.OdemSchema.filterItem( this.serializeItem( item, false ), user, Model, "list" );
|
|
408
440
|
} else {
|
|
409
441
|
filter = item => ( { uuid: item.uuid } );
|
|
410
442
|
}
|
|
@@ -444,12 +476,12 @@ export default function() {
|
|
|
444
476
|
throw new Error( "access forbidden" );
|
|
445
477
|
}
|
|
446
478
|
|
|
447
|
-
if ( !OdemSchema.mayBeExposed(
|
|
479
|
+
if ( !OdemSchema.mayBeExposed( user, Model ) ) {
|
|
448
480
|
throw new Error( "access forbidden by model" );
|
|
449
481
|
}
|
|
450
482
|
|
|
451
483
|
const schema = Model.schema;
|
|
452
|
-
const filtered = OdemSchema.filterItem( properties,
|
|
484
|
+
const filtered = OdemSchema.filterItem( properties, user, Model, "create" );
|
|
453
485
|
const instance = new Model();
|
|
454
486
|
|
|
455
487
|
for ( const propName of Object.keys( filtered ) ) {
|
|
@@ -462,7 +494,7 @@ export default function() {
|
|
|
462
494
|
|
|
463
495
|
response( {
|
|
464
496
|
success: true,
|
|
465
|
-
properties: OdemSchema.filterItem( this.serializeItem( instance, false ),
|
|
497
|
+
properties: OdemSchema.filterItem( this.serializeItem( instance, false ), user, Model, "read" ),
|
|
466
498
|
} );
|
|
467
499
|
} catch ( error ) {
|
|
468
500
|
logError( "creating %s instance failed: %s", Model.name, error.stack );
|
|
@@ -494,7 +526,7 @@ export default function() {
|
|
|
494
526
|
}
|
|
495
527
|
}
|
|
496
528
|
|
|
497
|
-
if ( !api.service.OdemSchema.mayBeExposed(
|
|
529
|
+
if ( !api.service.OdemSchema.mayBeExposed( user, Model ) ) {
|
|
498
530
|
throw new Error( "access forbidden by model" );
|
|
499
531
|
}
|
|
500
532
|
|
|
@@ -503,7 +535,7 @@ export default function() {
|
|
|
503
535
|
|
|
504
536
|
response( {
|
|
505
537
|
success: true,
|
|
506
|
-
properties: api.service.OdemSchema.filterItem( this.serializeItem( instance, true ),
|
|
538
|
+
properties: api.service.OdemSchema.filterItem( this.serializeItem( instance, true ), user, Model, "read" ),
|
|
507
539
|
} );
|
|
508
540
|
} catch ( error ) {
|
|
509
541
|
logError( "reading %s instance failed: %s", Model.name, error.stack );
|
|
@@ -536,12 +568,12 @@ export default function() {
|
|
|
536
568
|
}
|
|
537
569
|
}
|
|
538
570
|
|
|
539
|
-
if ( !api.service.OdemSchema.mayBeExposed(
|
|
571
|
+
if ( !api.service.OdemSchema.mayBeExposed( user, Model ) ) {
|
|
540
572
|
throw new Error( "access forbidden by model" );
|
|
541
573
|
}
|
|
542
574
|
|
|
543
575
|
const schema = Model.schema;
|
|
544
|
-
const filtered = api.service.OdemSchema.filterItem( properties,
|
|
576
|
+
const filtered = api.service.OdemSchema.filterItem( properties, user, Model, "write" );
|
|
545
577
|
const instance = new Model( uuid );
|
|
546
578
|
await instance.load();
|
|
547
579
|
|
|
@@ -555,7 +587,7 @@ export default function() {
|
|
|
555
587
|
|
|
556
588
|
response( {
|
|
557
589
|
success: true,
|
|
558
|
-
properties: api.service.OdemSchema.filterItem( this.serializeItem( instance, false ),
|
|
590
|
+
properties: api.service.OdemSchema.filterItem( this.serializeItem( instance, false ), user, Model, "read" ),
|
|
559
591
|
} );
|
|
560
592
|
} catch ( error ) {
|
|
561
593
|
logError( "updating %s instance failed: %s", Model.name, error.stack );
|
|
@@ -587,7 +619,7 @@ export default function() {
|
|
|
587
619
|
}
|
|
588
620
|
}
|
|
589
621
|
|
|
590
|
-
if ( !api.service.OdemSchema.mayBeExposed(
|
|
622
|
+
if ( !api.service.OdemSchema.mayBeExposed( user, Model ) ) {
|
|
591
623
|
throw new Error( "access forbidden by model" );
|
|
592
624
|
}
|
|
593
625
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hitchy/plugin-odem-socket.io",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "exposing Hitchy's document-oriented database via websocket",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -30,18 +30,24 @@
|
|
|
30
30
|
"index.js"
|
|
31
31
|
],
|
|
32
32
|
"peerDependencies": {
|
|
33
|
-
"@hitchy/core": "1.
|
|
34
|
-
"@hitchy/plugin-odem": "0.
|
|
35
|
-
"@hitchy/plugin-socket.io": "0.1.
|
|
33
|
+
"@hitchy/core": "^1.5.5",
|
|
34
|
+
"@hitchy/plugin-odem": "^0.14.0",
|
|
35
|
+
"@hitchy/plugin-socket.io": "^0.1.3"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
|
-
"@hitchy/server-dev-tools": "^0.
|
|
38
|
+
"@hitchy/server-dev-tools": "^0.9.6",
|
|
39
39
|
"c8": "^10.1.3",
|
|
40
|
-
"eslint": "^9.
|
|
40
|
+
"eslint": "^9.39.2",
|
|
41
41
|
"eslint-config-cepharum": "^2.0.2",
|
|
42
|
-
"
|
|
43
|
-
"mocha": "^11.1.0",
|
|
42
|
+
"mocha": "^11.7.5",
|
|
44
43
|
"should": "^13.2.3",
|
|
45
|
-
"socket.io-client": "^4.8.
|
|
44
|
+
"socket.io-client": "^4.8.3"
|
|
45
|
+
},
|
|
46
|
+
"c8": {
|
|
47
|
+
"reporter": [
|
|
48
|
+
"text",
|
|
49
|
+
"html"
|
|
50
|
+
],
|
|
51
|
+
"all": true
|
|
46
52
|
}
|
|
47
53
|
}
|
package/readme.md
CHANGED
|
@@ -166,8 +166,12 @@ Last argument is a callback to be invoked with the resulting response from serve
|
|
|
166
166
|
The type of change is
|
|
167
167
|
|
|
168
168
|
* `created` on notifications about having created another instance of model,
|
|
169
|
-
* `
|
|
170
|
-
* `
|
|
169
|
+
* `changed` on notifications about having adjusted one or more properties of an existing instance of model and
|
|
170
|
+
* `removed` on notifications about having removed an existing instance of model.
|
|
171
|
+
|
|
172
|
+
:::danger Breaking change
|
|
173
|
+
Notification types have changed in v0.5.0 from `updated` to `changed` and from `deleted` to `removed` to match names used on server side.
|
|
174
|
+
:::
|
|
171
175
|
|
|
172
176
|
In last case the provided set of properties is limited to the instance's UUID. The common pattern on client-side for listening to server-side notifications looks like this:
|
|
173
177
|
|
|
@@ -175,7 +179,7 @@ In last case the provided set of properties is limited to the instance's UUID. T
|
|
|
175
179
|
const socket = io( "/hitchy/odem" );
|
|
176
180
|
|
|
177
181
|
socket.on( "<modelName>:changed", info => {
|
|
178
|
-
// type of change is in `info.change`, e.g. "
|
|
182
|
+
// type of change is in `info.change`, e.g. "created", "changed" or "removed"
|
|
179
183
|
// process properties of changed instance in `info.properties`
|
|
180
184
|
} );
|
|
181
185
|
```
|