diff --git a/packages/backend/src/misc/kvp-array.ts b/packages/backend/src/misc/kvp-array.ts new file mode 100644 index 0000000000..8ed8f230c1 --- /dev/null +++ b/packages/backend/src/misc/kvp-array.ts @@ -0,0 +1,48 @@ +/* + * SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +/** + * Key-Value Pair Array - stores a collection of Key/Value pairs with helper methods to access ordered keys/values. + * Keys and Values can be of any type, and Keys default to type "string" if unspecified. + */ +export type KVPArray = KVPs & { + /** + * Lazy-loaded array of all keys in the array, matching the order of the pairs. + */ + readonly keys: readonly K[], + + /** + * Lazy-loaded array of all values in the array, matching the order of the pairs. + */ + readonly values: readonly T[], +}; + +type KVPs = Omit[], 'keys' | 'values' | 'entries'>; +type KVP = readonly [key: K, value: T]; + +/** + * Wraps an array of Key/Value pairs into a KVPArray. + */ +export function makeKVPArray(pairs: KVPs): KVPArray { + let keys: K[] | null = null; + let values: T[] | null = null; + + Object.defineProperties(pairs, { + keys: { + get() { + return keys ??= pairs.map(pair => pair[0]); + }, + enumerable: false, + }, + values: { + get() { + return values ??= pairs.map(pair => pair[1]); + }, + enumerable: false, + }, + }); + + return pairs as KVPArray; +} diff --git a/packages/backend/test/unit/misc/kvp-array.ts b/packages/backend/test/unit/misc/kvp-array.ts new file mode 100644 index 0000000000..07a3943098 --- /dev/null +++ b/packages/backend/test/unit/misc/kvp-array.ts @@ -0,0 +1,78 @@ +/* + * SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { makeKVPArray } from '@/misc/kvp-array.js'; + +describe(makeKVPArray, () => { + it('should add keys property', () => { + const array = [['1', 1], ['2', 2], ['3', 3]] as const; + + const result = makeKVPArray(array); + + expect(result).toHaveProperty('keys'); + }); + + it('should add values property', () => { + const array = [['1', 1], ['2', 2], ['3', 3]] as const; + + const result = makeKVPArray(array); + + expect(result).toHaveProperty('values'); + }); + + it('should preserve values', () => { + const array: [string, number][] = [['1', 1], ['2', 2], ['3', 3]]; + + const result = makeKVPArray(array); + + expect(result).toEqual(array); + }); + + it('should accept empty array', () => { + const array = [] as const; + + const result = makeKVPArray(array); + + expect(result).toHaveProperty('keys'); + expect(result).toHaveProperty('values'); + expect(result).toHaveLength(0); + }); +}); + +describe('keys', () => { + it('should return all keys', () => { + const array = [['1', 1], ['2', 2], ['3', 3]] as const; + + const result = makeKVPArray(array); + + expect(result.keys).toEqual(['1', '2', '3']); + }); + + it('should preserve duplicates', () => { + const array = [['1', 1], ['1', 1], ['1', 1]] as const; + + const result = makeKVPArray(array); + + expect(result.keys).toEqual(['1', '1', '1']); + }); +}); + +describe('values', () => { + it('should return all values', () => { + const array = [['1', 1], ['2', 2], ['3', 3]] as const; + + const result = makeKVPArray(array); + + expect(result.values).toEqual([1, 2, 3]); + }); + + it('should preserve duplicates', () => { + const array = [['1', 1], ['1', 1], ['1', 1]] as const; + + const result = makeKVPArray(array); + + expect(result.values).toEqual([1, 1, 1]); + }); +});