Skip to content

Commit

Permalink
Update updated_at field automatically
Browse files Browse the repository at this point in the history
  • Loading branch information
hectorgomezv committed Jun 19, 2024
1 parent 5a04f38 commit a22412f
Show file tree
Hide file tree
Showing 5 changed files with 92 additions and 19 deletions.
23 changes: 21 additions & 2 deletions migrations/00001_accounts/index.sql
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,31 @@ DROP TABLE IF EXISTS groups,
accounts CASCADE;

CREATE TABLE
groups (id SERIAL PRIMARY KEY);
groups (
id SERIAL PRIMARY KEY,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);

CREATE TABLE
accounts (
id SERIAL PRIMARY KEY,
group_id INTEGER REFERENCES groups (id),
address CHARACTER VARYING(42) NOT NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
UNIQUE (address)
);
);

CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE OR REPLACE TRIGGER update_accounts_updated_at
BEFORE UPDATE ON accounts
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
51 changes: 47 additions & 4 deletions migrations/__tests__/00001_accounts.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,22 @@ import { dbFactory } from '@/__tests__/db.factory';
import { PostgresDatabaseMigrator } from '@/datasources/db/postgres-database.migrator';
import { Sql } from 'postgres';

interface AccountRow {
id: number;
group_id: number;
created_at: Date;
updated_at: Date;
address: string;
}

describe('Migration 00001_accounts', () => {
const sql = dbFactory();
const migrator = new PostgresDatabaseMigrator(sql);

afterAll(async () => {
await sql.end();
});

it('runs successfully', async () => {
await sql`DROP TABLE IF EXISTS groups, accounts CASCADE;`;

Expand All @@ -32,20 +44,51 @@ describe('Migration 00001_accounts', () => {
columns: [
{ column_name: 'id' },
{ column_name: 'group_id' },
{ column_name: 'created_at' },
{ column_name: 'updated_at' },
{ column_name: 'address' },
],
rows: [],
},
groups: {
columns: [
{
column_name: 'id',
},
{ column_name: 'id' },
{ column_name: 'created_at' },
{ column_name: 'updated_at' },
],
rows: [],
},
});
});

await sql.end();
it('should add and update row timestamps', async () => {
await sql`DROP TABLE IF EXISTS groups, accounts CASCADE;`;

const result: {
before: unknown;
after: AccountRow[];
} = await migrator.test({
migration: '00001_accounts',
after: async (sql: Sql): Promise<AccountRow[]> => {
await sql`INSERT INTO groups (id) VALUES (1);`;
await sql`INSERT INTO accounts (id, group_id, address) VALUES (1, 1, '0x0000');`;
await sql`UPDATE accounts set address = '0x0001' WHERE id = 1;`;
return await sql<AccountRow[]>`SELECT * FROM accounts`;
},
});

const createdAt = new Date(result.after[0].created_at);
const updatedAt = new Date(result.after[0].updated_at);

expect(result.after).toStrictEqual(
expect.arrayContaining([
expect.objectContaining({
created_at: createdAt,
updated_at: updatedAt,
}),
]),
);

expect(updatedAt.getTime()).toBeGreaterThan(createdAt.getTime());
});
});
4 changes: 4 additions & 0 deletions src/datasources/accounts/accounts.datasource.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ describe('AccountsDatasource tests', () => {
id: expect.any(Number),
group_id: null,
address,
created_at: expect.any(Date),
updated_at: expect.any(Date),
});
});

Expand All @@ -67,6 +69,8 @@ describe('AccountsDatasource tests', () => {
id: expect.any(Number),
group_id: null,
address,
created_at: expect.any(Date),
updated_at: expect.any(Date),
});
});

Expand Down
4 changes: 2 additions & 2 deletions src/datasources/db/postgres-database.migrator.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ describe('PostgresDatabaseMigrator tests', () => {
await expect(
target.test({
migration: migration1.name,
before: (sql) => sql`SELECT * FROM test`,
before: (sql) => sql`SELECT * FROM test`.catch(() => undefined),
after: (sql) => sql`SELECT * FROM test`,
folder,
}),
Expand Down Expand Up @@ -265,7 +265,7 @@ describe('PostgresDatabaseMigrator tests', () => {
target.test({
migration: migration3.name,
before: (sql) => sql`SELECT * FROM test`,
after: (sql) => sql`SELECT * FROM test`,
after: (sql) => sql`SELECT * FROM test`.catch(() => undefined),
folder,
}),
).resolves.toStrictEqual({
Expand Down
29 changes: 18 additions & 11 deletions src/datasources/db/postgres-database.migrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ type Migration = {
name: string;
};

type TestResult<BeforeType, AfterType> = {
before: BeforeType | undefined;
after: AfterType;
};

/**
* Migrates a Postgres database using SQL and JavaScript files.
*
Expand Down Expand Up @@ -51,7 +56,8 @@ export class PostgresDatabaseMigrator {
}

/**
* @private migrates up to/allows for querying before/after migration to test it.
* Migrates up to/allows for querying before/after migration to test it.
* Uses generics to allow the caller to specify the return type of the before/after functions.
*
* Note: each migration is ran in separate transaction to allow queries in between.
*
Expand All @@ -72,15 +78,12 @@ export class PostgresDatabaseMigrator {
* expect(result.after).toStrictEqual(expected);
* ```
*/
async test(args: {
async test<BeforeType, AfterType>(args: {
migration: string;
before?: (sql: Sql) => Promise<unknown>;
after: (sql: Sql) => Promise<unknown>;
before?: (sql: Sql) => Promise<BeforeType>;
after: (sql: Sql) => Promise<AfterType>;
folder?: string;
}): Promise<{
before: unknown;
after: unknown;
}> {
}): Promise<TestResult<BeforeType, AfterType>> {
const migrations = this.getMigrations(
args.folder ?? PostgresDatabaseMigrator.MIGRATIONS_FOLDER,
);
Expand All @@ -97,21 +100,25 @@ export class PostgresDatabaseMigrator {
// Get migrations up to the specified migration
const migrationsToTest = migrations.slice(0, migrationIndex + 1);

let before: unknown;
let before: BeforeType | undefined;

for await (const migration of migrationsToTest) {
const isMigrationBeingTested = migration.path.includes(args.migration);

if (isMigrationBeingTested && args.before) {
before = await args.before(this.sql).catch(() => undefined);
before = await args.before(this.sql).catch((err) => {
throw Error(`Error running before function: ${err}`);
});
}

await this.sql.begin((transaction) => {
return this.run({ transaction, migration });
});
}

const after = await args.after(this.sql).catch(() => undefined);
const after = await args.after(this.sql).catch((err) => {
throw Error(`Error running after function: ${err}`);
});

return { before, after };
}
Expand Down

0 comments on commit a22412f

Please sign in to comment.