Skip to content

Instantly share code, notes, and snippets.

@orrisroot
Created November 23, 2025 09:39
Show Gist options
  • Select an option

  • Save orrisroot/29e090c5d6f055fe230cd6f0f6dc43af to your computer and use it in GitHub Desktop.

Select an option

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.
/**
* @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