Skip to content

NestJS ChangeLog is a change tracker for NestJS.

Notifications You must be signed in to change notification settings

Shift3/nestjs-changelog

Repository files navigation

NestJS ChangeLog

CircleCI

NestJS ChangeLog is a change tracker for NestJS. It is similar to PaperTrail in that it hooks into entity events and saves the changes. It stores a ChangeLog that you can view or even revert to previous versions of a tracked entity.

Getting Started

First install the package.

npm install --save nestjs-changelog

Wrap your typeorm ConnectionOptions with our helper function that adds the necessary entity and subscriber.

import { addChangeDetectionToConnection } from 'nestjs-changelog';
const TypeOrmConfig = addChangeDetectionToConnection({
    type: 'postgres',
    // ...
    // the rest of your regular typeorm config
    // ...
});

Add the Change entity to typeorm's ormconfig.json so that you can generate a migration for the new entity.

{
    "entities": [
        "src/database/models/**/*.entity.ts",
        "node_modules/nestjs-changelog/dist/change.entity.js"
    ],
}

Generate the new migration

# generate it
npx ts-node -r tsconfig-paths/register node_modules/.bin/typeorm migration:generate -n add_nestjs_changelog
# run it
npx ts-node -r tsconfig-paths/register node_modules/.bin/typeorm migration:run

Finally, add the module to your AppModule

import { ChangeModule } from 'nestjs-changelog';

@Module({
    imports: [
        TypeOrmModule.forRoot(TypeOrmConfig),
        ChangeModule.register({
            // this tells the change logger how to turn a user into a display name
            userToDisplayName: (user: any) => `${user.firstName} ${user.lastName}`
        }),
        // your other modules imports...
    ]
})

Tracking Changes

Now all you need to do to start tracking changes is pick an entity to track changes on and decorate it with the @TrackChanges decorator, for example:

@Entity()
@TrackChanges()
export class MyTrackedEntity extends BaseEntity {
    // ...
}

Any change that goes through typeorm's lifecycle hooks in subscribers will now be tracked. Each change will create a Change record in the database.

There are some options you can pass to @TrackChanges

/* only the attributes listed will trigger the creation of a change record,
   by default all attributes are watched */
only?: string[],

/* the attributes listed in `except` will not trigger the creation of a
   change record if only they are changed */
except?: string[],

/* allows you to choose whether to ignore timestamps like created at,
   updated at, and deleted at. By default, timestamps are ignored, set this
   to false to track changes to timestamps */
ignoresTimestamps?: boolean,

Retrieving the ChangeLog

You can retrieve change records for a tracked entity by using the ChangeRepository's changeLogQuery method. We return a SelectQueryBuilder<Change> from this method so that you can chain any additional methods you would like onto it, for example pagination.

const trackedEntity = await this.myTrackedEntityRepository.findOne(id);
const allChanges = await this.changeRepository
	.changeLogQuery(trackedEntity)
	.getMany();

Reverting a Change

You can revert any change like so:

const change = await this.changeRepository.findOne(changeId);
await this.changeRepository.revert(change);

Working with Change

You can preview what kind of entity reverting a change will generate. This would be useful for displaying to the user what reverting will do, before asking the user to confirm.

const entity = await this.changeRepository.retreiveEntityBeforeChange(change)

You can traverse the ChangeLog

// get the most recent change
const lastChange = await this.changeRepository.lastChange(entity); 

// get the oldest change.
const firstChange = await this.changeRepository.firstChange(entity); 

// get the next change (if any)
const nextChange = await this.changeRepository.next(change);

// get the previous change (if any)
const previousChange = await this.changeRepository.previous(change);

Caveats and Limitations

Associations

Currently the tracking of associations is limited to tracking @ManyToOne changes. We cannot track changes to @ManyToMany or @OneToMany without a more complicated procedure. This may be in the works later, depending on need.

Database Decisions

This package was made with the focus of keeping a valid changelog regardless of anything that happens. As a result of this a few design choices have been made. The users identifying name is denormalized in the database (we do not store a foreign key to the user) to the whoDisplay column. This is to ensure that even if a user is deleted from the database, we are still able to an identifying display name, as well as store their old id.

We also store the actual changes as a json blob as well. This is to ensure that the changes at the time are captured, regardless of how the database schema looks.

TypeORM API

TypeORM has a few eccentricities around which events can be monitored and which can't, that users must keep in mind. Since nestjs-changelog is based around detecting when an entity has changed, there are a few things to be aware of. Typeorm has methods of inserting, updating and deleting that talk directly with the database. These methods are .delete, .update, and insert on repository and entity manager. Below is a demonstration of what works and doesn't work in terms of automatic creation of Change entries.

let entity = new SomeEntity();
await entity.save(); // works
await repository.save(entity); // works
await repository.insert({name: 'name'}); // does NOT cause a change to be created

await repository.save(entity); // works
await connection.manager.save(entity); // works
await entity.save(); // works
await repository.update(entity.id, {name: 'name'}); // does NOT cause a change to be created

await repository.remove(entity); // works
await connection.manager.remove(entity); // works
await entity.remove(); // works
await repository.delete(entity); // does NOT cause a change to be created

// using the query builder creates a direct sql query that does NOT trigger typeorm
await repository
  .createQueryBuilder()
  .anything()

As a workaround, if you still want to create change entries while utilizing the query builder or direct database calls, you can use the changeRepository.createDatabaseEntry method.

// example of manually creating a change record because we are working with the direct db methods
// .insert
const insertResult = await connection.manager.insert(SomeEntity, { name: 'name' });
const entity = await connection.manager.findOne(SomeEntity, insertResult.identifiers[0].id);
// manually create an entry
await changeRepository.createChangeEntry(entity, ChangeAction.CREATE);

// .update
const entityBefore = await connection.manager.findOne(SomeEntity, 1);
await connection.manager.update(SomeEntity, { id: entityBefore.id }, { name: 'name' });
const entityAfter = await connection.manager.findOne(SomeEntity, entityBefore.id);
// manually create an entry
await changeRepository.createChangeEntry(entityAfter, ChangeAction.UPDATE, entityBefore);

// .delete
const entity = await connection.manager.findOne(SomeEntity, 1);
await connection.manager.delete(SomeEntity, { id: 1 });
// manually create an entry
await changeRepository.createChangeEntry(entity, ChangeAction.DELETE);

About

NestJS ChangeLog is a change tracker for NestJS.

Resources

Stars

Watchers

Forks

Packages

No packages published

Languages