@live-change/security-service 0.2.5 → 0.2.17

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/ban.js CHANGED
@@ -1,6 +1,6 @@
1
1
  const app = require("@live-change/framework").app()
2
2
  const definition = require('./definition.js')
3
- const { getClientKeysStrings } = require('./utils.js')
3
+ const { getClientKeysStrings, multiKeyIndexQuery, fastMultiKeyIndexQuery } = require('./utils.js')
4
4
  const lcp = require('@live-change/pattern')
5
5
 
6
6
  const banProperties = {
@@ -49,7 +49,10 @@ const Ban = definition.model({
49
49
  },
50
50
  actionBans: {
51
51
  function: async function(input, output) {
52
- const values = (ban) => {
52
+ function prefixes(ban) {
53
+ //output.debug("BAN", ban)
54
+ if(!ban.keys) return []
55
+ if(!ban.actions) return []
53
56
  const v = ban.keys.length
54
57
  const w = ban.actions.length
55
58
  let res = new Array(v * w)
@@ -59,26 +62,87 @@ const Ban = definition.model({
59
62
  res[i * v + j] = `${ban.actions[j]}:${key.key}:${key.value}`
60
63
  }
61
64
  }
65
+ //output.debug("BAN ACTIONS", res)
62
66
  return res
63
67
  }
64
- await input.table("securityService_Ban").onChange((obj, oldObj) => {
68
+ function indexObject(prefix, obj) {
69
+ return {
70
+ id: prefix+'_'+obj.id,
71
+ to: obj.id,
72
+ type: obj.type,
73
+ expire: obj.expire,
74
+ actions: obj.actions
75
+ }
76
+ }
77
+ await input.table("security_Ban").onChange((obj, oldObj) => {
65
78
  if(obj && oldObj) {
66
- let pointers = obj && new Set(values(obj))
67
- let oldPointers = oldObj && new Set(values(oldObj))
79
+ //output.debug("CHANGE!", obj, oldObj)
80
+ let pointers = obj && new Set(prefixes(obj))
81
+ let oldPointers = oldObj && new Set(prefixes(oldObj))
68
82
  for(let pointer of pointers) {
69
- if(!!oldPointers.has(pointer)) output.change({ id: pointer+'_'+obj.id, to: obj.id }, null)
83
+ if(!!oldPointers.has(pointer)) output.change(indexObject(pointer, obj), null)
70
84
  }
71
85
  for(let pointer of oldPointers) {
72
- if(!!pointers.has(pointer)) output.change(null, { id: pointer+'_'+obj.id, to: obj.id })
86
+ if(!!pointers.has(pointer)) output.change(null, indexObject(pointer, obj))
73
87
  }
74
88
  } else if(obj) {
75
- values(obj).forEach(v => output.change({ id: v+'_'+obj.id, to: obj.id }, null))
89
+ //output.debug("CREATE!", obj, oldObj)
90
+ prefixes(obj).forEach(v => output.change(indexObject(v, obj), null))
76
91
  } else if(oldObj) {
77
- values(oldObj).forEach(v => output.change(null, { id: v+'_'+obj.id, to: obj.id }))
92
+ //output.debug("DELETE!", obj, oldObj)
93
+ prefixes(oldObj).forEach(v => output.change(null, indexObject(v, obj)))
78
94
  }
79
95
  })
80
96
  }
81
- }
97
+ },
98
+ actionBansByType: {
99
+ function: async function(input, output) {
100
+ function prefixes(ban) {
101
+ //output.debug("BAN", ban)
102
+ if(!ban.keys) return []
103
+ if(!ban.actions) return []
104
+ const v = ban.keys.length
105
+ const w = ban.actions.length
106
+ let res = new Array(v * w)
107
+ for(let i = 0; i < v; i++) {
108
+ for(let j = 0; j < w; j++) {
109
+ const key = ban.keys[i]
110
+ res[i * v + j] = `${ban.actions[j]}:${key.key}:${key.value}:${ban.type}:${ban.expire}`
111
+ }
112
+ }
113
+ output.debug("BAN PREFIXES", res)
114
+ return res
115
+ }
116
+ function indexObject(prefix, obj) {
117
+ return {
118
+ id: prefix+'_'+obj.id,
119
+ to: obj.id,
120
+ type: obj.type,
121
+ expire: obj.expire,
122
+ actions: obj.actions
123
+ }
124
+ }
125
+ await input.table("security_Ban").onChange((obj, oldObj) => {
126
+ if(obj && oldObj) {
127
+ //output.debug("CHANGE!", obj, oldObj)
128
+ let pointers = obj && new Set(prefixes(obj))
129
+ let oldPointers = oldObj && new Set(prefixes(oldObj))
130
+ for(let pointer of pointers) {
131
+ if(!!oldPointers.has(pointer)) output.change(indexObject(pointer, obj), null)
132
+ }
133
+ for(let pointer of oldPointers) {
134
+ if(!!pointers.has(pointer)) output.change(null, indexObject(pointer, obj))
135
+ }
136
+ } else if(obj) {
137
+ //output.debug("CREATE!", obj, oldObj)
138
+ prefixes(obj).forEach(v => output.change(indexObject(v, obj), null))
139
+ } else if(oldObj) {
140
+ //output.debug("DELETE!", obj, oldObj)
141
+ prefixes(oldObj).forEach(v => output.change(null, indexObject(v, obj)))
142
+ }
143
+ })
144
+ }
145
+ },
82
146
  }
83
147
  })
84
148
 
@@ -101,23 +165,87 @@ definition.view({
101
165
  properties: {},
102
166
  daoPath(params, { client, service }) {
103
167
  const keys = getClientKeysStrings(client)
104
- return multiKeyIndexQuery(keys, 'Ban_bans')
168
+ return multiKeyIndexQuery(keys, 'security_Ban_bans', Ban.tableName)
105
169
  },
106
170
  })
107
171
 
108
172
  definition.view({
109
- name: "myActionBans",
173
+ name: "myActionsBans",
110
174
  properties: {
111
- action: {
175
+ actions: {
112
176
  type: String
113
177
  }
114
178
  },
115
- daoPath({ action }, { client, service }) {
116
- const keys = getClientKeysStrings(client, action + ':')
117
- return multiKeyIndexQuery(keys, 'Ban_actionBans')
179
+ daoPath({ actions }, { client, service }) {
180
+ const keys = []
181
+ for(const action of actions) {
182
+ keys.push(...getClientKeysStrings(client, action + ':'))
183
+ }
184
+ return fastMultiKeyIndexQuery(keys, 'security_Ban_actionBans', Ban.tableName)
118
185
  },
119
186
  })
120
187
 
188
+ definition.view({
189
+ name: "myActionsBansByTypes",
190
+ properties: {
191
+ actions: {
192
+ type: Array,
193
+ of: {
194
+ type: String
195
+ }
196
+ },
197
+ types: {
198
+ type: Array,
199
+ of: {
200
+ type: String
201
+ }
202
+ }
203
+ },
204
+ daoPath({ actions, types }, { client, service }) {
205
+ const keys = []
206
+ for(const type of types) {
207
+ for (const action of actions) {
208
+ keys.push(...getClientKeysStrings(client, action + ':',':' + type))
209
+ }
210
+ }
211
+ console.log("BAN KEYS", keys)
212
+ return ['database', 'query', app.databaseName, `(${
213
+ async (input, output, { keys, indexName }) => {
214
+ function mapper(obj) {
215
+ if(!obj) return null
216
+ const { id, to, ...safeObj } = obj
217
+ return { ...safeObj, id: to }
218
+ }
219
+ function onIndexChange(obj, oldObj) {
220
+ output.change(mapper(obj), mapper(oldObj))
221
+ }
222
+ await Promise.all(keys.map(async (encodedKey) => {
223
+ const range = {
224
+ gte: encodedKey,
225
+ lte: encodedKey + "\xFF",
226
+ reverse: true,
227
+ limit: 1 // only last expire
228
+ }
229
+ await (await input.index(indexName)).range(range).onChange(onIndexChange)
230
+ }))
231
+ }
232
+ })`, { keys, indexName: 'security_Ban_actionBansByType' }]
233
+ },
234
+ })
235
+
236
+ /*function getBanPrefixes(keys, actions) {
237
+ const v = keys.length
238
+ const w = actions.length
239
+ let res = new Array(v * w)
240
+ for(let i = 0; i < v; i++) {
241
+ for(let j = 0; j < w; j++) {
242
+ const key = keys[i]
243
+ res[i * v + j] = `${actions[j]}:${key.key}:${key.value}`
244
+ }
245
+ }
246
+ return res
247
+ }*/
248
+
121
249
  definition.trigger({
122
250
  name: "securityActionBan",
123
251
  properties: {
@@ -136,7 +264,6 @@ definition.trigger({
136
264
  }
137
265
  },
138
266
  async execute({ keys, event, ban: { actions, expire, type } }, { service }, emit) {
139
- const ban = app.generateUid()
140
267
 
141
268
  console.log("SECURITY BAN!", arguments[0])
142
269
 
@@ -152,23 +279,25 @@ definition.trigger({
152
279
  console.log("BAN KEYS", banKeys)
153
280
  console.log("BAN EXPIRE", banExpire)
154
281
 
155
- // service.trigger({
156
- // type: 'createTimer',
157
- // timer: {
158
- // timestamp: banExpire.getTime() + 1000,
159
- // service: 'security',
160
- // trigger: {
161
- // type: 'removeExpiredBan',
162
- // ban
163
- // }
164
- // }
165
- // })
166
-
167
- // emit({
168
- // type: "banCreated",
169
- // ban,
170
- // data: { actions, banKeys, expire, type }
171
- // })
282
+ const ban = app.generateUid()
283
+
284
+ service.trigger({
285
+ type: 'createTimer',
286
+ timer: {
287
+ timestamp: banExpire.getTime() + 1000,
288
+ service: 'security',
289
+ trigger: {
290
+ type: 'removeExpiredBan',
291
+ ban
292
+ }
293
+ }
294
+ })
295
+
296
+ emit({
297
+ type: "banCreated",
298
+ ban,
299
+ data: { actions, keys: banKeys, expire: banExpire, type }
300
+ })
172
301
  }
173
302
  })
174
303
 
@@ -177,7 +306,7 @@ definition.trigger({
177
306
  properties: {
178
307
  ...banProperties
179
308
  },
180
- async execute({ ban }, {client, service}, emit) {
309
+ async execute({ ban }, { client, service }, emit) {
181
310
  emit({
182
311
  type: "banRemoved",
183
312
  ban
package/definition.js CHANGED
@@ -1,7 +1,7 @@
1
1
  const app = require("@live-change/framework").app()
2
2
 
3
3
  const definition = app.createServiceDefinition({
4
- name: "securityService"
4
+ name: "security"
5
5
  })
6
6
  const config = definition.config
7
7
 
package/event.js CHANGED
@@ -1,14 +1,14 @@
1
1
  const crypto = require('crypto')
2
2
  const app = require("@live-change/framework").app()
3
3
  const definition = require('./definition.js')
4
- const { getClientKeysObject } = require('./utils.js')
4
+ const { getClientKeysObject, getClientKeysStrings} = require('./utils.js')
5
5
 
6
6
  const lcp = require('@live-change/pattern')
7
7
  const lcpDb = require('@live-change/pattern-db')
8
8
  const { request } = require('http')
9
9
 
10
10
  const securityPatterns = definition.config.patterns
11
- const relationsStore = lcpDb.relationsStore(app.dao, app.databaseName, 'securityService_relations')
11
+ const relationsStore = lcpDb.relationsStore(app.dao, app.databaseName, 'security_relations')
12
12
  lcp.prepareModelForLive(securityPatterns)
13
13
  //console.log("SECURITY PATTERNS", securityPatterns)
14
14
 
@@ -23,22 +23,10 @@ const eventProperties = {
23
23
  type: String
24
24
  },
25
25
  keys: {
26
- type: Array,
27
- of: {
28
- type: Object,
29
- properties: {
30
- key: {
31
- type: {
32
- String
33
- }
34
- },
35
- value: {
36
- type: {
37
- String
38
- }
39
- }
40
- }
41
- }
26
+ type: Object
27
+ },
28
+ properties: {
29
+ type: Object
42
30
  },
43
31
  timestamp: {
44
32
  type: Date
@@ -51,8 +39,21 @@ const Event = definition.model({
51
39
  ...eventProperties
52
40
  },
53
41
  indexes: {
54
- byTypeAndTimestamp: {
55
- property: ['type', 'timestamp']
42
+ byKeyTypeAndTimestamp: {
43
+ function: async function(input, output) {
44
+ async function spread(event) {
45
+ for(const key in event.keys) {
46
+ output.put({
47
+ id: `${key}:${JSON.stringify(event.keys[key])}:${event.type}:${event.timestamp}_${event.id}`,
48
+ type: event.type,
49
+ timestamp: event.timestamp
50
+ })
51
+ }
52
+ }
53
+ await input.table('security_Event').onChange(
54
+ (obj, oldObj) => spread(obj)
55
+ )
56
+ }
56
57
  }
57
58
  }
58
59
  })
@@ -140,30 +141,42 @@ async function processSecurityCounters(event, timestamp, service) {
140
141
  for(const counter of counters) {
141
142
  const duration = lcp.parseDuration(counter.duration)
142
143
  for(const eventType of counter.match) {
143
- const request = counterEventsRequests.find(req => req.type == eventType)
144
- if(request) {
145
- request.max = Math.max(request.max, counter.max)
146
- request.duration = Math.max(request.duration, duration)
147
- } else {
148
- counterEventsRequests.push({
149
- type: eventType,
150
- max: counter.max,
151
- duration
152
- })
144
+ for(const key of counter.keys) {
145
+ const request = counterEventsRequests.find(req => req.type == eventType && req.key == key)
146
+ if(request) {
147
+ request.max = Math.max(request.max, counter.max)
148
+ request.duration = Math.max(request.duration, duration)
149
+ } else {
150
+ counterEventsRequests.push({
151
+ type: eventType,
152
+ key,
153
+ max: counter.max,
154
+ duration
155
+ })
156
+ }
153
157
  }
154
158
  }
155
159
  }
156
- console.log("COUNTER EVENTS REQUESTS", counterEventsRequests)
157
- const counterEvents = await Promise.all(counterEventsRequests.map(async request => ({
158
- type: request.type,
159
- events: await Event.indexRangeGet('byTypeAndTimestamp',{
160
- gt: `"${request.type}":"${(new Date(now - request.duration - 1000)).toISOString()}"`,
161
- lt: `"${request.type}":\xFF`,
160
+ const indexName = Event.tableName+'_byKeyTypeAndTimestamp'
161
+ //console.log("ALL COUNTER EVENTS REQUESTS", counterEventsRequests)
162
+ const counterEvents = await Promise.all(counterEventsRequests.map(async request => {
163
+ const back = (new Date(now - request.duration - 1000)).toISOString()
164
+ const prefix = `${request.key}:${JSON.stringify(event.keys[request.key])}:${request.type}:`
165
+ console.log("{", prefix)
166
+ const range = {
167
+ gt: prefix + back, // time limited
168
+ lt: prefix + '\xFF',
162
169
  reverse: true,
163
170
  limit: request.max
164
- })
165
- })))
166
- console.log("COUNTER EVENTS", counterEvents)
171
+ }
172
+ console.log("R", range)
173
+ const events = await app.dao.get(['database', 'indexRange', service.databaseName, indexName, range])
174
+ console.log("RR", events)
175
+ return {
176
+ type: request.type,
177
+ events
178
+ }
179
+ }))
167
180
  for(const counter of counters) {
168
181
  const duration = lcp.parseDuration(counter.duration)
169
182
  const fromTime = (new Date(now - duration)).toISOString()
@@ -177,7 +190,7 @@ async function processSecurityCounters(event, timestamp, service) {
177
190
  }
178
191
  }
179
192
  if(count + 1 > counter.max) { // +1 for the new event, not added yet
180
- console.log("COUNTER FIRE", counter)
193
+ //console.log("COUNTER FIRE", counter)
181
194
  actions.push(...counter.actions)
182
195
  }
183
196
  }
@@ -189,24 +202,14 @@ definition.trigger({
189
202
  properties: {
190
203
  event: {
191
204
  type: Object,
192
- properties: {
193
- type: {
194
- type: String
195
- },
196
- keys: {
197
- type: Object
198
- },
199
- properties: {
200
- type: Object
201
- }
202
- }
205
+ properties: eventProperties
203
206
  },
204
207
  client: {
205
208
  type: Object
206
209
  }
207
210
  },
208
211
  async execute({ event, client, timestamp }, { service }, emit) {
209
- console.log("SECURITY EVENT TRIGGERED", arguments[0])
212
+ //console.log("SECURITY EVENT TRIGGERED", arguments[0])
210
213
  event.id = event.id || app.generateUid()
211
214
  event.time = event.time || timestamp
212
215
  const time = (typeof event.time == 'number') ? event.time : new Date(event.time).getTime()
@@ -215,7 +218,7 @@ definition.trigger({
215
218
  event.keys = { ...getClientKeysObject(client), ...event.keys }
216
219
  }
217
220
 
218
- console.log("PROCESS EVENT", event)
221
+ //console.log("PROCESS EVENT", event)
219
222
  let [
220
223
  { newRelations, canceledRelations, actions: patternsActions },
221
224
  { actions: countersActions }
@@ -229,7 +232,7 @@ definition.trigger({
229
232
 
230
233
  for(const action of actions) {
231
234
  const actionTypeUpperCase = action.type[0].toUpperCase() + action.type.slice(1)
232
- console.log("ACTION", JSON.stringify(action, null, ' '))
235
+ //console.log("ACTION", JSON.stringify(action, null, ' '))
233
236
  promises.push(service.trigger({
234
237
  ...action,
235
238
  event,
@@ -252,3 +255,158 @@ definition.trigger({
252
255
  }
253
256
  })
254
257
 
258
+ definition.view({
259
+ name: 'myCountersState',
260
+ properties: {
261
+ events: {
262
+ type: String
263
+ }
264
+ },
265
+ daoPath({ events }, { client, service }) {
266
+ const keys = getClientKeysObject(client)
267
+ const eventRequests = []
268
+ for(const eventType of events) {
269
+ for(const counter of securityCounters) {
270
+ if(!counter.visible) continue
271
+ if(!counter.match.includes(eventType)) continue
272
+ const duration = lcp.parseDuration(counter.duration)
273
+ for(const keyName in keys) {
274
+ const keyValue = keys[keyName]
275
+ if(keyValue === undefined) continue
276
+ eventRequests.push({
277
+ eventType,
278
+ counter: counter.id,
279
+ duration,
280
+ keyName,
281
+ keyValue,
282
+ max: counter.max
283
+ })
284
+ }
285
+ }
286
+ }
287
+ return ['database', 'query', service.databaseName, `(${
288
+ async (input, output, { eventRequests, indexName } ) => {
289
+ const index = await input.index(indexName)
290
+
291
+ class Request {
292
+ constructor(eventRequest) {
293
+ this.eventRequest = eventRequest
294
+
295
+ this.id = eventRequest.counter + ':' + eventRequest.keyName
296
+ this.count = 0
297
+ this.max = this.eventRequest.max
298
+ this.remaining = this.max + 1
299
+
300
+ this.prefix = `${eventRequest.keyName}:${JSON.stringify(eventRequest.keyValue)}:${eventRequest.eventType}:`
301
+ this.range = undefined
302
+ this.indexRange = undefined
303
+ this.fromTime = 0
304
+ this.value = undefined
305
+ this.observer = undefined
306
+ this.oldest = undefined
307
+ this.expire = Infinity
308
+
309
+ this.loading = false
310
+ }
311
+ async refresh() {
312
+ this.loading = true
313
+ const newFromTime = Date.now() - this.eventRequest.duration
314
+ //output.debug("REFRESH", newFromTime, this.fromTime, newFromTime - this.fromTime)
315
+ if(Math.abs(newFromTime - this.fromTime) > 5000) { // refresh only when time is different by 5s
316
+ //output.debug("OBSERVER REFRESH!")
317
+ this.fromTime = newFromTime
318
+ const newRange = {
319
+ gt: this.prefix + (new Date(this.fromTime)).toISOString(), // time limited
320
+ lt: this.prefix + '\xFF',
321
+ reverse: true,
322
+ limit: this.eventRequest.max
323
+ }
324
+ this.range = newRange
325
+ //output.debug("NEW RANGE", this.range)
326
+ if(this.observer) {
327
+ this.indexRange.unobserve(this.observer)
328
+ }
329
+ //output.debug("OBSERVE RANGE!", this.range)
330
+ this.indexRange = index.range(this.range)
331
+
332
+ this.observer = await this.indexRange.onChange(async (ind, oldInd) => {
333
+ //output.debug("RANGE CHANGE!", newRange, ind)
334
+ if(!this.loading) {
335
+ await this.refresh()
336
+ }
337
+ })
338
+ }
339
+ const newValue = await this.indexRange.get()
340
+ this.loading = false
341
+ if(JSON.stringify(this.value) != JSON.stringify(newValue)) {
342
+ this.value = newValue
343
+ if(this.value) {
344
+ this.count = this.value.length
345
+ this.oldest = this.value[this.count - 1]
346
+ this.remaining = this.max - this.count + 1
347
+ this.expire = this.oldest
348
+ ? (new Date(this.oldest.timestamp)).getTime() + this.eventRequest.duration
349
+ : Infinity
350
+ }
351
+ await recompute()
352
+ }
353
+ }
354
+ }
355
+
356
+ const requests = new Array(eventRequests.length)
357
+
358
+ let currentTimeout = null
359
+ let expire = Infinity
360
+ async function recompute() {
361
+ //output.debug("RECOMPUTE?")
362
+ for(const request of requests) {
363
+ if(request.value === undefined) return // no recompute until all readed
364
+ }
365
+ //output.debug("RECOMPUTE!")
366
+ let firstExpire = Infinity
367
+ let firstExpireRequest = null
368
+ for(const request of requests) {
369
+ //console.log("REQUEST EXPIRE", request.prefix, request.expire)
370
+ if(request.expire < firstExpire) {
371
+ firstExpire = request.expire
372
+ firstExpireRequest = request
373
+ }
374
+ const { id, count, remaining, max, oldest, value } = request
375
+ output.change({
376
+ id,
377
+ count, remaining, max,
378
+ oldest: oldest?.timestamp,
379
+ expire: request.expire ? new Date(request.expire) : null,
380
+ //value
381
+ })
382
+ }
383
+ //console.log("FIRST EXPIRE", firstExpire)
384
+ //console.log("LAST EXPIRE", expire)
385
+ if(firstExpire != expire) {
386
+ expire = firstExpire
387
+ if(currentTimeout) { // clears timeout
388
+ currentTimeout()
389
+ }
390
+ expire += 1000 // additional delay
391
+ //console.log("SET TIMEOUT", new Date(expire))
392
+ currentTimeout = output.timeout(expire, () => {
393
+ //output.debug("EXPIRE TIMEOUT!", expire)
394
+ firstExpireRequest.refresh()
395
+ }) // time parameter can be number or date or iso
396
+ }
397
+ }
398
+
399
+ if(eventRequests.length == 0) return
400
+ for(const i in eventRequests) {
401
+ const eventRequest = eventRequests[i]
402
+ const request = new Request(eventRequest)
403
+ requests[i] = request
404
+ //output.debug("REQUEST", eventRequest, '=>', request)
405
+ }
406
+ await Promise.all(requests.map(async request => {
407
+ await request.refresh()
408
+ }))
409
+ }
410
+ })`, { eventRequests, indexName: Event.tableName + '_byKeyTypeAndTimestamp' }]
411
+ }
412
+ })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@live-change/security-service",
3
- "version": "0.2.5",
3
+ "version": "0.2.17",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -26,5 +26,5 @@
26
26
  "@live-change/pattern-db": "^0.2.2",
27
27
  "nodemailer": "^6.7.2"
28
28
  },
29
- "gitHead": "7691357c7569a18373668691eb6a81a9e161194d"
29
+ "gitHead": "9737b14fc9fb856731bbb4d43fe3bc4e0be7bab2"
30
30
  }
package/utils.js CHANGED
@@ -1,9 +1,10 @@
1
+ const app = require("@live-change/framework").app()
1
2
  const definition = require('./definition.js')
2
3
 
3
4
  const clientKeys = definition.config.clientKeys
4
5
 
5
- function multiKeyIndexQuery(keys, indexName) {
6
- return ['database', 'query', database, `(${
6
+ function multiKeyIndexQuery(keys, indexName, tableName) {
7
+ return ['database', 'query', app.databaseName, `(${
7
8
  async (input, output, { keys, indexName, tableName }) => {
8
9
  const objectStates = new Map()
9
10
  async function mapper(res) {
@@ -55,16 +56,38 @@ function multiKeyIndexQuery(keys, indexName) {
55
56
  await (await input.index(indexName)).range(range).onChange(onIndexChange)
56
57
  }))
57
58
  }
58
- })`, { keys, indexName, tableName: Ban.tableName }]
59
+ })`, { keys, indexName, tableName }]
59
60
  }
60
61
 
61
- function getClientKeysStrings(client, prefix = '') {
62
+ function fastMultiKeyIndexQuery(keys, indexName) {
63
+ return ['database', 'query', app.databaseName, `(${
64
+ async (input, output, { keys, indexName }) => {
65
+ function mapper(obj) {
66
+ if(!obj) return null
67
+ const { id, to, ...safeObj } = obj
68
+ return { ...safeObj, id: to }
69
+ }
70
+ function onIndexChange(obj, oldObj) {
71
+ output.change(mapper(obj), mapper(oldObj))
72
+ }
73
+ await Promise.all(keys.map(async (encodedKey) => {
74
+ const range = {
75
+ gte: encodedKey + '_',
76
+ lte: encodedKey + "_\xFF\xFF\xFF\xFF"
77
+ }
78
+ await (await input.index(indexName)).range(range).onChange(onIndexChange)
79
+ }))
80
+ }
81
+ })`, { keys, indexName}]
82
+ }
83
+
84
+ function getClientKeysStrings(client, prefix = '', suffix = '') {
62
85
  if(clientKeys) {
63
- return clientKeys(client).map(k => prefix + k.key + ':' + k.value)
86
+ return clientKeys(client).filter(k => k.value).map(k => prefix + k.key + ':' + k.value + suffix)
64
87
  } else {
65
88
  const keys = []
66
89
  for(let key in client) {
67
- keys.push(prefix + key + ':' + client[key])
90
+ if(client[key]) keys.push(prefix + key + ':' + client[key] + suffix)
68
91
  }
69
92
  return keys
70
93
  }
@@ -82,4 +105,4 @@ function getClientKeysObject(client, prefix = '') {
82
105
  }
83
106
  }
84
107
 
85
- module.exports = { multiKeyIndexQuery, getClientKeysStrings, getClientKeysObject }
108
+ module.exports = { multiKeyIndexQuery, fastMultiKeyIndexQuery, getClientKeysStrings, getClientKeysObject }