Node's Perpetuum Mobile


    0.2.0 • Public • Published


    Yet another change tracker for SQL tables. Most software systems require a Change Tracker component for the auditors. This component should record the 5Ws (who, when, where, why and what) of any database table change.

    Microsoft's SQL Server has an out of the box solution called Change Data Capture, which is great. However, you need an Enterprise license to enable this feature and its very expensive; at least for us startups and open sorcerers. yact implements a poor man's change tracking by using SQL triggers to insert an audit row.

    yact's twist is to store the before and after images of the change as XML content as opposed to most other implementations that add a row for each field that was changed. I believe this gives a light weight and flexible (read efficient) to change tracking.

    Getting started

    Install with npm

    > npm install -g yact

    Generate the script for the table triggers and the audit table into yact.sql

    > yact -a Employee Payroll


    > yact -help
    Usage: yact-cl [options] [table[:key]...]
     Generate the audit trigger script for the table(s)
       -h, --help           output usage information
       -V, --version        output the version number
       -a, --audit          include the script to create the Audit table
       -i, --individual     save the script(s) as individual files.
       -o, --output [file]  save the script(s) to the specified file, the default is "yact.sql"
       # create trigger script for contact table, primary key is contact_id
       yact contact
       # create trigger script for contact table, primary key is id
       yact contact:id

    The trigger

    The magic in the trigger is to join the inserted and deleted tables and then convert the rows to XML. These tables are supplied by the SQL server when the trigger is invoked. Each table has the same columns as table_name.

    insert into audit (table_name, old_content, new_content) 
        case when d.table_id is null then null else (select d.* for xml raw) end,
        case when i.table_id is null then null else (select i.* for xml raw) end
      from inserted as i
        full outer join deleted as d on i.table_id = d.table_id

    All you need to change is

    • declare\set @table_name
    • change table_id to the name of table's primary key.

    Caveat Emptor

    Triggers that insert (as yact does) change the @@identity value. All stored procedures should at least use scope_identity() instead of @@identity; see how not to retrieve identity value for more issues.

    audit table

    All changes (insert, update or delete) to a yact monitored table are stored in the audit table.

    Column Description
    audit_id A unique key for this audit entry. Keeps an ORM happy.
    operation The SQL operation (insert, update or delete) performed on the table_name.
    table_name What information was changed.
    old_content What was the old information; formatted as XML. Each column of the row is an XML attribute.
    new_content What is the new information; foratted as XML. Each column of the row is an XML attribute.
    who Who changed this information.
    when When was the information changed.
    where Where was the change performed from (IP address of the SQL client).
    why Why was this change performed. Not yet implemented.

    Who are you

    Identity is hard, queue Keith Moon's drum roll. yact uses the system_user for the default who value. This is appopriate for client/server (2-tier) systems; where each user logs into the database. But, for most 3-tier systems, the server has its own account for the database. In this scenario the who must be supplied by the trigger.

    Many 3-tier systems have a modifiedBy column in each table. In this case the trigger can be changed to use this column

    insert into audit (table_name, who, old_content, new_content) 
        case when d.table_id is null then null else (select d.* for xml raw) end,
        case when i.table_id is null then null else (select i.* for xml raw) end
      from inserted as i
        full outer join deleted as d on i.table_id = d.table_id

    Time is relative

    yact defaults when to sysdatetimeoffset, which includes the time zone offset of the SQL server. This works if all users are in the same time zone. If the software system has the user's time, then this value should be used in trigger's insert.

    By using the time zone offset, its possible to determine if the user accessed the system after working hours.

    However, its not possible to determine if the user accessed the system during a public holiday.


    npm i yact

    DownloadsWeekly Downloads






    Last publish


    • makaretu