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

panates / opra / 29494915195

16 Jul 2026 11:33AM UTC coverage: 83.024% (+0.4%) from 82.653%
29494915195

push

github

erayhanoglu
1.29.1

4118 of 5238 branches covered (78.62%)

Branch coverage included in aggregate %.

34348 of 41093 relevant lines covered (83.59%)

239.47 hits per line

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

84.3
/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';
639!
12
import type { ApiField } from './api-field.js';
639✔
13
import type { ComplexType } from './complex-type.js';
639✔
14
import { DataType } from './data-type.js';
639✔
15

639✔
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
 */
×
22
interface ComplexTypeBaseStatic {
1✔
23
  /**
1✔
24
   * Class constructor of MappedType
1✔
25
   *
1!
26
   * @param owner
×
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,039✔
37
  prototype: ComplexTypeBase;
1,039✔
38
}
1,039✔
39

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

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

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

976✔
77
  fieldCount(scope?: string): number {
976✔
78
    if (scope === '*') return this._fields.size;
976✔
79
    let count = 0;
976✔
80
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
10,048!
81
    for (const i of this.fields(scope)) count++;
×
82
    return count;
10,048✔
83
  }
10,048✔
84

10,048✔
85
  fieldEntries(scope?: string): IterableIterator<[string, ApiField]> {
10,048✔
86
    let iterator: IterableIterator<[string, ApiField]> | undefined =
10,048✔
87
      this._fields.entries();
976✔
88
    if (scope === '*') return iterator;
976✔
89
    let r: IteratorResult<[string, ApiField]>;
×
90
    return {
×
91
      next() {
×
92
        while (iterator) {
×
93
          r = iterator.next();
×
94
          if (r.done) break;
976✔
95
          if (r.value && r.value[1].inScope(scope)) break;
976✔
96
        }
976✔
97
        if (r.done) return { done: r.done, value: undefined };
1!
98
        return {
12✔
99
          done: r.done,
12✔
100
          value: [r.value[0], r.value[1].forScope(scope)],
12✔
101
        };
12✔
102
      },
34✔
103
      return(value?: [string, ApiField]) {
34!
104
        iterator = undefined;
×
105
        return { done: true, value };
×
106
      },
×
107
      [Symbol.iterator]() {
34✔
108
        return this;
34✔
109
      },
34✔
110
    };
34✔
111
  }
34✔
112

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

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

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

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

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

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

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

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

108✔
259
  /**
108✔
260
   *
108✔
261
   */
108✔
262
  normalizeFieldPath(
559✔
263
    fieldPath: string,
451✔
264
    options?: {
451✔
265
      allowSigns?: 'first' | 'each';
451!
266
      scope?: string;
×
267
    },
×
268
  ): string {
×
269
    return this.parseFieldPath(fieldPath, options)
×
270
      .map(x => (x.sign || '') + x.fieldName)
×
271
      .join('.');
×
272
  }
×
273

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

×
291
    const schema = this._generateSchema(codec, context);
×
292

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

4,780✔
308
    const fn = vg.isObject(schema, {
4,780✔
309
      ctor: this.name === 'object' ? Object : this.ctor,
4,780✔
310
      additionalFields,
4,721✔
311
      name: this.name,
3,712✔
312
      coerce: true,
3,712✔
313
      caseInSensitive: options?.caseInSensitive,
3,712✔
314
      onFail: options?.onFail,
3,712✔
315
    });
3,583✔
316
    if (context.level === 0 && context.forwardCallbacks?.size) {
3,583✔
317
      for (const cb of context.forwardCallbacks) {
4,780✔
318
        cb();
188✔
319
      }
188✔
320
    }
188✔
321
    return fn;
188✔
322
  }
188✔
323

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

1✔
428
ComplexTypeBase.prototype = ComplexTypeBaseClass.prototype;
1✔
429

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

© 2026 Coveralls, Inc