@nymphjs/pubsub 1.0.0-beta.11 → 1.0.0-beta.111

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/src/PubSub.ts CHANGED
@@ -7,16 +7,10 @@ import {
7
7
  SerializedEntityData,
8
8
  classNamesToEntityConstructors,
9
9
  } from '@nymphjs/nymph';
10
- import {
11
- request,
12
- client as WebSocketClient,
13
- server as WebSocketServer,
14
- connection,
15
- Message,
16
- } from 'websocket';
17
- import { difference } from 'lodash';
18
-
19
- import { Config, ConfigDefaults as defaults } from './conf';
10
+ import ws from 'websocket';
11
+ import { difference } from 'lodash-es';
12
+
13
+ import { Config, ConfigDefaults as defaults } from './conf/index.js';
20
14
  import type {
21
15
  QuerySubscriptionData,
22
16
  AuthenticateMessageData,
@@ -28,7 +22,7 @@ import type {
28
22
  PublishMessageData,
29
23
  MessageData,
30
24
  MessageOptions,
31
- } from './PubSub.types';
25
+ } from './PubSub.types.js';
32
26
 
33
27
  /**
34
28
  * A publish/subscribe server for Nymph.
@@ -53,207 +47,289 @@ export default class PubSub {
53
47
  /**
54
48
  * The WebSocket server.
55
49
  */
56
- public server: WebSocketServer;
50
+ public server: ws.server;
57
51
 
58
- private sessions = new Map<connection, string>();
52
+ private sessions = new Map<
53
+ ws.connection,
54
+ { authToken: string; switchToken?: string }
55
+ >();
59
56
  protected querySubs: {
60
57
  [etype: string]: {
61
- [query: string]: Map<connection, QuerySubscriptionData>;
58
+ [query: string]: Map<ws.connection, QuerySubscriptionData>;
62
59
  };
63
60
  } = {};
64
61
  protected uidSubs: {
65
- [uidName: string]: Map<connection, { count: boolean }>;
62
+ [uidName: string]: Map<ws.connection, { count: boolean }>;
66
63
  } = {};
64
+ protected static transactionPublishes: {
65
+ nymph: Nymph;
66
+ payload: string;
67
+ config: Config;
68
+ }[] = [];
67
69
 
68
70
  public static initPublisher(config: Partial<Config>, nymph: Nymph) {
69
71
  const configWithDefaults: Config = { ...defaults, ...config };
72
+ const unsubscribers: (() => boolean)[] = [];
70
73
 
71
- nymph.on(
72
- 'beforeSaveEntity',
73
- async (nymph: Nymph, entity: EntityInterface) => {
74
+ unsubscribers.push(
75
+ nymph.on('beforeSaveEntity', async (enymph, entity) => {
74
76
  const guid = entity.guid;
75
- const etype = (entity.constructor as EntityConstructor).ETYPE;
77
+ const EntityClass = entity.constructor as EntityConstructor;
78
+ const etype = EntityClass.ETYPE;
76
79
 
77
- const off = nymph.on(
78
- 'afterSaveEntity',
79
- async (_nymph: Nymph, result: Promise<boolean>) => {
80
- off();
81
- if (!(await result)) {
82
- return;
83
- }
84
- this.publish(
85
- JSON.stringify({
86
- action: 'publish',
87
- event: guid == null ? 'create' : 'update',
88
- guid: entity.guid,
89
- entity: entity.toJSON(),
90
- etype: etype,
91
- }),
92
- configWithDefaults
93
- );
80
+ if (!EntityClass.pubSubEnabled) {
81
+ return;
82
+ }
83
+
84
+ const off = enymph.on('afterSaveEntity', async (curNymph, result) => {
85
+ off();
86
+ off2();
87
+ if (!(await result)) {
88
+ return;
94
89
  }
95
- );
96
- }
90
+ const payload = JSON.stringify({
91
+ action: 'publish',
92
+ event: guid == null ? 'create' : 'update',
93
+ guid: entity.guid,
94
+ entity: entity.toJSON(),
95
+ etype: etype,
96
+ });
97
+ this.transactionPublishes.push({
98
+ nymph: curNymph,
99
+ payload,
100
+ config: configWithDefaults,
101
+ });
102
+ await this.publishTransactionPublishes(curNymph);
103
+ });
104
+ const off2 = enymph.on('failedSaveEntity', async () => {
105
+ off();
106
+ off2();
107
+ });
108
+ }),
97
109
  );
98
110
 
99
- nymph.on(
100
- 'beforeDeleteEntity',
101
- async (nymph: Nymph, entity: EntityInterface) => {
111
+ unsubscribers.push(
112
+ nymph.on('beforeDeleteEntity', async (enymph, entity) => {
102
113
  const guid = entity.guid;
103
- const etype = (entity.constructor as EntityConstructor).ETYPE;
114
+ const EntityClass = entity.constructor as EntityConstructor;
115
+ const etype = EntityClass.ETYPE;
104
116
 
105
- const off = nymph.on(
106
- 'afterDeleteEntity',
107
- async (_nymph: Nymph, result: Promise<boolean>) => {
108
- off();
109
- if (!(await result)) {
110
- return;
111
- }
112
- this.publish(
113
- JSON.stringify({
114
- action: 'publish',
115
- event: 'delete',
116
- guid: guid,
117
- etype: etype,
118
- }),
119
- configWithDefaults
120
- );
117
+ if (!EntityClass.pubSubEnabled) {
118
+ return;
119
+ }
120
+
121
+ const off = enymph.on('afterDeleteEntity', async (curNymph, result) => {
122
+ off();
123
+ off2();
124
+ if (!(await result)) {
125
+ return;
121
126
  }
122
- );
123
- }
127
+ const payload = JSON.stringify({
128
+ action: 'publish',
129
+ event: 'delete',
130
+ guid: guid,
131
+ etype: etype,
132
+ });
133
+ this.transactionPublishes.push({
134
+ nymph: curNymph,
135
+ payload,
136
+ config: configWithDefaults,
137
+ });
138
+ await this.publishTransactionPublishes(curNymph);
139
+ });
140
+ const off2 = enymph.on('failedDeleteEntity', async () => {
141
+ off();
142
+ off2();
143
+ });
144
+ }),
124
145
  );
125
146
 
126
- nymph.on(
127
- 'beforeDeleteEntityByID',
128
- async (nymph: Nymph, guid: string, className?: string) => {
147
+ unsubscribers.push(
148
+ nymph.on('beforeDeleteEntityByID', async (enymph, guid, className) => {
129
149
  try {
130
- const etype = nymph.getEntityClass(className ?? 'Entity').ETYPE;
150
+ const EntityClass = enymph.getEntityClass(className ?? 'Entity');
151
+ const etype = EntityClass.ETYPE;
131
152
 
132
- const off = nymph.on(
153
+ if (!EntityClass.pubSubEnabled) {
154
+ return;
155
+ }
156
+
157
+ const off = enymph.on(
133
158
  'afterDeleteEntityByID',
134
- async (_nymph: Nymph, result: Promise<boolean>) => {
159
+ async (curNymph, result) => {
135
160
  off();
161
+ off2();
136
162
  if (!(await result)) {
137
163
  return;
138
164
  }
139
- this.publish(
140
- JSON.stringify({
141
- action: 'publish',
142
- event: 'delete',
143
- guid: guid,
144
- etype: etype,
145
- }),
146
- configWithDefaults
147
- );
148
- }
165
+ const payload = JSON.stringify({
166
+ action: 'publish',
167
+ event: 'delete',
168
+ guid: guid,
169
+ etype: etype,
170
+ });
171
+ this.transactionPublishes.push({
172
+ nymph: curNymph,
173
+ payload,
174
+ config: configWithDefaults,
175
+ });
176
+ await this.publishTransactionPublishes(curNymph);
177
+ },
149
178
  );
179
+ const off2 = enymph.on('failedDeleteEntityByID', async () => {
180
+ off();
181
+ off2();
182
+ });
150
183
  } catch (e: any) {
151
184
  return;
152
185
  }
153
- }
186
+ }),
154
187
  );
155
188
 
156
- nymph.on('beforeNewUID', async (nymph: Nymph, name: string) => {
157
- const off = nymph.on(
158
- 'afterNewUID',
159
- async (_nymph: Nymph, result: Promise<number | null>) => {
189
+ unsubscribers.push(
190
+ nymph.on('beforeNewUID', async (enymph, name) => {
191
+ const off = enymph.on('afterNewUID', async (curNymph, result) => {
160
192
  off();
193
+ off2();
161
194
  const value = await result;
162
195
  if (value == null) {
163
196
  return;
164
197
  }
165
- this.publish(
166
- JSON.stringify({
167
- action: 'publish',
168
- event: 'newUID',
169
- name: name,
170
- value: value,
171
- }),
172
- configWithDefaults
173
- );
174
- }
175
- );
176
- });
198
+ const payload = JSON.stringify({
199
+ action: 'publish',
200
+ event: 'newUID',
201
+ name: name,
202
+ value: value,
203
+ });
204
+ this.transactionPublishes.push({
205
+ nymph: curNymph,
206
+ payload,
207
+ config: configWithDefaults,
208
+ });
209
+ await this.publishTransactionPublishes(curNymph);
210
+ });
211
+ const off2 = enymph.on('failedNewUID', async () => {
212
+ off();
213
+ off2();
214
+ });
215
+ }),
216
+ );
177
217
 
178
- nymph.on(
179
- 'beforeSetUID',
180
- async (nymph: Nymph, name: string, value: number) => {
181
- const off = nymph.on(
182
- 'afterSetUID',
183
- async (_nymph: Nymph, result: Promise<boolean>) => {
184
- off();
185
- if (!(await result)) {
186
- return;
187
- }
188
- this.publish(
189
- JSON.stringify({
190
- action: 'publish',
191
- event: 'setUID',
192
- name: name,
193
- value: value,
194
- }),
195
- configWithDefaults
196
- );
218
+ unsubscribers.push(
219
+ nymph.on('beforeSetUID', async (enymph, name, value) => {
220
+ const off = enymph.on('afterSetUID', async (curNymph, result) => {
221
+ off();
222
+ off2();
223
+ if (!(await result)) {
224
+ return;
197
225
  }
198
- );
199
- }
226
+ const payload = JSON.stringify({
227
+ action: 'publish',
228
+ event: 'setUID',
229
+ name: name,
230
+ value: value,
231
+ });
232
+ this.transactionPublishes.push({
233
+ nymph: curNymph,
234
+ payload,
235
+ config: configWithDefaults,
236
+ });
237
+ await this.publishTransactionPublishes(curNymph);
238
+ });
239
+ const off2 = enymph.on('failedSetUID', async () => {
240
+ off();
241
+ off2();
242
+ });
243
+ }),
200
244
  );
201
245
 
202
- nymph.on(
203
- 'beforeRenameUID',
204
- async (nymph: Nymph, oldName: string, newName: string) => {
205
- const off = nymph.on(
206
- 'afterRenameUID',
207
- async (_nymph: Nymph, result: Promise<boolean>) => {
208
- off();
209
- if (!(await result)) {
210
- return;
211
- }
212
- this.publish(
213
- JSON.stringify({
214
- action: 'publish',
215
- event: 'renameUID',
216
- oldName: oldName,
217
- newName: newName,
218
- }),
219
- configWithDefaults
220
- );
246
+ unsubscribers.push(
247
+ nymph.on('beforeRenameUID', async (enymph, oldName, newName) => {
248
+ const off = enymph.on('afterRenameUID', async (curNymph, result) => {
249
+ off();
250
+ off2();
251
+ if (!(await result)) {
252
+ return;
221
253
  }
222
- );
223
- }
254
+ const payload = JSON.stringify({
255
+ action: 'publish',
256
+ event: 'renameUID',
257
+ oldName: oldName,
258
+ newName: newName,
259
+ });
260
+ this.transactionPublishes.push({
261
+ nymph: curNymph,
262
+ payload,
263
+ config: configWithDefaults,
264
+ });
265
+ await this.publishTransactionPublishes(curNymph);
266
+ });
267
+ const off2 = enymph.on('failedRenameUID', async () => {
268
+ off();
269
+ off2();
270
+ });
271
+ }),
224
272
  );
225
273
 
226
- nymph.on('beforeDeleteUID', async (nymph: Nymph, name: string) => {
227
- const off = nymph.on(
228
- 'afterDeleteUID',
229
- async (_nymph: Nymph, result: Promise<boolean>) => {
274
+ unsubscribers.push(
275
+ nymph.on('beforeDeleteUID', async (enymph, name) => {
276
+ const off = enymph.on('afterDeleteUID', async (curNymph, result) => {
230
277
  off();
278
+ off2();
231
279
  if (!(await result)) {
232
280
  return;
233
281
  }
234
- this.publish(
235
- JSON.stringify({
236
- action: 'publish',
237
- event: 'deleteUID',
238
- name: name,
239
- }),
240
- configWithDefaults
241
- );
282
+ const payload = JSON.stringify({
283
+ action: 'publish',
284
+ event: 'deleteUID',
285
+ name: name,
286
+ });
287
+ this.transactionPublishes.push({
288
+ nymph: curNymph,
289
+ payload,
290
+ config: configWithDefaults,
291
+ });
292
+ await this.publishTransactionPublishes(curNymph);
293
+ });
294
+ const off2 = enymph.on('failedDeleteUID', async () => {
295
+ off();
296
+ off2();
297
+ });
298
+ }),
299
+ );
300
+
301
+ unsubscribers.push(
302
+ nymph.on('afterCommitTransaction', async (enymph, _name, result) => {
303
+ if (result) {
304
+ await this.publishTransactionPublishes(enymph);
242
305
  }
243
- );
244
- });
306
+ }),
307
+ );
308
+
309
+ unsubscribers.push(
310
+ nymph.on('afterRollbackTransaction', async (enymph) => {
311
+ this.removeTransactionPublishes(enymph);
312
+ }),
313
+ );
314
+
315
+ return () => {
316
+ for (let unsubscribe of unsubscribers) {
317
+ unsubscribe();
318
+ }
319
+ this.transactionPublishes = [];
320
+ };
245
321
  }
246
322
 
247
323
  private static publish(message: string, config: Config) {
248
324
  for (let host of config.entries ?? []) {
249
- const client = new WebSocketClient();
325
+ const client = new ws.client();
250
326
 
251
327
  client.on('connectFailed', (error) => {
252
328
  if (config.logger) {
253
329
  config.logger(
254
330
  'error',
255
331
  new Date().toISOString(),
256
- `Publish connection failed. (${error.toString()}, ${host})`
332
+ `Publish connection failed. (${error.toString()}, ${host})`,
257
333
  );
258
334
  }
259
335
  });
@@ -264,7 +340,7 @@ export default class PubSub {
264
340
  config.logger(
265
341
  'error',
266
342
  new Date().toISOString(),
267
- `Publish connect error. (${error.toString()}, ${host})`
343
+ `Publish connect error. (${error.toString()}, ${host})`,
268
344
  );
269
345
  }
270
346
  });
@@ -280,16 +356,60 @@ export default class PubSub {
280
356
  }
281
357
  }
282
358
 
359
+ private static isOrIsDescendant(parent: Nymph, child: Nymph) {
360
+ let check: Nymph | null = child;
361
+ while (check) {
362
+ if (check === parent) {
363
+ return true;
364
+ }
365
+ check = check.parent;
366
+ }
367
+ return false;
368
+ }
369
+
370
+ private static async publishTransactionPublishes(nymph: Nymph) {
371
+ if (await nymph.inTransaction()) {
372
+ // This instance is still in a transaction, so nothing gets published yet.
373
+ return;
374
+ }
375
+
376
+ this.transactionPublishes = (
377
+ await Promise.all(
378
+ this.transactionPublishes.map(async (publish) => {
379
+ // Check that this instance is a parent and the instance is not in a
380
+ // transaction.
381
+ if (
382
+ !this.isOrIsDescendant(nymph, publish.nymph) ||
383
+ (await publish.nymph.inTransaction())
384
+ ) {
385
+ return publish;
386
+ }
387
+ this.publish(publish.payload, publish.config);
388
+ return null;
389
+ }),
390
+ )
391
+ ).filter((value) => value != null) as {
392
+ nymph: Nymph;
393
+ payload: string;
394
+ config: Config;
395
+ }[];
396
+ }
397
+
398
+ private static removeTransactionPublishes(nymph: Nymph) {
399
+ this.transactionPublishes = this.transactionPublishes.filter((publish) => {
400
+ if (this.isOrIsDescendant(nymph, publish.nymph)) {
401
+ return false;
402
+ }
403
+ return true;
404
+ });
405
+ }
406
+
283
407
  /**
284
408
  * Initialize Nymph PubSub.
285
409
  *
286
410
  * @param config The PubSub configuration.
287
411
  */
288
- public constructor(
289
- config: Partial<Config>,
290
- nymph: Nymph,
291
- server: WebSocketServer
292
- ) {
412
+ public constructor(config: Partial<Config>, nymph: Nymph, server: ws.server) {
293
413
  this.nymph = nymph;
294
414
  this.config = { ...defaults, ...config };
295
415
  this.server = server;
@@ -301,14 +421,14 @@ export default class PubSub {
301
421
  this.server.shutDown();
302
422
  }
303
423
 
304
- public handleRequest(request: request) {
424
+ public handleRequest(request: ws.request) {
305
425
  if (!this.config.originIsAllowed(request.origin)) {
306
426
  // Make sure we only accept requests from an allowed origin
307
427
  request.reject();
308
428
  this.config.logger(
309
429
  'log',
310
430
  new Date().toISOString(),
311
- 'Client from origin ' + request.origin + ' was kicked by the bouncer.'
431
+ 'Client from origin ' + request.origin + ' was kicked by the bouncer.',
312
432
  );
313
433
  return;
314
434
  }
@@ -317,7 +437,7 @@ export default class PubSub {
317
437
  this.config.logger(
318
438
  'log',
319
439
  new Date().toISOString(),
320
- `Client joined the party! (${connection.remoteAddress}).`
440
+ `Client joined the party! (${connection.remoteAddress}).`,
321
441
  );
322
442
 
323
443
  connection.on('error', (err) => {
@@ -336,7 +456,7 @@ export default class PubSub {
336
456
  /**
337
457
  * Handle a message from a client.
338
458
  */
339
- public async onMessage(from: connection, msg: Message) {
459
+ public async onMessage(from: ws.connection, msg: ws.Message) {
340
460
  if (msg.type !== 'utf8') {
341
461
  throw new Error("This server doesn't accept binary messages.");
342
462
  }
@@ -344,7 +464,7 @@ export default class PubSub {
344
464
  if (
345
465
  !data.action ||
346
466
  ['authenticate', 'subscribe', 'unsubscribe', 'publish'].indexOf(
347
- data.action
467
+ data.action,
348
468
  ) === -1
349
469
  ) {
350
470
  return;
@@ -366,11 +486,11 @@ export default class PubSub {
366
486
  /**
367
487
  * Clean up after users who leave.
368
488
  */
369
- public onClose(conn: connection, description: string) {
489
+ public onClose(conn: ws.connection, description: string) {
370
490
  this.config.logger(
371
491
  'log',
372
492
  new Date().toISOString(),
373
- `Client skedaddled. (${description}, ${conn.remoteAddress})`
493
+ `Client skedaddled. (${description}, ${conn.remoteAddress})`,
374
494
  );
375
495
 
376
496
  let mess = 0;
@@ -398,7 +518,7 @@ export default class PubSub {
398
518
  JSON.stringify({
399
519
  query: curData.query,
400
520
  count,
401
- })
521
+ }),
402
522
  );
403
523
  }
404
524
  }
@@ -428,7 +548,7 @@ export default class PubSub {
428
548
  JSON.stringify({
429
549
  uid: curUID,
430
550
  count,
431
- })
551
+ }),
432
552
  );
433
553
  }
434
554
  }
@@ -447,16 +567,16 @@ export default class PubSub {
447
567
  this.config.logger(
448
568
  'log',
449
569
  new Date().toISOString(),
450
- `Cleaned up client's mess. (${mess}, ${conn.remoteAddress})`
570
+ `Cleaned up client's mess. (${mess}, ${conn.remoteAddress})`,
451
571
  );
452
572
  }
453
573
  }
454
574
 
455
- public onError(conn: connection, e: Error) {
575
+ public onError(conn: ws.connection, e: Error) {
456
576
  this.config.logger(
457
577
  'error',
458
578
  new Date().toISOString(),
459
- `An error occured. (${e.message}, ${conn.remoteAddress})`
579
+ `An error occured. (${e.message}, ${conn.remoteAddress})`,
460
580
  );
461
581
  }
462
582
 
@@ -464,13 +584,14 @@ export default class PubSub {
464
584
  * Handle an authentication from a client.
465
585
  */
466
586
  private handleAuthentication(
467
- from: connection,
468
- data: AuthenticateMessageData
587
+ from: ws.connection,
588
+ data: AuthenticateMessageData,
469
589
  ) {
470
590
  // Save the user's auth token in session storage.
471
- const token = data.token;
472
- if (token != null) {
473
- this.sessions.set(from, token);
591
+ const authToken = data.authToken;
592
+ const switchToken = data.switchToken;
593
+ if (authToken != null) {
594
+ this.sessions.set(from, { authToken, switchToken });
474
595
  } else if (this.sessions.has(from)) {
475
596
  this.sessions.delete(from);
476
597
  }
@@ -480,21 +601,31 @@ export default class PubSub {
480
601
  * Handle a subscribe or unsubscribe from a client.
481
602
  */
482
603
  private async handleSubscription(
483
- from: connection,
484
- data: SubscribeMessageData
604
+ from: ws.connection,
605
+ data: SubscribeMessageData,
485
606
  ) {
486
- if ('query' in data && data.query != null) {
487
- // Request is for a query.
488
-
489
- await this.handleSubscriptionQuery(from, data);
490
- } else if (
491
- 'uid' in data &&
492
- data.uid != null &&
493
- typeof data.uid == 'string'
494
- ) {
495
- // Request is for a UID.
496
-
497
- await this.handleSubscriptionUid(from, data);
607
+ try {
608
+ if ('query' in data && data.query != null) {
609
+ // Request is for a query.
610
+
611
+ await this.handleSubscriptionQuery(from, data);
612
+ } else if (
613
+ 'uid' in data &&
614
+ data.uid != null &&
615
+ typeof data.uid == 'string'
616
+ ) {
617
+ // Request is for a UID.
618
+
619
+ await this.handleSubscriptionUid(from, data);
620
+ }
621
+ } catch (e: any) {
622
+ if ('query' in data && data.query != null) {
623
+ from.sendUTF(JSON.stringify({ query: data.query, error: e.message }));
624
+ } else if ('uid' in data && data.uid != null) {
625
+ from.sendUTF(JSON.stringify({ uid: data.uid, error: e.message }));
626
+ } else {
627
+ from.sendUTF(JSON.stringify({ error: e.message }));
628
+ }
498
629
  }
499
630
  }
500
631
 
@@ -502,33 +633,39 @@ export default class PubSub {
502
633
  * Handle a subscribe or unsubscribe for a query from a client.
503
634
  */
504
635
  private async handleSubscriptionQuery(
505
- from: connection,
636
+ from: ws.connection,
506
637
  data: QuerySubscribeMessageData,
507
638
  qrefParent?: {
508
639
  etype: string;
509
640
  query: string;
510
- }
641
+ },
511
642
  ) {
512
- let args: [MessageOptions, ...Selector[]];
513
- let EntityClass: EntityConstructor;
514
- try {
515
- args = JSON.parse(data.query);
516
- EntityClass = this.nymph.getEntityClass(args[0].class);
517
- } catch (e: any) {
518
- return;
643
+ let args: [MessageOptions, ...Selector[]] = JSON.parse(data.query);
644
+ let EntityClass = this.nymph.getEntityClass(args[0].class);
645
+ if (!EntityClass.restEnabled) {
646
+ throw new Error('Not accessible.');
519
647
  }
520
648
  const etype = EntityClass.ETYPE;
521
649
  const serialArgs = JSON.stringify(args);
522
650
  const [clientOptions, ...selectors] = args;
523
- const options: Options & { return: 'guid' } = {
651
+ const options: Options & { return: 'entity' } = {
524
652
  ...clientOptions,
525
653
  class: EntityClass,
526
- return: 'guid',
654
+ return: 'entity',
527
655
  source: 'client',
528
656
  };
529
657
  // Find qref queries.
530
658
  const qrefQueries = this.findQRefQueries(clientOptions, ...selectors);
531
659
 
660
+ // Check that all qref queries are accessible classes.
661
+ for (const qrefQuery of qrefQueries) {
662
+ const args = qrefQuery;
663
+ const EntityClass = this.nymph.getEntityClass(args[0].class);
664
+ if (!EntityClass.restEnabled) {
665
+ throw new Error('Not accessible.');
666
+ }
667
+ }
668
+
532
669
  if (data.action === 'subscribe') {
533
670
  // Client is subscribing to a query.
534
671
 
@@ -543,7 +680,7 @@ export default class PubSub {
543
680
  {
544
681
  etype,
545
682
  query: serialArgs,
546
- }
683
+ },
547
684
  );
548
685
  }
549
686
 
@@ -554,18 +691,33 @@ export default class PubSub {
554
691
  if (!(serialArgs in this.querySubs[etype])) {
555
692
  this.querySubs[etype][serialArgs] = new Map();
556
693
  }
557
- let token = null;
694
+
695
+ let authToken = null;
696
+ let switchToken = null;
558
697
  if (this.sessions.has(from)) {
559
- token = this.sessions.get(from);
698
+ const session = this.sessions.get(from);
699
+ authToken = session?.authToken;
700
+ switchToken = session?.switchToken;
560
701
  }
561
702
  const nymph = this.nymph.clone();
562
- if (nymph.tilmeld != null && token != null) {
563
- const user = await nymph.tilmeld.extractToken(token);
564
- if (user) {
565
- // Log in the user for access controls.
566
- nymph.tilmeld.fillSession(user);
703
+ if (nymph.tilmeld != null && authToken != null) {
704
+ const user = await nymph.tilmeld.extractToken(authToken);
705
+ if (user && user.enabled) {
706
+ if (switchToken != null) {
707
+ const switchUser = await nymph.tilmeld.extractToken(switchToken);
708
+ if (switchUser) {
709
+ // Log in the switchUser for access controls.
710
+ await nymph.tilmeld.fillSession(switchUser);
711
+ }
712
+ } else {
713
+ // Log in the user for access controls.
714
+ await nymph.tilmeld.fillSession(user);
715
+ }
567
716
  }
568
717
  }
718
+
719
+ let entities: EntityInterface[] = [];
720
+ let sendEntities = false;
569
721
  const existingSub = this.querySubs[etype][serialArgs].get(from);
570
722
  if (existingSub) {
571
723
  if (qrefParent) {
@@ -577,23 +729,49 @@ export default class PubSub {
577
729
  if (data.count && !existingSub.count) {
578
730
  existingSub.count = true;
579
731
  }
732
+ sendEntities = existingSub.direct;
733
+ if (sendEntities) {
734
+ entities = existingSub.current.length
735
+ ? await nymph.getEntities(
736
+ {
737
+ class: EntityClass,
738
+ source: 'client',
739
+ },
740
+ { type: '|', guid: existingSub.current },
741
+ )
742
+ : [];
743
+ }
580
744
  } else {
745
+ entities = await nymph.getEntities(options, ...selectors);
581
746
  this.querySubs[etype][serialArgs].set(from, {
582
- current: await nymph.getEntities(options, ...selectors),
747
+ current: entities.map((e) => e.guid as string),
583
748
  query: data.query,
584
749
  qrefParents: qrefParent ? [qrefParent] : [],
585
750
  direct: !qrefParent,
586
751
  count: !!data.count,
587
752
  });
753
+ sendEntities = !qrefParent;
754
+ }
755
+
756
+ if (sendEntities) {
757
+ // Notify the client of the current value.
758
+ from.sendUTF(
759
+ JSON.stringify({
760
+ query: data.query,
761
+ set: true,
762
+ data: entities,
763
+ }),
764
+ );
588
765
  }
589
- if (nymph.tilmeld != null && token != null) {
766
+
767
+ if (nymph.tilmeld != null && authToken != null) {
590
768
  // Clear the user that was temporarily logged in.
591
769
  nymph.tilmeld.clearSession();
592
770
  }
593
771
  this.config.logger(
594
772
  'log',
595
773
  new Date().toISOString(),
596
- `Client subscribed to a query! (${serialArgs}, ${from.remoteAddress})`
774
+ `Client subscribed to a query! (${serialArgs}, ${from.remoteAddress})`,
597
775
  );
598
776
 
599
777
  if (this.config.broadcastCounts) {
@@ -606,7 +784,7 @@ export default class PubSub {
606
784
  JSON.stringify({
607
785
  query: curData.query,
608
786
  count,
609
- })
787
+ }),
610
788
  );
611
789
  }
612
790
  }
@@ -627,7 +805,7 @@ export default class PubSub {
627
805
  {
628
806
  etype,
629
807
  query: serialArgs,
630
- }
808
+ },
631
809
  );
632
810
  }
633
811
 
@@ -649,7 +827,7 @@ export default class PubSub {
649
827
  !(
650
828
  qrefParent.etype === parent.etype &&
651
829
  qrefParent.query === parent.query
652
- )
830
+ ),
653
831
  );
654
832
  }
655
833
  if (!qrefParent) {
@@ -662,7 +840,7 @@ export default class PubSub {
662
840
  this.config.logger(
663
841
  'log',
664
842
  new Date().toISOString(),
665
- `Client unsubscribed from a query! (${serialArgs}, ${from.remoteAddress})`
843
+ `Client unsubscribed from a query! (${serialArgs}, ${from.remoteAddress})`,
666
844
  );
667
845
 
668
846
  const count = this.querySubs[etype][serialArgs].size;
@@ -685,7 +863,7 @@ export default class PubSub {
685
863
  JSON.stringify({
686
864
  query: curData.query,
687
865
  count,
688
- })
866
+ }),
689
867
  );
690
868
  }
691
869
  }
@@ -697,8 +875,8 @@ export default class PubSub {
697
875
  * Handle a subscribe or unsubscribe for a UID from a client.
698
876
  */
699
877
  private async handleSubscriptionUid(
700
- from: connection,
701
- data: UidSubscribeMessageData
878
+ from: ws.connection,
879
+ data: UidSubscribeMessageData,
702
880
  ) {
703
881
  if (data.action === 'subscribe') {
704
882
  // Client is subscribing to a UID.
@@ -708,10 +886,49 @@ export default class PubSub {
708
886
  this.uidSubs[data['uid']].set(from, {
709
887
  count: !!data.count,
710
888
  });
889
+
890
+ let authToken = null;
891
+ let switchToken = null;
892
+ if (this.sessions.has(from)) {
893
+ const session = this.sessions.get(from);
894
+ authToken = session?.authToken;
895
+ switchToken = session?.switchToken;
896
+ }
897
+ const nymph = this.nymph.clone();
898
+ if (nymph.tilmeld != null && authToken != null) {
899
+ const user = await nymph.tilmeld.extractToken(authToken);
900
+ if (user && user.enabled) {
901
+ if (switchToken != null) {
902
+ const switchUser = await nymph.tilmeld.extractToken(switchToken);
903
+ if (switchUser) {
904
+ // Log in the switchUser for access controls.
905
+ await nymph.tilmeld.fillSession(switchUser);
906
+ }
907
+ } else {
908
+ // Log in the user for access controls.
909
+ await nymph.tilmeld.fillSession(user);
910
+ }
911
+ }
912
+ }
913
+
914
+ // Notify the client of the current value.
915
+ from.sendUTF(
916
+ JSON.stringify({
917
+ uid: data.uid,
918
+ set: true,
919
+ data: await this.nymph.getUID(data.uid),
920
+ }),
921
+ );
922
+
923
+ if (nymph.tilmeld != null && authToken != null) {
924
+ // Clear the user that was temporarily logged in.
925
+ nymph.tilmeld.clearSession();
926
+ }
927
+
711
928
  this.config.logger(
712
929
  'log',
713
930
  new Date().toISOString(),
714
- `Client subscribed to a UID! (${data.uid}, ${from.remoteAddress})`
931
+ `Client subscribed to a UID! (${data.uid}, ${from.remoteAddress})`,
715
932
  );
716
933
 
717
934
  if (this.config.broadcastCounts) {
@@ -724,7 +941,7 @@ export default class PubSub {
724
941
  JSON.stringify({
725
942
  uid: data.uid,
726
943
  count,
727
- })
944
+ }),
728
945
  );
729
946
  }
730
947
  }
@@ -743,7 +960,7 @@ export default class PubSub {
743
960
  this.config.logger(
744
961
  'log',
745
962
  new Date().toISOString(),
746
- `Client unsubscribed from a UID! (${data.uid}, ${from.remoteAddress})`
963
+ `Client unsubscribed from a UID! (${data.uid}, ${from.remoteAddress})`,
747
964
  );
748
965
 
749
966
  const count = this.uidSubs[data.uid].size;
@@ -763,7 +980,7 @@ export default class PubSub {
763
980
  JSON.stringify({
764
981
  uid: data.uid,
765
982
  count,
766
- })
983
+ }),
767
984
  );
768
985
  }
769
986
  }
@@ -775,9 +992,9 @@ export default class PubSub {
775
992
  * Handle a publish from a client.
776
993
  */
777
994
  private async handlePublish(
778
- from: connection,
779
- msg: Message,
780
- data: PublishMessageData
995
+ from: ws.connection,
996
+ msg: ws.Message,
997
+ data: PublishMessageData,
781
998
  ) {
782
999
  if (
783
1000
  'guid' in data &&
@@ -817,13 +1034,13 @@ export default class PubSub {
817
1034
  * Handle an entity publish from a client.
818
1035
  */
819
1036
  private async handlePublishEntity(
820
- from: connection,
821
- data: PublishEntityMessageData
1037
+ from: ws.connection,
1038
+ data: PublishEntityMessageData,
822
1039
  ) {
823
1040
  this.config.logger(
824
1041
  'log',
825
1042
  new Date().toISOString(),
826
- `Received an entity publish! (${data.guid}, ${data.event}, ${from.remoteAddress})`
1043
+ `Received an entity publish! (${data.guid}, ${data.event}, ${from.remoteAddress})`,
827
1044
  );
828
1045
 
829
1046
  const etype = data.etype;
@@ -834,7 +1051,7 @@ export default class PubSub {
834
1051
 
835
1052
  for (let curQuery in this.querySubs[etype]) {
836
1053
  const curClients = this.querySubs[etype][curQuery];
837
- const updatedClients = new Set<connection>();
1054
+ const updatedClients = new Set<ws.connection>();
838
1055
 
839
1056
  if (data.event === 'delete' || data.event === 'update') {
840
1057
  // Check if it is in any client's currents.
@@ -853,7 +1070,7 @@ export default class PubSub {
853
1070
  this.config.logger(
854
1071
  'error',
855
1072
  new Date().toISOString(),
856
- `Error checking for client updates! (${e?.message})`
1073
+ `Error checking for client updates! (${e?.message})`,
857
1074
  );
858
1075
  }
859
1076
  }
@@ -870,7 +1087,7 @@ export default class PubSub {
870
1087
  const entitySData: SerializedEntityData = {};
871
1088
  if (typeof data.entity.class !== 'string') {
872
1089
  throw new Error(
873
- `Received entity data class is not valid: ${data.entity.class}`
1090
+ `Received entity data class is not valid: ${data.entity.class}`,
874
1091
  );
875
1092
  }
876
1093
  const DataEntityClass = this.nymph.getEntityClass(data.entity.class);
@@ -879,11 +1096,12 @@ export default class PubSub {
879
1096
  EntityClass.ETYPE === DataEntityClass.ETYPE &&
880
1097
  (qrefQueries.length ||
881
1098
  this.nymph.driver.checkData(
1099
+ EntityClass.ETYPE,
882
1100
  entityData,
883
1101
  entitySData,
884
1102
  selectors,
885
1103
  data.guid,
886
- data.entity?.tags ?? []
1104
+ data.entity?.tags ?? [],
887
1105
  ))
888
1106
  ) {
889
1107
  // It either matches the query, or there are qref queries.
@@ -903,15 +1121,16 @@ export default class PubSub {
903
1121
  if (qrefQueries.length) {
904
1122
  const translatedSelectors = this.translateQRefSelectors(
905
1123
  curClient,
906
- selectors
1124
+ selectors,
907
1125
  );
908
1126
  if (
909
1127
  !this.nymph.driver.checkData(
1128
+ EntityClass.ETYPE,
910
1129
  entityData,
911
1130
  entitySData,
912
1131
  translatedSelectors,
913
1132
  data.guid,
914
- data.entity?.tags ?? []
1133
+ data.entity?.tags ?? [],
915
1134
  )
916
1135
  ) {
917
1136
  // The query doesn't match when the qref queries are filled.
@@ -926,7 +1145,7 @@ export default class PubSub {
926
1145
  this.config.logger(
927
1146
  'error',
928
1147
  new Date().toISOString(),
929
- `Error checking for client updates! (${e?.message})`
1148
+ `Error checking for client updates! (${e?.message})`,
930
1149
  );
931
1150
  }
932
1151
  }
@@ -934,13 +1153,14 @@ export default class PubSub {
934
1153
  }
935
1154
 
936
1155
  private async updateClient(
937
- curClient: connection,
1156
+ curClient: ws.connection,
938
1157
  curData: QuerySubscriptionData,
939
- data: PublishEntityMessageData
1158
+ data: PublishEntityMessageData,
940
1159
  ) {
941
1160
  // Update currents list.
942
1161
  let current: EntityInterface[];
943
- let token: string | undefined;
1162
+ let authToken: string | undefined;
1163
+ let switchToken: string | undefined;
944
1164
  const nymph = this.nymph.clone();
945
1165
  try {
946
1166
  const [clientOptions, ...clientSelectors] = JSON.parse(curData.query);
@@ -951,15 +1171,29 @@ export default class PubSub {
951
1171
  source: 'client',
952
1172
  skipAc: false,
953
1173
  };
954
- const selectors = classNamesToEntityConstructors(nymph, clientSelectors);
1174
+ const selectors = classNamesToEntityConstructors(
1175
+ nymph,
1176
+ clientSelectors,
1177
+ true,
1178
+ );
955
1179
  if (this.sessions.has(curClient)) {
956
- token = this.sessions.get(curClient);
1180
+ const session = this.sessions.get(curClient);
1181
+ authToken = session?.authToken;
1182
+ switchToken = session?.switchToken;
957
1183
  }
958
- if (nymph.tilmeld != null && token != null) {
959
- const user = await nymph.tilmeld.extractToken(token);
960
- if (user) {
961
- // Log in the user for access controls.
962
- nymph.tilmeld.fillSession(user);
1184
+ if (nymph.tilmeld != null && authToken != null) {
1185
+ const user = await nymph.tilmeld.extractToken(authToken);
1186
+ if (user && user.enabled) {
1187
+ if (switchToken != null) {
1188
+ const switchUser = await nymph.tilmeld.extractToken(switchToken);
1189
+ if (switchUser) {
1190
+ // Log in the switchUser for access controls.
1191
+ await nymph.tilmeld.fillSession(switchUser);
1192
+ }
1193
+ } else {
1194
+ // Log in the user for access controls.
1195
+ await nymph.tilmeld.fillSession(user);
1196
+ }
963
1197
  }
964
1198
  }
965
1199
  current = await nymph.getEntities(options, ...selectors);
@@ -967,13 +1201,13 @@ export default class PubSub {
967
1201
  this.config.logger(
968
1202
  'error',
969
1203
  new Date().toISOString(),
970
- `Error updating client! (${e?.message}, ${curClient.remoteAddress})`
1204
+ `Error updating client! (${e?.message}, ${curClient.remoteAddress})`,
971
1205
  );
972
1206
  return;
973
1207
  }
974
1208
 
975
1209
  const entityMap = Object.fromEntries(
976
- current.map((entity) => [entity.guid, entity])
1210
+ current.map((entity) => [entity.guid, entity]),
977
1211
  );
978
1212
  const currentGuids = current.map((entity) => entity.guid ?? '');
979
1213
  const removed = difference(curData.current, currentGuids);
@@ -985,13 +1219,13 @@ export default class PubSub {
985
1219
  this.config.logger(
986
1220
  'log',
987
1221
  new Date().toISOString(),
988
- `Notifying client of removal! (${curClient.remoteAddress})`
1222
+ `Notifying client of removal! (${curClient.remoteAddress})`,
989
1223
  );
990
1224
  curClient.sendUTF(
991
1225
  JSON.stringify({
992
1226
  query: curData.query,
993
1227
  removed: guid,
994
- })
1228
+ }),
995
1229
  );
996
1230
  }
997
1231
 
@@ -1001,7 +1235,7 @@ export default class PubSub {
1001
1235
  this.config.logger(
1002
1236
  'log',
1003
1237
  new Date().toISOString(),
1004
- `Notifying client of new match! (${curClient.remoteAddress})`
1238
+ `Notifying client of new match! (${curClient.remoteAddress})`,
1005
1239
  );
1006
1240
  if (typeof entity.updateDataProtection === 'function') {
1007
1241
  entity.updateDataProtection();
@@ -1011,7 +1245,7 @@ export default class PubSub {
1011
1245
  query: curData.query,
1012
1246
  added: guid,
1013
1247
  data: entity,
1014
- })
1248
+ }),
1015
1249
  );
1016
1250
  }
1017
1251
 
@@ -1021,7 +1255,7 @@ export default class PubSub {
1021
1255
  this.config.logger(
1022
1256
  'log',
1023
1257
  new Date().toISOString(),
1024
- `Notifying client of update! (${curClient.remoteAddress})`
1258
+ `Notifying client of update! (${curClient.remoteAddress})`,
1025
1259
  );
1026
1260
  if (typeof entity.updateDataProtection === 'function') {
1027
1261
  entity.updateDataProtection();
@@ -1031,7 +1265,7 @@ export default class PubSub {
1031
1265
  query: curData.query,
1032
1266
  updated: data.guid,
1033
1267
  data: entity,
1034
- })
1268
+ }),
1035
1269
  );
1036
1270
  }
1037
1271
  }
@@ -1039,7 +1273,7 @@ export default class PubSub {
1039
1273
  // Update curData.
1040
1274
  curData.current = currentGuids;
1041
1275
 
1042
- if (nymph.tilmeld != null && token != null) {
1276
+ if (nymph.tilmeld != null && authToken != null) {
1043
1277
  // Clear the user that was temporarily logged in.
1044
1278
  nymph.tilmeld.clearSession();
1045
1279
  }
@@ -1060,19 +1294,19 @@ export default class PubSub {
1060
1294
  * Handle a UID publish from a client.
1061
1295
  */
1062
1296
  private async handlePublishUid(
1063
- from: connection,
1064
- data: PublishUidMessageData
1297
+ from: ws.connection,
1298
+ data: PublishUidMessageData,
1065
1299
  ) {
1066
1300
  this.config.logger(
1067
1301
  'log',
1068
1302
  new Date().toISOString(),
1069
1303
  `Received a UID publish! (${
1070
1304
  'name' in data ? data.name : `${data.oldName} => ${data.newName}`
1071
- }, ${data.event}, ${from.remoteAddress})`
1305
+ }, ${data.event}, ${from.remoteAddress})`,
1072
1306
  );
1073
1307
 
1074
1308
  const names = [data.name, data.oldName].filter(
1075
- (name) => name != null
1309
+ (name) => name != null,
1076
1310
  ) as string[];
1077
1311
  let value = data.value;
1078
1312
  if (data.event === 'renameUID' && data.newName) {
@@ -1087,7 +1321,7 @@ export default class PubSub {
1087
1321
  this.config.logger(
1088
1322
  'log',
1089
1323
  new Date().toISOString(),
1090
- `Notifying client of ${data.event}! (${name}, ${curClient.remoteAddress})`
1324
+ `Notifying client of ${data.event}! (${name}, ${curClient.remoteAddress})`,
1091
1325
  );
1092
1326
  const payload: {
1093
1327
  uid: string;
@@ -1113,14 +1347,14 @@ export default class PubSub {
1113
1347
  this.config.logger(
1114
1348
  'log',
1115
1349
  new Date().toISOString(),
1116
- `Notifying client of new value after rename! (${data.newName}, ${curClient.remoteAddress})`
1350
+ `Notifying client of new value after rename! (${data.newName}, ${curClient.remoteAddress})`,
1117
1351
  );
1118
1352
  curClient.sendUTF(
1119
1353
  JSON.stringify({
1120
1354
  uid: data.newName,
1121
1355
  event: 'setUID',
1122
1356
  value,
1123
- })
1357
+ }),
1124
1358
  );
1125
1359
  }
1126
1360
  }
@@ -1129,24 +1363,24 @@ export default class PubSub {
1129
1363
  /**
1130
1364
  * Relay publish data to other servers.
1131
1365
  */
1132
- private relay(message: Message) {
1366
+ private relay(message: ws.Message) {
1133
1367
  if (message.type !== 'utf8') {
1134
1368
  this.config.logger(
1135
1369
  'error',
1136
1370
  new Date().toISOString(),
1137
- `Can't relay non UTF8 message.`
1371
+ `Can't relay non UTF8 message.`,
1138
1372
  );
1139
1373
  return;
1140
1374
  }
1141
1375
 
1142
1376
  for (let host of this.config.relays) {
1143
- const client = new WebSocketClient();
1377
+ const client = new ws.client();
1144
1378
 
1145
1379
  client.on('connectFailed', (error) => {
1146
1380
  this.config.logger(
1147
1381
  'error',
1148
1382
  new Date().toISOString(),
1149
- `Relay connection failed. (${error.toString()}, ${host})`
1383
+ `Relay connection failed. (${error.toString()}, ${host})`,
1150
1384
  );
1151
1385
  });
1152
1386
 
@@ -1155,7 +1389,7 @@ export default class PubSub {
1155
1389
  this.config.logger(
1156
1390
  'error',
1157
1391
  new Date().toISOString(),
1158
- `Relay connect error. (${error.toString()}, ${host})`
1392
+ `Relay connect error. (${error.toString()}, ${host})`,
1159
1393
  );
1160
1394
  });
1161
1395
 
@@ -1170,8 +1404,8 @@ export default class PubSub {
1170
1404
  }
1171
1405
  }
1172
1406
 
1173
- private findQRefQueries(options: Options, ...selectors: Selector[]) {
1174
- const qrefQueries: [Options, ...Selector[]][] = [];
1407
+ private findQRefQueries(options: MessageOptions, ...selectors: Selector[]) {
1408
+ const qrefQueries: [MessageOptions, ...Selector[]][] = [];
1175
1409
 
1176
1410
  for (const curSelector of selectors) {
1177
1411
  for (const k in curSelector) {
@@ -1191,7 +1425,7 @@ export default class PubSub {
1191
1425
  Array.isArray(((value as Selector['qref']) ?? [])[0])
1192
1426
  ? value
1193
1427
  : [value]
1194
- ) as [string, [Options, ...Selector[]]][];
1428
+ ) as [string, [MessageOptions, ...Selector[]]][];
1195
1429
  for (let i = 0; i < tmpArr.length; i++) {
1196
1430
  qrefQueries.push(tmpArr[i][1]);
1197
1431
  }
@@ -1209,7 +1443,7 @@ export default class PubSub {
1209
1443
  * This translates qref selectors into ref selectors using the "current" GUID
1210
1444
  * list in the existing subscriptions.
1211
1445
  */
1212
- private translateQRefSelectors(client: connection, selectors: Selector[]) {
1446
+ private translateQRefSelectors(client: ws.connection, selectors: Selector[]) {
1213
1447
  const newSelectors: Selector[] = [];
1214
1448
 
1215
1449
  for (const curSelector of selectors) {
@@ -1238,28 +1472,56 @@ export default class PubSub {
1238
1472
  const name = tmpArr[i][0];
1239
1473
  const [qrefOptions, ...qrefSelectors] = tmpArr[i][1];
1240
1474
  const query = JSON.stringify(tmpArr[i][1]);
1241
- const QrefEntityClass =
1242
- typeof qrefOptions.class === 'string'
1243
- ? this.nymph.getEntityClass(qrefOptions.class)
1244
- : qrefOptions.class ?? this.nymph.getEntityClass('Entity');
1475
+ const QrefEntityClass = qrefOptions.class
1476
+ ? this.nymph.getEntityClass(qrefOptions.class)
1477
+ : this.nymph.getEntityClass('Entity');
1245
1478
  const data =
1246
1479
  this.querySubs[QrefEntityClass.ETYPE][query].get(client);
1247
1480
  if (data) {
1248
1481
  const guids = data.current;
1249
- let newValue: [string, string | EntityInterface][];
1250
- const oldValue = newSelector[newKey];
1251
- if (!oldValue) {
1252
- newValue = [];
1253
- } else if (oldValue.length && !Array.isArray(oldValue[0])) {
1254
- newValue = [oldValue] as [string, string | EntityInterface][];
1482
+ if (newKey === '!ref') {
1483
+ // Insert the qref results as a not ref clause.
1484
+ let newValue: [string, string | EntityInterface][];
1485
+ const oldValue = newSelector[newKey];
1486
+ if (
1487
+ oldValue == null ||
1488
+ (Array.isArray(oldValue) && !oldValue.length)
1489
+ ) {
1490
+ newValue = [];
1491
+ } else if (
1492
+ Array.isArray(oldValue) &&
1493
+ !Array.isArray(oldValue[0])
1494
+ ) {
1495
+ newValue = [oldValue] as [string, string | EntityInterface][];
1496
+ } else {
1497
+ newValue = oldValue as [string, string | EntityInterface][];
1498
+ }
1499
+ newValue.push(
1500
+ ...(guids.map((guid) => [name, guid]) as [string, string][]),
1501
+ );
1502
+ newSelector[newKey] = newValue;
1255
1503
  } else {
1256
- newValue = oldValue as [string, string | EntityInterface][];
1504
+ // Insert the qref results as an "or" selector clause with a ref
1505
+ // selector.
1506
+ let newValue: Selector[];
1507
+ const oldValue = newSelector['selector'];
1508
+ delete newSelector[key];
1509
+ if (
1510
+ oldValue == null ||
1511
+ (Array.isArray(oldValue) && !oldValue.length)
1512
+ ) {
1513
+ newValue = [];
1514
+ } else if (!Array.isArray(oldValue)) {
1515
+ newValue = [oldValue] as Selector[];
1516
+ } else {
1517
+ newValue = oldValue as Selector[];
1518
+ }
1519
+ newValue.push({
1520
+ type: '|',
1521
+ ref: guids.map((guid) => [name, guid]) as [string, string][],
1522
+ });
1523
+ newSelector['selector'] = newValue;
1257
1524
  }
1258
- newValue.push(
1259
- ...(guids.map((guid) => [name, guid]) as [string, string][])
1260
- );
1261
- // Insert the qref results as a ref clause.
1262
- newSelector[newKey] = newValue;
1263
1525
  } else {
1264
1526
  // Can't translate, so put the original back in.
1265
1527
  if (!newSelector[key]) {
@@ -1285,7 +1547,7 @@ export default class PubSub {
1285
1547
  newSelector[key] = [];
1286
1548
  }
1287
1549
  (newSelector[key] as [string, string | EntityInterface][]).push(
1288
- ...tmpArr
1550
+ ...tmpArr,
1289
1551
  );
1290
1552
  } else {
1291
1553
  // @ts-ignore: ts doesn't know what value is here.