@hitchy/plugin-odem-socket.io 0.2.3 → 0.3.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 +397 -164
- package/package.json +13 -14
- package/readme.md +20 -18
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
+
const { promisify } = require( "node:util" );
|
|
4
|
+
|
|
3
5
|
module.exports = function() {
|
|
4
6
|
const api = this;
|
|
5
7
|
|
|
6
|
-
const logDebug = api.log( "hitchy:odem:socket.io:debug" );
|
|
7
|
-
const
|
|
8
|
+
const logDebug = api.log( "hitchy:plugin:odem:socket.io:debug" );
|
|
9
|
+
const logInfo = api.log( "hitchy:plugin:odem:socket.io:info" );
|
|
10
|
+
const logError = api.log( "hitchy:plugin:odem:socket.io:error" );
|
|
8
11
|
|
|
9
12
|
/**
|
|
10
|
-
* Implements API for controlling and monitoring server-side
|
|
11
|
-
* websocket.
|
|
13
|
+
* Implements API for controlling and monitoring server-side
|
|
14
|
+
* document-oriented database via websocket.
|
|
12
15
|
*/
|
|
13
16
|
class OdemWebsocketProvider {
|
|
17
|
+
static #socketUsers = new Map();
|
|
18
|
+
|
|
14
19
|
/**
|
|
15
20
|
* Integrates this provider with server-side websocket.
|
|
16
21
|
*
|
|
@@ -19,183 +24,347 @@ module.exports = function() {
|
|
|
19
24
|
static start() {
|
|
20
25
|
api.once( "websocket", io => {
|
|
21
26
|
const { crud, notifications } = api.config.socket.odem;
|
|
22
|
-
|
|
23
27
|
const namespace = io.of( "/hitchy/odem" );
|
|
24
28
|
|
|
29
|
+
|
|
30
|
+
// - transmit notifications to clients ------------------------
|
|
31
|
+
|
|
25
32
|
const txPerModel = {};
|
|
26
33
|
|
|
27
34
|
for ( const [ modelName, model ] of Object.entries( api.models ) ) {
|
|
28
|
-
|
|
35
|
+
if ( api.service.OdemSchema.mayBeExposed( { user: undefined }, model ) ) {
|
|
36
|
+
txPerModel[modelName] = this.broadcastModelNotifications( notifications, namespace, model );
|
|
37
|
+
}
|
|
29
38
|
}
|
|
30
39
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
for ( const [ modelName, model ] of Object.entries( api.models ) ) {
|
|
35
|
-
rxPerModel[modelName] = this.handleModelRequests( crud, socket, modelName, model );
|
|
36
|
-
}
|
|
40
|
+
api.once( "close", () => {
|
|
41
|
+
namespace.disconnectSockets( true );
|
|
37
42
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const prefix = api.utility.case.pascalToKebab( modelName );
|
|
43
|
+
for ( const [ modelName, handlers ] of Object.entries( txPerModel ) ) {
|
|
44
|
+
if ( handlers ) {
|
|
45
|
+
const model = api.models[modelName];
|
|
42
46
|
|
|
43
|
-
|
|
44
|
-
|
|
47
|
+
model.notifications
|
|
48
|
+
.off( "created", handlers.created )
|
|
49
|
+
.off( "changed", handlers.changed )
|
|
50
|
+
.off( "removed", handlers.removed );
|
|
45
51
|
}
|
|
46
|
-
}
|
|
52
|
+
}
|
|
47
53
|
} );
|
|
48
54
|
|
|
49
|
-
api.once( "close", () => {
|
|
50
|
-
namespace.disconnectSockets( true );
|
|
51
55
|
|
|
52
|
-
|
|
53
|
-
const handlers = txPerModel[modelName];
|
|
56
|
+
// - receive requests from clients ----------------------------
|
|
54
57
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
namespace.on( "connection", socket => {
|
|
59
|
+
this.getUserForSocket( socket );
|
|
60
|
+
|
|
61
|
+
const listener = ( query, ...args ) => this.handleControlRequest( crud, socket, query, args );
|
|
62
|
+
|
|
63
|
+
socket.onAny( listener );
|
|
64
|
+
|
|
65
|
+
socket.once( "disconnect", () => {
|
|
66
|
+
socket.offAny( listener );
|
|
67
|
+
|
|
68
|
+
this.#socketUsers.delete( socket.id );
|
|
69
|
+
} );
|
|
60
70
|
} );
|
|
61
71
|
} );
|
|
62
72
|
}
|
|
63
73
|
|
|
64
74
|
/**
|
|
65
|
-
*
|
|
66
|
-
* model
|
|
75
|
+
* Monitors models of document-oriented database and sends notifications
|
|
76
|
+
* to connected clients in case items of either model have changed.
|
|
67
77
|
*
|
|
68
78
|
* @param {boolean} enabled true if command handling is enabled in configuration
|
|
69
|
-
* @param {
|
|
70
|
-
* @param {
|
|
71
|
-
* @
|
|
72
|
-
* @returns {Object<string,function>} map of registered handler functions
|
|
79
|
+
* @param {Server} namespace namespace managing a list of server-side sockets with common prefix
|
|
80
|
+
* @param {class<Hitchy.Plugin.Odem.Model>} Model model to monitor for change of items
|
|
81
|
+
* @returns {Object<string,function>|undefined} map of registered handler functions
|
|
73
82
|
*/
|
|
74
|
-
static
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
if ( !enabled ) {
|
|
78
|
-
logDebug( "disabling socket-based action requests for model %s (%s)", modelName, prefix );
|
|
79
|
-
|
|
83
|
+
static broadcastModelNotifications( enabled, namespace, Model ) {
|
|
84
|
+
if ( enabled ) {
|
|
80
85
|
const handlers = {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
86
|
+
created: ( uuid, newRecord, asyncGeneratorFn ) => {
|
|
87
|
+
asyncGeneratorFn()
|
|
88
|
+
.then( item => this.namespaceEmit( namespace, Model, {
|
|
89
|
+
change: "created",
|
|
90
|
+
model: Model.name,
|
|
91
|
+
properties: item.toObject( { serialized: true } ),
|
|
92
|
+
}, true ) )
|
|
93
|
+
.catch( cause => {
|
|
94
|
+
logError( "broadcasting notification on having created item %s of %s has failed:", uuid, Model.name, cause.stack );
|
|
95
|
+
} );
|
|
96
|
+
},
|
|
97
|
+
changed: ( uuid, newRecord, oldRecord, asyncGeneratorFn ) => {
|
|
98
|
+
asyncGeneratorFn()
|
|
99
|
+
.then( item => this.namespaceEmit( namespace, Model, {
|
|
100
|
+
change: "updated",
|
|
101
|
+
model: Model.name,
|
|
102
|
+
properties: item.toObject( { serialized: true } ),
|
|
103
|
+
}, true ) )
|
|
104
|
+
.catch( cause => {
|
|
105
|
+
logError( "broadcasting notification on having updated item %s of %s has failed:", uuid, Model.name, cause.stack );
|
|
106
|
+
} );
|
|
107
|
+
},
|
|
108
|
+
removed: uuid => {
|
|
109
|
+
this.namespaceEmit( namespace, Model, {
|
|
110
|
+
change: "deleted",
|
|
111
|
+
model: Model.name,
|
|
112
|
+
properties: { uuid: Model.formatUUID( uuid ) },
|
|
113
|
+
}, false )
|
|
114
|
+
.catch( cause => {
|
|
115
|
+
logError( "broadcasting notification on having removed item %s of %s has failed:", uuid, Model.name, cause.stack );
|
|
116
|
+
} );
|
|
117
|
+
},
|
|
87
118
|
};
|
|
88
119
|
|
|
89
|
-
|
|
90
|
-
.on(
|
|
91
|
-
.on(
|
|
92
|
-
.on(
|
|
93
|
-
.on( `${prefix}:read`, handlers.read )
|
|
94
|
-
.on( `${prefix}:update`, handlers.update )
|
|
95
|
-
.on( `${prefix}:delete`, handlers.delete );
|
|
120
|
+
Model.notifications
|
|
121
|
+
.on( "created", handlers.created )
|
|
122
|
+
.on( "changed", handlers.changed )
|
|
123
|
+
.on( "removed", handlers.removed );
|
|
96
124
|
|
|
97
125
|
return handlers;
|
|
98
126
|
}
|
|
99
127
|
|
|
100
|
-
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
101
130
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
131
|
+
/**
|
|
132
|
+
* Reads promise for given socket's requesting user from local cache.
|
|
133
|
+
*
|
|
134
|
+
* This method is mocking Hitchy's request dispatching to invoke
|
|
135
|
+
* policies of @hitchy/plugin-auth involved in setting up a requesting
|
|
136
|
+
* user to be returned here eventually. The mocked request is based on
|
|
137
|
+
* the provided socket's initial request.
|
|
138
|
+
*
|
|
139
|
+
* @param {WebSocket} socket socket to look up
|
|
140
|
+
* @returns {Promise<User|undefined>} promise for user of provided socket
|
|
141
|
+
*/
|
|
142
|
+
static async getUserForSocket( socket ) {
|
|
143
|
+
if ( !this.#socketUsers.has( socket.id ) ) {
|
|
144
|
+
this.#socketUsers.set( socket.id, ( async _socket => {
|
|
145
|
+
if ( api.plugins.authentication ) {
|
|
146
|
+
const { SessionInjector, Cookies } = api.service;
|
|
147
|
+
const { Authentication } = api.policy;
|
|
148
|
+
const useSession = api.plugins.session && !api.config?.session?.disable;
|
|
149
|
+
const headers = _socket.request?.headers;
|
|
150
|
+
|
|
151
|
+
// mock context used by Hitchy for dispatching requests
|
|
152
|
+
const request = {
|
|
153
|
+
headers,
|
|
154
|
+
cookies: Cookies.parseFromString( headers?.cookie ),
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const response = {
|
|
158
|
+
set: () => {}, // eslint-disable-line no-empty-function
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const context = {
|
|
162
|
+
local: {},
|
|
163
|
+
updateSessionId: ( _, __, fn ) => fn(),
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
// invoke policies usually injecting a requesting user into a request
|
|
167
|
+
if ( useSession ) {
|
|
168
|
+
try {
|
|
169
|
+
await SessionInjector.injectIntoRequest( request, response );
|
|
170
|
+
await promisify( Authentication.injectPassport.bind( context ) )( request, response );
|
|
171
|
+
} catch ( cause ) {
|
|
172
|
+
logError( "mocking session-handling and passport injection has failed:", cause );
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if ( !request.user ) {
|
|
177
|
+
try {
|
|
178
|
+
await promisify( Authentication.handleBasicAuth.bind( context ) )( request, response );
|
|
179
|
+
} catch ( cause ) {
|
|
180
|
+
logError( "mocking Basic authentication handling has failed:", cause );
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// pick user eventually injected into mocked request
|
|
185
|
+
return request.user || undefined;
|
|
186
|
+
}
|
|
110
187
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
.on( `${prefix}:create`, handlers.create )
|
|
115
|
-
.on( `${prefix}:read`, handlers.read )
|
|
116
|
-
.on( `${prefix}:update`, handlers.update )
|
|
117
|
-
.on( `${prefix}:delete`, handlers.delete );
|
|
188
|
+
return undefined;
|
|
189
|
+
} )( socket ) );
|
|
190
|
+
}
|
|
118
191
|
|
|
119
|
-
return
|
|
192
|
+
return await this.#socketUsers.get( socket.id );
|
|
120
193
|
}
|
|
121
194
|
|
|
122
195
|
/**
|
|
123
|
-
*
|
|
124
|
-
*
|
|
196
|
+
* Broadcasts a change notification to every connected peer in a given
|
|
197
|
+
* namespace individually considering either peer's authorization for
|
|
198
|
+
* accessing the changed model and/or its properties.
|
|
125
199
|
*
|
|
126
|
-
* @param {
|
|
127
|
-
* @param {
|
|
128
|
-
* @param {string}
|
|
129
|
-
* @param {
|
|
130
|
-
* @returns {
|
|
200
|
+
* @param {Server} namespace namespace managing a pool of sockets that have joined the namespace
|
|
201
|
+
* @param {class<Hitchy.Plugin.Odem.Model>} Model model of changed item
|
|
202
|
+
* @param {{change: string, model: string, properties: Object}} payload raw payload of model-specific notification
|
|
203
|
+
* @param {boolean} filterProperties if true, properties need to be filtered per peer based on its authorization
|
|
204
|
+
* @returns {Promise<void>} promise resolved when all authorized peers have been notified
|
|
131
205
|
*/
|
|
132
|
-
static
|
|
133
|
-
|
|
134
|
-
|
|
206
|
+
static async namespaceEmit( namespace, Model, payload, filterProperties ) {
|
|
207
|
+
const hasPluginAuth = Boolean( api.plugins.authentication );
|
|
208
|
+
const sockets = await namespace.fetchSockets();
|
|
135
209
|
|
|
136
|
-
|
|
137
|
-
created: async( uuid, newRecord, asyncGeneratorFn ) => {
|
|
138
|
-
const event = {
|
|
139
|
-
change: "created",
|
|
140
|
-
properties: ( await asyncGeneratorFn() ).toObject( { serialized: true } ),
|
|
141
|
-
};
|
|
210
|
+
logDebug( "forwarding notification to %d socket(s)", sockets.length );
|
|
142
211
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
const event = {
|
|
147
|
-
change: "updated",
|
|
148
|
-
properties: ( await asyncGeneratorFn() ).toObject( { serialized: true } ),
|
|
149
|
-
};
|
|
212
|
+
const notifyPeer = async socket => {
|
|
213
|
+
const { OdemSchema, Authorization } = api.service;
|
|
214
|
+
let user;
|
|
150
215
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
};
|
|
216
|
+
try {
|
|
217
|
+
user = await this.#socketUsers.get( socket.id );
|
|
218
|
+
} catch ( cause ) {
|
|
219
|
+
logError( `discovering user of socket ${socket.id} has failed:`, cause );
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
158
222
|
|
|
159
|
-
|
|
160
|
-
}
|
|
161
|
-
|
|
223
|
+
if ( !OdemSchema.mayBeExposed( { user }, Model ) ) {
|
|
224
|
+
logDebug( `omit notification to user ${user?.uuid} (${user.name}) at peer ${socket.id} with access on ${Model.name} forbidden by model` );
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
162
227
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
.on( "changed", handlers.changed )
|
|
166
|
-
.on( "removed", handlers.removed );
|
|
228
|
+
let mayRead = OdemSchema.isAdmin( user );
|
|
229
|
+
let scope = "read";
|
|
167
230
|
|
|
168
|
-
|
|
231
|
+
if ( !mayRead && hasPluginAuth ) {
|
|
232
|
+
mayRead = await Authorization.mayAccess( user, `@hitchy.odem.model.${Model.name}.read` );
|
|
233
|
+
|
|
234
|
+
if ( !mayRead ) {
|
|
235
|
+
mayRead = await Authorization.mayAccess( user, `@hitchy.odem.model.${Model.name}.list` );
|
|
236
|
+
scope = "list";
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const prefix = api.utility.case.pascalToKebab( Model.name );
|
|
241
|
+
|
|
242
|
+
if ( !mayRead ) {
|
|
243
|
+
// user must not read items of this model -> do not provide any identifier, but basically notify on some change to the model
|
|
244
|
+
socket.emit( `${prefix}:changed`, { ...payload, properties: {} } );
|
|
245
|
+
socket.emit( "*:changed", { ...payload, properties: {} } );
|
|
246
|
+
} else if ( filterProperties ) {
|
|
247
|
+
const copy = { ...payload };
|
|
248
|
+
|
|
249
|
+
copy.properties = OdemSchema.filterItem( copy.properties, { user }, Model, scope );
|
|
250
|
+
|
|
251
|
+
socket.emit( `${prefix}:changed`, copy );
|
|
252
|
+
socket.emit( "*:changed", copy );
|
|
253
|
+
} else {
|
|
254
|
+
socket.emit( `${prefix}:changed`, payload );
|
|
255
|
+
socket.emit( "*:changed", payload );
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
for ( const socket of sockets ) {
|
|
260
|
+
notifyPeer( socket ).catch( cause => {
|
|
261
|
+
logError(
|
|
262
|
+
`notifying peer ${socket.id} on having ${payload.change} item of ${Model.name} has failed:`,
|
|
263
|
+
cause.stack
|
|
264
|
+
);
|
|
265
|
+
} );
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Commonly handles incoming messages to process those requesting
|
|
271
|
+
* supported control actions on existing models.
|
|
272
|
+
*
|
|
273
|
+
* @param {boolean} enabled if true, this plugin's control API has been enabled in configuration
|
|
274
|
+
* @param {WebSocket} socket request-receiving socket
|
|
275
|
+
* @param {string} query name of event that has been emitted by peer socket to request some action
|
|
276
|
+
* @param {any[]} args arguments of incoming request and a callback for sending a response in final argument
|
|
277
|
+
* @returns {void}
|
|
278
|
+
*/
|
|
279
|
+
static handleControlRequest( enabled, socket, query, args ) {
|
|
280
|
+
const match = /^([a-zA-Z][a-zA-Z0-9]+):(list|find|create|read|write|update|remove|delete)$/.exec( query );
|
|
281
|
+
|
|
282
|
+
if ( !match ) {
|
|
283
|
+
logDebug( "ignoring socket-based request %s due to mismatching format", query );
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const [ , prefix, action ] = match;
|
|
288
|
+
const modelName = api.utility.case.kebabToPascal( prefix );
|
|
289
|
+
const Model = api.model[modelName];
|
|
290
|
+
|
|
291
|
+
if ( !Model ) {
|
|
292
|
+
logDebug( "rejecting socket-based request %s due to addressing unknown model %s", query, modelName );
|
|
293
|
+
|
|
294
|
+
args[args.length - 1]( {
|
|
295
|
+
error: "no such model " + modelName,
|
|
296
|
+
} );
|
|
297
|
+
return;
|
|
169
298
|
}
|
|
170
299
|
|
|
171
|
-
|
|
300
|
+
const handler = {
|
|
301
|
+
list: "handleListRequest",
|
|
302
|
+
find: "handleFindRequest",
|
|
303
|
+
create: "handleCreateRequest",
|
|
304
|
+
read: "handleReadRequest",
|
|
305
|
+
write: "handleUpdateRequest",
|
|
306
|
+
update: "handleUpdateRequest",
|
|
307
|
+
remove: "handleDeleteRequest",
|
|
308
|
+
delete: "handleDeleteRequest",
|
|
309
|
+
}[action];
|
|
310
|
+
|
|
311
|
+
if ( !handler ) {
|
|
312
|
+
logDebug( "ignoring socket-based request for actin %s on model %s due to lack of handler", action, Model.name );
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if ( enabled ) {
|
|
317
|
+
this[handler]( socket, Model, ...args );
|
|
318
|
+
} else {
|
|
319
|
+
logDebug( "rejecting socket-based request for action %s on model %s", action, Model.name );
|
|
320
|
+
|
|
321
|
+
reject( args[args.length - 1] );
|
|
322
|
+
}
|
|
172
323
|
}
|
|
173
324
|
|
|
174
325
|
/**
|
|
175
326
|
* Handles incoming request for listing items of a model.
|
|
176
327
|
*
|
|
177
|
-
* @param {
|
|
178
|
-
* @param {
|
|
179
|
-
* @param {string} sessionId client-provided session ID
|
|
328
|
+
* @param {WebSocket} socket receiving socket of request
|
|
329
|
+
* @param {class<Hitchy.Plugin.Odem.Model>} Model named server-side model
|
|
180
330
|
* @param {Hitchy.Odem.ListOptions} queryOptions client-provided options for listing items
|
|
181
331
|
* @param {boolean} loadRecords if true, whole records shall be fetched per item
|
|
182
332
|
* @param {function} response callback to invoke with response
|
|
183
333
|
* @returns {Promise<void>} promise settled on request handled
|
|
184
334
|
*/
|
|
185
|
-
static async handleListRequest(
|
|
186
|
-
logDebug( "request for listing %s instances w/ queryOptions %j and loadRecords %j",
|
|
335
|
+
static async handleListRequest( socket, Model, queryOptions, loadRecords, response ) {
|
|
336
|
+
logDebug( "request for listing %s instances w/ queryOptions %j and loadRecords %j", Model.name, queryOptions, Boolean( loadRecords ) );
|
|
187
337
|
|
|
188
338
|
try {
|
|
339
|
+
const user = await this.getUserForSocket( socket );
|
|
340
|
+
|
|
341
|
+
if ( api.plugins.authentication ) {
|
|
342
|
+
if ( !await api.service.Authorization.mayAccess( user, `@hitchy.odem.model.${Model.name}.list` ) ) {
|
|
343
|
+
throw new Error( "access forbidden" );
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if ( !api.service.OdemSchema.mayBeExposed( { user }, Model ) ) {
|
|
348
|
+
throw new Error( "access forbidden by model" );
|
|
349
|
+
}
|
|
350
|
+
|
|
189
351
|
const meta = {};
|
|
190
|
-
const items = await
|
|
352
|
+
const items = await Model.list( queryOptions, { loadRecords: Boolean( loadRecords ), metaCollector: meta } );
|
|
353
|
+
let filter;
|
|
354
|
+
|
|
355
|
+
if ( loadRecords ) {
|
|
356
|
+
filter = item => api.service.OdemSchema.filterItem( item.toObject( { serialized: true } ), { user }, Model, "list" );
|
|
357
|
+
} else {
|
|
358
|
+
filter = item => ( { uuid: item.uuid } );
|
|
359
|
+
}
|
|
191
360
|
|
|
192
361
|
response( {
|
|
193
362
|
success: true,
|
|
194
363
|
count: meta.count,
|
|
195
|
-
items: items.map(
|
|
364
|
+
items: items.map( filter ),
|
|
196
365
|
} );
|
|
197
366
|
} catch ( error ) {
|
|
198
|
-
logError( "listing %s instances failed: %s",
|
|
367
|
+
logError( "listing %s instances failed: %s", Model.name, error.stack );
|
|
199
368
|
|
|
200
369
|
response( {
|
|
201
370
|
error: error.message,
|
|
@@ -207,29 +376,47 @@ module.exports = function() {
|
|
|
207
376
|
* Handles incoming request for finding items of a model matching some
|
|
208
377
|
* provided query.
|
|
209
378
|
*
|
|
210
|
-
* @param {
|
|
211
|
-
* @param {
|
|
212
|
-
* @param {string} sessionId client-provided session ID
|
|
379
|
+
* @param {WebSocket} socket receiving socket of request
|
|
380
|
+
* @param {class<Hitchy.Plugin.Odem.Model>} Model named server-side model
|
|
213
381
|
* @param {Hitchy.Odem.Query} query client-provided constraints to be met by returned items
|
|
214
382
|
* @param {Hitchy.Odem.ListOptions} queryOptions client-provided options for listing items
|
|
215
383
|
* @param {boolean} loadRecords if true, whole records shall be fetched per item
|
|
216
384
|
* @param {function} response callback to invoke with response
|
|
217
385
|
* @returns {Promise<void>} promise settled on request handled
|
|
218
386
|
*/
|
|
219
|
-
static async handleFindRequest(
|
|
220
|
-
logDebug( "request for finding %s instances matching %j w/ queryOptions %j and uuidsOnly %j",
|
|
387
|
+
static async handleFindRequest( socket, Model, query, queryOptions, loadRecords, response ) {
|
|
388
|
+
logDebug( "request for finding %s instances matching %j w/ queryOptions %j and uuidsOnly %j", Model.name, query, queryOptions, Boolean( loadRecords ) );
|
|
221
389
|
|
|
222
390
|
try {
|
|
391
|
+
const user = await this.getUserForSocket( socket );
|
|
392
|
+
|
|
393
|
+
if ( api.plugins.authentication ) {
|
|
394
|
+
if ( !await api.service.Authorization.mayAccess( user, `@hitchy.odem.model.${Model.name}.list` ) ) {
|
|
395
|
+
throw new Error( "access forbidden" );
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if ( !api.service.OdemSchema.mayBeExposed( { user }, Model ) ) {
|
|
400
|
+
throw new Error( "access forbidden by model" );
|
|
401
|
+
}
|
|
402
|
+
|
|
223
403
|
const meta = {};
|
|
224
|
-
const items = await
|
|
404
|
+
const items = await Model.find( query, queryOptions, { loadRecords: Boolean( loadRecords ), metaCollector: meta } );
|
|
405
|
+
let filter;
|
|
406
|
+
|
|
407
|
+
if ( loadRecords ) {
|
|
408
|
+
filter = item => api.service.OdemSchema.filterItem( item.toObject( { serialized: true } ), { user }, Model, "list" );
|
|
409
|
+
} else {
|
|
410
|
+
filter = item => ( { uuid: item.uuid } );
|
|
411
|
+
}
|
|
225
412
|
|
|
226
413
|
response( {
|
|
227
414
|
success: true,
|
|
228
415
|
count: meta.count,
|
|
229
|
-
items: items.map(
|
|
416
|
+
items: items.map( filter ),
|
|
230
417
|
} );
|
|
231
418
|
} catch ( error ) {
|
|
232
|
-
logError( "finding %s instances matching %j failed: %s",
|
|
419
|
+
logError( "finding %s instances matching %j failed: %s", Model.name, query, error.stack );
|
|
233
420
|
|
|
234
421
|
response( {
|
|
235
422
|
error: error.message,
|
|
@@ -240,24 +427,35 @@ module.exports = function() {
|
|
|
240
427
|
/**
|
|
241
428
|
* Handles incoming request for creating item of a model.
|
|
242
429
|
*
|
|
243
|
-
* @param {
|
|
244
|
-
* @param {
|
|
245
|
-
* @param {string} sessionId client-provided session ID
|
|
430
|
+
* @param {WebSocket} socket receiving socket of request
|
|
431
|
+
* @param {class<Hitchy.Plugin.Odem.Model>} Model named server-side model
|
|
246
432
|
* @param {Hitchy.Odem.ListOptions} properties properties of item to create
|
|
247
433
|
* @param {function} response callback to invoke with response
|
|
248
434
|
* @returns {Promise<void>} promise settled on request handled
|
|
249
435
|
*/
|
|
250
|
-
static async handleCreateRequest(
|
|
251
|
-
logDebug( "request for creating %s instance with %j",
|
|
436
|
+
static async handleCreateRequest( socket, Model, properties, response ) {
|
|
437
|
+
logDebug( "request for creating %s instance with %j", Model.name, properties );
|
|
438
|
+
|
|
439
|
+
const { Authorization, OdemSchema } = api.service;
|
|
252
440
|
|
|
253
441
|
try {
|
|
254
|
-
const
|
|
255
|
-
|
|
442
|
+
const user = await this.getUserForSocket( socket );
|
|
443
|
+
|
|
444
|
+
if ( api.plugins.authentication && !await Authorization.mayAccess( user, `@hitchy.odem.model.${Model.name}.create` ) ) {
|
|
445
|
+
throw new Error( "access forbidden" );
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if ( !OdemSchema.mayBeExposed( { user }, Model ) ) {
|
|
449
|
+
throw new Error( "access forbidden by model" );
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const schema = Model.schema;
|
|
453
|
+
const filtered = OdemSchema.filterItem( properties, { user }, Model, "create" );
|
|
454
|
+
const instance = new Model();
|
|
256
455
|
|
|
257
|
-
for ( const propName of Object.
|
|
258
|
-
if ( schema.props.hasOwnProperty( propName ) ||
|
|
259
|
-
|
|
260
|
-
instance[propName] = properties[propName];
|
|
456
|
+
for ( const [ propName, propValue ] of Object.entries( filtered ) ) {
|
|
457
|
+
if ( schema.props.hasOwnProperty( propName ) || schema.computed.hasOwnProperty( propName ) ) {
|
|
458
|
+
instance[propName] = propValue;
|
|
261
459
|
}
|
|
262
460
|
}
|
|
263
461
|
|
|
@@ -265,10 +463,10 @@ module.exports = function() {
|
|
|
265
463
|
|
|
266
464
|
response( {
|
|
267
465
|
success: true,
|
|
268
|
-
properties: instance.toObject( { serialized: true } ),
|
|
466
|
+
properties: OdemSchema.filterItem( instance.toObject( { serialized: true } ), { user }, Model, "read" ),
|
|
269
467
|
} );
|
|
270
468
|
} catch ( error ) {
|
|
271
|
-
logError( "creating %s instance failed: %s",
|
|
469
|
+
logError( "creating %s instance failed: %s", Model.name, error.stack );
|
|
272
470
|
|
|
273
471
|
response( {
|
|
274
472
|
error: error.message,
|
|
@@ -279,26 +477,37 @@ module.exports = function() {
|
|
|
279
477
|
/**
|
|
280
478
|
* Handles incoming request for reading properties of a single item.
|
|
281
479
|
*
|
|
282
|
-
* @param {
|
|
283
|
-
* @param {
|
|
284
|
-
* @param {string} sessionId client-provided session ID
|
|
480
|
+
* @param {WebSocket} socket receiving socket of request
|
|
481
|
+
* @param {class<Hitchy.Plugin.Odem.Model>} Model named server-side model
|
|
285
482
|
* @param {string} uuid client-provided UUID of item to read
|
|
286
483
|
* @param {function} response callback to invoke with response
|
|
287
484
|
* @returns {Promise<void>} promise settled on request handled
|
|
288
485
|
*/
|
|
289
|
-
static async handleReadRequest(
|
|
290
|
-
logDebug( "request for reading %s instance %s",
|
|
486
|
+
static async handleReadRequest( socket, Model, uuid, response ) {
|
|
487
|
+
logDebug( "request for reading %s instance %s", Model.name, uuid );
|
|
291
488
|
|
|
292
489
|
try {
|
|
293
|
-
const
|
|
490
|
+
const user = await this.getUserForSocket( socket );
|
|
491
|
+
|
|
492
|
+
if ( api.plugins.authentication ) {
|
|
493
|
+
if ( !await api.service.Authorization.mayAccess( user, `@hitchy.odem.model.${Model.name}.read` ) ) {
|
|
494
|
+
throw new Error( "access forbidden" );
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
if ( !api.service.OdemSchema.mayBeExposed( { user }, Model ) ) {
|
|
499
|
+
throw new Error( "access forbidden by model" );
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const instance = new Model( uuid );
|
|
294
503
|
await instance.load();
|
|
295
504
|
|
|
296
505
|
response( {
|
|
297
506
|
success: true,
|
|
298
|
-
properties: instance.toObject( { serialized: true } ),
|
|
507
|
+
properties: api.service.OdemSchema.filterItem( instance.toObject( { serialized: true } ), { user }, Model, "read" ),
|
|
299
508
|
} );
|
|
300
509
|
} catch ( error ) {
|
|
301
|
-
logError( "reading %s instance failed: %s",
|
|
510
|
+
logError( "reading %s instance failed: %s", Model.name, error.stack );
|
|
302
511
|
|
|
303
512
|
response( {
|
|
304
513
|
error: error.message,
|
|
@@ -309,24 +518,37 @@ module.exports = function() {
|
|
|
309
518
|
/**
|
|
310
519
|
* Handles incoming request for adjusting properties of a single item.
|
|
311
520
|
*
|
|
312
|
-
* @param {
|
|
313
|
-
* @param {
|
|
314
|
-
* @param {string} sessionId client-provided session ID
|
|
521
|
+
* @param {WebSocket} socket receiving socket of request
|
|
522
|
+
* @param {class<Hitchy.Plugin.Odem.Model>} Model named server-side model
|
|
315
523
|
* @param {string} uuid client-provided UUID of item to modify
|
|
316
524
|
* @param {Object} properties client-provided properties of item replacing its existing ones
|
|
317
525
|
* @param {function} response callback to invoke with response
|
|
318
526
|
* @returns {Promise<void>} promise settled on request handled
|
|
319
527
|
*/
|
|
320
|
-
static async handleUpdateRequest(
|
|
321
|
-
logDebug( "request for updating %s instance %s with %j",
|
|
528
|
+
static async handleUpdateRequest( socket, Model, uuid, properties, response ) {
|
|
529
|
+
logDebug( "request for updating %s instance %s with %j", Model.name, uuid, properties );
|
|
322
530
|
|
|
323
531
|
try {
|
|
324
|
-
const
|
|
532
|
+
const user = await this.getUserForSocket( socket );
|
|
533
|
+
|
|
534
|
+
if ( api.plugins.authentication ) {
|
|
535
|
+
if ( !await api.service.Authorization.mayAccess( user, `@hitchy.odem.model.${Model.name}.write` ) ) {
|
|
536
|
+
throw new Error( "access forbidden" );
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
if ( !api.service.OdemSchema.mayBeExposed( { user }, Model ) ) {
|
|
541
|
+
throw new Error( "access forbidden by model" );
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
const schema = Model.schema;
|
|
545
|
+
const filtered = api.service.OdemSchema.filterItem( properties, { user }, Model, "write" );
|
|
546
|
+
const instance = new Model( uuid );
|
|
325
547
|
await instance.load();
|
|
326
548
|
|
|
327
|
-
for ( const propName of Object.
|
|
328
|
-
if (
|
|
329
|
-
instance[propName] =
|
|
549
|
+
for ( const [ propName, propValue ] of Object.entries( filtered ) ) {
|
|
550
|
+
if ( schema.props.hasOwnProperty( propName ) || schema.computed.hasOwnProperty( propName ) ) {
|
|
551
|
+
instance[propName] = propValue;
|
|
330
552
|
}
|
|
331
553
|
}
|
|
332
554
|
|
|
@@ -334,10 +556,10 @@ module.exports = function() {
|
|
|
334
556
|
|
|
335
557
|
response( {
|
|
336
558
|
success: true,
|
|
337
|
-
properties: instance.toObject( { serialized: true } ),
|
|
559
|
+
properties: api.service.OdemSchema.filterItem( instance.toObject( { serialized: true } ), { user }, Model, "read" ),
|
|
338
560
|
} );
|
|
339
561
|
} catch ( error ) {
|
|
340
|
-
logError( "updating %s instance failed: %s",
|
|
562
|
+
logError( "updating %s instance failed: %s", Model.name, error.stack );
|
|
341
563
|
|
|
342
564
|
response( {
|
|
343
565
|
error: error.message,
|
|
@@ -348,18 +570,29 @@ module.exports = function() {
|
|
|
348
570
|
/**
|
|
349
571
|
* Handles incoming request for removing a model's item.
|
|
350
572
|
*
|
|
351
|
-
* @param {
|
|
352
|
-
* @param {
|
|
353
|
-
* @param {string} sessionId client-provided session ID
|
|
573
|
+
* @param {WebSocket} socket receiving socket of request
|
|
574
|
+
* @param {class<Hitchy.Plugin.Odem.Model>} Model named server-side model
|
|
354
575
|
* @param {string} uuid client-provided UUID of item to remove
|
|
355
576
|
* @param {function} response callback to invoke with response
|
|
356
577
|
* @returns {Promise<void>} promise settled on request handled
|
|
357
578
|
*/
|
|
358
|
-
static async handleDeleteRequest(
|
|
359
|
-
logDebug( "request for deleting %s instance %s",
|
|
579
|
+
static async handleDeleteRequest( socket, Model, uuid, response ) {
|
|
580
|
+
logDebug( "request for deleting %s instance %s", Model.name, uuid );
|
|
360
581
|
|
|
361
582
|
try {
|
|
362
|
-
const
|
|
583
|
+
const user = await this.getUserForSocket( socket );
|
|
584
|
+
|
|
585
|
+
if ( api.plugins.authentication ) {
|
|
586
|
+
if ( !await api.service.Authorization.mayAccess( user, `@hitchy.odem.model.${Model.name}.remove` ) ) {
|
|
587
|
+
throw new Error( "access forbidden" );
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
if ( !api.service.OdemSchema.mayBeExposed( { user }, Model ) ) {
|
|
592
|
+
throw new Error( "access forbidden by model" );
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
const instance = new Model( uuid );
|
|
363
596
|
await instance.remove();
|
|
364
597
|
|
|
365
598
|
response( {
|
|
@@ -367,7 +600,7 @@ module.exports = function() {
|
|
|
367
600
|
properties: { uuid },
|
|
368
601
|
} );
|
|
369
602
|
} catch ( error ) {
|
|
370
|
-
logError( "deleting %s instance %s failed: %s",
|
|
603
|
+
logError( "deleting %s instance %s failed: %s", Model.name, uuid, error.stack );
|
|
371
604
|
|
|
372
605
|
response( {
|
|
373
606
|
error: error.message,
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hitchy/plugin-odem-socket.io",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "exposing Hitchy's
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "exposing Hitchy's document-oriented database via websocket",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"lint": "eslint .",
|
|
8
|
-
"test": "hitchy-pm odem socket.io --exec mocha --ui=tdd 'test/**/*.spec.js
|
|
9
|
-
"coverage": "hitchy-pm odem socket.io --exec c8 mocha --ui=tdd 'test/**/*.spec.js
|
|
8
|
+
"test": "hitchy-pm odem socket.io auth --exec mocha --ui=tdd 'test/**/*.spec.js'",
|
|
9
|
+
"coverage": "hitchy-pm odem socket.io auth --exec c8 mocha --ui=tdd 'test/**/*.spec.js'"
|
|
10
10
|
},
|
|
11
11
|
"repository": {
|
|
12
12
|
"type": "git",
|
|
@@ -14,8 +14,7 @@
|
|
|
14
14
|
},
|
|
15
15
|
"keywords": [
|
|
16
16
|
"Hitchy",
|
|
17
|
-
"socket.io"
|
|
18
|
-
"ODM"
|
|
17
|
+
"socket.io"
|
|
19
18
|
],
|
|
20
19
|
"author": "cepharum GmbH",
|
|
21
20
|
"license": "MIT",
|
|
@@ -31,17 +30,17 @@
|
|
|
31
30
|
],
|
|
32
31
|
"peerDependencies": {
|
|
33
32
|
"@hitchy/core": ">=0.8.1",
|
|
34
|
-
"@hitchy/plugin-odem": ">=0.
|
|
33
|
+
"@hitchy/plugin-odem": ">=0.9.1",
|
|
35
34
|
"@hitchy/plugin-socket.io": ">=0.1.1"
|
|
36
35
|
},
|
|
37
36
|
"devDependencies": {
|
|
38
|
-
"@hitchy/server-dev-tools": "^0.4.
|
|
39
|
-
"c8": "^
|
|
40
|
-
"eslint": "^8.
|
|
41
|
-
"eslint-config-cepharum": "^1.0.
|
|
42
|
-
"eslint-plugin-promise": "^
|
|
43
|
-
"mocha": "^10.
|
|
37
|
+
"@hitchy/server-dev-tools": "^0.4.9",
|
|
38
|
+
"c8": "^10.1.2",
|
|
39
|
+
"eslint": "^8.57.0",
|
|
40
|
+
"eslint-config-cepharum": "^1.0.14",
|
|
41
|
+
"eslint-plugin-promise": "^7.1.0",
|
|
42
|
+
"mocha": "^10.7.3",
|
|
44
43
|
"should": "^13.2.3",
|
|
45
|
-
"socket.io-client": "^4.7.
|
|
44
|
+
"socket.io-client": "^4.7.5"
|
|
46
45
|
}
|
|
47
46
|
}
|
package/readme.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @hitchy/plugin-odem-socket.io
|
|
2
2
|
|
|
3
|
-
_exposing Hitchy's
|
|
3
|
+
_exposing Hitchy's document-oriented database via websocket_
|
|
4
4
|
|
|
5
5
|
## License
|
|
6
6
|
|
|
@@ -25,11 +25,17 @@ npm i @hitchy/plugin-odem @hitchy/plugin-socket.io
|
|
|
25
25
|
|
|
26
26
|
* The parameter `uuidsOnly` of actions `<modelName>:list` and `<modelName>:find` has been replaced by `loadRecords` to match the semantics of server-side Odem API. You have to negate its value to adapt.
|
|
27
27
|
|
|
28
|
+
### v0.3
|
|
29
|
+
|
|
30
|
+
* This version is recognizing an existing authentication plugin and checks a user's authorization for accessing models and their properties based on authorization rules set on server.
|
|
31
|
+
* Previously, actions of control API required the provision of a session ID to read a requesting user from as first argument. This approach has been abandoned. All actions are automatically associated with the user which has been authenticated when the websocket connection has been established.
|
|
32
|
+
* Clients should drop and re-connect over websocket whenever authentication of a requesting user has changed.
|
|
33
|
+
|
|
28
34
|
## Usage
|
|
29
35
|
|
|
30
36
|
### Basics
|
|
31
37
|
|
|
32
|
-
This plugin is
|
|
38
|
+
This plugin is exposing an API via websocket for controlling and monitoring models of server-side document-oriented database. Relying on particular features of [socket.io](https://socket.io/), this API is exposed in [namespace](https://socket.io/docs/v4/namespaces/) `/hitchy/odem`.
|
|
33
39
|
|
|
34
40
|
For every server-side model, request listeners and/or notification broadcasters are set up. Either model's name is converted into its kebab-case variant and used as colon-separated prefix in event names emitted either by the server or by a client.
|
|
35
41
|
|
|
@@ -47,7 +53,7 @@ All action events listed in this section are to be sent by a client. The server
|
|
|
47
53
|
const socket = io( "/hitchy/odem" );
|
|
48
54
|
|
|
49
55
|
socket.on( "connect", () => {
|
|
50
|
-
socket.emit( actionName,
|
|
56
|
+
socket.emit( actionName, arg1, arg2, arg3, response => {
|
|
51
57
|
// process the response here
|
|
52
58
|
} );
|
|
53
59
|
} );
|
|
@@ -57,20 +63,17 @@ socket.on( "connect", () => {
|
|
|
57
63
|
|
|
58
64
|
The following code excerpts are focusing on the inner part of emitting an action event and processing the response.
|
|
59
65
|
|
|
60
|
-
In all these examples, a _sessionId_ is given. This is the current user's session ID usually found in a cookie set by a preceding HTTP request to authenticate the user. It is required on all requests to re-authenticate the user and to apply authorization checks to either request. It is okay to provide `null` here, however this might cause requests to be rejected based on server-side configuration.
|
|
61
|
-
|
|
62
66
|
#### <modelName>:list
|
|
63
67
|
|
|
64
68
|
This request is the websocket variant of [Model.list()](https://odem.hitchy.org/api/model.html#model-list). Supported arguments are
|
|
65
69
|
|
|
66
|
-
* the session ID,
|
|
67
70
|
* [query-related options](https://odem.hitchy.org/api/model.html#query-related-options) as supported by `Model.list()` and
|
|
68
71
|
* a boolean controlling `loadRecords` of [result-related options](https://odem.hitchy.org/api/model.html#result-related-options) supported by `Model.list()`.
|
|
69
72
|
|
|
70
|
-
> This boolean must be `true` to actually get
|
|
73
|
+
> This boolean must be `true` to actually get the properties of retrieved items. Otherwise, listed items are represented by their UUIDs, only.
|
|
71
74
|
|
|
72
75
|
```javascript
|
|
73
|
-
socket.emit( "<modelName>:list",
|
|
76
|
+
socket.emit( "<modelName>:list", { limit: 10 }, false, response => {
|
|
74
77
|
// - check for `response.success` or `response.error`
|
|
75
78
|
// - process total count of items in `response.count`
|
|
76
79
|
// - process requested excerpt of items in `response.items`
|
|
@@ -83,15 +86,14 @@ Last argument is a callback to be invoked with the resulting response from serve
|
|
|
83
86
|
|
|
84
87
|
This request is searching for instances of selected model matching some query. It is invoking [Model.find()](https://odem.hitchy.org/api/model.html#model-find). Arguments are
|
|
85
88
|
|
|
86
|
-
* the session ID,
|
|
87
89
|
* the [query](https://odem.hitchy.org/api/model.html#model-find) describing items to find,
|
|
88
90
|
* [query-related options](https://odem.hitchy.org/api/model.html#query-related-options) as supported by `Model.find()` and
|
|
89
91
|
* a boolean controlling `loadRecords` of [result-related options](https://odem.hitchy.org/api/model.html#result-related-options) supported by `Model.find()`.
|
|
90
92
|
|
|
91
|
-
> This boolean must be `true` to actually get
|
|
93
|
+
> This boolean must be `true` to actually get the properties of retrieved items. Otherwise, listed items are represented by their UUIDs, only.
|
|
92
94
|
|
|
93
95
|
```javascript
|
|
94
|
-
socket.emit( "<modelName>:find",
|
|
96
|
+
socket.emit( "<modelName>:find", {}, { limit: 10 }, true, response => {
|
|
95
97
|
// - check for `response.success` or `response.error`
|
|
96
98
|
// - process total count of items in `response.count`
|
|
97
99
|
// - process requested excerpt of items in `response.items`
|
|
@@ -102,10 +104,10 @@ Last argument is a callback to be invoked with the resulting response from serve
|
|
|
102
104
|
|
|
103
105
|
#### <modelName>:create
|
|
104
106
|
|
|
105
|
-
This request is creating another instance of selected model assigning provided properties as initial values.
|
|
107
|
+
This request is creating another instance of selected model assigning provided properties as initial values. Those properties are provided as serializable object.
|
|
106
108
|
|
|
107
109
|
```javascript
|
|
108
|
-
socket.emit( "<modelName>:create",
|
|
110
|
+
socket.emit( "<modelName>:create", { title: "important things" }, response => {
|
|
109
111
|
// - check for `response.success` or `response.error`
|
|
110
112
|
// - process properties of eventually created instance in `response.properties`
|
|
111
113
|
// - created instance's UUID is available as `response.properties.uuid`
|
|
@@ -119,7 +121,7 @@ Last argument is a callback to be invoked with the resulting response from serve
|
|
|
119
121
|
This request is fetching a single instance's properties.
|
|
120
122
|
|
|
121
123
|
```javascript
|
|
122
|
-
socket.emit( "<modelName>:read",
|
|
124
|
+
socket.emit( "<modelName>:read", "12345678-1234-1234-1234-123456789012", response => {
|
|
123
125
|
// - check for `response.success` or `response.error`
|
|
124
126
|
// - process properties of fetched instance in `response.properties`
|
|
125
127
|
} );
|
|
@@ -129,10 +131,10 @@ Last argument is a callback to be invoked with the resulting response from serve
|
|
|
129
131
|
|
|
130
132
|
#### <modelName>:update
|
|
131
133
|
|
|
132
|
-
This request is updating an existing instance of model.
|
|
134
|
+
This request is updating an existing instance of model. The UUID of item to update and the values per property to assign are provided as arguments.
|
|
133
135
|
|
|
134
136
|
```javascript
|
|
135
|
-
socket.emit( "<modelName>:update",
|
|
137
|
+
socket.emit( "<modelName>:update", "12345678-1234-1234-1234-123456789012", { prio: 1 }, response => {
|
|
136
138
|
// - check for `response.success` or `response.error`
|
|
137
139
|
// - properties of updated instance are provided in `response.properties`
|
|
138
140
|
} );
|
|
@@ -145,7 +147,7 @@ Last argument is a callback to be invoked with the resulting response from serve
|
|
|
145
147
|
This request is eventually completing the CRUD-support of this API by deleting an existing instance of model selected by its UUID in first argument.
|
|
146
148
|
|
|
147
149
|
```javascript
|
|
148
|
-
socket.emit( "<modelName>:delete",
|
|
150
|
+
socket.emit( "<modelName>:delete", "12345678-1234-1234-1234-123456789012", response => {
|
|
149
151
|
// - check for `response.success` or `response.error`
|
|
150
152
|
// - deleted instance's UUID is available as `response.properties.uuid`
|
|
151
153
|
} );
|
|
@@ -156,7 +158,7 @@ Last argument is a callback to be invoked with the resulting response from serve
|
|
|
156
158
|
|
|
157
159
|
### Notifications
|
|
158
160
|
|
|
159
|
-
[
|
|
161
|
+
[notifications of the document-oriented database](https://odem.hitchy.org/api/model.html#model-notifications) are forwarded as broadcast events named `<modelName>:changed`. Either event consists of
|
|
160
162
|
|
|
161
163
|
- a type of change and
|
|
162
164
|
- the changed item's properties (limited to its UUID on deletion).
|