Change Detection
Diff two snapshots of StarPlan-tracked entities into a serializable list of created/updated/deleted records
detectChanges is a generic snapshot-diff utility. Given a previous and a current snapshot of an entity collection (each row keyed by a stable id), it produces a ChangeRecord[] describing every create / update / delete, with the specific fields that changed.
The output is intentionally JSON-serializable so consumers can persist directly — for example into a CkStarPlanChangeLog table in their Prisma schema, or into the platform-wide UserStarPlanChangeFeed for per-user notifications.
Usage
import { detectChanges, type ChangeRecord } from '@hfu.digital/coursekit-starplan';
interface Course {
id: string;
title: string;
instructor: string | null;
room: string | null;
updatedAt: Date;
}
const previous: Course[] = await db.starPlanCourse.findMany();
const current: Course[] = await syncFromStarPlan();
const changes: ChangeRecord<Course>[] = detectChanges(
'starplan-course',
previous,
current,
{ ignoreFields: ['updatedAt'] },
);
for (const change of changes) {
switch (change.action) {
case 'created': /* insert */ break;
case 'updated': /* patch + log change.changedFields */ break;
case 'deleted': /* tombstone */ break;
}
}API
function detectChanges<T extends object>(
entityType: string,
previous: ReadonlyArray<T & { id: string }>,
current: ReadonlyArray<T & { id: string }>,
options?: DetectChangesOptions,
): ChangeRecord<T>[];DetectChangesOptions
| Option | Type | Default | Description |
|---|---|---|---|
ignoreFields | string[] | [] | Field names whose changes should never be reported (updatedAt, derived timestamps, computed fields) |
ChangeRecord
type ChangeAction = 'created' | 'updated' | 'deleted';
interface ChangeRecord<T extends object = Record<string, unknown>> {
entityType: string;
entityId: string;
action: ChangeAction;
/** Present on 'updated' and 'deleted'. */
previousData: T | null;
/** Present on 'created' and 'updated'. */
newData: T | null;
/** Field names that changed. Empty for create/delete. */
changedFields: string[];
}Equality semantics
- Primitive values compare with
===. Dateinstances compare by.getTime().- Objects and arrays compare by
JSON.stringify(deep, order-sensitive). nullandundefinedare distinct.
If you need different semantics (e.g. unordered set comparison) — pre-normalize the input before passing it in.
Pairing with content hashing
Use generateContentHash as the id field on each row when iCal UID is unstable. Then detectChanges correctly tracks "the same logical event" across syncs, even when upstream identifiers churn.
const events = parsed.map((ev) => ({
id: generateContentHash({ semesterId, summary: ev.summary, weekday: ev.weekday, studyBlock: ev.studyBlock, startTime: ev.dtstart }),
title: ev.summary,
/* ... */
}));
const diff = detectChanges('starplan-event', previousEvents, events, {
ignoreFields: ['lastSyncedAt'],
});