diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index c7e10a0..b1d3eea 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -48,7 +48,20 @@ jobs: - name: Run Test as Standalone working-directory: ./ts/testdata run: | - atlas migrate diff --env typeorm --var dialect=${{ matrix.dialect }} + atlas migrate diff --env typeorm -c "file://atlas-standalone.hcl" --var dialect=${{ matrix.dialect }} + - name: Verify migrations generated + run: | + status=$(git status --porcelain) + if [ -n "$status" ]; then + echo "you need to run 'atlas migrate diff --env typeorm' and commit the changes" + echo "$status" + git --no-pager diff + exit 1 + fi + - name: Run Test as ts Script + working-directory: ./ts/testdata + run: | + atlas migrate diff --env typeorm -c "file://atlas-script.hcl" --var dialect=${{ matrix.dialect }} - name: Verify migrations generated run: | status=$(git status --porcelain) diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..902eaa9 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "atlas-provider-typeorm", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/ts/package-lock.json b/ts/package-lock.json index 3c165b4..6198012 100644 --- a/ts/package-lock.json +++ b/ts/package-lock.json @@ -1,11 +1,11 @@ { - "name": "@ariga/ts-atlas-provider-sequelize", + "name": "@ariga/ts-atlas-provider-typeorm", "version": "0.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "@ariga/ts-atlas-provider-sequelize", + "name": "@ariga/ts-atlas-provider-typeorm", "version": "0.0.1", "dependencies": { "mssql": "^9.3.2", @@ -15,8 +15,12 @@ "sqlite3": "^5.1.6", "typeorm": "0.3.17" }, + "bin": { + "ts-atlas-provider-typeorm": "src/index.ts" + }, "devDependencies": { "@types/node": "^16.11.10", + "@types/yargs": "^17.0.26", "@typescript-eslint/eslint-plugin": "^6.7.3", "@typescript-eslint/parser": "^6.7.3", "eslint": "^8.50.0", @@ -625,6 +629,21 @@ "integrity": "sha512-OxepLK9EuNEIPxWNME+C6WwbRAOOI2o2BaQEGzz5Lu2e4Z5eDnEo+/aVEDMIXywoJitJ7xWd641wrGLZdtwRyw==", "dev": true }, + "node_modules/@types/yargs": { + "version": "17.0.26", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.26.tgz", + "integrity": "sha512-Y3vDy2X6zw/ZCumcwLpdhM5L7jmyGpmBCTYMHDLqT2IKVMYRRLdv6ZakA+wxhra6Z/3bwhNbNl9bDGXaFU+6rw==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.1", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.1.tgz", + "integrity": "sha512-axdPBuLuEJt0c4yI5OZssC19K2Mq1uKdrfZBzuxLvaztgqUtFYZUNw7lETExPYJR9jdEoIg4mb7RQKRQzOkeGQ==", + "dev": true + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "6.7.3", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.7.3.tgz", diff --git a/ts/package.json b/ts/package.json index d1af221..68fdcfb 100644 --- a/ts/package.json +++ b/ts/package.json @@ -1,10 +1,12 @@ { - "name": "@ariga/ts-atlas-provider-sequelize", + "name": "@ariga/ts-atlas-provider-typeorm", "description": "Load Typeorm models into an Atlas project.", "version": "0.0.1", "type": "commonjs", + "bin": "src/index.ts", "devDependencies": { "@types/node": "^16.11.10", + "@types/yargs": "^17.0.26", "@typescript-eslint/eslint-plugin": "^6.7.3", "@typescript-eslint/parser": "^6.7.3", "eslint": "^8.50.0", diff --git a/ts/src/index.ts b/ts/src/index.ts old mode 100644 new mode 100755 index df023dd..2880099 --- a/ts/src/index.ts +++ b/ts/src/index.ts @@ -1,66 +1,45 @@ -import { DataSource, EntitySchema, Table, TableForeignKey } from "typeorm"; -import { ConnectionMetadataBuilder } from "typeorm/connection/ConnectionMetadataBuilder"; -import { EntityMetadataValidator } from "typeorm/metadata-builder/EntityMetadataValidator"; -import { View } from "typeorm/schema-builder/view/View"; +#! /usr/bin/env ts-node-script -export type Dialect = "mysql" | "postgres" | "mariadb" | "sqlite" | "mssql"; +import yargs from "yargs"; +import { hideBin } from "yargs/helpers"; +import { Dialect, loadEntities } from "./load"; +import * as fs from "fs"; -// Load typeorm entities and return SQL statements describing the schema of the entities. -export async function loadEntities( - dialect: Dialect, - // eslint-disable-next-line @typescript-eslint/ban-types - entities: (Function | EntitySchema | string)[], -) { - const mockDB = new DataSource({ - type: dialect, - database: ":memory:", - }); - const driver = mockDB.driver; - const metadataBuilder = new ConnectionMetadataBuilder(mockDB); - const entityMetadataValidator = new EntityMetadataValidator(); - const queryRunner = mockDB.createQueryRunner(); - queryRunner.enableSqlMemory(); - - const entityMetadatas = await metadataBuilder.buildEntityMetadatas(entities); - // validate all created entity metadatas to make sure user created entities are valid and correct - entityMetadataValidator.validateMany( - entityMetadatas.filter((metadata) => metadata.tableType !== "view"), - driver, - ); - - // create tables statements - for (const metadata of entityMetadatas) { - if (metadata.tableType === "view") { - continue; - } - const table = Table.create(metadata, driver); - await queryRunner.createTable(table); - } - - // create foreign keys statements, - // executed after all tables created since foreign keys can reference tables that were created afterwards. - for (const metadata of entityMetadatas) { - const table = Table.create(metadata, driver); - const foreignKeys = metadata.foreignKeys.map((foreignKeyMetadata) => - TableForeignKey.create(foreignKeyMetadata, driver), - ); - await queryRunner.createForeignKeys(table, foreignKeys); - } - - // create views - for (const metadata of entityMetadatas) { - if (metadata.tableType !== "view") { - continue; +const y = yargs(hideBin(process.argv)) + .usage( + "npx @ariga/ts-atlas-provider-typeorm load --path ./models --dialect mysql", + ) + .alias("h", "help"); +y.command( + "load", + "load sql state of typeorm entities", + { + path: { + type: "string", + demandOption: true, + describe: "Path to models folder", + }, + dialect: { + type: "string", + choices: ["mysql", "postgres", "sqlite", "mariadb", "mssql"], + demandOption: true, + describe: "Dialect of database", + }, + }, + async function (argv) { + try { + const path = argv.path; + if (!fs.existsSync(path)) { + throw new Error(`path ${path} does not exist`); + } + const sql = await loadEntities(argv.dialect as Dialect, [path + "/*.ts"]); + console.log(sql); + } catch (e) { + if (e instanceof Error) { + console.error(e.message); + } else { + console.error(e); + } } - const view = View.create(metadata, driver); - await queryRunner.createView(view, false); - } - - let sql = ""; - const memorySql = queryRunner.getMemorySql(); - memorySql.upQueries.forEach((query) => { - sql += query.query + ";\n"; - }); - queryRunner.clearSqlMemory(); - return sql; -} + }, +).parse(); diff --git a/ts/src/load.ts b/ts/src/load.ts new file mode 100644 index 0000000..5d7c961 --- /dev/null +++ b/ts/src/load.ts @@ -0,0 +1,62 @@ +import { DataSource, EntitySchema, Table, TableForeignKey } from "typeorm"; +import { ConnectionMetadataBuilder } from "typeorm/connection/ConnectionMetadataBuilder"; +import { EntityMetadataValidator } from "typeorm/metadata-builder/EntityMetadataValidator"; +import { View } from "typeorm/schema-builder/view/View"; + +export type Dialect = "mysql" | "postgres" | "mariadb" | "sqlite" | "mssql"; + +// Load typeorm entities and return SQL statements describing the schema of the entities. +export async function loadEntities( + dialect: Dialect, + // eslint-disable-next-line @typescript-eslint/ban-types + entities: (Function | EntitySchema | string)[], +) { + const mockDB = new DataSource({ + type: dialect, + database: ":memory:", + }); + const driver = mockDB.driver; + const metadataBuilder = new ConnectionMetadataBuilder(mockDB); + const entityMetadataValidator = new EntityMetadataValidator(); + const queryRunner = mockDB.createQueryRunner(); + queryRunner.enableSqlMemory(); + + const entityMetadatas = await metadataBuilder.buildEntityMetadatas(entities); + // validate all created entity metadatas to make sure user created entities are valid and correct + entityMetadataValidator.validateMany( + entityMetadatas.filter((metadata) => metadata.tableType !== "view"), + driver, + ); + + // create tables statements + for (const metadata of entityMetadatas) { + if (metadata.tableType === "view") { + continue; + } + const table = Table.create(metadata, driver); + await queryRunner.createTable(table); + } + + // creating foreign keys statements are executed after all tables created since foreign keys can reference tables that were created afterwards. + for (const metadata of entityMetadatas) { + const table = Table.create(metadata, driver); + const foreignKeys = metadata.foreignKeys.map((foreignKeyMetadata) => + TableForeignKey.create(foreignKeyMetadata, driver), + ); + await queryRunner.createForeignKeys(table, foreignKeys); + } + + // create views + for (const metadata of entityMetadatas) { + if (metadata.tableType !== "view") { + continue; + } + const view = View.create(metadata, driver); + await queryRunner.createView(view, false); + } + + const memorySql = queryRunner.getMemorySql(); + const queries = memorySql.upQueries.map((query) => query.query); + queryRunner.clearSqlMemory(); + return queries.join(";\n"); +} diff --git a/ts/testdata/atlas.hcl b/ts/testdata/atlas-script.hcl similarity index 100% rename from ts/testdata/atlas.hcl rename to ts/testdata/atlas-script.hcl diff --git a/ts/testdata/atlas-standalone.hcl b/ts/testdata/atlas-standalone.hcl new file mode 100644 index 0000000..147b8db --- /dev/null +++ b/ts/testdata/atlas-standalone.hcl @@ -0,0 +1,34 @@ +variable "dialect" { + type = string +} + +locals { + dev_url = { + mysql = "docker://mysql/8/dev" + postgres = "docker://postgres/15" + sqlite = "sqlite://file::memory:?cache=shared" + }[var.dialect] +} + +data "external_schema" "typeorm" { + program = [ + "npx", + "../", + "load", + "--path", "./entities", + "--dialect", var.dialect, + ] +} + +env "typeorm" { + src = data.external_schema.typeorm.url + dev = local.dev_url + migration { + dir = "file://migrations/${var.dialect}" + } + format { + migrate { + diff = "{{ sql . \" \" }}" + } + } +} diff --git a/ts/testdata/load-entities.ts b/ts/testdata/load-entities.ts index da4ebea..b038ccb 100644 --- a/ts/testdata/load-entities.ts +++ b/ts/testdata/load-entities.ts @@ -1,6 +1,6 @@ #! /usr/bin/env ts-node-script -import { Dialect, loadEntities } from "../src"; +import { Dialect, loadEntities } from "../src/load"; import { User } from "./entities/User"; import { Blog } from "./entities/Blog";