• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

safe-global / safe-client-gateway / 9554816047

17 Jun 2024 09:10PM CUT coverage: 49.427%. Remained the same
9554816047

Pull #1661

github

web-flow
Bump docker/build-push-action from 5 to 6

Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 5 to 6.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v5...v6)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Pull Request #1661: Bump docker/build-push-action from 5 to 6

392 of 2367 branches covered (16.56%)

Branch coverage included in aggregate %.

3967 of 6452 relevant lines covered (61.48%)

12.47 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

8.6
/src/datasources/db/postgres-database.migrator.ts
1
import { Inject, Injectable } from '@nestjs/common';
16✔
2
import fs from 'node:fs';
16✔
3
import { join } from 'node:path';
16✔
4
import type { Sql, TransactionSql } from 'postgres';
5

6
type Migration = {
7
  path: string;
8
  id: number;
9
  name: string;
10
};
11

12
/**
13
 * Migrates a Postgres database using SQL and JavaScript files.
14
 *
15
 * Migrations should be in a directory, prefixed with a 5-digit number,
16
 * and contain either an `index.sql` or `index.js` file.
17
 *
18
 * This is heavily inspired by `postgres-shift`
19
 * @see https://github.com/porsager/postgres-shift/blob/master/index.js
20
 */
21
@Injectable()
22
export class PostgresDatabaseMigrator {
16✔
23
  private static readonly MIGRATIONS_FOLDER = join(process.cwd(), 'migrations');
16✔
24
  private static readonly SQL_MIGRATION_FILE = 'index.sql';
16✔
25
  private static readonly JS_MIGRATION_FILE = 'index.js';
16✔
26
  private static readonly MIGRATIONS_TABLE = 'migrations';
16✔
27

28
  constructor(@Inject('DB_INSTANCE') private readonly sql: Sql) {}
×
29

30
  /**
31
   * Runs/records migrations not present in the {@link PostgresMigrator.MIGRATIONS_TABLE} table.
32
   *
33
   * Note: all migrations are run in a single transaction for optimal performance.
34
   */
35
  async migrate(
36
    path = PostgresDatabaseMigrator.MIGRATIONS_FOLDER,
×
37
  ): Promise<void> {
38
    const migrations = this.getMigrations(path);
×
39

40
    await this.assertMigrationsTable();
×
41

42
    const last = await this.getLastRunMigration();
×
43
    const remaining = migrations.slice(last?.id ?? 0);
×
44

45
    await this.sql.begin(async (transaction: TransactionSql) => {
×
46
      for (const current of remaining) {
×
47
        await this.run({ transaction, migration: current });
×
48
        await this.setLastRunMigration({ transaction, migration: current });
×
49
      }
50
    });
51
  }
52

53
  /**
54
   * @private migrates up to/allows for querying before/after migration to test it.
55
   *
56
   * Note: each migration is ran in separate transaction to allow queries in between.
57
   *
58
   * @param args.migration - migration to test
59
   * @param args.folder - folder to search for migrations
60
   * @param args.before - function to run before each migration
61
   * @param args.after - function to run after each migration
62
   *
63
   * @example
64
   * ```typescript
65
   * const result = await migrator.test({
66
   *   migration: '00001_initial',
67
   *   before: (sql) => sql`SELECT * FROM <table_name>`,
68
   *   after: (sql) => sql`SELECT * FROM <table_name>`,
69
   * });
70
   *
71
   * expect(result.before).toBeUndefined();
72
   * expect(result.after).toStrictEqual(expected);
73
   * ```
74
   */
75
  async test(args: {
76
    migration: string;
77
    before?: (sql: Sql) => Promise<unknown>;
78
    after: (sql: Sql) => Promise<unknown>;
79
    folder?: string;
80
  }): Promise<{
81
    before: unknown;
82
    after: unknown;
83
  }> {
84
    const migrations = this.getMigrations(
×
85
      args.folder ?? PostgresDatabaseMigrator.MIGRATIONS_FOLDER,
×
86
    );
87

88
    // Find index of migration to test
89
    const migrationIndex = migrations.findIndex((migration) => {
×
90
      return migration.path.includes(args.migration);
×
91
    });
92

93
    if (migrationIndex === -1) {
×
94
      throw new Error(`Migration ${args.migration} not found`);
×
95
    }
96

97
    // Get migrations up to the specified migration
98
    const migrationsToTest = migrations.slice(0, migrationIndex + 1);
×
99

100
    let before: unknown;
101

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

105
      if (isMigrationBeingTested && args.before) {
×
106
        before = await args.before(this.sql).catch(() => undefined);
×
107
      }
108

109
      await this.sql.begin((transaction) => {
×
110
        return this.run({ transaction, migration });
×
111
      });
112
    }
113

114
    const after = await args.after(this.sql).catch(() => undefined);
×
115

116
    return { before, after };
×
117
  }
118

119
  /**
120
   * Retrieves all migrations found at the specified path.
121
   *
122
   * @param path - path to search for migrations
123
   *
124
   * @returns array of {@link Migration}
125
   */
126
  private getMigrations(path: string): Array<Migration> {
127
    const migrations = fs
×
128
      .readdirSync(path)
129
      .filter((file) => {
130
        const isDirectory = fs.statSync(join(path, file)).isDirectory();
×
131
        const isMigration = file.match(/^[0-9]{5}_/);
×
132
        return isDirectory && isMigration;
×
133
      })
134
      .sort()
135
      .map((file) => {
136
        return {
×
137
          path: join(path, file),
138
          id: parseInt(file.slice(0, 5)),
139
          name: file.slice(6),
140
        };
141
      });
142

143
    if (migrations.length === 0) {
×
144
      throw new Error('No migrations found');
×
145
    }
146

147
    const latest = migrations.at(-1);
×
148
    if (latest?.id !== migrations.length) {
×
149
      throw new Error('Migrations numbered inconsistency');
×
150
    }
151

152
    return migrations;
×
153
  }
154

155
  /**
156
   * Adds specified migration to the transaction if supported.
157
   *
158
   * @param args.transaction - {@link TransactionSql} to migration within
159
   * @param args.migration - {@link Migration} to add
160
   */
161
  private async run(args: {
162
    transaction: TransactionSql;
163
    migration: Migration;
164
  }): Promise<void> {
165
    const isSql = fs.existsSync(
×
166
      join(args.migration.path, PostgresDatabaseMigrator.SQL_MIGRATION_FILE),
167
    );
168
    const isJs = fs.existsSync(
×
169
      join(args.migration.path, PostgresDatabaseMigrator.JS_MIGRATION_FILE),
170
    );
171

172
    if (isSql) {
×
173
      await args.transaction.file(
×
174
        join(args.migration.path, PostgresDatabaseMigrator.SQL_MIGRATION_FILE),
175
      );
176
    } else if (isJs) {
×
177
      const file = (await import(
×
178
        join(args.migration.path, PostgresDatabaseMigrator.JS_MIGRATION_FILE)
×
179
      )) as {
180
        default: (transaction: TransactionSql) => Promise<void>;
181
      };
182
      await file.default(args.transaction);
×
183
    } else {
184
      throw new Error(`No migration file found for ${args.migration.path}`);
×
185
    }
186
  }
187
  /**
188
   * Creates the {@link PostgresDatabaseMigrator.MIGRATIONS_TABLE} table if it does not exist.
189
   */
190
  private async assertMigrationsTable(): Promise<void> {
191
    try {
×
192
      await this.sql`SELECT
×
193
                        '${this.sql(PostgresDatabaseMigrator.MIGRATIONS_TABLE)}'::regclass`;
194
    } catch {
195
      await this.sql`CREATE TABLE
×
196
                        ${this.sql(PostgresDatabaseMigrator.MIGRATIONS_TABLE)} (
197
                            id SERIAL PRIMARY KEY,
198
                            created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
199
                            name TEXT
200
                        )`;
201
    }
202
  }
203

204
  /**
205
   * Retrieves the last run migration from the {@link PostgresDatabaseMigrator.MIGRATIONS_TABLE} table.
206
   *
207
   * @returns last run {@link Migration}
208
   */
209
  private async getLastRunMigration(): Promise<Migration> {
210
    const [last] = await this.sql<Array<Migration>>`SELECT
×
211
                                                        id
212
                                                    FROM
213
                                                        ${this.sql(PostgresDatabaseMigrator.MIGRATIONS_TABLE)}
214
                                                    ORDER BY
215
                                                        id DESC
216
                                                    LIMIT
217
                                                        1`;
218

219
    return last;
×
220
  }
221

222
  /**
223
   * Adds the last run migration to the {@link PostgresDatabaseMigrator.MIGRATIONS_TABLE} table.
224
   *
225
   * @param args.transaction - {@link TransactionSql} to set within
226
   * @param args.migration - {@link Migration} to set
227
   */
228
  private async setLastRunMigration(args: {
229
    transaction: TransactionSql;
230
    migration: Migration;
231
  }): Promise<void> {
232
    await args.transaction`INSERT INTO ${this.sql(PostgresDatabaseMigrator.MIGRATIONS_TABLE)} (
×
233
                               id,
234
                               name
235
                           ) VALUES (
236
                               ${args.migration.id},
237
                               ${args.migration.name}
238
                           )`;
239
  }
240
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc