• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

panates / opra / 25956913535

16 May 2026 08:06AM UTC coverage: 83.031% (+0.004%) from 83.027%
25956913535

push

github

erayhanoglu
1.28.4

3730 of 4808 branches covered (77.58%)

Branch coverage included in aggregate %.

33693 of 40263 relevant lines covered (83.68%)

222.65 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

85.59
/packages/common/src/document/data-type/complex-type-base.ts
1
import hashObject from 'object-hash';
1✔
2
import { asMutable, type StrictOmit, type Type } from 'ts-gems';
1✔
3
import { type Validator, validator, vg } from 'valgen';
1✔
4
import {
1✔
5
  FieldsProjection,
1✔
6
  parseFieldsProjection,
1✔
7
  ResponsiveMap,
1✔
8
} from '../../helpers/index.js';
1✔
9
import { OpraSchema } from '../../schema/index.js';
1✔
10
import type { DocumentElement } from '../common/document-element.js';
1✔
11
import { DocumentInitContext } from '../common/document-init-context.js';
1✔
12
import type { ApiField } from './api-field.js';
1✔
13
import type { ComplexType } from './complex-type.js';
1✔
14
import { DataType } from './data-type.js';
1✔
15

1✔
16
export const FIELD_PATH_PATTERN = /^([+-])?([a-z$_][\w.]*)$/i;
1✔
17

1✔
18
/**
1✔
19
 * Type definition of class constructor for ComplexTypeBase
1✔
20
 * @class ComplexTypeBase
1✔
21
 */
1✔
22
interface ComplexTypeBaseStatic {
1✔
23
  /**
1✔
24
   * Class constructor of MappedType
1✔
25
   *
1✔
26
   * @param owner
1✔
27
   * @param initArgs
1✔
28
   * @param context
1✔
29
   * @constructor
1✔
30
   */
1✔
31
  new (
1✔
32
    owner: DocumentElement,
1✔
33
    initArgs: DataType.InitArguments,
1✔
34
    context?: DocumentInitContext,
1✔
35
  ): ComplexTypeBase;
1✔
36

1✔
37
  prototype: ComplexTypeBase;
1✔
38
}
1✔
39

1✔
40
/**
1✔
41
 * Type definition of ComplexTypeBase prototype
1✔
42
 * @interface ComplexTypeBase
1✔
43
 */
1✔
44
export interface ComplexTypeBase extends ComplexTypeBaseClass {}
1✔
45

1✔
46
/**
1✔
47
 *
1✔
48
 * @constructor
1✔
49
 */
1✔
50
export const ComplexTypeBase = function (
1✔
51
  this: ComplexTypeBase | void,
636✔
52
  ...args: any[]
636✔
53
) {
636✔
54
  if (!this)
636✔
55
    throw new TypeError('"this" should be passed to call class constructor');
636!
56
  // Constructor
636✔
57
  const [owner, initArgs, context] = args as [
636✔
58
    DocumentElement,
636✔
59
    ComplexType.InitArguments,
636✔
60
    DocumentInitContext | undefined,
636✔
61
  ];
636✔
62
  DataType.call(this, owner, initArgs, context);
636✔
63
  const _this = asMutable(this);
636✔
64
  (_this as any)._fields = new ResponsiveMap();
636✔
65
} as Function as ComplexTypeBaseStatic;
636✔
66

1✔
67
/**
1✔
68
 *
1✔
69
 */
1✔
70
abstract class ComplexTypeBaseClass extends DataType {
1✔
71
  readonly ctor?: Type;
1!
72
  declare protected _fields: ResponsiveMap<ApiField>;
×
73
  readonly additionalFields?:
×
74
    | boolean
×
75
    | DataType
×
76
    | ['error']
×
77
    | ['error', string];
×
78
  readonly keyField?: OpraSchema.Field.Name;
×
79

1✔
80
  fieldCount(scope?: string): number {
1✔
81
    if (scope === '*') return this._fields.size;
1!
82
    let count = 0;
1✔
83
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
1✔
84
    for (const i of this.fields(scope)) count++;
1✔
85
    return count;
1✔
86
  }
1✔
87

1✔
88
  fieldEntries(scope?: string): IterableIterator<[string, ApiField]> {
1✔
89
    let iterator: IterableIterator<[string, ApiField]> | undefined =
1,026✔
90
      this._fields.entries();
1,026✔
91
    if (scope === '*') return iterator;
1,026✔
92
    let r: IteratorResult<[string, ApiField]>;
247✔
93
    return {
247✔
94
      next() {
247✔
95
        while (iterator) {
3,723✔
96
          r = iterator.next();
3,833✔
97
          if (r.done) break;
3,833✔
98
          if (r.value && r.value[1].inScope(scope)) break;
3,833✔
99
        }
3,833✔
100
        if (r.done) return { done: r.done, value: undefined };
3,723✔
101
        return {
3,476✔
102
          done: r.done,
3,476✔
103
          value: [r.value[0], r.value[1].forScope(scope)],
3,476✔
104
        };
3,476✔
105
      },
3,476✔
106
      return(value?: [string, ApiField]) {
247✔
107
        iterator = undefined;
×
108
        return { done: true, value };
×
109
      },
×
110
      [Symbol.iterator]() {
247✔
111
        return this;
×
112
      },
×
113
    };
247✔
114
  }
247✔
115

1✔
116
  fields(scope?: string): IterableIterator<ApiField> {
1✔
117
    let iterator: IterableIterator<[string, ApiField]> | undefined =
963✔
118
      this.fieldEntries(scope);
963✔
119
    let r: IteratorResult<[string, ApiField]>;
963✔
120
    return {
963✔
121
      next() {
963✔
122
        if (!iterator) return { done: true, value: undefined };
9,944!
123
        r = iterator!.next();
9,944✔
124
        return { done: r.done, value: r.value?.[1] };
9,944✔
125
      },
9,944✔
126
      return(value?: ApiField) {
963✔
127
        iterator = undefined;
×
128
        return { done: true, value };
×
129
      },
×
130
      [Symbol.iterator]() {
963✔
131
        return this;
963✔
132
      },
963✔
133
    };
963✔
134
  }
963✔
135

1✔
136
  fieldNames(scope?: string): IterableIterator<string> {
1✔
137
    if (scope === '*') return this._fields.keys();
12!
138
    let iterator: IterableIterator<[string, ApiField]> | undefined =
12✔
139
      this.fieldEntries(scope);
12✔
140
    let r: IteratorResult<[string, ApiField]>;
12✔
141
    return {
12✔
142
      next() {
12✔
143
        if (!iterator) return { done: true, value: undefined };
34!
144
        r = iterator!.next();
34✔
145
        return { done: r.done, value: r.value?.[0] };
34✔
146
      },
34✔
147
      return(value?: string) {
12✔
148
        iterator = undefined;
×
149
        return { done: true, value };
×
150
      },
×
151
      [Symbol.iterator]() {
12✔
152
        return this;
12✔
153
      },
12✔
154
    };
12✔
155
  }
12✔
156

1✔
157
  /**
1✔
158
   *
1✔
159
   */
1✔
160
  findField(nameOrPath: string, scope?: string | '*'): ApiField | undefined {
1✔
161
    if (nameOrPath.includes('.')) {
604✔
162
      const fieldPath = this.parseFieldPath(nameOrPath, { scope });
3✔
163
      if (fieldPath.length === 0)
3✔
164
        throw new Error(
3!
165
          `Field "${nameOrPath}" does not exist in scope "${scope}"`,
✔
166
        );
2✔
167
      const lastItem = fieldPath.pop();
2✔
168
      return lastItem?.field;
2✔
169
    }
3✔
170
    const field = this._fields.get(nameOrPath);
601✔
171
    if (field && field.inScope(scope)) return field.forScope(scope);
604✔
172
  }
604✔
173

1✔
174
  /**
1✔
175
   *
1✔
176
   */
1✔
177
  getField(nameOrPath: string, scope?: string): ApiField {
1✔
178
    const field = this.findField(nameOrPath, '*');
235✔
179
    if (field && !field.inScope(scope))
235✔
180
      throw new Error(
235!
181
        `Field "${nameOrPath}" does not exist in scope "${scope || 'null'}"`,
✔
182
      );
234✔
183
    if (!field) {
235✔
184
      throw new Error(`Field (${nameOrPath}) does not exist`);
1✔
185
    }
1✔
186
    return field.forScope(scope);
233✔
187
  }
233✔
188

1✔
189
  /**
1✔
190
   *
1✔
191
   */
1✔
192
  parseFieldPath(
1✔
193
    fieldPath: string,
206✔
194
    options?: {
206✔
195
      allowSigns?: 'first' | 'each';
206✔
196
      scope?: string | '*';
206✔
197
    },
206✔
198
  ): ComplexType.ParsedFieldPath[] {
206✔
199
    let dataType: DataType | undefined = this;
206✔
200
    let field: ApiField | undefined;
206✔
201
    const arr = fieldPath.split('.');
206✔
202
    const len = arr.length;
206✔
203
    const out: ComplexType.ParsedFieldPath[] = [];
206✔
204
    const objectType = this.owner.node.getDataType('object');
206✔
205
    const allowSigns = options?.allowSigns;
206✔
206
    const getStrPath = () => out.map(x => x.fieldName).join('.');
206✔
207

206✔
208
    for (let i = 0; i < len; i++) {
206✔
209
      const item: ComplexType.ParsedFieldPath = {
214✔
210
        fieldName: arr[i],
214✔
211
        dataType: objectType,
214✔
212
      };
214✔
213
      out.push(item);
214✔
214

214✔
215
      const m = FIELD_PATH_PATTERN.exec(arr[i]);
214✔
216
      if (!m) throw new TypeError(`Invalid field name (${getStrPath()})`);
214!
217
      if (m[1]) {
214✔
218
        if ((i === 0 && allowSigns === 'first') || allowSigns === 'each')
46✔
219
          item.sign = m[1] as any;
46✔
220
        item.fieldName = m[2];
46✔
221
      }
46✔
222

214✔
223
      if (dataType) {
214✔
224
        if (dataType instanceof ComplexTypeBase) {
214✔
225
          field = dataType.findField(item.fieldName, options?.scope);
213✔
226
          if (field) {
213✔
227
            item.fieldName = field.name;
211✔
228
            item.field = field;
211✔
229
            item.dataType = field.type;
211✔
230
            dataType = field.type;
211✔
231
            continue;
211✔
232
          }
211✔
233
          if (dataType.additionalFields?.[0] === true) {
213!
234
            item.additionalField = true;
×
235
            item.dataType = objectType;
×
236
            dataType = undefined;
×
237
            continue;
×
238
          }
✔
239
          if (
2✔
240
            dataType.additionalFields?.[0] === 'type' &&
213!
241
            dataType.additionalFields?.[1] instanceof DataType
×
242
          ) {
213!
243
            item.additionalField = true;
×
244
            item.dataType = dataType.additionalFields[1];
×
245
            dataType = dataType.additionalFields[1];
×
246
            continue;
×
247
          }
✔
248
          throw new Error(
2✔
249
            `Unknown field (${out.map(x => x.fieldName).join('.')})`,
2✔
250
          );
2✔
251
        }
2✔
252
        throw new TypeError(
1✔
253
          `"${out.map(x => x.fieldName).join('.')}" field is not a complex type and has no child fields`,
1✔
254
        );
1✔
255
      }
1!
256
      item.additionalField = true;
×
257
      item.dataType = objectType;
×
258
    }
✔
259
    return out;
211✔
260
  }
211✔
261

1✔
262
  /**
1✔
263
   *
1✔
264
   */
1✔
265
  normalizeFieldPath(
1✔
266
    fieldPath: string,
203✔
267
    options?: {
203✔
268
      allowSigns?: 'first' | 'each';
203✔
269
      scope?: string;
203✔
270
    },
203✔
271
  ): string {
203✔
272
    return this.parseFieldPath(fieldPath, options)
203✔
273
      .map(x => (x.sign || '') + x.fieldName)
203✔
274
      .join('.');
203✔
275
  }
203✔
276

1✔
277
  /**
1✔
278
   *
1✔
279
   */
1✔
280
  generateCodec(
1✔
281
    codec: 'encode' | 'decode',
546✔
282
    options?: DataType.GenerateCodecOptions,
546✔
283
  ): Validator {
546✔
284
    const context: GenerateCodecContext = (options as any)?.cache
546!
285
      ? (options as GenerateCodecContext)
×
286
      : {
546✔
287
          ...options,
546✔
288
          projection: Array.isArray(options?.projection)
546✔
289
            ? parseFieldsProjection(options.projection)
43✔
290
            : options?.projection,
503✔
291
          currentPath: '',
546✔
292
        };
546✔
293

546✔
294
    const schema = this._generateSchema(codec, context);
546✔
295

546✔
296
    let additionalFields: any;
546✔
297
    if (this.additionalFields instanceof DataType) {
546!
298
      additionalFields = this.additionalFields.generateCodec(codec, options);
×
299
    } else if (typeof this.additionalFields === 'boolean')
546✔
300
      additionalFields = this.additionalFields;
546✔
301
    else if (Array.isArray(this.additionalFields)) {
442!
302
      if (this.additionalFields.length < 2) additionalFields = 'error';
×
303
      else {
×
304
        const message = additionalFields[1] as string;
×
305
        additionalFields = validator((input, ctx, _this) =>
×
306
          ctx.fail(_this, message, input),
×
307
        );
×
308
      }
×
309
    }
×
310

546✔
311
    const fn = vg.isObject(schema, {
546✔
312
      ctor: this.name === 'object' ? Object : this.ctor,
546✔
313
      additionalFields,
546✔
314
      name: this.name,
546✔
315
      coerce: true,
546✔
316
      caseInSensitive: options?.caseInSensitive,
546✔
317
      onFail: options?.onFail,
546✔
318
    });
546✔
319
    if (context.level === 0 && context.forwardCallbacks?.size) {
546!
320
      for (const cb of context.forwardCallbacks) {
×
321
        cb();
×
322
      }
×
323
    }
×
324
    return fn;
546✔
325
  }
546✔
326

1✔
327
  protected _generateSchema(
1✔
328
    codec: 'encode' | 'decode',
555✔
329
    context: GenerateCodecContext,
555✔
330
  ): vg.isObject.Schema {
555✔
331
    context.fieldCache = context.fieldCache || new Map();
555✔
332
    context.level = context.level || 0;
555✔
333
    context.forwardCallbacks = context.forwardCallbacks || new Set();
555✔
334
    const schema: vg.isObject.Schema = {};
555✔
335
    const { currentPath, projection } = context;
555✔
336
    const pickList = !!(
555✔
337
      projection && Object.values(projection).find(p => !p.sign)
555✔
338
    );
555✔
339
    // Process fields
555✔
340
    let fieldName: string;
555✔
341
    for (const field of this.fields('*')) {
555✔
342
      if (
4,689✔
343
        /* Ignore field if required scope(s) do not match field scopes */
4,689✔
344
        !field.inScope(context.scope) ||
4,689✔
345
        (!(context.keepKeyFields && this.keyField) &&
4,632✔
346
          /* Ignore field if readonly and ignoreReadonlyFields option true */
3,623✔
347
          ((context.ignoreReadonlyFields && field.readonly) ||
3,623✔
348
            /* Ignore field if writeonly and ignoreWriteonlyFields option true */
3,494✔
349
            (context.ignoreWriteonlyFields && field.writeonly)))
3,623✔
350
      ) {
4,689✔
351
        schema[field.name] = vg.isUndefined({ coerce: true });
186✔
352
        continue;
186✔
353
      }
186✔
354
      fieldName = field.name;
4,503✔
355
      let p: any;
4,503✔
356
      if (projection !== '*') {
4,689✔
357
        p = projection?.[fieldName.toLowerCase()];
826✔
358
        if (
826✔
359
          /* Ignore if field is omitted */
826✔
360
          p?.sign === '-' ||
826✔
361
          /* Ignore if default fields ignored and field is not in projection */
801✔
362
          (pickList && !p) ||
801✔
363
          /* Ignore if default fields enabled and fields is exclusive */
598✔
364
          (!pickList && field.exclusive && !p)
598✔
365
        ) {
826✔
366
          schema[field.name] = vg.isUndefined({ coerce: true });
292✔
367
          continue;
292✔
368
        }
292✔
369
      }
826✔
370
      const subProjection =
4,211✔
371
        typeof projection === 'object'
4,211✔
372
          ? projection[fieldName]?.projection || '*'
355✔
373
          : projection;
3,856✔
374
      let cacheItem = context.fieldCache.get(field);
4,689✔
375
      const cacheKey =
4,689✔
376
        typeof subProjection === 'string'
4,689✔
377
          ? subProjection
4,032✔
378
          : hashObject(subProjection || {});
179✔
379
      if (!cacheItem) {
4,689✔
380
        cacheItem = {};
4,211✔
381
        context.fieldCache.set(field, cacheItem);
4,211✔
382
      }
4,211✔
383
      let fn = cacheItem[cacheKey];
4,211✔
384
      /* If in progress (circular) */
4,211✔
385
      if (fn === null) {
4,689!
386
        // Temporary set any
×
387
        fn = vg.isAny();
×
388
        context.forwardCallbacks.add(() => {
×
389
          fn = cacheItem[cacheKey];
×
390
          schema[fieldName] =
×
391
            context.partial || !field.required
×
392
              ? context.allowNullOptionals
×
393
                ? vg.nullable(fn!)
×
394
                : vg.optional(fn!)
×
395
              : vg.required(fn!);
×
396
        });
×
397
      } else if (!fn) {
4,689✔
398
        const defaultGenerator = () => {
4,211✔
399
          cacheItem[cacheKey] = null;
4,211✔
400
          const xfn = field.generateCodec(codec, {
4,211✔
401
            ...context,
4,211✔
402
            partial: context.partial === 'deep' ? context.partial : undefined,
4,211✔
403
            projection: subProjection,
4,211✔
404
            currentPath: currentPath + (currentPath ? '.' : '') + fieldName,
4,211!
405
            level: context.level! + 1,
4,211✔
406
          } as GenerateCodecContext);
4,211✔
407
          cacheItem[cacheKey] = xfn;
4,211✔
408
          return xfn;
4,211✔
409
        };
4,211✔
410
        if (context.fieldHook)
4,211✔
411
          fn = context.fieldHook(field, context.currentPath, defaultGenerator);
4,211✔
412
        else fn = defaultGenerator();
3,939✔
413
      }
4,211✔
414
      schema[fieldName] =
4,211✔
415
        context.partial || !(field.required || fn.id === 'required')
4,689✔
416
          ? context.allowNullOptionals
4,198✔
417
            ? vg.nullable(fn)
1,276✔
418
            : vg.optional(fn)
2,922✔
419
          : fn.id === 'required'
13✔
420
            ? fn
13!
421
            : vg.required(fn);
×
422
    }
4,689✔
423
    if (context.allowPatchOperators) {
555✔
424
      schema._$pull = vg.optional(vg.isAny());
130✔
425
      schema._$push = vg.optional(vg.isAny());
130✔
426
    }
130✔
427
    return schema;
555✔
428
  }
555✔
429
}
1✔
430

1✔
431
ComplexTypeBase.prototype = ComplexTypeBaseClass.prototype;
1✔
432

1✔
433
type GenerateCodecContext = StrictOmit<
1✔
434
  DataType.GenerateCodecOptions,
1✔
435
  'projection'
1✔
436
> & {
1✔
437
  currentPath: string;
1✔
438
  projection?: FieldsProjection | '*';
1✔
439
  level?: number;
1✔
440
  fieldCache?: Map<ApiField, Record<string, Validator | null>>;
1✔
441
  forwardCallbacks?: Set<Function>;
1✔
442
};
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc