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

WolferyScripting / resclient-ts / #25

24 Aug 2025 12:37PM UTC coverage: 51.153% (+0.9%) from 50.293%
#25

push

DonovanDMC
1.1.0

227 of 281 branches covered (80.78%)

Branch coverage included in aggregate %.

1570 of 3232 relevant lines covered (48.58%)

10.67 hits per line

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

55.31
/lib/includes/utils/obj.ts
1
/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function */
1✔
2

1✔
3
import type ResModel from "../../models/ResModel.js";
1✔
4
import type { AnyClass, AnyObject } from "../../util/types.js";
1✔
5
import type ResCollection from "../../models/ResCollection.js";
1✔
6
import { format } from "node:util";
1✔
7
import assert from "node:assert";
1✔
8

1✔
9
export function equal(a: unknown, b: unknown): boolean {
1✔
10
    // Check if a is a non-object
24✔
11
    if (a === null || typeof a !== "object") {
24✔
12
        return a === b;
24✔
13
    }
24✔
14

×
15
    // Make sure b is also an object
×
16
    if (b === null || typeof b !== "object") {
24!
17
        return false;
×
18
    }
×
19

×
20
    const ak = Object.keys(a).sort();
×
21
    const bk = Object.keys(b).sort();
×
22

×
23
    if (ak.length !== bk.length) {
×
24
        return false;
×
25
    }
×
26
    for (let i = 0, k: string; (k = ak[i]!); i++) {
×
27
        if (k !== bk[i]) {
×
28
            return false;
×
29
        }
×
30

×
31
        if (!equal((a as Record<string, unknown>)[k], (b as Record<string, unknown>)[k])) {
×
32
            return false;
×
33
        }
×
34
    }
×
35

×
36
    return true;
×
37
}
×
38
export const TYPES = {
1✔
39
    "any": {
1✔
40
        default(): null {
1✔
41
            return  null;
×
42
        },
1✔
43
        assert(_v: unknown): void {},
1✔
44
        fromString(v: string): string {
1✔
45
            return v;
×
46
        }
×
47
    },
1✔
48
    "string": {
1✔
49
        default(): string {
1✔
50
            return "";
×
51
        },
1✔
52
        assert(v: unknown): void {
1✔
53
            if (typeof v !== "string") {
×
54
                throw new TypeError("Not a string");
×
55
            }
×
56
        },
1✔
57
        fromString: String
1✔
58
    },
1✔
59
    "?string": {
1✔
60
        default(): null {
1✔
61
            return null;
×
62
        },
1✔
63
        assert(v: unknown): void {
1✔
64
            if (typeof v !== "string" && v !== null) {
×
65
                throw new Error("Not a string or null");
×
66
            }
×
67
        },
1✔
68
        fromString: String // Not possible to set null
1✔
69
    },
1✔
70
    "number": {
1✔
71
        default(): number {
1✔
72
            return 0;
×
73
        },
1✔
74
        assert(v: unknown): void {
1✔
75
            if (typeof v !== "number") {
×
76
                throw new TypeError("Not a number");
×
77
            }
×
78
        },
1✔
79
        fromString(v: string | number): number {
1✔
80
            v = Number(v);
×
81
            if (isNaN(v)) {
×
82
                throw new TypeError("Not a number format");
×
83
            }
×
84
            return v;
×
85
        }
×
86
    },
1✔
87
    "?number": {
1✔
88
        default(): null {
1✔
89
            return null;
×
90
        },
1✔
91
        assert(v: unknown): void {
1✔
92
            if (typeof v !== "number" && v !== null) {
×
93
                throw new Error("Not a number or null");
×
94
            }
×
95
        },
1✔
96
        fromString(v: string | number | null): number | null {
1✔
97
            v = !v || String(v).toLowerCase() === "null" ? null : Number(v);
×
98
            if (v !== null && isNaN(v)) {
×
99
                throw new TypeError("Not a number format");
×
100
            }
×
101
            return v;
×
102
        }
×
103
    },
1✔
104
    "boolean": {
1✔
105
        default(): boolean {
1✔
106
            return false;
×
107
        },
1✔
108
        assert(v: unknown): void {
1✔
109
            if (typeof v !== "boolean") {
×
110
                throw new TypeError("Not a boolean");
×
111
            }
×
112
        },
1✔
113
        fromString(v: string | boolean): boolean {
1✔
114
            v = String(v).toLowerCase();
×
115
            if (v === "true" || v === "1" || v === "yes") {
×
116
                v = true;
×
117
            } else if (v === "false" || v === "0" || v === "no") {
×
118
                v = false;
×
119
            } else {
×
120
                throw new Error("Not a boolean format");
×
121
            }
×
122
            return v;
×
123
        }
×
124
    },
1✔
125
    "?boolean": {
1✔
126
        default(): null {
1✔
127
            return null;
×
128
        },
1✔
129
        assert(v: unknown): void {
1✔
130
            if (typeof v !== "boolean" && v !== null) {
×
131
                throw new Error("Not a boolean or null");
×
132
            }
×
133
        },
1✔
134
        fromString(v: string | boolean | null): boolean | null {
1✔
135
            v = String(v).toLowerCase();
×
136
            switch (v) {
×
137
                case "true":
×
138
                case "1":
×
139
                case "yes": {
×
140
                    return true;
×
141
                }
×
142

×
143
                case "false":
×
144
                case "0":
×
145
                case "no": {
×
146
                    return false;
×
147
                }
×
148

×
149
                case "null": {
×
150
                    return null;
×
151
                }
×
152
                default: {
×
153
                    throw new Error("Not a nullable boolean format");
×
154
                }
×
155
            }
×
156
        }
×
157
    },
1✔
158
    "object": {
1✔
159
        default(): AnyObject {
1✔
160
            return {};
×
161
        },
1✔
162
        assert(v: unknown): void {
1✔
163
            if (typeof v !== "object" || v === null) {
×
164
                throw new Error("Not an object");
×
165
            }
×
166
        },
1✔
167
        fromString(v: string): AnyObject {
1✔
168
            return JSON.parse(v) as AnyObject;
×
169
        }
×
170
    },
1✔
171
    "?object": {
1✔
172
        default(): null {
1✔
173
            return null;
×
174
        },
1✔
175
        assert(v: unknown): void {
1✔
176
            if (typeof v !== "object") {
×
177
                throw new TypeError("Not an object or null");
×
178
            }
×
179
        },
1✔
180
        fromString(v: string): AnyObject | null {
1✔
181
            if (v === "null") {
×
182
                return null;
×
183
            }
×
184
            return JSON.parse(v) as AnyObject;
×
185
        }
×
186
    },
1✔
187
    "array": {
1✔
188
        default(): Array<unknown> {
1✔
189
            return [];
×
190
        },
1✔
191
        assert(v: unknown): void {
1✔
192
            if (!Array.isArray(v)) {
×
193
                throw new TypeError("Not an array");
×
194
            }
×
195
        },
1✔
196
        fromString(v: string): Array<unknown> {
1✔
197
            return JSON.parse(v) as Array<unknown>;
×
198
        }
×
199
    },
1✔
200
    "?array": {
1✔
201
        default(): Array<unknown> {
1✔
202
            return [];
×
203
        },
1✔
204
        assert(v: unknown): void {
1✔
205
            if (!Array.isArray(v) && v !== null) {
×
206
                throw new TypeError("Not an array or null");
×
207
            }
×
208
        },
1✔
209
        fromString(v: string): Array<unknown> | null {
1✔
210
            if (v === "null") {
×
211
                return null;
×
212
            }
×
213
            return JSON.parse(v) as Array<unknown>;
×
214
        }
×
215
    },
1✔
216
    "array[string]": {
1✔
217
        default(): Array<string> {
1✔
218
            return [];
×
219
        },
1✔
220
        assert(v: unknown): void {
1✔
221
            if (!Array.isArray(v)) {
×
222
                throw new TypeError("Not an array");
×
223
            }
×
224
            if (!v.every(e => typeof e === "string")) {
×
225
                throw new TypeError("Not all array elements are strings");
×
226
            }
×
227
        },
1✔
228
        fromString(v: string): Array<string> {
1✔
229
            return JSON.parse(v) as Array<string>;
×
230
        }
×
231
    },
1✔
232
    "?array[string]": {
1✔
233
        default(): Array<string> {
1✔
234
            return [];
×
235
        },
1✔
236
        assert(v: unknown): void {
1✔
237
            if (!Array.isArray(v) && v !== null) {
×
238
                throw new TypeError("Not an array or null");
×
239
            }
×
240
            if (v && !v.every(e => typeof e === "string")) {
×
241
                throw new TypeError("Not all array elements are strings");
×
242
            }
×
243
        },
1✔
244
        fromString(v: string): Array<string> | null {
1✔
245
            if (v === "null") {
×
246
                return null;
×
247
            }
×
248
            return JSON.parse(v) as Array<string>;
×
249
        }
×
250
    },
1✔
251
    "array[number]": {
1✔
252
        default(): Array<number> {
1✔
253
            return [];
×
254
        },
1✔
255
        assert(v: unknown): void {
1✔
256
            if (!Array.isArray(v)) {
×
257
                throw new TypeError("Not an array");
×
258
            }
×
259
            if (!v.every(e => typeof e === "number")) {
×
260
                throw new TypeError("Not all array elements are numbers");
×
261
            }
×
262
        },
1✔
263
        fromString(v: string): Array<number> {
1✔
264
            return JSON.parse(v) as Array<number>;
×
265
        }
×
266
    },
1✔
267
    "?array[number]": {
1✔
268
        default(): Array<number> {
1✔
269
            return [];
×
270
        },
1✔
271
        assert(v: unknown): void {
1✔
272
            if (!Array.isArray(v) && v !== null) {
×
273
                throw new TypeError("Not an array or null");
×
274
            }
×
275
            if (v && !v.every(e => typeof e === "number")) {
×
276
                throw new TypeError("Not all array elements are numbers");
×
277
            }
×
278
        },
1✔
279
        fromString(v: string): Array<number> | null {
1✔
280
            if (v === "null") {
×
281
                return null;
×
282
            }
×
283
            return JSON.parse(v) as Array<number>;
×
284
        }
×
285
    },
1✔
286
    "function": {
1✔
287
        // eslint-disable-next-line unicorn/consistent-function-scoping
1✔
288
        default(): () => void {
1✔
289
            return (): void => {};
×
290
        },
1✔
291
        assert(v: unknown): void {
1✔
292
            if (typeof v !== "function") {
×
293
                throw new TypeError("Not a function");
×
294
            }
×
295
        },
1✔
296
        fromString(_v: unknown): void {} // Evaluating functions from strings is not allowed
1✔
297

1✔
298
    },
1✔
299
    "?function": {
1✔
300
        default(): null {
1✔
301
            return null;
12✔
302
        },
1✔
303
        assert(v: unknown): void {
1✔
304
            if (typeof v !== "function" && v !== null) {
6!
305
                throw new Error("Not a function or null");
×
306
            }
×
307
        },
1✔
308
        fromString(_v: unknown): void {} // Evaluating functions from strings is not allowed
1✔
309

1✔
310
    }
1✔
311
} satisfies Record<string, TypeDefinition>;
1✔
312

1✔
313
export interface TypeDefinition {
1✔
314
    assert(val: unknown): void;
1✔
315
    default(): unknown;
1✔
316
    fromString(val: string): unknown;
1✔
317
}
1✔
318
export interface PropertyDefinition {
1✔
319
    default?: unknown;
1✔
320
    property?: string;
1✔
321
    type: keyof typeof TYPES;
1✔
322
    assert?(val: unknown): void;
1✔
323
    filter?(val: unknown): PropertyDefinition;
1✔
324
}
1✔
325

1✔
326
/* eslint-disable @typescript-eslint/ban-types */
1✔
327
export interface DefinitionTypeMap {
1✔
328
    "?array": Array<unknown> | null;
1✔
329
    "?array[number]": Array<number> | null;
1✔
330
    "?array[string]": Array<string> | null;
1✔
331
    "?boolean": boolean | null;
1✔
332
    "?function": Function | null;
1✔
333
    "?number": number | null;
1✔
334
    "?object": object | null;
1✔
335
    "?string": string | null;
1✔
336
    "any": unknown;
1✔
337
    "array": Array<unknown>;
1✔
338
    "array[number]": Array<number>;
1✔
339
    "array[string]": Array<string>;
1✔
340
    "boolean": boolean;
1✔
341
    "function": Function;
1✔
342
    "number": number;
1✔
343
    "object": object;
1✔
344
    "string": string;
1✔
345
}
1✔
346
/* eslint-enable @typescript-eslint/ban-types */
1✔
347

1✔
348
/**
1✔
349
 * Updates an target object from a source object based upon a definition
1✔
350
 * @param target Target object
1✔
351
 * @param source Source object
1✔
352
 * @param def Definition object which is a key/value object where the key is the property and the value is the property type or a property definition.
1✔
353
 * @param strict Strict flag. If true, exceptions will be thrown on errors. If false, errors will be ignored. Default is true.
1✔
354
 * @returns Key/value object where the key is the updated properties and the value is the old values.
1✔
355
 */
1✔
356
export function update(target: object, source: object, def: Record<string, PropertyDefinition | keyof typeof TYPES>, strict = true): AnyObject | null {
1✔
357
    if (!def || typeof def !== "object") {
50!
358
        throw new Error("Invalid definition");
×
359
    }
×
360

50✔
361
    let updated = false;
50✔
362
    const updateObj = {} as AnyObject;
50✔
363

50✔
364
    // eslint-disable-next-line guard-for-in
50✔
365
    for (const key in def) {
50✔
366
        let d = def[key]!;
50✔
367
        if (typeof d === "string") {
50!
368
            d = {
×
369
                type: d
×
370
            };
×
371
        }
×
372

50✔
373
        const t = TYPES[d.type];
50✔
374
        if (!t) {
50!
375
            throw new Error("Invalid definition type: " + d.type);
×
376
        }
×
377

50✔
378
        const tkey = Object.hasOwn(d, "property") && d.property ? d.property : key;
50✔
379

50✔
380
        // Check if target has any value set. If not, use default.
50✔
381
        if (!Object.hasOwn(target, tkey)) {
50✔
382
            updated = true;
12✔
383
            updateObj[tkey] = undefined;
12✔
384
            (target as Record<string, unknown>)[tkey] = (Object.hasOwn(d, "default") && d.default ? d.default : t.default()) as never;
12!
385
        }
12✔
386

50✔
387
        // Check if source has value for the property. If not, continue to next property.
50✔
388
        if (!Object.hasOwn(source, key)) {
50✔
389
            continue;
44✔
390
        }
44✔
391

6✔
392
        let v = (source as Record<string, unknown>)[key];
6✔
393
        if (v === undefined) {
50!
394
            continue;
×
395
        }
×
396

6✔
397
        try {
6✔
398
            // Convert from string
6✔
399
            if (typeof v === "string") {
50!
400
                v = t.fromString(v);
×
401
            }
×
402

6✔
403
            // Apply filter to value
6✔
404
            if (d.filter) {
50!
405
                v = d.filter(v);
×
406
            }
×
407

6✔
408
            // Type assertion
6✔
409
            t.assert(v);
6✔
410

6✔
411
            // Definition assertion
6✔
412
            if (d.assert) {
50!
413
                d.assert(v);
×
414
            }
×
415

6✔
416
            // Check if the property value differs and set it as updated
6✔
417
            if ((target as Record<string, unknown>)[tkey] !== v) {
6✔
418
                updated = true;
6✔
419
                updateObj[tkey] = (target as Record<string, unknown>)[tkey];
6✔
420
                (target as Record<string, unknown>)[tkey] = v as never;
6✔
421
            }
6✔
422
        } catch (ex) {
50!
423
            if (strict) {
×
424
                throw ex;
×
425
            }
×
426
        }
×
427
    }
50✔
428

50✔
429
    if (!updated) {
50✔
430
        return null;
38✔
431
    }
38✔
432

12✔
433
    return updateObj;
12✔
434
}
12✔
435

1✔
436
/**
1✔
437
 * Copies a source object based upon a definition
1✔
438
 * @param source Source object
1✔
439
 * @param def Definition object which is a key/value object where the key is the property and the value is the value type.
1✔
440
 * @param strict Strict flag. If true, exceptions will be thrown on errors. If false, errors will be ignored. Default is false.
1✔
441
 * @returns Copy of the object
1✔
442
 */
1✔
443
export function copy<T extends AnyObject>(source: T, def: Record<string, PropertyDefinition | keyof typeof TYPES>, strict = false): T {
1✔
444
    const obj = {} as T;
12✔
445
    update(obj, source, def, strict);
12✔
446
    return obj;
12✔
447
}
12✔
448

1✔
449
export function modelProperty(property: string, model: AnyClass<ResModel>, optional: boolean): PropertyDefinition {
1✔
450
    return {
×
451
        property,
×
452
        type: optional ? "?object" : "object",
×
453
        assert(value: unknown): void {
×
454
            if (optional && value === null) return;
×
455
            assert(value instanceof model, `Expected instance of ${model.name} for ${property}, got ${format(value)}`);
×
456
        }
×
457
    };
×
458
}
×
459

1✔
460
export function collectionProperty(property: string, model: AnyClass<ResCollection>, optional: boolean): PropertyDefinition {
1✔
461
    return {
×
462
        type: optional ? "?object" : "object",
×
463
        assert(value: unknown): void {
×
464
            if (optional && value === null) return;
×
465
            assert(value instanceof model, `Expected instance of ${model.name} for ${property}, got ${format(value)}`);
×
466
        }
×
467
    };
×
468
}
×
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