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

safe-global / safe-client-gateway / 10670149631

02 Sep 2024 03:53PM CUT coverage: 46.327%. Remained the same
10670149631

Pull #1884

github

hectorgomezv
Add NativeStakingMapper.getValueFromDataDecoded TSDoc
Pull Request #1884: Add NativeStakingMapper.getValueFromDataDecoded TSDoc

499 of 3099 branches covered (16.1%)

Branch coverage included in aggregate %.

4811 of 8363 relevant lines covered (57.53%)

12.25 hits per line

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

8.33
/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
type TestResult<BeforeType, AfterType> = {
13
  before: BeforeType | undefined;
14
  after: AfterType;
15
};
16

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

33
  constructor(@Inject('DB_INSTANCE') private readonly sql: Sql) {}
×
34

35
  /**
36
   * Runs/records migrations not present in the {@link PostgresDatabaseMigrator.MIGRATIONS_TABLE} table.
37
   *
38
   * Note: all migrations are run in a single transaction for optimal performance.
39
   */
40
  async migrate(
41
    path = PostgresDatabaseMigrator.MIGRATIONS_FOLDER,
×
42
  ): Promise<Migration[]> {
43
    const migrations = this.getMigrations(path);
×
44

45
    await this.assertMigrationsTable();
×
46

47
    const last = await this.getLastRunMigration();
×
48
    const remaining = migrations.slice(last?.id ?? 0);
×
49

50
    await this.sql.begin(async (transaction: TransactionSql) => {
×
51
      for (const current of remaining) {
×
52
        await this.run({ transaction, migration: current });
×
53
        await this.setLastRunMigration({ transaction, migration: current });
×
54
      }
55
    });
56

57
    return remaining;
×
58
  }
59

60
  /**
61
   * Migrates up to/allows for querying before/after migration to test it.
62
   * Uses generics to allow the caller to specify the return type of the before/after functions.
63
   *
64
   * Note: each migration is ran in separate transaction to allow queries in between.
65
   *
66
   * @param args.migration - migration to test
67
   * @param args.folder - folder to search for migrations
68
   * @param args.before - function to run before each migration
69
   * @param args.after - function to run after each migration
70
   *
71
   * @example
72
   * ```typescript
73
   * const result = await migrator.test({
74
   *   migration: '00001_initial',
75
   *   before: (sql) => sql`SELECT * FROM <table_name>`,
76
   *   after: (sql) => sql`SELECT * FROM <table_name>`,
77
   * });
78
   *
79
   * expect(result.before).toBeUndefined();
80
   * expect(result.after).toStrictEqual(expected);
81
   * ```
82
   */
83
  async test<BeforeType, AfterType>(args: {
84
    migration: string;
85
    before?: (sql: Sql) => Promise<BeforeType>;
86
    after: (sql: Sql) => Promise<AfterType>;
87
    folder?: string;
88
  }): Promise<TestResult<BeforeType, AfterType>> {
89
    const migrations = this.getMigrations(
×
90
      args.folder ?? PostgresDatabaseMigrator.MIGRATIONS_FOLDER,
×
91
    );
92

93
    // Find index of migration to test
94
    const migrationIndex = migrations.findIndex((migration) => {
×
95
      return migration.path.includes(args.migration);
×
96
    });
97

98
    if (migrationIndex === -1) {
×
99
      throw new Error(`Migration ${args.migration} not found`);
×
100
    }
101

102
    // Get migrations up to the specified migration
103
    const migrationsToTest = migrations.slice(0, migrationIndex + 1);
×
104

105
    let before: BeforeType | undefined;
106

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

110
      if (isMigrationBeingTested && args.before) {
×
111
        before = await args.before(this.sql).catch((err) => {
×
112
          throw Error(`Error running before function: ${err}`);
×
113
        });
114
      }
115

116
      await this.sql.begin((transaction) => {
×
117
        return this.run({ transaction, migration });
×
118
      });
119
    }
120

121
    const after = await args.after(this.sql).catch((err) => {
×
122
      throw Error(`Error running after function: ${err}`);
×
123
    });
124

125
    return { before, after };
×
126
  }
127

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

152
    if (migrations.length === 0) {
×
153
      throw new Error('No migrations found');
×
154
    }
155

156
    const latest = migrations.at(-1);
×
157
    if (latest?.id !== migrations.length) {
×
158
      throw new Error('Migrations numbered inconsistency');
×
159
    }
160

161
    return migrations;
×
162
  }
163

164
  /**
165
   * Adds specified migration to the transaction if supported.
166
   *
167
   * @param args.transaction - {@link TransactionSql} to migration within
168
   * @param args.migration - {@link Migration} to add
169
   */
170
  private async run(args: {
171
    transaction: TransactionSql;
172
    migration: Migration;
173
  }): Promise<void> {
174
    const isSql = fs.existsSync(
×
175
      join(args.migration.path, PostgresDatabaseMigrator.SQL_MIGRATION_FILE),
176
    );
177
    const isJs = fs.existsSync(
×
178
      join(args.migration.path, PostgresDatabaseMigrator.JS_MIGRATION_FILE),
179
    );
180

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

213
  /**
214
   * Retrieves the last run migration from the {@link PostgresDatabaseMigrator.MIGRATIONS_TABLE} table.
215
   *
216
   * @returns last run {@link Migration}
217
   */
218
  private async getLastRunMigration(): Promise<Migration> {
219
    const [last] = await this.sql<Array<Migration>>`SELECT
×
220
                                                        id
221
                                                    FROM
222
                                                        ${this.sql(PostgresDatabaseMigrator.MIGRATIONS_TABLE)}
223
                                                    ORDER BY
224
                                                        id DESC
225
                                                    LIMIT
226
                                                        1`;
227

228
    return last;
×
229
  }
230

231
  /**
232
   * Adds the last run migration to the {@link PostgresDatabaseMigrator.MIGRATIONS_TABLE} table.
233
   *
234
   * @param args.transaction - {@link TransactionSql} to set within
235
   * @param args.migration - {@link Migration} to set
236
   */
237
  private async setLastRunMigration(args: {
238
    transaction: TransactionSql;
239
    migration: Migration;
240
  }): Promise<void> {
241
    await args.transaction`INSERT INTO ${this.sql(PostgresDatabaseMigrator.MIGRATIONS_TABLE)} (
×
242
                               id,
243
                               name
244
                           ) VALUES (
245
                               ${args.migration.id},
246
                               ${args.migration.name}
247
                           )`;
248
  }
249
}
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