From 26bce2969930206eaf49210f72bc8e067dcb4a81 Mon Sep 17 00:00:00 2001 From: Theo Sun Date: Wed, 1 Feb 2023 21:12:16 +0800 Subject: [PATCH] feat: #217 migrate with transparent approach --- .vscode/launch.json | 21 +- src/AdminTool.ts | 21 +- src/constants.ts | 2 +- src/scripts/build.ts | 82 +++-- src/typeorm/migrate.ts | 105 +++--- src/types.ts | 23 +- src/utils.ts | 9 +- test/__snapshots__/utils.test.ts.snap | 6 +- test/resources/integration/db/db.cds | 1 + test/resources/transparent/db/db.cds | 17 + .../transparent/db/last-dev/mysql.json | 309 ++++++++++++++++++ test/resources/transparent/db/migrations.sql | 55 ++++ test/resources/transparent/package.json | 31 ++ test/resources/transparent/srv/srv.cds | 11 + test/transparent.test.ts | 18 + test/utils.test.ts | 10 +- 16 files changed, 630 insertions(+), 91 deletions(-) create mode 100644 test/resources/transparent/db/db.cds create mode 100644 test/resources/transparent/db/last-dev/mysql.json create mode 100644 test/resources/transparent/db/migrations.sql create mode 100644 test/resources/transparent/package.json create mode 100644 test/resources/transparent/srv/srv.cds create mode 100644 test/transparent.test.ts diff --git a/.vscode/launch.json b/.vscode/launch.json index 0d90325..f372c83 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -4,7 +4,7 @@ { "type": "node", "request": "launch", - "name": "run integration", + "name": "run test app integration", "runtimeExecutable": "npx", "runtimeArgs": [ "cds-ts", @@ -19,20 +19,35 @@ { "type": "node", "request": "launch", - "name": "run build", + "name": "run build - transparent", "runtimeExecutable": "npx", "runtimeArgs": [ "ts-node", "-e", "import {build} from \"${workspaceFolder}/src/scripts/build\"; build().catch(console.error);", ], - "cwd": "${workspaceFolder}/test/resources/integration", + "cwd": "${workspaceFolder}/test/resources/transparent", "skipFiles": [ "/**" ], "sourceMaps": true, "envFile": "${workspaceFolder}/.env" }, + { + "type": "node", + "request": "launch", + "name": "run test app transparent", + "runtimeExecutable": "npx", + "runtimeArgs": [ + "cds-ts", + "run" + ], + "cwd": "${workspaceFolder}/test/resources/transparent", + "skipFiles": [ + "/**" + ], + "envFile": "${workspaceFolder}/.env" + }, { "type": "node", "name": "vscode-jest-tests", diff --git a/src/AdminTool.ts b/src/AdminTool.ts index 016a8c4..bd7623f 100644 --- a/src/AdminTool.ts +++ b/src/AdminTool.ts @@ -9,6 +9,8 @@ import { csnToEntity } from "./typeorm/entity"; import { TypeORMLogger } from "./typeorm/logger"; import { CDSMySQLDataSource } from "./typeorm/mysql"; import { MysqlDatabaseOptions } from "./types"; +import fs from "fs/promises"; +import { migration_tool } from "./utils"; /** @@ -260,10 +262,25 @@ export class AdminTool { public async deploy(model: CSN, tenant: string = TENANT_DEFAULT) { try { this._logger.info("migrating schema for tenant", tenant.green); + if (tenant !== TENANT_DEFAULT) { await this.createDatabase(tenant); } - const entities = csnToEntity(model); const migrateOptions = await this.getDataSourceOption(tenant); - await migrate({ ...migrateOptions, entities }); + + if (this._options?.tenant?.deploy?.transparent === true) { + this._logger.info("migrate with transparent approach"); + const migrations = migration_tool.parse( + await fs.readFile( + path.join(this._cds.root, "db/migrations.sql"), + { encoding: "utf-8" } + ) + ); + await migrate({ ...migrateOptions }, false, migrations); + } + else { + const entities = csnToEntity(model); + await migrate({ ...migrateOptions, entities }); + } + if (this._options?.csv?.migrate !== false) { await this.deployCSV(tenant); } diff --git a/src/constants.ts b/src/constants.ts index bd9f213..cea85de 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -43,4 +43,4 @@ export const DEFAULT_CSV_IDENTITY_CONCURRENCY = 10; */ export const DEFAULT_MAX_ALLOWED_PACKED_MB = 512; -export const MIGRATION_VERSION_PREFIX = "-- version number: "; \ No newline at end of file +export const MIGRATION_VERSION_PREFIX = "-- version: "; \ No newline at end of file diff --git a/src/scripts/build.ts b/src/scripts/build.ts index 71135f8..5cd8f68 100644 --- a/src/scripts/build.ts +++ b/src/scripts/build.ts @@ -1,7 +1,7 @@ // @ts-nocheck import { cwdRequireCDS } from "cds-internal-tool"; import { readFile, mkdir, writeFile } from "fs/promises"; -import { Table } from "typeorm"; +import { EntitySchema, Table } from "typeorm"; import { existsSync } from "fs"; import path from "path"; import { csnToEntity } from "../typeorm/entity"; @@ -11,11 +11,19 @@ import { View } from "typeorm/schema-builder/view/View"; import { CDSMySQLDataSource } from "../typeorm/mysql"; import { Query } from "typeorm/driver/Query"; import { MigrationHistory, Query, Migration } from "../types"; -import { migration as migrationScript } from "../utils"; +import { migration_tool as migrationScript } from "../utils"; + + +const INITIAL_VERSION = 100; export async function build() { const cds = cwdRequireCDS(); const logger = cds.log("build|cds|deploy"); + + if (cds.env.requires.db?.tenant?.deploy?.transparent !== true) { + logger.error("must enable transport deployment for 'cds-mysql'"); + return; + } const base = path.join(cds.root, "db"); const mysql_last_file_path = path.join(base, "last-dev", "mysql.json"); const mysql_migration_file_path = path.join(base, "migrations.sql"); @@ -25,7 +33,7 @@ export async function build() { const current_hash = sha256(current_entities); const statements: Array = []; const migrations: Array = []; - let nextVersion = 100; + let nextVersion = INITIAL_VERSION; const last_version_views: Array = []; const last_version_tables: Array = []; @@ -67,34 +75,13 @@ export async function build() { // >> typeorm internal hack - const ds = new CDSMySQLDataSource({ - type: "mysql", - database: "__database_placeholder__", // dummy, it should never use - entities: current_entities, - }); - - await ds.buildMetadatas(); - const queryRunner = ds.createQueryRunner(); - queryRunner.loadedTables = last_version_tables; - queryRunner.loadedViews = last_version_views; - queryRunner.enableSqlMemory(); - queryRunner.getCurrentDatabase = function () { return ds.options.database; }; - queryRunner.getCurrentSchema = function () { return ds.options.database; }; - queryRunner.insertViewDefinitionSql = function () { return undefined; }; - queryRunner.deleteViewDefinitionSql = function () { return undefined; }; - - const builder = ds.driver.createSchemaBuilder(); - builder.queryRunner = queryRunner; - await builder.executeSchemaSyncOperationsInProperOrder(); + const queries = await build_migration_scripts(current_entities, last_version_tables, last_version_views); // << typeorm internal hack - statements.push(...queryRunner.getMemorySql().upQueries.filter(query => query !== undefined)); + statements.push(...queries); - migrations.push({ - version: nextVersion, - statements: statements - }); + migrations.push({ version: nextVersion, at: new Date(), statements: statements, hash: current_hash }); await writeFile( mysql_migration_file_path, @@ -119,4 +106,43 @@ export async function build() { ); logger.info("Written last dev json", mysql_last_file_path); -} \ No newline at end of file +} + +/** + * + * @ignore + * @internal + * @private + * @param current_entities + * @param last_version_tables + * @param last_version_views + * @returns + */ +export async function build_migration_scripts( + current_entities: Array, + last_version_tables: Table[], + last_version_views: View[] +) { + const ds = new CDSMySQLDataSource({ + type: "mysql", + database: "__database_placeholder__", + entities: current_entities, + }); + + await ds.buildMetadatas(); + const queryRunner = ds.createQueryRunner(); + queryRunner.loadedTables = last_version_tables; + queryRunner.loadedViews = last_version_views; + queryRunner.enableSqlMemory(); + queryRunner.getCurrentDatabase = function () { return ds.options.database; }; + queryRunner.getCurrentSchema = function () { return ds.options.database; }; + queryRunner.insertViewDefinitionSql = function () { return undefined; }; + queryRunner.deleteViewDefinitionSql = function () { return undefined; }; + + const builder = ds.driver.createSchemaBuilder(); + builder.queryRunner = queryRunner; + await builder.executeSchemaSyncOperationsInProperOrder(); + + const queries = queryRunner.getMemorySql().upQueries.filter(query => query !== undefined); + return queries; +} diff --git a/src/typeorm/migrate.ts b/src/typeorm/migrate.ts index 3d2a577..ca99971 100644 --- a/src/typeorm/migrate.ts +++ b/src/typeorm/migrate.ts @@ -7,6 +7,7 @@ import { TypeORMLogger } from "./logger"; import { createHash } from "crypto"; import { CDSMySQLDataSource } from "./mysql"; import { last6Chars } from "../utils"; +import { Migration } from "../types"; const MigrationHistory = new EntitySchema({ @@ -67,69 +68,93 @@ export function sha256(entities: Array) { return hash.digest("hex"); } +async function migrateMetadata(connectionOptions: DataSourceOptions): Promise { + const ds = new CDSMySQLDataSource({ + ...connectionOptions, + entities: CDSMysqlMetaTables, + logger: TypeORMLogger, + }); + await ds.initialize(); + const builder = ds.driver.createSchemaBuilder(); + return await builder.build(); +} + export async function migrate(connectionOptions: DataSourceOptions, dryRun: true): Promise; +export async function migrate(connectionOptions: DataSourceOptions, dryRun: false, migrations: Array): Promise; export async function migrate(connectionOptions: DataSourceOptions, dryRun?: false): Promise; -export async function migrate(connectionOptions: DataSourceOptions, dryRun = false): Promise { - - const isMetaMigration = connectionOptions.entities === CDSMysqlMetaTables; - const isTenantMigration = dryRun === false && !isMetaMigration; +export async function migrate(connectionOptions: DataSourceOptions, dryRun = false, migrations?: Array): Promise { const logger = cwdRequireCDS().log("db|deploy|mysql|migrate|typeorm"); - const entityHash = sha256(connectionOptions.entities as any as Array); - - if (isTenantMigration) { - logger.debug("start migrate meta tables for cds-mysql"); - await migrate({ ...connectionOptions, entities: CDSMysqlMetaTables }, false); - logger.debug("migrate meta tables for cds-mysql successful"); - } + logger.debug("start migrate meta tables for cds-mysql"); + await migrateMetadata(connectionOptions); + logger.debug("migrate meta tables for cds-mysql successful"); if (connectionOptions.entities === undefined || connectionOptions.entities?.length === 0) { - logger.warn("there is no entities provided, skip processing"); + if (migrations === undefined) { + logger.warn("there is no entities provided, skip processing"); + return; + } } const ds = new CDSMySQLDataSource({ ...connectionOptions, - entities: connectionOptions.entities, - logging: true, logger: TypeORMLogger, }); try { - (isTenantMigration ? logger.info : logger.debug)( - "migrate database", - String(connectionOptions.database).green, - "with hash", - last6Chars(entityHash).green, - ); await ds.initialize(); - // not dry run and not meta table migration - if (isTenantMigration) { - const [record] = await ds.query("SELECT HASH, MIGRATED_AT FROM cds_mysql_migration_history ORDER BY MIGRATED_AT DESC LIMIT 1 FOR UPDATE"); - if (record?.HASH === entityHash) { - logger.debug( - "database model with hash", - last6Chars(entityHash).green, - "was ALREADY migrated at", String(record.MIGRATED_AT).green, - ); + return await ds.transaction(async tx => { + const [record] = await tx.query("SELECT ID, HASH, MIGRATED_AT FROM cds_mysql_migration_history ORDER BY MIGRATED_AT DESC LIMIT 1 FOR UPDATE"); + + if (migrations !== undefined) { + // transparent migration + for (const m of migrations.filter(migration => migration.version > (record?.ID ?? Number.MIN_VALUE))) { + logger.info("migrate version", m.version, "generated at", m.at, "to hash", last6Chars(m.hash).green); + for (const ddl of m.statements) { await tx.query(ddl.query); } + await tx.createQueryBuilder() + .insert() + .into(MigrationHistory) + .values({ id: m.version, hash: m.hash }) + .execute(); + } return; } - await ds.createQueryBuilder() - .insert() - .into(MigrationHistory) - .values({ hash: entityHash }) - .execute(); - } + else { + // traditional way + const entityHash = sha256(connectionOptions.entities as any as Array); - const builder = ds.driver.createSchemaBuilder(); + logger.info( + "migrate database", String(connectionOptions.database).green, + "with hash", last6Chars(entityHash).green, + ); + + if (!dryRun) { + if (record?.HASH === entityHash) { + logger.debug( + "database model with hash", last6Chars(entityHash).green, + "was ALREADY migrated at", String(record.MIGRATED_AT).green, + ); + return; + } + await tx.createQueryBuilder() + .insert() + .into(MigrationHistory) + .values({ hash: entityHash }) + .execute(); + } + + const builder = ds.driver.createSchemaBuilder(); + // dry run and return the DDL SQL + if (dryRun) { return await builder.log(); } + // perform DDL + return await builder.build(); + } + }); - // dry run and return the DDL SQL - if (dryRun) { return await builder.log(); } - // perform DDL - await builder.build(); } catch (error) { logger.error("migrate database failed:", error); diff --git a/src/types.ts b/src/types.ts index 2fad893..473c04e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -57,7 +57,7 @@ export interface MysqlDatabaseOptions { /** * auto migrate database schema when connect to it (create pool), * - * default `true` + * @default true */ auto?: boolean; /** @@ -65,16 +65,22 @@ export interface MysqlDatabaseOptions { * * schema sync of these tenants will be performed when server startup * - * default value is ['default'] + * @default ['default'] */ eager?: Array | string; /** * eager deploy will also include tenants from cds.env.requires.auth.users * - * default value is `false` + * @default false */ withMockUserTenants?: boolean; + /** + * transparent migrate, require to use `cds-mysql-build` to generate migrations.sql + * + * @default false + */ + transparent: boolean; }; /** * tenant database name prefix @@ -96,11 +102,14 @@ export interface MysqlDatabaseOptions { * connection pool options for each tenant */ pool?: PoolOptions; + /** + * csv configurations + */ csv?: { /** * migrate CSV on deployment * - * default value `true` + * @default false */ migrate?: boolean; @@ -117,14 +126,14 @@ export interface MysqlDatabaseOptions { * * update or skip that. * - * default value `false` + * @default false */ update?: boolean; }; /** * enhanced csv processing for `preDelivery` aspect * - * default value is `false` + * @default false */ enhancedProcessing: boolean; }; @@ -133,7 +142,6 @@ export interface MysqlDatabaseOptions { export type CQNKind = "SELECT" | "UPDATE" | "DELETE" | "INSERT" | "CREATE" | "DROP"; - export interface Query { query: string } @@ -141,6 +149,7 @@ export interface Query { export interface Migration { version: number; at: Date; + hash: string; statements: Array; } diff --git a/src/utils.ts b/src/utils.ts index b22f362..9f0d744 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -53,7 +53,7 @@ export const isPreDeliveryModel = memorized((entifyDef: LinkedEntityDefinition) /** * migration file generation tool */ -export const migration = { +export const migration_tool = { stringify(migrations: Array): string { const parts = []; @@ -63,7 +63,7 @@ export const migration = { parts.push(""); // empty line for (const migration of migrations) { - parts.push(`${MIGRATION_VERSION_PREFIX}${migration.version} at ${migration.at.toISOString()}`); + parts.push(`${MIGRATION_VERSION_PREFIX}${migration.version} hash: ${migration.hash} at: ${migration.at.toISOString()}`); parts.push( ...migration.statements.map(statement => statement.query + "\n") ); @@ -79,11 +79,11 @@ export const migration = { for (const line of content.split("\n")) { if (line.trim().startsWith("--")) { if (line.startsWith(MIGRATION_VERSION_PREFIX)) { - const r = /-- version number: (\d+) at (.*)/.exec(line); + const r = /-- version: (\d+) hash: ([a-z0-9]{64}) at: (.*)/.exec(line); if (r === null) { throw new TypeError(`line '${line}' is not a valid comment for migration`); } - const [, version, at] = r; + const [, version, hash, at] = r; if (current_migration !== undefined) { migrations.push(current_migration); } @@ -91,6 +91,7 @@ export const migration = { current_migration = { version: parseInt(version), at: new Date(at), + hash, statements: [] }; } diff --git a/test/__snapshots__/utils.test.ts.snap b/test/__snapshots__/utils.test.ts.snap index a55e61e..287d3cf 100644 --- a/test/__snapshots__/utils.test.ts.snap +++ b/test/__snapshots__/utils.test.ts.snap @@ -5,12 +5,12 @@ exports[`Utils Test Suite should support generate/parse migration script 1`] = ` -- database migration scripts -- do not manually change this file --- version number: 100 at 2023-01-01T00:00:00.000Z +-- version: 100 hash: 1b8700e97f93691ca852b6c5ed29b247448a265356f4c6d8650e50e4f62652c7 at: 2023-01-01T00:00:00.000Z CREATE TABLE \`sap_common_Currencies_texts\` (\`locale\` varchar(14) NOT NULL, \`name\` varchar(255) NULL, \`descr\` varchar(1000) NULL, \`code\` varchar(3) NOT NULL, PRIMARY KEY (\`locale\`, \`code\`)) ENGINE=InnoDB CHARACTER SET 'utf8mb4' COLLATE 'utf8mb4_unicode_ci' CREATE TABLE \`test_int_Product_texts\` (\`locale\` varchar(14) NOT NULL, \`ID\` varchar(36) NOT NULL, \`Name\` varchar(255) NULL, PRIMARY KEY (\`locale\`, \`ID\`)) ENGINE=InnoDB CHARACTER SET 'utf8mb4' COLLATE 'utf8mb4_unicode_ci' --- version number: 102 at 2023-01-01T00:00:00.000Z +-- version: 102 hash: 1b8700e97f93691ca852b6c5ed29b247448a265356f4c6d8650e50e4f62652c7 at: 2023-01-01T00:00:00.000Z DROP VIEW \`test_int_BankService_Peoples\` ALTER TABLE \`test_int_People\` ADD \`Name3\` varchar(30) NULL DEFAULT 'dummy' @@ -35,6 +35,7 @@ exports[`Utils Test Suite should support generate/parse migration script: parsed [ { "at": 2023-01-01T00:00:00.000Z, + "hash": "1b8700e97f93691ca852b6c5ed29b247448a265356f4c6d8650e50e4f62652c7", "statements": [ { "query": "CREATE TABLE \`sap_common_Currencies_texts\` (\`locale\` varchar(14) NOT NULL, \`name\` varchar(255) NULL, \`descr\` varchar(1000) NULL, \`code\` varchar(3) NOT NULL, PRIMARY KEY (\`locale\`, \`code\`)) ENGINE=InnoDB CHARACTER SET 'utf8mb4' COLLATE 'utf8mb4_unicode_ci'", @@ -47,6 +48,7 @@ exports[`Utils Test Suite should support generate/parse migration script: parsed }, { "at": 2023-01-01T00:00:00.000Z, + "hash": "1b8700e97f93691ca852b6c5ed29b247448a265356f4c6d8650e50e4f62652c7", "statements": [ { "query": "DROP VIEW \`test_int_BankService_Peoples\`", diff --git a/test/resources/integration/db/db.cds b/test/resources/integration/db/db.cds index f60f48e..cefae2e 100644 --- a/test/resources/integration/db/db.cds +++ b/test/resources/integration/db/db.cds @@ -12,6 +12,7 @@ using { entity People : cuid, managed { Name : String(30); + Name2 : String(30); Age : Integer default 18; virtual RealAge : Integer; RegisterDate : Date; diff --git a/test/resources/transparent/db/db.cds b/test/resources/transparent/db/db.cds new file mode 100644 index 0000000..98e189f --- /dev/null +++ b/test/resources/transparent/db/db.cds @@ -0,0 +1,17 @@ +namespace test.resources.fiori.db; + +using {cuid} from '@sap/cds/common'; + +entity Person : cuid { + Name : String(255); + Age : Integer default 25; + Address : String(255); + Country : String(40) default 'CN'; +} + +entity Form : cuid { + f1 : String(255); + f2 : String(255); + f3 : Integer; + f4 : Decimal; +} diff --git a/test/resources/transparent/db/last-dev/mysql.json b/test/resources/transparent/db/last-dev/mysql.json new file mode 100644 index 0000000..567a1c8 --- /dev/null +++ b/test/resources/transparent/db/last-dev/mysql.json @@ -0,0 +1,309 @@ +{ + "version": 101, + "entities": [ + { + "options": { + "name": "FioriService_Forms_drafts", + "tableName": "FioriService_Forms_drafts", + "synchronize": true, + "columns": { + "ID": { + "name": "ID", + "nullable": false, + "type": "varchar", + "primary": true, + "length": 36 + }, + "f1": { + "name": "f1", + "nullable": true, + "type": "varchar", + "length": 255 + }, + "f2": { + "name": "f2", + "nullable": true, + "type": "varchar", + "length": 255 + }, + "f3": { + "name": "f3", + "nullable": true, + "type": "integer" + }, + "f4": { + "name": "f4", + "nullable": true, + "type": "decimal" + }, + "IsActiveEntity": { + "name": "IsActiveEntity", + "nullable": false, + "type": "boolean", + "primary": true, + "default": true + }, + "HasActiveEntity": { + "name": "HasActiveEntity", + "nullable": false, + "type": "boolean", + "default": false + }, + "HasDraftEntity": { + "name": "HasDraftEntity", + "nullable": false, + "type": "boolean", + "default": false + }, + "DraftAdministrativeData_DraftUUID": { + "name": "DraftAdministrativeData_DraftUUID", + "nullable": true, + "type": "varchar", + "length": 36 + } + } + } + }, + { + "options": { + "name": "FioriService_Persons_drafts", + "tableName": "FioriService_Persons_drafts", + "synchronize": true, + "columns": { + "ID": { + "name": "ID", + "nullable": false, + "type": "varchar", + "primary": true, + "length": 36 + }, + "Name": { + "name": "Name", + "nullable": true, + "type": "varchar", + "length": 255 + }, + "Age": { + "name": "Age", + "nullable": true, + "type": "integer", + "default": 25 + }, + "Address": { + "name": "Address", + "nullable": true, + "type": "varchar", + "length": 255 + }, + "Country": { + "name": "Country", + "nullable": true, + "type": "varchar", + "length": 40, + "default": "CN" + }, + "IsActiveEntity": { + "name": "IsActiveEntity", + "nullable": false, + "type": "boolean", + "primary": true, + "default": true + }, + "HasActiveEntity": { + "name": "HasActiveEntity", + "nullable": false, + "type": "boolean", + "default": false + }, + "HasDraftEntity": { + "name": "HasDraftEntity", + "nullable": false, + "type": "boolean", + "default": false + }, + "DraftAdministrativeData_DraftUUID": { + "name": "DraftAdministrativeData_DraftUUID", + "nullable": true, + "type": "varchar", + "length": 36 + } + } + } + }, + { + "options": { + "name": "DRAFT_DraftAdministrativeData", + "tableName": "DRAFT_DraftAdministrativeData", + "synchronize": true, + "columns": { + "DraftUUID": { + "name": "DraftUUID", + "nullable": false, + "type": "varchar", + "primary": true, + "length": 36 + }, + "CreationDateTime": { + "name": "CreationDateTime", + "nullable": true, + "type": "datetime", + "precision": 3 + }, + "CreatedByUser": { + "name": "CreatedByUser", + "nullable": true, + "type": "varchar", + "length": 256 + }, + "DraftIsCreatedByMe": { + "name": "DraftIsCreatedByMe", + "nullable": true, + "type": "boolean" + }, + "LastChangeDateTime": { + "name": "LastChangeDateTime", + "nullable": true, + "type": "datetime", + "precision": 3 + }, + "LastChangedByUser": { + "name": "LastChangedByUser", + "nullable": true, + "type": "varchar", + "length": 256 + }, + "InProcessByUser": { + "name": "InProcessByUser", + "nullable": true, + "type": "varchar", + "length": 256 + }, + "DraftIsProcessedByMe": { + "name": "DraftIsProcessedByMe", + "nullable": true, + "type": "boolean" + } + } + } + }, + { + "options": { + "type": "view", + "name": "FioriService_DraftAdministrativeData", + "columns": {}, + "tableName": "FioriService_DraftAdministrativeData", + "synchronize": true, + "deps": [ + "DRAFT_DraftAdministrativeData" + ], + "expression": "SELECT\n DraftAdministrativeData.DraftUUID,\n DraftAdministrativeData.CreationDateTime,\n DraftAdministrativeData.CreatedByUser,\n DraftAdministrativeData.DraftIsCreatedByMe,\n DraftAdministrativeData.LastChangeDateTime,\n DraftAdministrativeData.LastChangedByUser,\n DraftAdministrativeData.InProcessByUser,\n DraftAdministrativeData.DraftIsProcessedByMe\nFROM DRAFT_DraftAdministrativeData AS DraftAdministrativeData;" + } + }, + { + "options": { + "name": "test_resources_fiori_db_Form", + "tableName": "test_resources_fiori_db_Form", + "synchronize": true, + "columns": { + "ID": { + "name": "ID", + "nullable": false, + "type": "varchar", + "primary": true, + "length": 36 + }, + "f1": { + "name": "f1", + "nullable": true, + "type": "varchar", + "length": 255 + }, + "f2": { + "name": "f2", + "nullable": true, + "type": "varchar", + "length": 255 + }, + "f3": { + "name": "f3", + "nullable": true, + "type": "integer" + }, + "f4": { + "name": "f4", + "nullable": true, + "type": "decimal" + } + } + } + }, + { + "options": { + "type": "view", + "name": "FioriService_Forms", + "columns": {}, + "tableName": "FioriService_Forms", + "synchronize": true, + "deps": [ + "test_resources_fiori_db_Form" + ], + "expression": "SELECT\n Form_0.ID,\n Form_0.f1,\n Form_0.f2,\n Form_0.f3,\n Form_0.f4\nFROM test_resources_fiori_db_Form AS Form_0;" + } + }, + { + "options": { + "name": "test_resources_fiori_db_Person", + "tableName": "test_resources_fiori_db_Person", + "synchronize": true, + "columns": { + "ID": { + "name": "ID", + "nullable": false, + "type": "varchar", + "primary": true, + "length": 36 + }, + "Name": { + "name": "Name", + "nullable": true, + "type": "varchar", + "length": 255 + }, + "Age": { + "name": "Age", + "nullable": true, + "type": "integer", + "default": 25 + }, + "Address": { + "name": "Address", + "nullable": true, + "type": "varchar", + "length": 255 + }, + "Country": { + "name": "Country", + "nullable": true, + "type": "varchar", + "length": 40, + "default": "CN" + } + } + } + }, + { + "options": { + "type": "view", + "name": "FioriService_Persons", + "columns": {}, + "tableName": "FioriService_Persons", + "synchronize": true, + "deps": [ + "test_resources_fiori_db_Person" + ], + "expression": "SELECT\n Person_0.ID,\n Person_0.Name,\n Person_0.Age,\n Person_0.Address,\n Person_0.Country\nFROM test_resources_fiori_db_Person AS Person_0;" + } + } + ], + "hash": "06962d18b51963e273375acc512ea8e96b69325620662a78d57b4cf9c2bc1877" +} \ No newline at end of file diff --git a/test/resources/transparent/db/migrations.sql b/test/resources/transparent/db/migrations.sql new file mode 100644 index 0000000..03a5ee0 --- /dev/null +++ b/test/resources/transparent/db/migrations.sql @@ -0,0 +1,55 @@ +-- generated by cds-mysql +-- database migration scripts +-- do not manually change this file + +-- version: 100 hash: 89d52ab9f80a0fb38b9d52bc1caeeaf532d208e4b5671818830f4fa2032c45d1 at: 2023-02-01T12:03:22.489Z +CREATE TABLE `FioriService_Forms_drafts` (`ID` varchar(36) NOT NULL, `f1` varchar(255) NULL, `f2` varchar(255) NULL, `f3` int NULL, `f4` decimal NULL, `IsActiveEntity` tinyint NOT NULL DEFAULT 1, `HasActiveEntity` tinyint NOT NULL DEFAULT 0, `HasDraftEntity` tinyint NOT NULL DEFAULT 0, `DraftAdministrativeData_DraftUUID` varchar(36) NULL, PRIMARY KEY (`ID`, `IsActiveEntity`)) ENGINE=InnoDB CHARACTER SET 'utf8mb4' COLLATE 'utf8mb4_unicode_ci' + +CREATE TABLE `FioriService_Persons_drafts` (`ID` varchar(36) NOT NULL, `Name` varchar(255) NULL, `Age` int NULL DEFAULT '25', `Address` varchar(255) NULL, `IsActiveEntity` tinyint NOT NULL DEFAULT 1, `HasActiveEntity` tinyint NOT NULL DEFAULT 0, `HasDraftEntity` tinyint NOT NULL DEFAULT 0, `DraftAdministrativeData_DraftUUID` varchar(36) NULL, PRIMARY KEY (`ID`, `IsActiveEntity`)) ENGINE=InnoDB CHARACTER SET 'utf8mb4' COLLATE 'utf8mb4_unicode_ci' + +CREATE TABLE `DRAFT_DraftAdministrativeData` (`DraftUUID` varchar(36) NOT NULL, `CreationDateTime` datetime(3) NULL, `CreatedByUser` varchar(256) NULL, `DraftIsCreatedByMe` tinyint NULL, `LastChangeDateTime` datetime(3) NULL, `LastChangedByUser` varchar(256) NULL, `InProcessByUser` varchar(256) NULL, `DraftIsProcessedByMe` tinyint NULL, PRIMARY KEY (`DraftUUID`)) ENGINE=InnoDB CHARACTER SET 'utf8mb4' COLLATE 'utf8mb4_unicode_ci' + +CREATE TABLE `test_resources_fiori_db_Form` (`ID` varchar(36) NOT NULL, `f1` varchar(255) NULL, `f2` varchar(255) NULL, `f3` int NULL, `f4` decimal NULL, PRIMARY KEY (`ID`)) ENGINE=InnoDB CHARACTER SET 'utf8mb4' COLLATE 'utf8mb4_unicode_ci' + +CREATE TABLE `test_resources_fiori_db_Person` (`ID` varchar(36) NOT NULL, `Name` varchar(255) NULL, `Age` int NULL DEFAULT '25', `Address` varchar(255) NULL, PRIMARY KEY (`ID`)) ENGINE=InnoDB CHARACTER SET 'utf8mb4' COLLATE 'utf8mb4_unicode_ci' + +CREATE VIEW `FioriService_DraftAdministrativeData` AS SELECT + DraftAdministrativeData.DraftUUID, + DraftAdministrativeData.CreationDateTime, + DraftAdministrativeData.CreatedByUser, + DraftAdministrativeData.DraftIsCreatedByMe, + DraftAdministrativeData.LastChangeDateTime, + DraftAdministrativeData.LastChangedByUser, + DraftAdministrativeData.InProcessByUser, + DraftAdministrativeData.DraftIsProcessedByMe +FROM DRAFT_DraftAdministrativeData AS DraftAdministrativeData; + +CREATE VIEW `FioriService_Forms` AS SELECT + Form_0.ID, + Form_0.f1, + Form_0.f2, + Form_0.f3, + Form_0.f4 +FROM test_resources_fiori_db_Form AS Form_0; + +CREATE VIEW `FioriService_Persons` AS SELECT + Person_0.ID, + Person_0.Name, + Person_0.Age, + Person_0.Address +FROM test_resources_fiori_db_Person AS Person_0; + +-- version: 101 hash: 06962d18b51963e273375acc512ea8e96b69325620662a78d57b4cf9c2bc1877 at: 2023-02-01T12:04:16.622Z +DROP VIEW `FioriService_Persons` + +ALTER TABLE `FioriService_Persons_drafts` ADD `Country` varchar(40) NULL DEFAULT 'CN' + +ALTER TABLE `test_resources_fiori_db_Person` ADD `Country` varchar(40) NULL DEFAULT 'CN' + +CREATE VIEW `FioriService_Persons` AS SELECT + Person_0.ID, + Person_0.Name, + Person_0.Age, + Person_0.Address, + Person_0.Country +FROM test_resources_fiori_db_Person AS Person_0; diff --git a/test/resources/transparent/package.json b/test/resources/transparent/package.json new file mode 100644 index 0000000..dbacdbf --- /dev/null +++ b/test/resources/transparent/package.json @@ -0,0 +1,31 @@ +{ + "name": "transparent-migration", + "cds": { + "requires": { + "db": { + "kind": "mysql", + "tenant": { + "deploy": { + "transparent": true + } + }, + "csv": { + "migrate": true + } + }, + "auth": { + "kind": "mocked", + "users": { + "alice": { + "tenant": "default" + } + } + }, + "kinds": { + "mysql": { + "impl": "../../../src/index" + } + } + } + } +} \ No newline at end of file diff --git a/test/resources/transparent/srv/srv.cds b/test/resources/transparent/srv/srv.cds new file mode 100644 index 0000000..fce1d14 --- /dev/null +++ b/test/resources/transparent/srv/srv.cds @@ -0,0 +1,11 @@ +using {test.resources.fiori.db} from '../db/db.cds'; + +service FioriService { + + @odata.draft.enabled + entity Persons as projection on db.Person; + + @odata.draft.enabled + entity Forms as projection on db.Form; + +} diff --git a/test/transparent.test.ts b/test/transparent.test.ts new file mode 100644 index 0000000..ba68330 --- /dev/null +++ b/test/transparent.test.ts @@ -0,0 +1,18 @@ +/* eslint-disable max-len */ +import { setupTest } from "cds-internal-tool"; +import { doAfterAll } from "./utils"; + +describe("transparent Test Suite", () => { + + const client = setupTest(__dirname, "./resources/transparent"); + client.defaults.auth = { username: "alice", password: "admin" }; + + afterAll(doAfterAll); + + it("should support get metadata", async () => { + const response = await client.get("/fiori/$metadata"); + expect(response.status).toBe(200); + expect(response.data).toMatch(/Persons/); + }); + +}); \ No newline at end of file diff --git a/test/utils.test.ts b/test/utils.test.ts index f583fa5..ec3b488 100644 --- a/test/utils.test.ts +++ b/test/utils.test.ts @@ -1,6 +1,6 @@ /* eslint-disable max-len */ // @ts-nocheck -import { checkCdsVersion, migration } from "../src/utils"; +import { checkCdsVersion, migration_tool } from "../src/utils"; import { VERSION } from "../src/cds.version"; const mockCds = { version: VERSION.substring(1) }; @@ -23,11 +23,12 @@ describe("Utils Test Suite", () => { }); it("should support generate/parse migration script", () => { - const script = migration.stringify( + const script = migration_tool.stringify( [ { version: 100, at: new Date(), + hash: "1b8700e97f93691ca852b6c5ed29b247448a265356f4c6d8650e50e4f62652c7", statements: [ { "query": "CREATE TABLE `sap_common_Currencies_texts` (`locale` varchar(14) NOT NULL, `name` varchar(255) NULL, `descr` varchar(1000) NULL, `code` varchar(3) NOT NULL, PRIMARY KEY (`locale`, `code`)) ENGINE=InnoDB CHARACTER SET 'utf8mb4' COLLATE 'utf8mb4_unicode_ci'" @@ -40,6 +41,7 @@ describe("Utils Test Suite", () => { { version: 102, at: new Date(), + hash: "1b8700e97f93691ca852b6c5ed29b247448a265356f4c6d8650e50e4f62652c7", statements: [ { "query": "DROP VIEW `test_int_BankService_Peoples`" @@ -55,8 +57,8 @@ describe("Utils Test Suite", () => { ] ); expect(script).toMatchSnapshot(); - expect(migration.parse(script)).toMatchSnapshot("parsed object"); - expect(migration.stringify(migration.parse(script))).toBe(script); + expect(migration_tool.parse(script)).toMatchSnapshot("parsed object"); + expect(migration_tool.stringify(migration_tool.parse(script))).toBe(script); }); }); \ No newline at end of file