Skip to content

Commit 8de8bb9

Browse files
committed
:sparkles feat(migrations): scaffold compass migrations
1 parent 5e82182 commit 8de8bb9

File tree

8 files changed

+521
-22
lines changed

8 files changed

+521
-22
lines changed

packages/backend/src/user/queries/user.queries.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
1+
import { ClientSession } from "mongodb";
12
import { getIdFilter } from "@backend/common/helpers/mongo.utils";
23
import mongoService from "@backend/common/services/mongo.service";
34

45
type Ids_User = "email" | "_id" | "google.googleId";
56

6-
export const findCompassUserBy = async (key: Ids_User, value: string) => {
7+
export const findCompassUserBy = async (
8+
key: Ids_User,
9+
value: string,
10+
session?: ClientSession,
11+
) => {
712
const filter = getIdFilter(key, value);
8-
const user = await mongoService.user.findOne(filter);
13+
const user = await mongoService.user.findOne(filter, { session });
914

1015
return user;
1116
};

packages/scripts/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
"dotenv": "^16.0.1",
1212
"inquirer": "^8.0.0",
1313
"lodash.uniqby": "^4.7.0",
14-
"shelljs": "^0.8.5"
14+
"shelljs": "^0.8.5",
15+
"umzug": "^3.8.2"
1516
},
1617
"devDependencies": {
1718
"@types/inquirer": "^9.0.1",

packages/scripts/src/cli.ts

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
// sort-imports-ignore
2-
import { Command } from "commander";
3-
import "./init";
2+
import "@scripts/init";
43

5-
import { CliValidator } from "./cli.validator";
6-
import { runBuild } from "./commands/build";
7-
import { startDeleteFlow } from "./commands/delete";
8-
import { inviteWaitlist } from "./commands/invite";
9-
import { runSeed } from "./commands/seed";
10-
import { ALL_PACKAGES, CATEGORY_VM } from "./common/cli.constants";
4+
import { CliValidator } from "@scripts/cli.validator";
5+
import { runBuild } from "@scripts/commands/build";
6+
import { startDeleteFlow } from "@scripts/commands/delete";
7+
import { inviteWaitlist } from "@scripts/commands/invite";
8+
import { runMigrator } from "@scripts/commands/migrate";
9+
import { ALL_PACKAGES, CATEGORY_VM } from "@scripts/common/cli.constants";
10+
import { MigratorType } from "@scripts/common/cli.types";
11+
import { Command } from "commander";
1112

1213
class CompassCli {
1314
private program: Command;
@@ -39,9 +40,8 @@ class CompassCli {
3940
await inviteWaitlist();
4041
break;
4142
}
43+
case cmd === "migrate":
4244
case cmd === "seed": {
43-
this.validator.validateSeed(options);
44-
await runSeed(user as string, force);
4545
break;
4646
}
4747
default:
@@ -84,9 +84,23 @@ class CompassCli {
8484
program.command("invite").description("invite users from the waitlist");
8585

8686
program
87+
.enablePositionalOptions(true)
88+
.passThroughOptions(true)
89+
.command("migrate")
90+
.helpOption(false)
91+
.allowUnknownOption(true)
92+
.description("run database schema migrations")
93+
.action(() => runMigrator(MigratorType.MIGRATION));
94+
95+
program
96+
.enablePositionalOptions(true)
97+
.passThroughOptions(true)
8798
.command("seed")
88-
.description("seed the database with events")
89-
.option("-u, --user <id>", "specify which user to seed events for");
99+
.helpOption(false)
100+
.allowUnknownOption(true)
101+
.description("run seed migrations to populate the database with data")
102+
.action(() => runMigrator(MigratorType.SEEDER));
103+
90104
return program;
91105
}
92106
}
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import glob from "fast-glob";
2+
import { readFile } from "node:fs/promises";
3+
import { parse, resolve, sep } from "node:path";
4+
import type { MigrationParams, RunnableMigration } from "umzug";
5+
import { MongoDBStorage, Umzug, UmzugCLI } from "umzug";
6+
import { MigrationContext, MigratorType } from "@scripts/common/cli.types";
7+
import { Logger } from "@core/logger/winston.logger";
8+
import mongoService from "@backend/common/services/mongo.service";
9+
10+
const logger = Logger("scripts.commands.migrations");
11+
12+
async function migrations(
13+
context: MigrationContext,
14+
): Promise<Array<RunnableMigration<MigrationContext>>> {
15+
const { unsafe } = context;
16+
const folder = `${context.migratorType.toLowerCase()}s`;
17+
const migrationsRoot = resolve(__dirname, "..", folder);
18+
const unsafeText = unsafe ? "unsafe " : "";
19+
const fileGlob = `${migrationsRoot}/**/*.{ts,js}`;
20+
const files = glob.sync(fileGlob, { absolute: true });
21+
22+
const migrations = await Promise.all(
23+
files.map(async (path) => {
24+
const { default: Migration } = (await import(path ?? "")) as {
25+
default: { new (): RunnableMigration<MigrationContext> };
26+
};
27+
28+
const migration = new Migration();
29+
const name = parse(path).name;
30+
31+
return {
32+
name,
33+
path,
34+
up: async (
35+
params: MigrationParams<MigrationContext>,
36+
): Promise<void> => {
37+
const { logger } = params.context;
38+
39+
logger.debug(`Running ${unsafeText}up migration(${name})`);
40+
logger.debug(path);
41+
42+
await migration.up(params);
43+
44+
logger.debug(`Up migration(${name}) run successful`);
45+
},
46+
down: async (
47+
params: MigrationParams<MigrationContext>,
48+
): Promise<void> => {
49+
if (!migration.down) return;
50+
51+
const { logger } = params.context;
52+
53+
logger.debug(`Running ${unsafeText}down migration(${name})`);
54+
logger.debug(path);
55+
56+
await migration.down(params);
57+
58+
logger.debug(`Down migration(${name}) ran successful`);
59+
},
60+
};
61+
}),
62+
);
63+
64+
return migrations.sort((prev, next) =>
65+
prev.path.split("/").pop()!.localeCompare(next.path.split("/").pop()!),
66+
);
67+
}
68+
69+
async function template({
70+
migratorType,
71+
filePath,
72+
migrationsRoot,
73+
}: {
74+
migratorType: MigratorType;
75+
filePath: string;
76+
migrationsRoot: string;
77+
}): Promise<[string, string][]> {
78+
const { base, name } = parse(filePath);
79+
const path = resolve(migrationsRoot, base);
80+
81+
return readFile(
82+
resolve(migrationsRoot, "..", "common", "migrator-template.ts"),
83+
).then((contents): [string, string][] => [
84+
[
85+
path,
86+
contents
87+
.toString()
88+
.replace("class Template", `class ${migratorType}`)
89+
.replace("{{name}}", name.split(".").pop()!)
90+
.replace("{{path}}", path.replace(`${migrationsRoot}${sep}`, "")),
91+
],
92+
]);
93+
}
94+
95+
async function createMigrationCli(
96+
migratorType: MigratorType,
97+
): Promise<UmzugCLI> {
98+
const folder = `${migratorType.toLowerCase()}s`;
99+
const migrationsRoot = resolve(__dirname, "..", folder);
100+
const collection = mongoService.db.collection(folder);
101+
const storage = new MongoDBStorage({ collection });
102+
103+
const umzug = new Umzug<MigrationContext>({
104+
storage,
105+
logger: undefined,
106+
migrations,
107+
create: {
108+
folder: migrationsRoot,
109+
template: (filePath) =>
110+
template({ migratorType, filePath, migrationsRoot }),
111+
},
112+
context: async (): Promise<MigrationContext> => {
113+
const unsafe = cli.getFlagParameter("--unsafe").value;
114+
115+
return { logger, migratorType, unsafe };
116+
},
117+
});
118+
119+
const cli = new UmzugCLI(umzug as Umzug<object>, {
120+
toolDescription: "Compass migrator",
121+
toolFileName: "cli.ts",
122+
});
123+
124+
cli.defineFlagParameter({
125+
parameterLongName: "--unsafe",
126+
parameterShortName: "-u",
127+
description: "Run unsafe migration code within up and down methods",
128+
environmentVariable: "MIGRATION_UNSAFE",
129+
});
130+
131+
return cli;
132+
}
133+
134+
export const runMigrator = async (
135+
migratorType: MigratorType,
136+
useDynamicDb = false,
137+
): Promise<void> => {
138+
try {
139+
await mongoService.start(useDynamicDb);
140+
141+
const cli = await createMigrationCli(migratorType);
142+
143+
await cli.executeAsync(process.argv.slice(3));
144+
145+
await mongoService.stop();
146+
147+
process.exit(0);
148+
} catch (error) {
149+
logger.error(error);
150+
151+
process.exit(1);
152+
}
153+
};

packages/scripts/src/common/cli.types.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { z } from "zod";
2+
import type { Logger } from "@core/logger/winston.logger";
23

34
export const Schema_Options_Cli_Root = z.object({
45
force: z.boolean().optional(),
@@ -22,3 +23,14 @@ export type Options_Cli = Options_Cli_Root &
2223
Options_Cli_Delete;
2324

2425
export type Environment_Cli = "local" | "staging" | "production";
26+
27+
export enum MigratorType {
28+
SEEDER = "Seeder",
29+
MIGRATION = "Migration",
30+
}
31+
32+
export interface MigrationContext {
33+
logger: ReturnType<typeof Logger>;
34+
migratorType: MigratorType;
35+
unsafe: boolean;
36+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import type { MigrationParams, RunnableMigration } from "umzug";
2+
import { MigrationContext } from "@scripts/common/cli.types";
3+
4+
export default class Template implements RunnableMigration<MigrationContext> {
5+
readonly name: string = "{{name}}";
6+
readonly path: string = "{{path}}";
7+
8+
async up(params: MigrationParams<MigrationContext>): Promise<void> {
9+
const { context } = params;
10+
const { logger } = context;
11+
12+
logger.debug(`running up migrations(${this.name}).`);
13+
14+
return Promise.resolve();
15+
}
16+
17+
async down(params: MigrationParams<MigrationContext>): Promise<void> {
18+
const { context, name, path } = params;
19+
const { unsafe } = context;
20+
const { logger } = context;
21+
const unsafeText = unsafe ? " unsafe " : "";
22+
23+
logger.debug(`running${unsafeText}down migration`, { name, path });
24+
25+
return Promise.resolve();
26+
}
27+
}

0 commit comments

Comments
 (0)