foxhound 2.0.18 → 2.0.19

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.
@@ -0,0 +1,551 @@
1
+ /**
2
+ * Unit tests for FoxHound Solr Dialect
3
+ *
4
+ * @license MIT
5
+ *
6
+ * @author Steven Velozo <steven@velozo.com>
7
+ */
8
+
9
+ var Chai = require('chai');
10
+ var Expect = Chai.expect;
11
+ var Assert = Chai.assert;
12
+
13
+ var libFable = require('fable');
14
+ const _Fable = new libFable({Product:'FoxhoundTestsSolr'});
15
+ var libFoxHound = require('../source/FoxHound.js');
16
+
17
+ var _AnimalSchema = (
18
+ [
19
+ { Column: "IDAnimal", Type:"AutoIdentity" },
20
+ { Column: "GUIDAnimal", Type:"AutoGUID" },
21
+ { Column: "CreateDate", Type:"CreateDate" },
22
+ { Column: "CreatingIDUser", Type:"CreateIDUser" },
23
+ { Column: "UpdateDate", Type:"UpdateDate" },
24
+ { Column: "UpdatingIDUser", Type:"UpdateIDUser" },
25
+ { Column: "Deleted", Type:"Deleted" },
26
+ { Column: "DeletingIDUser", Type:"DeleteIDUser" },
27
+ { Column: "DeleteDate", Type:"DeleteDate" }
28
+ ]);
29
+
30
+ var _AnimalSchemaWithoutDeleted = (
31
+ [
32
+ { Column: "IDAnimal", Type:"AutoIdentity" },
33
+ { Column: "GUIDAnimal", Type:"AutoGUID" },
34
+ { Column: "CreateDate", Type:"CreateDate" },
35
+ { Column: "CreatingIDUser", Type:"CreateIDUser" },
36
+ { Column: "UpdateDate", Type:"UpdateDate" },
37
+ { Column: "UpdatingIDUser", Type:"UpdateIDUser" }
38
+ ]);
39
+
40
+ suite
41
+ (
42
+ 'FoxHound-Dialect-Solr',
43
+ function()
44
+ {
45
+ setup
46
+ (
47
+ function()
48
+ {
49
+ }
50
+ );
51
+
52
+ suite
53
+ (
54
+ 'Object Sanity',
55
+ function()
56
+ {
57
+ test
58
+ (
59
+ 'initialize should build a happy little object',
60
+ function()
61
+ {
62
+ var testFoxHound = libFoxHound.new(_Fable).setDialect('Solr');
63
+ Expect(testFoxHound.dialect.name)
64
+ .to.equal('Solr');
65
+ Expect(testFoxHound)
66
+ .to.be.an('object', 'FoxHound with Solr should initialize as an object directly from the require statement.');
67
+ }
68
+ );
69
+ }
70
+ );
71
+
72
+ suite
73
+ (
74
+ 'Basic Query Generation',
75
+ function()
76
+ {
77
+ test
78
+ (
79
+ 'Create Query',
80
+ function()
81
+ {
82
+ var tmpQuery = libFoxHound.new(_Fable)
83
+ .setDialect('Solr')
84
+ .setScope('Animal')
85
+ .addRecord({IDAnimal:null, Name:'Foo Foo', Age:15});
86
+ tmpQuery.buildCreateQuery();
87
+ _Fable.log.trace('Create Query', tmpQuery.query);
88
+ var tmpOp = JSON.parse(tmpQuery.query.body);
89
+ Expect(tmpOp.collection).to.equal('Animal');
90
+ Expect(tmpOp.operation).to.equal('add');
91
+ Expect(tmpOp.document.Name).to.equal('Foo Foo');
92
+ Expect(tmpOp.document.Age).to.equal(15);
93
+ }
94
+ );
95
+ test
96
+ (
97
+ 'Create Query with Schema uses $$AUTOINCREMENT for AutoIdentity',
98
+ function()
99
+ {
100
+ var tmpQuery = libFoxHound.new(_Fable)
101
+ .setDialect('Solr')
102
+ .setScope('Animal')
103
+ .addRecord({IDAnimal:null, GUIDAnimal:false, CreateDate:null, CreatingIDUser:null, UpdateDate:null, UpdatingIDUser:null, Deleted:0, Name:'Foo Foo', Age:15});
104
+ tmpQuery.query.schema = _AnimalSchema;
105
+ tmpQuery.query.IDUser = 37;
106
+ tmpQuery.query.UUID = 'test-guid-value';
107
+ tmpQuery.buildCreateQuery();
108
+ _Fable.log.trace('Create Query with Schema', tmpQuery.query);
109
+ var tmpOp = JSON.parse(tmpQuery.query.body);
110
+ Expect(tmpOp.document.IDAnimal).to.equal('$$AUTOINCREMENT');
111
+ Expect(tmpOp.document.GUIDAnimal).to.equal('test-guid-value');
112
+ Expect(tmpOp.document.CreateDate).to.equal('$$NOW');
113
+ Expect(tmpOp.document.CreatingIDUser).to.equal(37);
114
+ Expect(tmpOp.document.UpdateDate).to.equal('$$NOW');
115
+ Expect(tmpOp.document.UpdatingIDUser).to.equal(37);
116
+ Expect(tmpOp.document.Deleted).to.equal(0);
117
+ // DeleteDate and DeletingIDUser should be skipped
118
+ Expect(tmpOp.document).to.not.have.property('DeleteDate');
119
+ Expect(tmpOp.document).to.not.have.property('DeletingIDUser');
120
+ Expect(tmpOp.document.Name).to.equal('Foo Foo');
121
+ // counterScope should be present
122
+ Expect(tmpOp.counterScope).to.equal('Animal.IDAnimal');
123
+ }
124
+ );
125
+ test
126
+ (
127
+ 'Create Query with existing GUID passes it through',
128
+ function()
129
+ {
130
+ var tmpQuery = libFoxHound.new(_Fable)
131
+ .setDialect('Solr')
132
+ .setScope('Animal')
133
+ .addRecord({IDAnimal:null, GUIDAnimal:'my-custom-guid-value'});
134
+ tmpQuery.query.schema = _AnimalSchema;
135
+ tmpQuery.query.UUID = 'should-not-use-this';
136
+ tmpQuery.buildCreateQuery();
137
+ var tmpOp = JSON.parse(tmpQuery.query.body);
138
+ Expect(tmpOp.document.GUIDAnimal).to.equal('my-custom-guid-value');
139
+ }
140
+ );
141
+ test
142
+ (
143
+ 'Bad Create Query',
144
+ function()
145
+ {
146
+ var tmpQuery = libFoxHound.new(_Fable).setDialect('Solr');
147
+ tmpQuery.buildCreateQuery();
148
+ tmpQuery.addRecord({});
149
+ tmpQuery.buildCreateQuery();
150
+ _Fable.log.trace('Create Query', tmpQuery.query);
151
+ Expect(tmpQuery.query.body)
152
+ .to.equal(false);
153
+ }
154
+ );
155
+ test
156
+ (
157
+ 'Read Query',
158
+ function()
159
+ {
160
+ var tmpQuery = libFoxHound.new(_Fable).setDialect('Solr').setScope('Animal');
161
+ tmpQuery.buildReadQuery();
162
+ _Fable.log.trace('Simple Select Query', tmpQuery.query);
163
+ var tmpOp = JSON.parse(tmpQuery.query.body);
164
+ Expect(tmpOp.collection).to.equal('Animal');
165
+ Expect(tmpOp.operation).to.equal('search');
166
+ Expect(tmpOp.query).to.equal('*:*');
167
+ }
168
+ );
169
+ test
170
+ (
171
+ 'Read Query with Sort',
172
+ function()
173
+ {
174
+ var tmpQuery = libFoxHound.new(_Fable).setDialect('Solr').setScope('Animal');
175
+ tmpQuery.addSort({Column:'Cost',Direction:'Descending'});
176
+ tmpQuery.buildReadQuery();
177
+ var tmpOp = JSON.parse(tmpQuery.query.body);
178
+ Expect(tmpOp.sort).to.equal('Cost desc');
179
+ }
180
+ );
181
+ test
182
+ (
183
+ 'Read Query with Distinct',
184
+ function()
185
+ {
186
+ var tmpQuery = libFoxHound.new(_Fable).setDialect('Solr').setScope('Animal');
187
+ tmpQuery.addSort({Column:'Cost',Direction:'Descending'})
188
+ .setDistinct(true);
189
+ tmpQuery.buildReadQuery();
190
+ var tmpOp = JSON.parse(tmpQuery.query.body);
191
+ Expect(tmpOp.distinct).to.equal(true);
192
+ }
193
+ );
194
+ test
195
+ (
196
+ 'Complex Read Query with cap, begin, dataElements, sort, filter',
197
+ function()
198
+ {
199
+ var tmpQuery = libFoxHound.new(_Fable)
200
+ .setDialect('Solr')
201
+ .setScope('Animal')
202
+ .setCap(10)
203
+ .setBegin(5)
204
+ .setDataElements(['Name', 'Age', 'Cost'])
205
+ .setSort([{Column:'Age',Direction:'Ascending'}])
206
+ .setFilter({Column:'Age',Operator:'=',Value:'15',Connector:'AND',Parameter:'Age'});
207
+ tmpQuery.addSort('Cost');
208
+ tmpQuery.buildReadQuery();
209
+ _Fable.log.trace('Select Query', tmpQuery.query);
210
+ var tmpOp = JSON.parse(tmpQuery.query.body);
211
+ Expect(tmpOp.rows).to.equal(10);
212
+ Expect(tmpOp.start).to.equal(5);
213
+ Expect(tmpOp.sort).to.contain('Age asc');
214
+ Expect(tmpOp.sort).to.contain('Cost asc');
215
+ Expect(tmpOp.filterQuery).to.contain('Age:"15"');
216
+ Expect(tmpOp.fields).to.contain('Name');
217
+ Expect(tmpOp.fields).to.contain('Age');
218
+ Expect(tmpOp.fields).to.contain('Cost');
219
+ }
220
+ );
221
+ test
222
+ (
223
+ 'Complex Read Query with Filters including OR, IN, IS NOT NULL',
224
+ function()
225
+ {
226
+ var tmpQuery = libFoxHound.new(_Fable)
227
+ .setDialect('Solr')
228
+ .setScope('Animal')
229
+ .setDataElements(['Name', 'Age', 'Cost'])
230
+ .setCap(100)
231
+ .addFilter('Age', '25')
232
+ .addFilter('', '', '(')
233
+ .addFilter('Color', 'Red')
234
+ .addFilter('Color', 'Green', '=', 'OR')
235
+ .addFilter('', '', ')')
236
+ .addFilter('Description', '', 'IS NOT NULL')
237
+ .addFilter('IDOffice', [10, 11, 15, 18, 22], 'IN');
238
+ tmpQuery.buildReadQuery();
239
+ _Fable.log.trace('Select Query', tmpQuery.query);
240
+ var tmpOp = JSON.parse(tmpQuery.query.body);
241
+ // Should have filter query
242
+ Expect(tmpOp.filterQuery).to.contain('Age:"25"');
243
+ // OR group in parentheses
244
+ Expect(tmpOp.filterQuery).to.contain('(Color:"Red" OR Color:"Green")');
245
+ // IS NOT NULL
246
+ Expect(tmpOp.filterQuery).to.contain('Description:[* TO *]');
247
+ // IN
248
+ Expect(tmpOp.filterQuery).to.contain('IDOffice:(10 OR 11 OR 15 OR 18 OR 22)');
249
+ }
250
+ );
251
+ test
252
+ (
253
+ 'Read Query with Deleted schema auto-adds Deleted filter',
254
+ function()
255
+ {
256
+ var tmpQuery = libFoxHound.new(_Fable)
257
+ .setDialect('Solr')
258
+ .setScope('Animal')
259
+ .addFilter('Age', '3');
260
+ tmpQuery.query.schema = _AnimalSchema;
261
+ tmpQuery.buildReadQuery();
262
+ var tmpOp = JSON.parse(tmpQuery.query.body);
263
+ Expect(tmpOp.filterQuery).to.contain('Deleted:0');
264
+ }
265
+ );
266
+ test
267
+ (
268
+ 'Read Query with Deleted tracking disabled does NOT add Deleted filter',
269
+ function()
270
+ {
271
+ var tmpQuery = libFoxHound.new(_Fable)
272
+ .setDialect('Solr')
273
+ .setScope('Animal')
274
+ .addFilter('Age', '3');
275
+ tmpQuery.query.schema = _AnimalSchema;
276
+ tmpQuery.query.disableDeleteTracking = true;
277
+ tmpQuery.buildReadQuery();
278
+ var tmpOp = JSON.parse(tmpQuery.query.body);
279
+ Expect(tmpOp.filterQuery).to.not.contain('Deleted:0');
280
+ }
281
+ );
282
+ test
283
+ (
284
+ 'Read Query without limit has no rows or start',
285
+ function()
286
+ {
287
+ var tmpQuery = libFoxHound.new(_Fable)
288
+ .setDialect('Solr')
289
+ .setScope('Animal');
290
+ tmpQuery.buildReadQuery();
291
+ var tmpOp = JSON.parse(tmpQuery.query.body);
292
+ Expect(tmpOp).to.not.have.property('rows');
293
+ Expect(tmpOp).to.not.have.property('start');
294
+ }
295
+ );
296
+ test
297
+ (
298
+ 'Update Query',
299
+ function()
300
+ {
301
+ var tmpQuery = libFoxHound.new(_Fable)
302
+ .setDialect('Solr')
303
+ .setScope('Animal')
304
+ .addFilter('IDAnimal', 9)
305
+ .addRecord({Name:'Froggy', Age:12});
306
+ tmpQuery.buildUpdateQuery();
307
+ _Fable.log.trace('Update Query', tmpQuery.query);
308
+ var tmpOp = JSON.parse(tmpQuery.query.body);
309
+ Expect(tmpOp.collection).to.equal('Animal');
310
+ Expect(tmpOp.operation).to.equal('atomicUpdate');
311
+ Expect(tmpOp.filterQuery).to.contain('IDAnimal:9');
312
+ Expect(tmpOp.update.Name).to.deep.equal({ 'set': 'Froggy' });
313
+ Expect(tmpOp.update.Age).to.deep.equal({ 'set': 12 });
314
+ }
315
+ );
316
+ test
317
+ (
318
+ 'Update Query with Schema skips identity and create columns',
319
+ function()
320
+ {
321
+ var tmpQuery = libFoxHound.new(_Fable)
322
+ .setDialect('Solr')
323
+ .setScope('Animal')
324
+ .addFilter('IDAnimal', 9)
325
+ .addRecord({IDAnimal:9, CreateDate:'2020-01-01', CreatingIDUser:1, UpdateDate:null, UpdatingIDUser:null, Name:'Froggy', Age:12});
326
+ tmpQuery.query.schema = _AnimalSchema;
327
+ tmpQuery.query.IDUser = 37;
328
+ tmpQuery.buildUpdateQuery();
329
+ var tmpOp = JSON.parse(tmpQuery.query.body);
330
+ Expect(tmpOp.update).to.not.have.property('IDAnimal');
331
+ Expect(tmpOp.update).to.not.have.property('CreateDate');
332
+ Expect(tmpOp.update).to.not.have.property('CreatingIDUser');
333
+ Expect(tmpOp.update.UpdateDate).to.deep.equal({ 'set': '$$NOW' });
334
+ Expect(tmpOp.update.UpdatingIDUser).to.deep.equal({ 'set': 37 });
335
+ Expect(tmpOp.update.Name).to.deep.equal({ 'set': 'Froggy' });
336
+ }
337
+ );
338
+ test
339
+ (
340
+ 'Count Query',
341
+ function()
342
+ {
343
+ var tmpQuery = libFoxHound.new(_Fable)
344
+ .setDialect('Solr')
345
+ .setScope('Animal')
346
+ .addFilter('Age', '3');
347
+ tmpQuery.buildCountQuery();
348
+ _Fable.log.trace('Count Query', tmpQuery.query);
349
+ var tmpOp = JSON.parse(tmpQuery.query.body);
350
+ Expect(tmpOp.collection).to.equal('Animal');
351
+ Expect(tmpOp.operation).to.equal('search');
352
+ Expect(tmpOp.isCount).to.equal(true);
353
+ Expect(tmpOp.rows).to.equal(0);
354
+ Expect(tmpOp.filterQuery).to.contain('Age:"3"');
355
+ }
356
+ );
357
+ test
358
+ (
359
+ 'Delete Query with soft delete schema',
360
+ function()
361
+ {
362
+ var tmpQuery = libFoxHound.new(_Fable)
363
+ .setDialect('Solr')
364
+ .setScope('Animal')
365
+ .addFilter('IDAnimal', 9);
366
+ tmpQuery.query.schema = _AnimalSchema;
367
+ tmpQuery.query.IDUser = 37;
368
+ tmpQuery.buildDeleteQuery();
369
+ _Fable.log.trace('Delete Query', tmpQuery.query);
370
+ var tmpOp = JSON.parse(tmpQuery.query.body);
371
+ Expect(tmpOp.operation).to.equal('atomicUpdate');
372
+ Expect(tmpOp.filterQuery).to.contain('IDAnimal:9');
373
+ Expect(tmpOp.filterQuery).to.contain('Deleted:0');
374
+ Expect(tmpOp.update.Deleted).to.deep.equal({ 'set': 1 });
375
+ Expect(tmpOp.update.DeleteDate).to.deep.equal({ 'set': '$$NOW' });
376
+ Expect(tmpOp.update.DeletingIDUser).to.deep.equal({ 'set': 37 });
377
+ }
378
+ );
379
+ test
380
+ (
381
+ 'Delete Query without schema does hard delete',
382
+ function()
383
+ {
384
+ var tmpQuery = libFoxHound.new(_Fable)
385
+ .setDialect('Solr')
386
+ .setScope('Animal')
387
+ .addFilter('IDAnimal', 9);
388
+ tmpQuery.buildDeleteQuery();
389
+ _Fable.log.trace('Delete Query', tmpQuery.query);
390
+ var tmpOp = JSON.parse(tmpQuery.query.body);
391
+ Expect(tmpOp.operation).to.equal('deleteByQuery');
392
+ Expect(tmpOp.filterQuery).to.contain('IDAnimal:9');
393
+ }
394
+ );
395
+ test
396
+ (
397
+ 'Undelete Query',
398
+ function()
399
+ {
400
+ var tmpQuery = libFoxHound.new(_Fable)
401
+ .setDialect('Solr')
402
+ .setScope('Animal')
403
+ .addFilter('IDAnimal', 9);
404
+ tmpQuery.query.schema = _AnimalSchema;
405
+ tmpQuery.query.IDUser = 37;
406
+ tmpQuery.buildUndeleteQuery();
407
+ _Fable.log.trace('Undelete Query', tmpQuery.query);
408
+ var tmpOp = JSON.parse(tmpQuery.query.body);
409
+ Expect(tmpOp.operation).to.equal('atomicUpdate');
410
+ Expect(tmpOp.update.Deleted).to.deep.equal({ 'set': 0 });
411
+ Expect(tmpOp.update.UpdateDate).to.deep.equal({ 'set': '$$NOW' });
412
+ Expect(tmpOp.update.UpdatingIDUser).to.deep.equal({ 'set': 37 });
413
+ }
414
+ );
415
+ test
416
+ (
417
+ 'Undelete Query without Deleted column returns noop',
418
+ function()
419
+ {
420
+ var tmpQuery = libFoxHound.new(_Fable)
421
+ .setDialect('Solr')
422
+ .setScope('Animal')
423
+ .addFilter('IDAnimal', 9);
424
+ tmpQuery.query.schema = _AnimalSchemaWithoutDeleted;
425
+ tmpQuery.buildUndeleteQuery();
426
+ var tmpOp = JSON.parse(tmpQuery.query.body);
427
+ Expect(tmpOp.operation).to.equal('noop');
428
+ }
429
+ );
430
+ test
431
+ (
432
+ 'solrOperation is stored in query.parameters',
433
+ function()
434
+ {
435
+ var tmpQuery = libFoxHound.new(_Fable)
436
+ .setDialect('Solr')
437
+ .setScope('Animal');
438
+ tmpQuery.buildReadQuery();
439
+ Expect(tmpQuery.query.parameters.solrOperation).to.be.an('object');
440
+ Expect(tmpQuery.query.parameters.solrOperation.collection).to.equal('Animal');
441
+ Expect(tmpQuery.query.parameters.solrOperation.operation).to.equal('search');
442
+ }
443
+ );
444
+ test
445
+ (
446
+ 'Filter with comparison operators',
447
+ function()
448
+ {
449
+ var tmpQuery = libFoxHound.new(_Fable)
450
+ .setDialect('Solr')
451
+ .setScope('Animal')
452
+ .addFilter('Age', 10, '>')
453
+ .addFilter('Cost', 100, '<=');
454
+ tmpQuery.buildReadQuery();
455
+ var tmpOp = JSON.parse(tmpQuery.query.body);
456
+ Expect(tmpOp.filterQuery).to.contain('Age:{10 TO *}');
457
+ Expect(tmpOp.filterQuery).to.contain('Cost:[* TO 100]');
458
+ }
459
+ );
460
+ test
461
+ (
462
+ 'Filter with >= and < operators',
463
+ function()
464
+ {
465
+ var tmpQuery = libFoxHound.new(_Fable)
466
+ .setDialect('Solr')
467
+ .setScope('Animal')
468
+ .addFilter('Age', 10, '>=')
469
+ .addFilter('Cost', 50, '<');
470
+ tmpQuery.buildReadQuery();
471
+ var tmpOp = JSON.parse(tmpQuery.query.body);
472
+ Expect(tmpOp.filterQuery).to.contain('Age:[10 TO *]');
473
+ Expect(tmpOp.filterQuery).to.contain('Cost:{* TO 50}');
474
+ }
475
+ );
476
+ test
477
+ (
478
+ 'Filter with LIKE operator converts to wildcard',
479
+ function()
480
+ {
481
+ var tmpQuery = libFoxHound.new(_Fable)
482
+ .setDialect('Solr')
483
+ .setScope('Animal')
484
+ .addFilter('Name', '%Foo%', 'LIKE');
485
+ tmpQuery.buildReadQuery();
486
+ var tmpOp = JSON.parse(tmpQuery.query.body);
487
+ Expect(tmpOp.filterQuery).to.contain('Name:*Foo*');
488
+ }
489
+ );
490
+ test
491
+ (
492
+ 'Filter with IS NULL',
493
+ function()
494
+ {
495
+ var tmpQuery = libFoxHound.new(_Fable)
496
+ .setDialect('Solr')
497
+ .setScope('Animal')
498
+ .addFilter('Description', '', 'IS NULL');
499
+ tmpQuery.buildReadQuery();
500
+ var tmpOp = JSON.parse(tmpQuery.query.body);
501
+ Expect(tmpOp.filterQuery).to.contain('(*:* NOT Description:[* TO *])');
502
+ }
503
+ );
504
+ test
505
+ (
506
+ 'Filter with != operator',
507
+ function()
508
+ {
509
+ var tmpQuery = libFoxHound.new(_Fable)
510
+ .setDialect('Solr')
511
+ .setScope('Animal')
512
+ .addFilter('Name', 'Cat', '!=');
513
+ tmpQuery.buildReadQuery();
514
+ var tmpOp = JSON.parse(tmpQuery.query.body);
515
+ Expect(tmpOp.filterQuery).to.contain('(*:* NOT Name:"Cat")');
516
+ }
517
+ );
518
+ test
519
+ (
520
+ 'Filter with NOT IN operator',
521
+ function()
522
+ {
523
+ var tmpQuery = libFoxHound.new(_Fable)
524
+ .setDialect('Solr')
525
+ .setScope('Animal')
526
+ .addFilter('IDOffice', [1, 2, 3], 'NOT IN');
527
+ tmpQuery.buildReadQuery();
528
+ var tmpOp = JSON.parse(tmpQuery.query.body);
529
+ Expect(tmpOp.filterQuery).to.contain('(*:* NOT IDOffice:(1 OR 2 OR 3))');
530
+ }
531
+ );
532
+ test
533
+ (
534
+ 'Multiple sorts ascending and descending',
535
+ function()
536
+ {
537
+ var tmpQuery = libFoxHound.new(_Fable)
538
+ .setDialect('Solr')
539
+ .setScope('Animal')
540
+ .addSort({Column:'Name', Direction:'Ascending'})
541
+ .addSort({Column:'Age', Direction:'Descending'});
542
+ tmpQuery.buildReadQuery();
543
+ var tmpOp = JSON.parse(tmpQuery.query.body);
544
+ Expect(tmpOp.sort).to.contain('Name asc');
545
+ Expect(tmpOp.sort).to.contain('Age desc');
546
+ }
547
+ );
548
+ }
549
+ );
550
+ }
551
+ );