HFU Digital Docs
CourseKitStarPlan

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

OptionTypeDefaultDescription
ignoreFieldsstring[][]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 ===.
  • Date instances compare by .getTime().
  • Objects and arrays compare by JSON.stringify (deep, order-sensitive).
  • null and undefined are 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'],
});

On this page