@rws-framework/db 1.0.1

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,680 @@
1
+ import {DBService} from '../services/DBService';
2
+ import { IRWSModel } from '../types/IRWSModel';
3
+ import { IDbConfigHandler } from '../types/DbConfigHandler';
4
+
5
+ import {TrackType, IMetaOpts } from '../decorators';
6
+ import { FieldsHelper } from '../helper/FieldsHelper';
7
+ import { FindByType } from '../types/FindParams';
8
+
9
+ interface IModel{
10
+ [key: string]: any;
11
+ id: string | null;
12
+ save: () => void;
13
+ getCollection: () => string | null;
14
+ configService?: IDbConfigHandler;
15
+ dbService?: DBService;
16
+ }
17
+
18
+ interface IRWSModelServices {
19
+ configService?: IDbConfigHandler
20
+ dbService?: DBService
21
+ }
22
+
23
+ type RelationBindType = {
24
+ connect: { id: string }
25
+ };
26
+
27
+ type RelOneMetaType<T extends IRWSModel> = {[key: string]: {required: boolean, key?: string, model: OpModelType<T>, hydrationField: string, foreignKey: string}};
28
+ type RelManyMetaType<T extends IRWSModel> = {[key: string]: {key: string, inversionModel: OpModelType<T>, foreignKey: string}};
29
+
30
+ export interface OpModelType<ChildClass> {
31
+ new(data?: any | null): ChildClass;
32
+ name: string
33
+ _collection: string;
34
+ _RELATIONS: {[key: string]: boolean}
35
+ _CUT_KEYS: string[]
36
+ allModels: OpModelType<any>[];
37
+ loadModels: () => OpModelType<any>[];
38
+ checkForInclusionWithThrow: (className: string) => void;
39
+ checkForInclusion: (className: string) => boolean;
40
+ configService?: IDbConfigHandler;
41
+ dbService?: DBService;
42
+ findOneBy<T extends RWSModel<T>>(
43
+ this: OpModelType<T>,
44
+ findParams: FindByType
45
+ ): Promise<T | null>;
46
+ find<T extends RWSModel<T>>(
47
+ this: OpModelType<T>,
48
+ id: string,
49
+ findParams?: Omit<FindByType, 'conditions'>
50
+ ): Promise<T | null>;
51
+ findBy<T extends RWSModel<T>>(
52
+ this: OpModelType<T>,
53
+ findParams: FindByType
54
+ ): Promise<T[]>;
55
+ delete<ChildClass extends RWSModel<ChildClass>>(
56
+ this: OpModelType<ChildClass>,
57
+ conditions: any
58
+ ): Promise<void>
59
+ create<T extends RWSModel<T>>(this: OpModelType<T>, data: T): Promise<T>;
60
+ getRelationOneMeta(model: any, classFields: string[]): Promise<RelOneMetaType<IRWSModel>>;
61
+ getRelationManyMeta(model: any, classFields: string[]): Promise<RelManyMetaType<IRWSModel>>;
62
+ getCollection(): string;
63
+ }
64
+
65
+ class RWSModel<ChildClass> implements IModel{
66
+ static services: IRWSModelServices = {}
67
+
68
+ static configService: IDbConfigHandler;
69
+ static dbService: DBService
70
+
71
+ [key: string]: any;
72
+ @TrackType(String)
73
+ id: string;
74
+ static _collection: string = null;
75
+ static _RELATIONS = {};
76
+ static _BANNED_KEYS = ['_collection'];
77
+ static allModels: OpModelType<any>[] = [];
78
+ static _CUT_KEYS: string[] = [];
79
+
80
+ constructor(data: any) {
81
+ if(!this.getCollection()){
82
+ throw new Error('Model must have a collection defined');
83
+
84
+ }
85
+
86
+ this.dbService = RWSModel.dbService;
87
+ this.configService = RWSModel.configService;
88
+
89
+ if(!data){
90
+ return;
91
+ }
92
+
93
+ if(!this.hasTimeSeries()){
94
+ this._fill(data);
95
+ }else{
96
+ throw new Error('Time Series not supported in synchronous constructor. Use `await Model.create(data)` static method to instantiate this model.');
97
+ }
98
+ }
99
+
100
+ checkForInclusionWithThrow(): void
101
+ {
102
+ this.checkForInclusionWithThrow()
103
+ }
104
+
105
+ static checkForInclusionWithThrow(this: OpModelType<any>, checkModelType: string): void
106
+ {
107
+ if(!this.checkForInclusion(this.name)){
108
+ throw new Error('Model undefined: ' + this.name);
109
+ }
110
+ }
111
+
112
+ checkForInclusion(): boolean
113
+ {
114
+ return this.checkForInclusion();
115
+ }
116
+
117
+ static checkForInclusion(this: OpModelType<any>, checkModelType: string): boolean
118
+ {
119
+ return this.loadModels().find((definedModel: OpModelType<any>) => {
120
+ return definedModel.name === checkModelType
121
+ }) !== undefined
122
+ }
123
+
124
+ protected _fill(data: any): RWSModel<ChildClass>{
125
+ for (const key in data) {
126
+ if (data.hasOwnProperty(key)) {
127
+
128
+ const meta = Reflect.getMetadata(`InverseTimeSeries:${key}`, (this as any).constructor.prototype);
129
+
130
+ if(meta){
131
+ data[key] = {
132
+ create: data[key]
133
+ };
134
+ }else{
135
+ this[key] = data[key];
136
+ }
137
+ }
138
+ }
139
+
140
+ return this;
141
+ }
142
+
143
+ protected hasRelation(key: string): boolean
144
+ {
145
+ return !!this[key] && this[key] instanceof RWSModel;
146
+ }
147
+
148
+ protected bindRelation(key: string, relatedModel: RWSModel<any>): RelationBindType
149
+ {
150
+ return {
151
+ connect: {
152
+ id: relatedModel.id
153
+ }
154
+ };
155
+ }
156
+
157
+ public async _asyncFill(data: any, fullDataMode = false, allowRelations = true): Promise<ChildClass> {
158
+ const collections_to_models: {[key: string]: any} = {};
159
+ const timeSeriesIds = this.getTimeSeriesModelFields();
160
+
161
+ const classFields = FieldsHelper.getAllClassFields(this.constructor);
162
+
163
+ // Get both relation metadata types asynchronously
164
+ const [relOneData, relManyData] = await Promise.all([
165
+ this.getRelationOneMeta(classFields),
166
+ this.getRelationManyMeta(classFields)
167
+ ]);
168
+
169
+ this.loadModels().forEach((model) => {
170
+ collections_to_models[model.getCollection()] = model;
171
+ });
172
+
173
+ const seriesHydrationfields: string[] = [];
174
+
175
+ if (allowRelations) {
176
+ // Handle many-to-many relations
177
+ for (const key in relManyData) {
178
+ if(!fullDataMode && (this as any).constructor._CUT_KEYS.includes(key)){
179
+ continue;
180
+ }
181
+
182
+ const relMeta = relManyData[key];
183
+
184
+ const relationEnabled = this.checkRelEnabled(relMeta.key);
185
+ if (relationEnabled) {
186
+ this[relMeta.key] = await relMeta.inversionModel.findBy({
187
+ conditions: {
188
+ [relMeta.foreignKey]: data.id
189
+ },
190
+ allowRelations: false
191
+ });
192
+ }
193
+ }
194
+
195
+ // Handle one-to-one relations
196
+ for (const key in relOneData) {
197
+ if(!fullDataMode && (this as any).constructor._CUT_KEYS.includes(key)){
198
+ continue;
199
+ }
200
+
201
+ const relMeta = relOneData[key];
202
+ const relationEnabled = this.checkRelEnabled(relMeta.key);
203
+
204
+ if(!data[relMeta.hydrationField] && relMeta.required){
205
+ throw new Error(`Relation field "${relMeta.hydrationField}" is required in model ${this.constructor.name}.`)
206
+ }
207
+
208
+ if (relationEnabled && data[relMeta.hydrationField]) {
209
+ this[relMeta.key] = await relMeta.model.find(data[relMeta.hydrationField], { allowRelations: false });
210
+ }
211
+ else if(relationEnabled && !data[relMeta.hydrationField] && data[relMeta.key]){
212
+ const newRelModel: RWSModel<any> = await relMeta.model.create(data[relMeta.key]);
213
+ this[relMeta.key] = await newRelModel.save();
214
+ }
215
+
216
+ const cutKeys = ((this.constructor as any)._CUT_KEYS as string[]);
217
+
218
+ if(!cutKeys.includes(relMeta.hydrationField)){
219
+ cutKeys.push(relMeta.hydrationField)
220
+ }
221
+ }
222
+ }
223
+
224
+ // Process regular fields and time series
225
+ for (const key in data) {
226
+ if (data.hasOwnProperty(key)) {
227
+ if(!fullDataMode && (this as any).constructor._CUT_KEYS.includes(key)){
228
+ continue;
229
+ }
230
+
231
+ if (Object.keys(relOneData).includes(key)) {
232
+ continue;
233
+ }
234
+
235
+ if (seriesHydrationfields.includes(key)) {
236
+ continue;
237
+ }
238
+
239
+ const timeSeriesMetaData = timeSeriesIds[key];
240
+
241
+ if (timeSeriesMetaData) {
242
+ this[key] = data[key];
243
+ const seriesModel = collections_to_models[timeSeriesMetaData.collection];
244
+
245
+ const dataModels = await seriesModel.findBy({
246
+ id: { in: data[key] }
247
+ });
248
+
249
+ seriesHydrationfields.push(timeSeriesMetaData.hydrationField);
250
+
251
+ this[timeSeriesMetaData.hydrationField] = dataModels;
252
+ } else {
253
+ this[key] = data[key];
254
+ }
255
+ }
256
+ }
257
+
258
+ return this as any as ChildClass;
259
+ }
260
+
261
+ private getModelScalarFields(model: OpModelType<any>): string[]
262
+ {
263
+ return FieldsHelper.getAllClassFields(model)
264
+ .filter(item => item.indexOf('TrackType') === 0)
265
+ .map(item => item.split(':').at(-1))
266
+ }
267
+
268
+ private getTimeSeriesModelFields()
269
+ {
270
+ const timeSeriesIds: {[key: string]: {collection: string, hydrationField: string, ids: string[]}} = {};
271
+
272
+ for (const key in this as any) {
273
+ if (this.hasOwnProperty(key)) {
274
+
275
+ const meta = Reflect.getMetadata(`InverseTimeSeries:${key}`, (this as any));
276
+ if(meta){
277
+ if(!timeSeriesIds[key]){
278
+ timeSeriesIds[key] = {
279
+ collection: meta.timeSeriesModel,
280
+ hydrationField: meta.hydrationField,
281
+ ids: this[key]
282
+ };
283
+ }
284
+ }
285
+ }
286
+ }
287
+
288
+ return timeSeriesIds;
289
+ }
290
+
291
+ private async getRelationOneMeta(classFields: string[]): Promise<RelOneMetaType<RWSModel<any>>> {
292
+ return RWSModel.getRelationOneMeta(this, classFields);
293
+ }
294
+
295
+ static async getRelationOneMeta(model: any, classFields: string[]): Promise<RelOneMetaType<RWSModel<any>>>
296
+ {
297
+ const relIds: RelOneMetaType<RWSModel<any>> = {};
298
+ const relationFields = classFields
299
+ .filter((item: string) => item.indexOf('Relation') === 0 && !item.includes('Inverse'))
300
+ .map((item: string) => item.split(':').at(-1));
301
+
302
+ for (const key of relationFields) {
303
+ const metadataKey = `Relation:${key}`;
304
+ const metadata = Reflect.getMetadata(metadataKey, model);
305
+
306
+ if (metadata && metadata.promise) {
307
+ const resolvedMetadata = await metadata.promise;
308
+ if (!relIds[key]) {
309
+ relIds[key] = {
310
+ key: resolvedMetadata.key,
311
+ required: resolvedMetadata.required,
312
+ model: resolvedMetadata.relatedTo,
313
+ hydrationField: resolvedMetadata.relationField,
314
+ foreignKey: resolvedMetadata.relatedToField
315
+ };
316
+ }
317
+ }
318
+ }
319
+
320
+ return relIds;
321
+ }
322
+
323
+ private async getRelationManyMeta(classFields: string[]): Promise<RelManyMetaType<RWSModel<any>>> {
324
+ return RWSModel.getRelationManyMeta(this, classFields);
325
+ }
326
+
327
+ static async getRelationManyMeta(model: any, classFields: string[]): Promise<RelManyMetaType<RWSModel<any>>>
328
+ {
329
+ const relIds: RelManyMetaType<RWSModel<any>> = {};
330
+
331
+ const inverseFields = classFields
332
+ .filter((item: string) => item.indexOf('InverseRelation') === 0)
333
+ .map((item: string) => item.split(':').at(-1));
334
+
335
+ for (const key of inverseFields) {
336
+ const metadataKey = `InverseRelation:${key}`;
337
+ const metadata = Reflect.getMetadata(metadataKey, model);
338
+
339
+ if (metadata && metadata.promise) {
340
+ const resolvedMetadata = await metadata.promise;
341
+ if (!relIds[key]) {
342
+ relIds[key] = {
343
+ key: resolvedMetadata.key,
344
+ inversionModel: resolvedMetadata.inversionModel,
345
+ foreignKey: resolvedMetadata.foreignKey
346
+ };
347
+ }
348
+ }
349
+ }
350
+
351
+ return relIds;
352
+ }
353
+
354
+ public async toMongo(): Promise<any> {
355
+ const data: any = {};
356
+ const timeSeriesIds = this.getTimeSeriesModelFields();
357
+ const timeSeriesHydrationFields: string[] = [];
358
+
359
+ for (const key in (this as any)) {
360
+ if (this.hasRelation(key)) {
361
+ data[key] = this.bindRelation(key, this[key]);
362
+ continue;
363
+ }
364
+
365
+ if (!(await this.isDbVariable(key))) {
366
+ continue;
367
+ }
368
+
369
+ const passedFieldCondition: boolean = this.hasOwnProperty(key) &&
370
+ !((this as any).constructor._BANNED_KEYS
371
+ || RWSModel._BANNED_KEYS
372
+ ).includes(key) &&
373
+ !timeSeriesHydrationFields.includes(key)
374
+ ;
375
+
376
+ if (passedFieldCondition) {
377
+ data[key] = this[key];
378
+ }
379
+
380
+ if (timeSeriesIds[key]) {
381
+ data[key] = this[key];
382
+ timeSeriesHydrationFields.push(timeSeriesIds[key].hydrationField);
383
+ }
384
+ }
385
+
386
+ return data;
387
+ }
388
+
389
+ getCollection(): string | null {
390
+ return (this as any).constructor._collection || this._collection;
391
+ }
392
+
393
+ static getCollection(): string | null {
394
+ return (this as any).constructor._collection || this._collection;
395
+ }
396
+
397
+
398
+ async save(): Promise<this> {
399
+ const data = await this.toMongo();
400
+ let updatedModelData = data;
401
+ if (this.id) {
402
+ this.preUpdate();
403
+
404
+ updatedModelData = await this.dbService.update(data, this.getCollection());
405
+
406
+ await this._asyncFill(updatedModelData);
407
+ this.postUpdate();
408
+ } else {
409
+ this.preCreate();
410
+
411
+ const timeSeriesModel = await import('./TimeSeriesModel');
412
+ const isTimeSeries = this instanceof timeSeriesModel.default;
413
+
414
+ updatedModelData = await this.dbService.insert(data, this.getCollection(), isTimeSeries);
415
+
416
+ await this._asyncFill(updatedModelData);
417
+
418
+ this.postCreate();
419
+ }
420
+
421
+ return this;
422
+ }
423
+
424
+ static async getModelAnnotations<T extends unknown>(constructor: new () => T): Promise<Record<string, {annotationType: string, metadata: any}>> {
425
+ const annotationsData: Record<string, {annotationType: string, metadata: any}> = {};
426
+
427
+ const metadataKeys = Reflect.getMetadataKeys(constructor.prototype);
428
+
429
+ // Process all metadata keys and collect promises
430
+ const metadataPromises = metadataKeys.map(async (fullKey: string) => {
431
+ const [annotationType, propertyKey] = fullKey.split(':');
432
+ const metadata = Reflect.getMetadata(fullKey, constructor.prototype);
433
+
434
+ if (metadata) {
435
+ // If this is a relation metadata with a promise
436
+ if (metadata.promise && (annotationType === 'Relation' || annotationType === 'InverseRelation')) {
437
+ const resolvedMetadata = await metadata.promise;
438
+ annotationsData[propertyKey] = {
439
+ annotationType,
440
+ metadata: resolvedMetadata
441
+ };
442
+ } else {
443
+ // Handle non-relation metadata as before
444
+ const key = metadata.key || propertyKey;
445
+ annotationsData[key] = {
446
+ annotationType,
447
+ metadata
448
+ };
449
+ }
450
+ }
451
+ });
452
+
453
+ // Wait for all metadata to be processed
454
+ await Promise.all(metadataPromises);
455
+
456
+ return annotationsData;
457
+ }
458
+
459
+ public preUpdate(): void
460
+ {
461
+ return;
462
+ }
463
+
464
+ public postUpdate(): void
465
+ {
466
+ return;
467
+ }
468
+
469
+ public preCreate(): void
470
+ {
471
+ return;
472
+ }
473
+
474
+ public postCreate(): void
475
+ {
476
+ return;
477
+ }
478
+
479
+ public static isSubclass<T extends RWSModel<T>, C extends new () => T>(constructor: C, baseClass: new () => T): boolean {
480
+ return baseClass.prototype.isPrototypeOf(constructor.prototype);
481
+ }
482
+
483
+ hasTimeSeries(): boolean
484
+ {
485
+ return RWSModel.checkTimeSeries((this as any).constructor);
486
+ }
487
+
488
+ static checkTimeSeries(constructor: any): boolean
489
+ {
490
+ const data = constructor.prototype as any;
491
+
492
+ for (const key in data) {
493
+
494
+ if (data.hasOwnProperty(key)) {
495
+
496
+ if(Reflect.getMetadata(`InverseTimeSeries:${key}`, constructor.prototype)){
497
+ return true;
498
+ }
499
+ }
500
+ }
501
+
502
+ return false;
503
+ }
504
+
505
+ async isDbVariable(variable: string): Promise<boolean>
506
+ {
507
+ return RWSModel.checkDbVariable((this as any).constructor, variable);
508
+ }
509
+
510
+ static async checkDbVariable(constructor: any, variable: string): Promise<boolean> {
511
+ if(variable === 'id'){
512
+ return true;
513
+ }
514
+
515
+ const dbAnnotations = await RWSModel.getModelAnnotations(constructor);
516
+ type AnnotationType = { annotationType: string, key: string };
517
+
518
+ const dbProperties: string[] = Object.keys(dbAnnotations)
519
+ .map((key: string): AnnotationType => {return {...dbAnnotations[key], key};})
520
+ .filter((element: AnnotationType) => element.annotationType === 'TrackType' )
521
+ .map((element: AnnotationType) => element.key);
522
+
523
+ return dbProperties.includes(variable);
524
+ }
525
+
526
+ sanitizeDBData(data: any): any
527
+ {
528
+ const dataKeys = Object.keys(data);
529
+ const sanitizedData: {[key: string]: any} = {};
530
+
531
+ for (const key of dataKeys){
532
+ if(this.isDbVariable(key)){
533
+ sanitizedData[key] = data[key];
534
+ }
535
+ }
536
+
537
+ return sanitizedData;
538
+ }
539
+
540
+ public static async watchCollection<ChildClass extends RWSModel<ChildClass>>(
541
+ this: OpModelType<ChildClass>,
542
+ preRun: () => void
543
+ ){
544
+ const collection = Reflect.get(this, '_collection');
545
+ this.checkForInclusionWithThrow(this.name);
546
+ return await this.dbService.watchCollection(collection, preRun);
547
+ }
548
+
549
+ public static async findOneBy<ChildClass extends RWSModel<ChildClass>>(
550
+ this: OpModelType<ChildClass>,
551
+ findParams?: FindByType
552
+ ): Promise<ChildClass | null> {
553
+ const conditions = findParams?.conditions ?? {};
554
+ const ordering = findParams?.ordering ?? null;
555
+ const fields = findParams?.fields ?? null;
556
+ const allowRelations = findParams?.allowRelations ?? true;
557
+ const fullData = findParams?.fullData ?? false;
558
+
559
+ this.checkForInclusionWithThrow('');
560
+
561
+
562
+ const collection = Reflect.get(this, '_collection');
563
+ const dbData = await this.dbService.findOneBy(collection, conditions, fields, ordering, allowRelations);
564
+
565
+
566
+ if (dbData) {
567
+ const inst: ChildClass = new (this as { new(): ChildClass })();
568
+ return await inst._asyncFill(dbData, fullData, allowRelations);
569
+ }
570
+
571
+ return null;
572
+ }
573
+
574
+ public static async find<ChildClass extends RWSModel<ChildClass>>(
575
+ this: OpModelType<ChildClass>,
576
+ id: string,
577
+ findParams: Omit<FindByType, 'conditions'> = null
578
+ ): Promise<ChildClass | null> {
579
+ const ordering = findParams?.ordering ?? null;
580
+ const fields = findParams?.fields ?? null;
581
+ const allowRelations = findParams?.allowRelations ?? true;
582
+ const fullData = findParams?.fullData ?? false;
583
+
584
+ const collection = Reflect.get(this, '_collection');
585
+ this.checkForInclusionWithThrow(this.name);
586
+
587
+ const dbData = await this.dbService.findOneBy(collection, { id }, fields, ordering, allowRelations);
588
+
589
+ if (dbData) {
590
+ const inst: ChildClass = new (this as { new(): ChildClass })();
591
+ return await inst._asyncFill(dbData, fullData, allowRelations);
592
+ }
593
+
594
+ return null;
595
+ }
596
+
597
+ public static async findBy<ChildClass extends RWSModel<ChildClass>>(
598
+ this: OpModelType<ChildClass>,
599
+ findParams?: FindByType
600
+ ): Promise<ChildClass[]> {
601
+ const conditions = findParams?.conditions ?? {};
602
+ const ordering = findParams?.ordering ?? null;
603
+ const fields = findParams?.fields ?? null;
604
+ const allowRelations = findParams?.allowRelations ?? true;
605
+ const fullData = findParams?.fullData ?? false;
606
+
607
+ const collection = Reflect.get(this, '_collection');
608
+ this.checkForInclusionWithThrow(this.name);
609
+ try {
610
+ const dbData = await this.dbService.findBy(collection, conditions, fields, ordering, allowRelations);
611
+ if (dbData.length) {
612
+ const instanced: ChildClass[] = [];
613
+
614
+ for (const data of dbData) {
615
+ const inst: ChildClass = new (this as { new(): ChildClass })();
616
+ instanced.push((await inst._asyncFill(data, fullData,allowRelations)) as ChildClass);
617
+ }
618
+
619
+ return instanced;
620
+ }
621
+
622
+ return [];
623
+ } catch (rwsError: Error | any) {
624
+ console.error(rwsError);
625
+
626
+ throw rwsError;
627
+ }
628
+ }
629
+
630
+ public static async delete<ChildClass extends RWSModel<ChildClass>>(
631
+ this: OpModelType<ChildClass>,
632
+ conditions: any
633
+ ): Promise<void> {
634
+ const collection = Reflect.get(this, '_collection');
635
+ this.checkForInclusionWithThrow(this.name);
636
+ return await this.dbService.delete(collection, conditions);
637
+ }
638
+
639
+ public async delete<ChildClass extends RWSModel<ChildClass>>(): Promise<void> {
640
+ const collection = Reflect.get(this, '_collection');
641
+ this.checkForInclusionWithThrow();
642
+ return await this.dbService.delete(collection, {
643
+ id: this.id
644
+ });
645
+ }
646
+
647
+
648
+ static async create<T extends RWSModel<T>>(this: new () => T, data: any): Promise<T> {
649
+ const newModel = new this();
650
+
651
+ const sanitizedData = newModel.sanitizeDBData(data);
652
+
653
+ await newModel._asyncFill(sanitizedData);
654
+
655
+ return newModel;
656
+ }
657
+
658
+ static loadModels(): OpModelType<any>[]
659
+ {
660
+ return RWSModel.allModels;
661
+ }
662
+
663
+ loadModels(): OpModelType<any>[]
664
+ {
665
+ return RWSModel.loadModels();
666
+ }
667
+
668
+ private checkRelEnabled(key: string): boolean
669
+ {
670
+ return Object.keys((this as any).constructor._RELATIONS).includes(key) && (this as any).constructor._RELATIONS[key] === true
671
+ }
672
+
673
+ public static setServices(services: IRWSModelServices){
674
+ RWSModel.services = {...RWSModel.services, ...services};
675
+ }
676
+ }
677
+
678
+ export { IModel, TrackType, IMetaOpts, RWSModel };
679
+
680
+