Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 0 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,6 @@
"typescript": "~3.6.0"
},
"dependencies": {
"@sqltools/formatter": "1.2.2",
"app-root-path": "^3.0.0",
"buffer": "^5.5.0",
"chalk": "^4.1.0",
Expand Down
143 changes: 29 additions & 114 deletions src/commands/MigrationGenerateCommand.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import {ConnectionOptionsReader} from "../connection/ConnectionOptionsReader";
import {CommandUtils} from "./CommandUtils";
import {createConnection} from "../index";
import {MysqlDriver} from "../driver/mysql/MysqlDriver";
import {Query} from "../driver/Query";
import {SqlInMemory} from "../driver/SqlInMemory";
import {camelCase} from "../util/StringUtils";
import * as yargs from "yargs";
import {AuroraDataApiDriver} from "../driver/aurora-data-api/AuroraDataApiDriver";
import chalk from "chalk";
import { format } from "@sqltools/formatter/lib/sqlFormatter";

/**
* Generates a new migration file with sql needs to be executed to update schema.
Expand Down Expand Up @@ -34,22 +33,10 @@
alias: "dir",
describe: "Directory where migration should be created."
})
.option("p", {
alias: "pretty",
type: "boolean",
default: false,
describe: "Pretty-print generated SQL",
})
.option("f", {
alias: "config",
default: "ormconfig",
describe: "Name of the file with connection configuration."
})
.option("o", {
alias: "outputJs",
type: "boolean",
default: false,
describe: "Generate a migration file on Javascript instead of Typescript",
});
}

Expand All @@ -59,8 +46,7 @@
}

const timestamp = new Date().getTime();
const extension = args.outputJs ? ".js" : ".ts";
const filename = timestamp + "-" + args.name + extension;
const filename = timestamp + "-" + args.name + ".ts";
let directory = args.dir;

// if directory is not set then try to open tsconfig and find default path there
Expand Down Expand Up @@ -88,47 +74,23 @@
logging: false
});

const upSqls: string[] = [], downSqls: string[] = [];

const connection = await createConnection(connectionOptions);
let sqlInMemory: SqlInMemory;
try {
const sqlInMemory = await connection.driver.createSchemaBuilder().log();

if (args.pretty) {
sqlInMemory.upQueries.forEach(upQuery => {
upQuery.query = MigrationGenerateCommand.prettifyQuery(upQuery.query);
});
sqlInMemory.downQueries.forEach(downQuery => {
downQuery.query = MigrationGenerateCommand.prettifyQuery(downQuery.query);
});
}

// mysql is exceptional here because it uses ` character in to escape names in queries, that's why for mysql
// we are using simple quoted string instead of template string syntax
if (connection.driver instanceof MysqlDriver || connection.driver instanceof AuroraDataApiDriver) {
sqlInMemory.upQueries.forEach(upQuery => {
upSqls.push(" await queryRunner.query(\"" + upQuery.query.replace(new RegExp(`"`, "g"), `\\"`) + "\"" + MigrationGenerateCommand.queryParams(upQuery.parameters) + ");");
});
sqlInMemory.downQueries.forEach(downQuery => {
downSqls.push(" await queryRunner.query(\"" + downQuery.query.replace(new RegExp(`"`, "g"), `\\"`) + "\"" + MigrationGenerateCommand.queryParams(downQuery.parameters) + ");");
});
} else {
sqlInMemory.upQueries.forEach(upQuery => {
upSqls.push(" await queryRunner.query(`" + upQuery.query.replace(new RegExp("`", "g"), "\\`") + "`" + MigrationGenerateCommand.queryParams(upQuery.parameters) + ");");
});
sqlInMemory.downQueries.forEach(downQuery => {
downSqls.push(" await queryRunner.query(`" + downQuery.query.replace(new RegExp("`", "g"), "\\`") + "`" + MigrationGenerateCommand.queryParams(downQuery.parameters) + ");");
});
}
sqlInMemory = await connection.driver.createSchemaBuilder().log();
} finally {
await connection.close();
}

if (upSqls.length) {
if (sqlInMemory.upQueries.length) {
if (args.name) {
const fileContent = args.outputJs ?
MigrationGenerateCommand.getJavascriptTemplate(args.name as any, timestamp, upSqls, downSqls.reverse()) :
MigrationGenerateCommand.getTemplate(args.name as any, timestamp, upSqls, downSqls.reverse());
const migrationName = camelCase(args.name as any, true) + timestamp;
const templater = connection.options.migrationTemplater || defaultMigrationTemplater;
const fileContent = await templater({
name: migrationName,
upQueries: sqlInMemory.upQueries,
downQueries: sqlInMemory.downQueries,
});
const path = process.cwd() + "/" + (directory ? (directory + "/") : "") + filename;
await CommandUtils.createFile(path, fileContent);

Expand All @@ -146,76 +108,29 @@
process.exit(1);
}
}
}

// -------------------------------------------------------------------------
// Protected Static Methods
// -------------------------------------------------------------------------

/**
* Formats query parameters for migration queries if parameters actually exist
*/
protected static queryParams(parameters: any[] | undefined): string {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

one thing I discovered while doing this is that it is possible for typeorm to generate parameterized queries in migrations. I couldn't actually find any examples of these in our codebase (from looking at the typeorm code it seemed like maybe some DDL involving views might use parameters for some reason?). I opted to preserve this in my downstream change just in case there are scenarios where we need parameterization, as it wasn't too involved

if (!parameters || !parameters.length) {
return "";
}

return `, ${JSON.stringify(parameters)}`;
}

/**
* Gets contents of the migration file.
*/
protected static getTemplate(name: string, timestamp: number, upSqls: string[], downSqls: string[]): string {
const migrationName = `${camelCase(name, true)}${timestamp}`;

return `import {MigrationInterface, QueryRunner} from "typeorm";
export interface MigrationTemplateArgs {
name: string;
upQueries: Query[];
downQueries: Query[];
}
export type MigrationTemplater = (args: MigrationTemplateArgs) => string;
export async function defaultMigrationTemplater({ name, upQueries, downQueries }: MigrationTemplateArgs): Promise<string> {
const templateQuery = ({ query, parameters }: Query): string =>
" await queryRunner.query(`" + query.replace(new RegExp("`", "g"), "\\`") + "`" + (parameters && parameters.length ? `, ${JSON.stringify(parameters)}` : "") + ");";

Check failure

Code scanning / CodeQL

Incomplete string escaping or encoding High

This does not escape backslash characters in the input.

Copilot Autofix

AI about 2 months ago

To fix the problem, we must escape both backticks and backslashes in the query string before injecting it into the template literal within the migration file. The best approach is to first escape all backslashes, and then escape backticks: these should be done in this order, so that we don't double-escape backslashes that precede backticks. This can be done by chaining .replace(/\\/g, '\\\\') before .replace(//g, '\'). The fix is limited to the block within the function defaultMigrationTemplater, specifically in the function expression assigned to templateQuery (line 121); no external libraries are required for this basic escaping. No changes are needed outside this function or file.

Suggested changeset 1
src/commands/MigrationGenerateCommand.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/commands/MigrationGenerateCommand.ts b/src/commands/MigrationGenerateCommand.ts
--- a/src/commands/MigrationGenerateCommand.ts
+++ b/src/commands/MigrationGenerateCommand.ts
@@ -117,8 +117,14 @@
 }
 export type MigrationTemplater = (args: MigrationTemplateArgs) => string;
 export async function defaultMigrationTemplater({ name, upQueries, downQueries }: MigrationTemplateArgs): Promise<string> {
-    const templateQuery = ({ query, parameters }: Query): string => 
-        "        await queryRunner.query(`" + query.replace(new RegExp("`", "g"), "\\`") + "`" + (parameters && parameters.length ? `, ${JSON.stringify(parameters)}` : "") + ");";
+    const templateQuery = ({ query, parameters }: Query): string =>
+        "        await queryRunner.query(`" +
+            query
+                .replace(/\\/g, '\\\\')
+                .replace(/`/g, '\\`') +
+            "`" +
+            (parameters && parameters.length ? `, ${JSON.stringify(parameters)}` : "") +
+            ");";
     return `import { MigrationInterface, QueryRunner } from "typeorm";
 
 export class ${name} implements MigrationInterface {
EOF
@@ -117,8 +117,14 @@
}
export type MigrationTemplater = (args: MigrationTemplateArgs) => string;
export async function defaultMigrationTemplater({ name, upQueries, downQueries }: MigrationTemplateArgs): Promise<string> {
const templateQuery = ({ query, parameters }: Query): string =>
" await queryRunner.query(`" + query.replace(new RegExp("`", "g"), "\\`") + "`" + (parameters && parameters.length ? `, ${JSON.stringify(parameters)}` : "") + ");";
const templateQuery = ({ query, parameters }: Query): string =>
" await queryRunner.query(`" +
query
.replace(/\\/g, '\\\\')
.replace(/`/g, '\\`') +
"`" +
(parameters && parameters.length ? `, ${JSON.stringify(parameters)}` : "") +
");";
return `import { MigrationInterface, QueryRunner } from "typeorm";

export class ${name} implements MigrationInterface {
Copilot is powered by AI and may make mistakes. Always verify output.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am keeping this escaping consistent with the previous code

return `import { MigrationInterface, QueryRunner } from "typeorm";
export class ${migrationName} implements MigrationInterface {
name = '${migrationName}'
export class ${name} implements MigrationInterface {
name = '${name}'
public async up(queryRunner: QueryRunner): Promise<void> {
${upSqls.join(`
`)}
${upQueries.map(templateQuery).join("\n")}
}
public async down(queryRunner: QueryRunner): Promise<void> {
${downSqls.join(`
`)}
}
}
`;
}

/**
* Gets contents of the migration file in Javascript.
*/
protected static getJavascriptTemplate(name: string, timestamp: number, upSqls: string[], downSqls: string[]): string {
const migrationName = `${camelCase(name, true)}${timestamp}`;

return `const { MigrationInterface, QueryRunner } = require("typeorm");
module.exports = class ${migrationName} {
name = '${migrationName}'
async up(queryRunner) {
${upSqls.join(`
`)}
}
async down(queryRunner) {
${downSqls.join(`
`)}
${downQueries.map(templateQuery).join("\n")}
}
}
`;
}

/**
*
*/
protected static prettifyQuery(query: string) {
const formattedQuery = format(query, { indent: " " });
return "\n" + formattedQuery.replace(/^/gm, " ") + "\n ";
}
}
}
5 changes: 5 additions & 0 deletions src/connection/BaseConnectionOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,4 +179,9 @@ export interface BaseConnectionOptions {
* HEX: dynamically get metadata for use in query tagging
*/
readonly getMetadata?: () => Record<string, string> | null;

/**
* HEX: allow customization of migration templating
*/
readonly migrationTemplater?: (args: { name: string, upQueries: { query: string, parameters?: any[] }[], downQueries: { query: string, parameters?: any[] }[] }) => Promise<string>;
}
Loading