Created
November 23, 2025 09:39
-
-
Save orrisroot/29e090c5d6f055fe230cd6f0f6dc43af to your computer and use it in GitHub Desktop.
EntityData - A record structure for storing entities by their IDs and maintaining a list of all IDs.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * @type {EntityData} | |
| * A record structure for storing entities by their IDs and maintaining a list of all IDs. | |
| * | |
| * @template T The type of the items in the entity data. | |
| * @template K The type of the keys used to identify items. Defaults to `string`. | |
| * | |
| * @property {Record<K, T>} byId - A mapping of entity IDs to their corresponding items. | |
| * @property {K[]} allIds - An array of all entity IDs. | |
| */ | |
| export interface EntityData<T, K extends string | number | symbol = string> { | |
| byId: Record<K, T>; | |
| allIds: K[]; | |
| } | |
| /** | |
| * Create a new entity data. | |
| * | |
| * @template T The type of the items in the entity data. | |
| * @template K The type of the keys used to identify items. Defaults to `string`. | |
| * | |
| * @returns A new entity data object. | |
| */ | |
| export const createEntityData = <T, K extends string | number | symbol = string>(): EntityData<T, K> => ({ | |
| byId: {} as Record<K, T>, | |
| allIds: [] as K[], | |
| }); | |
| // --- Read/Access Helpers --- | |
| /** | |
| * Checks if an entity with the given ID exists in the data. | |
| * | |
| * @param data The EntityData object to read from. | |
| * @param id The ID to check for. | |
| * @returns true if the entity exists, false otherwise. | |
| */ | |
| export function entityHasId<T, K extends string | number | symbol = string>(data: EntityData<T, K>, id: K): boolean { | |
| return !!data.byId[id]; | |
| } | |
| /** | |
| * Retrieves all IDs stored in the data, preserving the order defined by allIds. | |
| * | |
| * @param data The EntityData object to read from. | |
| * @returns An array of all entity IDs (K[]). | |
| */ | |
| export function entityGetIds<T, K extends string | number | symbol = string>(data: EntityData<T, K>): K[] { | |
| // Returns a copy of the allIds array for safety. | |
| return [...data.allIds]; | |
| } | |
| /** | |
| * Retrieves a single entity by its ID. | |
| * | |
| * @param data The EntityData object to read from. | |
| * @param id The ID of the entity to retrieve. | |
| * @returns The entity (T) or undefined if not found. | |
| */ | |
| export function entityGetOne<T, K extends string | number | symbol = string>( | |
| data: EntityData<T, K>, | |
| id: K | |
| ): T | undefined { | |
| return data.byId[id]; | |
| } | |
| /** | |
| * Retrieves all items in the data as an array, preserving the order defined by allIds. | |
| * | |
| * @param data The EntityData object to read from. | |
| * @returns An array of all entities (T[]). | |
| */ | |
| export function entityGetAll<T, K extends string | number | symbol = string>(data: EntityData<T, K>): T[] { | |
| // Map over allIds to retrieve items from byId. | |
| return data.allIds.map((id) => data.byId[id]); | |
| } | |
| /** | |
| * Retrieves multiple entities by their IDs. | |
| * | |
| * @param data The EntityData object to read from. | |
| * @param ids The array of IDs to retrieve. | |
| * @returns An array of entities (T[]) corresponding to the provided IDs. Items not found are excluded. | |
| */ | |
| export function entitySelect<T, K extends string | number | symbol = string>(data: EntityData<T, K>, ids: K[]): T[] { | |
| // Filters out undefined results (items not found) | |
| return ids.map((id) => data.byId[id]).filter((item): item is T => !!item); | |
| } | |
| /** | |
| * Filters the items based on a provided predicate function and returns the result as an array. | |
| * | |
| * @param data The EntityData object to read from. | |
| * @param filterFn The filtering predicate function. | |
| * @returns An array of filtered entities (T[]). | |
| */ | |
| export function entityFilter<T, K extends string | number | symbol = string>( | |
| data: EntityData<T, K>, | |
| filterFn: (item: T) => boolean | |
| ): T[] { | |
| // Use entityGetAll to get the iterable array, then filter. | |
| return entityGetAll(data).filter(filterFn); | |
| } | |
| /** | |
| * Performs a callback function for every entity in the data (order is not guaranteed). | |
| * | |
| * @param data The EntityData object to read from. | |
| * @param callback The function to execute for each entity. | |
| */ | |
| export function entityForEach<T, K extends string | number | symbol = string>( | |
| data: EntityData<T, K>, | |
| callback: (item: T, id: K) => void | |
| ): void { | |
| // Iterating over the keys of the byId map for direct access. | |
| (Object.keys(data.byId) as K[]).forEach((id) => { | |
| callback(data.byId[id], id); | |
| }); | |
| } | |
| /** | |
| * Maps every entity in the data to a new value (order is not guaranteed). | |
| * | |
| * @param data The EntityData object to read from. | |
| * @param callback The function to execute for each entity to return a new value. | |
| * @returns An array of the new mapped values. | |
| */ | |
| export function entityMap<T, R, K extends string | number | symbol = string>( | |
| data: EntityData<T, K>, | |
| callback: (item: T, id: K) => R | |
| ): R[] { | |
| // Mapping over the keys of the byId map. | |
| return (Object.keys(data.byId) as K[]).map((id) => { | |
| return callback(data.byId[id], id); | |
| }); | |
| } | |
| // --- Mutation Helpers --- | |
| /** | |
| * Clears all entities from the EntityData, resetting it to an empty state. | |
| * | |
| * @param data The EntityData object to mutate. | |
| */ | |
| export function mutateEntityClear<T, K extends string | number | symbol = string>(data: EntityData<T, K>): void { | |
| data.byId = {} as Record<K, T>; | |
| data.allIds = []; | |
| } | |
| /** | |
| * Add items to an entity data. | |
| * | |
| * @template T The type of the items in the entity data. | |
| * @template K The type of the keys used to identify items. Defaults to `string`. | |
| * @template P The type of the input items to be transformed. Defaults to `T`. | |
| * | |
| * @param data The entity data to add items into. | |
| * @param items The array of items to add. | |
| * @param transformId A function to extract the ID from an item. | |
| * @param transformValue A function to transform an item into the desired value type. | |
| * | |
| * @example | |
| * ```ts | |
| * interface User { | |
| * id: string; | |
| * name: string; | |
| * } | |
| * | |
| * interface UserApiResponse { | |
| * ID: string; | |
| * NAME: string; | |
| * } | |
| * | |
| * const users = createEntityData<User, string>(); | |
| * | |
| * const usersApiResponse = [ | |
| * { ID: '1', NAME: 'Alice' }, | |
| * { ID: '2', NAME: 'Bob' }, | |
| * ]; | |
| * | |
| * populateEntityData<User, string, UserApiResponse>( | |
| * users, | |
| * usersApiResponse, | |
| * (user) => user.ID, | |
| * (user) => ({ id: user.ID, name: user.NAME }) | |
| * ); | |
| * | |
| * console.log(users); | |
| * // { | |
| * // byId: { | |
| * // '1': { id: '1', name: 'Alice' }, | |
| * // '2': { id: '2', name: 'Bob' }, | |
| * // }, | |
| * // allIds: ['1', '2'], | |
| * // } | |
| * ``` | |
| */ | |
| export const populateEntityData = <T, K extends string | number | symbol = string, P = T>( | |
| data: EntityData<T, K>, | |
| items: P[], | |
| transformId: (i: P) => K, | |
| transformValue: (i: P) => T = (i: P) => i as unknown as T | |
| ): void => { | |
| const processedIds = new Set(data.allIds); | |
| const newIds: K[] = []; | |
| items.forEach((item) => { | |
| const id = transformId(item); | |
| const value = transformValue(item); | |
| data.byId[id] = value; | |
| if (!processedIds.has(id)) { | |
| newIds.push(id); | |
| processedIds.add(id); | |
| } | |
| }); | |
| data.allIds.push(...newIds); | |
| }; | |
| /** | |
| * Adds or updates an item in the EntityData, requiring the ID to be passed externally (Upsert). | |
| * | |
| * @param data The EntityData object to mutate. | |
| * @param id The unique ID of the entity (Required externally). | |
| * @param item The partial data to insert or update. | |
| */ | |
| export function mutateEntityUpsert<T, K extends string | number | symbol = string>( | |
| data: EntityData<T, K>, | |
| id: K, | |
| item: Partial<T> | |
| ): void { | |
| // Update/Insert into byId map with partial merge | |
| // NOTE: This assumes 'item' is a subset of T and does not contain the ID itself, | |
| // or that the caller guarantees the consistency of ID if T contains one. | |
| data.byId[id] = { | |
| ...(data.byId[id] || {}), | |
| ...item, | |
| // If T must contain the ID property, you might need to manually ensure it's merged here. | |
| } as T; | |
| // Add to allIds array only if it's a new item (simple check is OK for single upsert) | |
| if (!data.allIds.includes(id)) { | |
| data.allIds.push(id); | |
| } | |
| } | |
| /** | |
| * Adds or updates multiple items in the EntityData based on their ID and partial data. | |
| * This is an efficient batch upsert operation. | |
| * | |
| * @param data The EntityData object to mutate. | |
| * @param updates An array of objects, each containing the ID and the partial entity data to update/insert. | |
| */ | |
| export function mutateEntityUpsertMany<T, K extends string | number | symbol = string>( | |
| data: EntityData<T, K>, | |
| updates: Array<{ id: K; item: Partial<T> }> | |
| ): void { | |
| const newIds: K[] = []; | |
| const existingIdsSet = new Set(data.allIds); | |
| updates.forEach(({ id, item }) => { | |
| // 1. Upsert/Update byId map with partial merge | |
| data.byId[id] = { | |
| ...(data.byId[id] || {}), | |
| ...item, | |
| } as T; | |
| // 2. Track new IDs efficiently | |
| if (!existingIdsSet.has(id)) { | |
| newIds.push(id); | |
| existingIdsSet.add(id); | |
| } | |
| }); | |
| // 3. Append all new IDs to the allIds array once | |
| if (newIds.length > 0) { | |
| data.allIds.push(...newIds); | |
| } | |
| } | |
| /** | |
| * Deletes an item from the EntityData by its ID. | |
| * | |
| * @param data The EntityData object to mutate. | |
| * @param id The ID of the item to delete. | |
| */ | |
| export function mutateEntityDelete<T, K extends string | number | symbol = string>( | |
| data: EntityData<T, K>, | |
| id: K | |
| ): void { | |
| if (!data.byId[id]) { | |
| return; | |
| } | |
| delete data.byId[id]; | |
| // Reassigning the filtered array triggers reactivity efficiently. | |
| data.allIds = data.allIds.filter((itemId) => itemId !== id); | |
| } | |
| /** | |
| * Deletes multiple items from the EntityData by their IDs. | |
| * | |
| * @param data The EntityData object to mutate. | |
| * @param ids The array of IDs of the items to delete. | |
| */ | |
| export function mutateEntityDeleteMany<T, K extends string | number | symbol = string>( | |
| data: EntityData<T, K>, | |
| ids: K[] | |
| ): void { | |
| const idsToDelete = new Set(ids); | |
| // 1. Delete from byId map | |
| ids.forEach((id) => { | |
| delete data.byId[id]; | |
| }); | |
| // 2. Filter allIds array | |
| data.allIds = data.allIds.filter((id) => !idsToDelete.has(id)); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment