@omnixal/openclaw-nats-plugin 0.2.15 → 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.
Files changed (27) hide show
  1. package/dashboard/src/lib/LogsPanel.svelte +50 -1
  2. package/dashboard/src/lib/RoutesPanel.svelte +120 -11
  3. package/dashboard/src/lib/api.ts +20 -0
  4. package/package.json +1 -1
  5. package/plugins/nats-context-engine/http-handler.ts +12 -67
  6. package/plugins/nats-context-engine/index.ts +17 -3
  7. package/sidecar/src/config.ts +4 -0
  8. package/sidecar/src/consumer/consumer.controller.ts +41 -4
  9. package/sidecar/src/consumer/consumer.module.ts +2 -1
  10. package/sidecar/src/db/migrations/0008_fluffy_pestilence.sql +1 -0
  11. package/sidecar/src/db/migrations/0009_sour_romulus.sql +6 -0
  12. package/sidecar/src/db/migrations/meta/0008_snapshot.json +492 -0
  13. package/sidecar/src/db/migrations/meta/0009_snapshot.json +514 -0
  14. package/sidecar/src/db/migrations/meta/_journal.json +14 -0
  15. package/sidecar/src/db/schema.ts +8 -2
  16. package/sidecar/src/pending/pending-flush.service.ts +70 -0
  17. package/sidecar/src/pending/pending.module.ts +5 -1
  18. package/sidecar/src/pending/pending.repository.ts +6 -2
  19. package/sidecar/src/pending/pending.service.ts +2 -2
  20. package/sidecar/src/route-filter/filter-expression.ts +10 -0
  21. package/sidecar/src/route-filter/route-filter.module.ts +8 -0
  22. package/sidecar/src/route-filter/route-filter.service.ts +61 -0
  23. package/sidecar/src/router/router.controller.ts +14 -1
  24. package/sidecar/src/router/router.repository.ts +15 -4
  25. package/sidecar/src/router/router.service.ts +14 -3
  26. package/sidecar/src/scheduler/scheduler.controller.ts +2 -2
  27. package/sidecar/src/validation/schemas.ts +16 -0
@@ -0,0 +1,514 @@
1
+ {
2
+ "version": "6",
3
+ "dialect": "sqlite",
4
+ "id": "0bd82560-d525-4af4-8c7d-930289e0499c",
5
+ "prevId": "e071c2ec-330f-4444-bc0d-1a840e0566a8",
6
+ "tables": {
7
+ "cron_jobs": {
8
+ "name": "cron_jobs",
9
+ "columns": {
10
+ "id": {
11
+ "name": "id",
12
+ "type": "text",
13
+ "primaryKey": true,
14
+ "notNull": true,
15
+ "autoincrement": false
16
+ },
17
+ "name": {
18
+ "name": "name",
19
+ "type": "text",
20
+ "primaryKey": false,
21
+ "notNull": true,
22
+ "autoincrement": false
23
+ },
24
+ "expr": {
25
+ "name": "expr",
26
+ "type": "text",
27
+ "primaryKey": false,
28
+ "notNull": true,
29
+ "autoincrement": false
30
+ },
31
+ "subject": {
32
+ "name": "subject",
33
+ "type": "text",
34
+ "primaryKey": false,
35
+ "notNull": true,
36
+ "autoincrement": false
37
+ },
38
+ "payload": {
39
+ "name": "payload",
40
+ "type": "text",
41
+ "primaryKey": false,
42
+ "notNull": false,
43
+ "autoincrement": false
44
+ },
45
+ "timezone": {
46
+ "name": "timezone",
47
+ "type": "text",
48
+ "primaryKey": false,
49
+ "notNull": true,
50
+ "autoincrement": false,
51
+ "default": "'UTC'"
52
+ },
53
+ "enabled": {
54
+ "name": "enabled",
55
+ "type": "integer",
56
+ "primaryKey": false,
57
+ "notNull": true,
58
+ "autoincrement": false,
59
+ "default": true
60
+ },
61
+ "last_run_at": {
62
+ "name": "last_run_at",
63
+ "type": "integer",
64
+ "primaryKey": false,
65
+ "notNull": false,
66
+ "autoincrement": false
67
+ },
68
+ "created_at": {
69
+ "name": "created_at",
70
+ "type": "integer",
71
+ "primaryKey": false,
72
+ "notNull": true,
73
+ "autoincrement": false
74
+ }
75
+ },
76
+ "indexes": {
77
+ "cron_jobs_name_unique": {
78
+ "name": "cron_jobs_name_unique",
79
+ "columns": [
80
+ "name"
81
+ ],
82
+ "isUnique": true
83
+ },
84
+ "cron_jobs_name_idx": {
85
+ "name": "cron_jobs_name_idx",
86
+ "columns": [
87
+ "name"
88
+ ],
89
+ "isUnique": false
90
+ }
91
+ },
92
+ "foreignKeys": {},
93
+ "compositePrimaryKeys": {},
94
+ "uniqueConstraints": {},
95
+ "checkConstraints": {}
96
+ },
97
+ "dedup_events": {
98
+ "name": "dedup_events",
99
+ "columns": {
100
+ "event_id": {
101
+ "name": "event_id",
102
+ "type": "text",
103
+ "primaryKey": true,
104
+ "notNull": true,
105
+ "autoincrement": false
106
+ },
107
+ "subject": {
108
+ "name": "subject",
109
+ "type": "text",
110
+ "primaryKey": false,
111
+ "notNull": true,
112
+ "autoincrement": false
113
+ },
114
+ "seen_at": {
115
+ "name": "seen_at",
116
+ "type": "integer",
117
+ "primaryKey": false,
118
+ "notNull": true,
119
+ "autoincrement": false
120
+ }
121
+ },
122
+ "indexes": {
123
+ "dedup_events_seen_at_idx": {
124
+ "name": "dedup_events_seen_at_idx",
125
+ "columns": [
126
+ "seen_at"
127
+ ],
128
+ "isUnique": false
129
+ }
130
+ },
131
+ "foreignKeys": {},
132
+ "compositePrimaryKeys": {},
133
+ "uniqueConstraints": {},
134
+ "checkConstraints": {}
135
+ },
136
+ "event_routes": {
137
+ "name": "event_routes",
138
+ "columns": {
139
+ "id": {
140
+ "name": "id",
141
+ "type": "text",
142
+ "primaryKey": true,
143
+ "notNull": true,
144
+ "autoincrement": false
145
+ },
146
+ "name": {
147
+ "name": "name",
148
+ "type": "text",
149
+ "primaryKey": false,
150
+ "notNull": true,
151
+ "autoincrement": false
152
+ },
153
+ "pattern": {
154
+ "name": "pattern",
155
+ "type": "text",
156
+ "primaryKey": false,
157
+ "notNull": true,
158
+ "autoincrement": false
159
+ },
160
+ "target": {
161
+ "name": "target",
162
+ "type": "text",
163
+ "primaryKey": false,
164
+ "notNull": true,
165
+ "autoincrement": false,
166
+ "default": "'main'"
167
+ },
168
+ "enabled": {
169
+ "name": "enabled",
170
+ "type": "integer",
171
+ "primaryKey": false,
172
+ "notNull": true,
173
+ "autoincrement": false,
174
+ "default": true
175
+ },
176
+ "priority": {
177
+ "name": "priority",
178
+ "type": "integer",
179
+ "primaryKey": false,
180
+ "notNull": true,
181
+ "autoincrement": false,
182
+ "default": 5
183
+ },
184
+ "filter": {
185
+ "name": "filter",
186
+ "type": "text",
187
+ "primaryKey": false,
188
+ "notNull": false,
189
+ "autoincrement": false
190
+ },
191
+ "filter_drop_count": {
192
+ "name": "filter_drop_count",
193
+ "type": "integer",
194
+ "primaryKey": false,
195
+ "notNull": true,
196
+ "autoincrement": false,
197
+ "default": 0
198
+ },
199
+ "created_at": {
200
+ "name": "created_at",
201
+ "type": "integer",
202
+ "primaryKey": false,
203
+ "notNull": true,
204
+ "autoincrement": false
205
+ },
206
+ "last_delivered_at": {
207
+ "name": "last_delivered_at",
208
+ "type": "integer",
209
+ "primaryKey": false,
210
+ "notNull": false,
211
+ "autoincrement": false
212
+ },
213
+ "last_event_subject": {
214
+ "name": "last_event_subject",
215
+ "type": "text",
216
+ "primaryKey": false,
217
+ "notNull": false,
218
+ "autoincrement": false
219
+ },
220
+ "delivery_count": {
221
+ "name": "delivery_count",
222
+ "type": "integer",
223
+ "primaryKey": false,
224
+ "notNull": true,
225
+ "autoincrement": false,
226
+ "default": 0
227
+ },
228
+ "last_delivery_lag_ms": {
229
+ "name": "last_delivery_lag_ms",
230
+ "type": "integer",
231
+ "primaryKey": false,
232
+ "notNull": false,
233
+ "autoincrement": false
234
+ },
235
+ "custom_payload": {
236
+ "name": "custom_payload",
237
+ "type": "text",
238
+ "primaryKey": false,
239
+ "notNull": false,
240
+ "autoincrement": false
241
+ }
242
+ },
243
+ "indexes": {
244
+ "event_routes_name_idx": {
245
+ "name": "event_routes_name_idx",
246
+ "columns": [
247
+ "name"
248
+ ],
249
+ "isUnique": true
250
+ },
251
+ "event_routes_pattern_idx": {
252
+ "name": "event_routes_pattern_idx",
253
+ "columns": [
254
+ "pattern"
255
+ ],
256
+ "isUnique": false
257
+ },
258
+ "event_routes_target_idx": {
259
+ "name": "event_routes_target_idx",
260
+ "columns": [
261
+ "target"
262
+ ],
263
+ "isUnique": false
264
+ }
265
+ },
266
+ "foreignKeys": {},
267
+ "compositePrimaryKeys": {},
268
+ "uniqueConstraints": {},
269
+ "checkConstraints": {}
270
+ },
271
+ "execution_logs": {
272
+ "name": "execution_logs",
273
+ "columns": {
274
+ "id": {
275
+ "name": "id",
276
+ "type": "text",
277
+ "primaryKey": true,
278
+ "notNull": true,
279
+ "autoincrement": false
280
+ },
281
+ "entity_type": {
282
+ "name": "entity_type",
283
+ "type": "text",
284
+ "primaryKey": false,
285
+ "notNull": true,
286
+ "autoincrement": false
287
+ },
288
+ "entity_id": {
289
+ "name": "entity_id",
290
+ "type": "text",
291
+ "primaryKey": false,
292
+ "notNull": true,
293
+ "autoincrement": false
294
+ },
295
+ "action": {
296
+ "name": "action",
297
+ "type": "text",
298
+ "primaryKey": false,
299
+ "notNull": true,
300
+ "autoincrement": false
301
+ },
302
+ "subject": {
303
+ "name": "subject",
304
+ "type": "text",
305
+ "primaryKey": false,
306
+ "notNull": true,
307
+ "autoincrement": false
308
+ },
309
+ "detail": {
310
+ "name": "detail",
311
+ "type": "text",
312
+ "primaryKey": false,
313
+ "notNull": false,
314
+ "autoincrement": false
315
+ },
316
+ "success": {
317
+ "name": "success",
318
+ "type": "integer",
319
+ "primaryKey": false,
320
+ "notNull": true,
321
+ "autoincrement": false,
322
+ "default": true
323
+ },
324
+ "created_at": {
325
+ "name": "created_at",
326
+ "type": "integer",
327
+ "primaryKey": false,
328
+ "notNull": true,
329
+ "autoincrement": false
330
+ }
331
+ },
332
+ "indexes": {
333
+ "execution_logs_entity_idx": {
334
+ "name": "execution_logs_entity_idx",
335
+ "columns": [
336
+ "entity_type",
337
+ "entity_id"
338
+ ],
339
+ "isUnique": false
340
+ },
341
+ "execution_logs_created_at_idx": {
342
+ "name": "execution_logs_created_at_idx",
343
+ "columns": [
344
+ "created_at"
345
+ ],
346
+ "isUnique": false
347
+ }
348
+ },
349
+ "foreignKeys": {},
350
+ "compositePrimaryKeys": {},
351
+ "uniqueConstraints": {},
352
+ "checkConstraints": {}
353
+ },
354
+ "pending_events": {
355
+ "name": "pending_events",
356
+ "columns": {
357
+ "id": {
358
+ "name": "id",
359
+ "type": "text",
360
+ "primaryKey": true,
361
+ "notNull": true,
362
+ "autoincrement": false
363
+ },
364
+ "session_key": {
365
+ "name": "session_key",
366
+ "type": "text",
367
+ "primaryKey": false,
368
+ "notNull": true,
369
+ "autoincrement": false
370
+ },
371
+ "subject": {
372
+ "name": "subject",
373
+ "type": "text",
374
+ "primaryKey": false,
375
+ "notNull": true,
376
+ "autoincrement": false
377
+ },
378
+ "payload": {
379
+ "name": "payload",
380
+ "type": "text",
381
+ "primaryKey": false,
382
+ "notNull": false,
383
+ "autoincrement": false
384
+ },
385
+ "priority": {
386
+ "name": "priority",
387
+ "type": "integer",
388
+ "primaryKey": false,
389
+ "notNull": true,
390
+ "autoincrement": false,
391
+ "default": 5
392
+ },
393
+ "created_at": {
394
+ "name": "created_at",
395
+ "type": "integer",
396
+ "primaryKey": false,
397
+ "notNull": true,
398
+ "autoincrement": false
399
+ },
400
+ "delivered_at": {
401
+ "name": "delivered_at",
402
+ "type": "integer",
403
+ "primaryKey": false,
404
+ "notNull": false,
405
+ "autoincrement": false
406
+ }
407
+ },
408
+ "indexes": {},
409
+ "foreignKeys": {},
410
+ "compositePrimaryKeys": {},
411
+ "uniqueConstraints": {},
412
+ "checkConstraints": {}
413
+ },
414
+ "timer_jobs": {
415
+ "name": "timer_jobs",
416
+ "columns": {
417
+ "id": {
418
+ "name": "id",
419
+ "type": "text",
420
+ "primaryKey": true,
421
+ "notNull": true,
422
+ "autoincrement": false
423
+ },
424
+ "name": {
425
+ "name": "name",
426
+ "type": "text",
427
+ "primaryKey": false,
428
+ "notNull": true,
429
+ "autoincrement": false
430
+ },
431
+ "subject": {
432
+ "name": "subject",
433
+ "type": "text",
434
+ "primaryKey": false,
435
+ "notNull": true,
436
+ "autoincrement": false
437
+ },
438
+ "payload": {
439
+ "name": "payload",
440
+ "type": "text",
441
+ "primaryKey": false,
442
+ "notNull": false,
443
+ "autoincrement": false
444
+ },
445
+ "delay_ms": {
446
+ "name": "delay_ms",
447
+ "type": "integer",
448
+ "primaryKey": false,
449
+ "notNull": true,
450
+ "autoincrement": false
451
+ },
452
+ "fire_at": {
453
+ "name": "fire_at",
454
+ "type": "integer",
455
+ "primaryKey": false,
456
+ "notNull": true,
457
+ "autoincrement": false
458
+ },
459
+ "fired": {
460
+ "name": "fired",
461
+ "type": "integer",
462
+ "primaryKey": false,
463
+ "notNull": true,
464
+ "autoincrement": false,
465
+ "default": false
466
+ },
467
+ "created_at": {
468
+ "name": "created_at",
469
+ "type": "integer",
470
+ "primaryKey": false,
471
+ "notNull": true,
472
+ "autoincrement": false
473
+ }
474
+ },
475
+ "indexes": {
476
+ "timer_jobs_name_unique": {
477
+ "name": "timer_jobs_name_unique",
478
+ "columns": [
479
+ "name"
480
+ ],
481
+ "isUnique": true
482
+ },
483
+ "timer_jobs_name_idx": {
484
+ "name": "timer_jobs_name_idx",
485
+ "columns": [
486
+ "name"
487
+ ],
488
+ "isUnique": false
489
+ },
490
+ "timer_jobs_fire_at_idx": {
491
+ "name": "timer_jobs_fire_at_idx",
492
+ "columns": [
493
+ "fire_at"
494
+ ],
495
+ "isUnique": false
496
+ }
497
+ },
498
+ "foreignKeys": {},
499
+ "compositePrimaryKeys": {},
500
+ "uniqueConstraints": {},
501
+ "checkConstraints": {}
502
+ }
503
+ },
504
+ "views": {},
505
+ "enums": {},
506
+ "_meta": {
507
+ "schemas": {},
508
+ "tables": {},
509
+ "columns": {}
510
+ },
511
+ "internal": {
512
+ "indexes": {}
513
+ }
514
+ }
@@ -57,6 +57,20 @@
57
57
  "when": 1774293614203,
58
58
  "tag": "0007_dizzy_komodo",
59
59
  "breakpoints": true
60
+ },
61
+ {
62
+ "idx": 8,
63
+ "version": "6",
64
+ "when": 1774530462833,
65
+ "tag": "0008_fluffy_pestilence",
66
+ "breakpoints": true
67
+ },
68
+ {
69
+ "idx": 9,
70
+ "version": "6",
71
+ "when": 1774535149039,
72
+ "tag": "0009_sour_romulus",
73
+ "breakpoints": true
60
74
  }
61
75
  ]
62
76
  }
@@ -1,4 +1,5 @@
1
- import { sqliteTable, text, integer, index } from '@onebun/drizzle/sqlite';
1
+ import { sqliteTable, text, integer, index, uniqueIndex } from '@onebun/drizzle/sqlite';
2
+ import type { FilterExpression } from '../route-filter/filter-expression';
2
3
 
3
4
  export const dedupEvents = sqliteTable('dedup_events', {
4
5
  eventId: text('event_id').primaryKey(),
@@ -23,16 +24,21 @@ export type NewPendingEvent = typeof pendingEvents.$inferInsert;
23
24
 
24
25
  export const eventRoutes = sqliteTable('event_routes', {
25
26
  id: text('id').primaryKey(),
26
- pattern: text('pattern').notNull().unique(),
27
+ name: text('name').notNull(),
28
+ pattern: text('pattern').notNull(),
27
29
  target: text('target').notNull().default('main'),
28
30
  enabled: integer('enabled', { mode: 'boolean' }).notNull().default(true),
29
31
  priority: integer('priority').notNull().default(5),
32
+ filter: text('filter', { mode: 'json' }).$type<FilterExpression | null>(),
33
+ filterDropCount: integer('filter_drop_count').notNull().default(0),
30
34
  createdAt: integer('created_at', { mode: 'timestamp_ms' }).notNull(),
31
35
  lastDeliveredAt: integer('last_delivered_at', { mode: 'timestamp_ms' }),
32
36
  lastEventSubject: text('last_event_subject'),
33
37
  deliveryCount: integer('delivery_count').notNull().default(0),
34
38
  lastDeliveryLagMs: integer('last_delivery_lag_ms'),
39
+ customPayload: text('custom_payload', { mode: 'json' }).$type<unknown>(),
35
40
  }, (table) => [
41
+ uniqueIndex('event_routes_name_idx').on(table.name),
36
42
  index('event_routes_pattern_idx').on(table.pattern),
37
43
  index('event_routes_target_idx').on(table.target),
38
44
  ]);
@@ -0,0 +1,70 @@
1
+ import { Service, BaseService, type OnModuleInit, type OnModuleDestroy } from '@onebun/core';
2
+ import { PendingService } from './pending.service';
3
+ import { GatewayClientService, GatewayRpcError } from '../gateway/gateway-client.service';
4
+ import { MetricsService } from '../metrics/metrics.service';
5
+ import { LogService } from '../logs/log.service';
6
+
7
+ @Service()
8
+ export class PendingFlushService extends BaseService implements OnModuleInit, OnModuleDestroy {
9
+ private flushTimer?: ReturnType<typeof setInterval>;
10
+
11
+ constructor(
12
+ private pendingService: PendingService,
13
+ private gatewayClient: GatewayClientService,
14
+ private metrics: MetricsService,
15
+ private logService: LogService,
16
+ ) {
17
+ super();
18
+ }
19
+
20
+ async onModuleInit(): Promise<void> {
21
+ const intervalMs = this.config.get('pending.flushIntervalMs');
22
+ this.flush().catch((e) => {
23
+ this.logger.warn('Initial pending flush failed', { error: String(e) });
24
+ });
25
+ this.flushTimer = setInterval(() => {
26
+ this.flush().catch((e) => {
27
+ this.logger.warn('Pending flush failed', { error: String(e) });
28
+ });
29
+ }, intervalMs);
30
+ }
31
+
32
+ async onModuleDestroy(): Promise<void> {
33
+ if (this.flushTimer) {
34
+ clearInterval(this.flushTimer);
35
+ this.flushTimer = undefined;
36
+ }
37
+ }
38
+
39
+ async flush(): Promise<void> {
40
+ if (!this.gatewayClient.isAlive()) {
41
+ return;
42
+ }
43
+
44
+ const batchSize = this.config.get('pending.flushBatchSize');
45
+ const pending = await this.pendingService.fetchPending('default', batchSize);
46
+ if (pending.length === 0) {
47
+ return;
48
+ }
49
+
50
+ this.logger.info(`Flushing ${pending.length} pending event(s)`);
51
+
52
+ for (const event of pending) {
53
+ try {
54
+ const message = `[NATS:${event.subject}] ${JSON.stringify(event.payload)}`;
55
+ await this.gatewayClient.inject({ message, eventId: event.id });
56
+ await this.pendingService.markDelivered([event.id]);
57
+ this.metrics.recordConsume(event.subject);
58
+ await this.logService.logDelivery('pending-flush', event.subject, JSON.stringify({ eventId: event.id }));
59
+ } catch (err) {
60
+ await this.logService.logError('route', 'pending-flush', event.subject, err);
61
+ if (err instanceof GatewayRpcError) {
62
+ this.logger.error(`Pending flush: gateway rejected event ${event.id}: ${err.errorCode}`);
63
+ continue;
64
+ }
65
+ this.logger.error(`Pending flush: network error on event ${event.id}, stopping batch`, err);
66
+ break;
67
+ }
68
+ }
69
+ }
70
+ }
@@ -2,10 +2,14 @@ import { Module } from '@onebun/core';
2
2
  import { PendingController } from './pending.controller';
3
3
  import { PendingService } from './pending.service';
4
4
  import { PendingRepository } from './pending.repository';
5
+ import { PendingFlushService } from './pending-flush.service';
6
+ import { MetricsModule } from '../metrics/metrics.module';
7
+ import { LogModule } from '../logs/log.module';
5
8
 
6
9
  @Module({
10
+ imports: [MetricsModule, LogModule],
7
11
  controllers: [PendingController],
8
- providers: [PendingService, PendingRepository],
12
+ providers: [PendingService, PendingRepository, PendingFlushService],
9
13
  exports: [PendingService],
10
14
  })
11
15
  export class PendingModule {}
@@ -28,12 +28,16 @@ export class PendingRepository extends BaseService {
28
28
  .onConflictDoNothing();
29
29
  }
30
30
 
31
- async fetchPending(sessionKey: string): Promise<DbPendingEvent[]> {
32
- return this.db
31
+ async fetchPending(sessionKey: string, limit?: number): Promise<DbPendingEvent[]> {
32
+ const query = this.db
33
33
  .select()
34
34
  .from(pendingEvents)
35
35
  .where(and(eq(pendingEvents.sessionKey, sessionKey), isNull(pendingEvents.deliveredAt)))
36
36
  .orderBy(desc(pendingEvents.priority));
37
+ if (limit !== undefined) {
38
+ return query.limit(limit);
39
+ }
40
+ return query;
37
41
  }
38
42
 
39
43
  async markDelivered(ids: string[]): Promise<void> {
@@ -19,8 +19,8 @@ export class PendingService extends BaseService {
19
19
  });
20
20
  }
21
21
 
22
- async fetchPending(sessionKey: string): Promise<DbPendingEvent[]> {
23
- return this.repo.fetchPending(sessionKey);
22
+ async fetchPending(sessionKey: string, limit?: number): Promise<DbPendingEvent[]> {
23
+ return this.repo.fetchPending(sessionKey, limit);
24
24
  }
25
25
 
26
26
  async markDelivered(ids: string[]): Promise<void> {
@@ -0,0 +1,10 @@
1
+ export interface FilterCondition {
2
+ field: string; // dot-path: "amount", "order.status", "tags.0"
3
+ op: 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'in' | 'nin' | 'contains' | 'exists';
4
+ value: unknown;
5
+ }
6
+
7
+ export interface FilterExpression {
8
+ logic: 'and' | 'or'; // default 'and'
9
+ conditions: FilterCondition[];
10
+ }