From f2e8ec6ce428547f22a31951adbfd49b7f13725d Mon Sep 17 00:00:00 2001 From: Stanislav Mishchyshyn Date: Wed, 19 Aug 2026 15:13:42 +0300 Subject: [PATCH] feat(registry): run migration using compiled js files --- registry/config/default.ts | 4 +- registry/config/knexfile.ts | 15 +- registry/package-lock.json | 230 ++++++++++++++++++++++++++ registry/package.json | 16 +- registry/scripts/docker-entrypoint.sh | 4 +- registry/server/db/migrationSource.ts | 35 ++++ 6 files changed, 293 insertions(+), 11 deletions(-) create mode 100644 registry/server/db/migrationSource.ts diff --git a/registry/config/default.ts b/registry/config/default.ts index 9f7f2426..1e94e597 100644 --- a/registry/config/default.ts +++ b/registry/config/default.ts @@ -7,7 +7,9 @@ module.exports = { client: 'sqlite3', connection: { database: 'registry_db', - filename: path.resolve(__dirname, '../server/dbfiles/db.sqlite'), + // Anchored to the registry root so that the source and the pre-compiled + // (build/config) versions of this config point to the same database file + filename: path.join(path.resolve(__dirname, '..').replace(/[\\/]build$/, ''), 'server/dbfiles/db.sqlite'), }, searchPath: ['public'], useNullAsDefault: true, diff --git a/registry/config/knexfile.ts b/registry/config/knexfile.ts index 49242035..556091a0 100644 --- a/registry/config/knexfile.ts +++ b/registry/config/knexfile.ts @@ -1,5 +1,3 @@ -require('ts-node/register'); - import path from 'node:path'; process.env.NODE_CONFIG_DIR = path.resolve(__dirname, '../config'); @@ -8,6 +6,7 @@ import { Knex, knex } from 'knex'; import { cascadeTruncatePlugin } from '../server/db/cascadeTruncate'; import { knexLoggerAdapter } from '../server/db/logger'; +import { PrecompiledMigrationSource } from '../server/db/migrationSource'; import { syncSequencePlugin } from '../server/db/syncSequence'; import { logConnectionString } from '../server/util/db'; import { loadPlugins } from '../server/util/pluginManager'; @@ -21,8 +20,20 @@ const knexConfig: Knex.Config = config.get('database'); const connection = typeof knexConfig.connection === 'object' ? { ...knexConfig.connection } : knexConfig.connection; +let migrations = knexConfig.migrations; +if (typeof migrations?.directory === 'string') { + // Knex resets a custom migrationSource whenever FS-related options (directory, loadExtensions) + // are present, so the compiled config must pass the migration source only + const { directory, loadExtensions, sortDirsSeparately, ...restMigrations } = migrations; + migrations = { + ...restMigrations, + migrationSource: new PrecompiledMigrationSource(directory), + }; +} + export default { ...knexConfig, connection, + migrations, log: knexLoggerAdapter(), }; diff --git a/registry/package-lock.json b/registry/package-lock.json index 6b1ff775..af010ba8 100644 --- a/registry/package-lock.json +++ b/registry/package-lock.json @@ -61,6 +61,7 @@ "@types/supertest": "^6.0.3", "chai": "6.2.1", "chai-as-promised": "^8.0.2", + "copyfiles": "^2.4.1", "cross-env": "10.1.0", "jsonwebtoken": "^9.0.2", "mocha": "^11.7.5", @@ -3174,6 +3175,176 @@ "dev": true, "license": "MIT" }, + "node_modules/copyfiles": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/copyfiles/-/copyfiles-2.4.1.tgz", + "integrity": "sha512-fereAvAvxDrQDOXybk3Qu3dPbOoKoysFMWtkY3mv5BsL8//OSZVL5DCLYqgRfY5cWirgRzlC+WSrxp6Bo3eNZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob": "^7.0.5", + "minimatch": "^3.0.3", + "mkdirp": "^1.0.4", + "noms": "0.0.0", + "through2": "^2.0.1", + "untildify": "^4.0.0", + "yargs": "^16.1.0" + }, + "bin": { + "copyfiles": "copyfiles", + "copyup": "copyfiles" + } + }, + "node_modules/copyfiles/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/copyfiles/node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/copyfiles/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/copyfiles/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/copyfiles/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/copyfiles/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/copyfiles/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/copyfiles/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/copyfiles/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/copyfiles/node_modules/yargs": { + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.2.tgz", + "integrity": "sha512-Nt9ZJjXTv5R8MHbqby/wXQ6Gi0Bb3TcYZkR1bzuL4yB2OxWPkXknz513gEF0GoA6tn00UpbPvERW8rzCuWCA6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/copyfiles/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", @@ -6924,6 +7095,44 @@ "node": ">=4" } }, + "node_modules/noms": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/noms/-/noms-0.0.0.tgz", + "integrity": "sha512-lNDU9VJaOPxUmXcLb+HQFeUgQQPtMI24Gt6hgfuMHRJgMRHMF/qZ4HJD3GDru4sSw9IQl2jPjAYnQrdIeLbwow==", + "dev": true, + "license": "ISC", + "dependencies": { + "inherits": "^2.0.1", + "readable-stream": "~1.0.31" + } + }, + "node_modules/noms/node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/noms/node_modules/readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "node_modules/noms/node_modules/string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==", + "dev": true, + "license": "MIT" + }, "node_modules/nopt": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", @@ -9667,6 +9876,17 @@ "real-require": "^0.2.0" } }, + "node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, "node_modules/tildify": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/tildify/-/tildify-2.0.0.tgz", @@ -9930,6 +10150,16 @@ "node": ">= 0.8" } }, + "node_modules/untildify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", + "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/update-browserslist-db": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", diff --git a/registry/package.json b/registry/package.json index cfb1e1c7..852e06d7 100644 --- a/registry/package.json +++ b/registry/package.json @@ -5,16 +5,19 @@ "main": "build/index.js", "scripts": { "tsc": "tsc", - "compile": "tsc --incremental", + "compile": "tsc --incremental && copyfiles \"server/seeds/data/**/*\" build", "dev": "rimraf build && nodemon -e ts,json5 --exec \"tsc --incremental && npm run start | pino-pretty\" ", "build": "npm run compile && cd ./client && npm run build", "start": "dotenv -- node -r source-map-support/register ./build/server/index.js", - "start-docker": "npm run migrate && npm start", + "start-docker": "npm run migrate:compiled && npm start", "knex": "knex", - "migrate": "dotenv -- knex --knexfile config/knexfile.ts migrate:latest", - "migrate:make": "knex --knexfile config/knexfile.ts migrate:make", - "migrate:rollback": "dotenv -- knex --knexfile config/knexfile.ts migrate:rollback", - "seed": "dotenv -- knex --knexfile config/knexfile.ts seed:run", + "migrate": "npm run compile && dotenv -- npm run migrate:compiled", + "migrate:compiled": "knex --knexfile build/config/knexfile.js migrate:latest", + "migrate:make": "knex --knexfile config/knexfile.ts migrate:make --migrations-directory ../server/migrations", + "migrate:rollback": "npm run compile && dotenv -- npm run migrate:rollback:compiled", + "migrate:rollback:compiled": "knex --knexfile build/config/knexfile.js migrate:rollback", + "seed": "npm run compile && dotenv -- npm run seed:compiled", + "seed:compiled": "knex --knexfile build/config/knexfile.js seed:run", "seed:make": "knex --knexfile config/knexfile.ts seed:make -x ts", "test:sqlite": "cross-env-shell DB_CLIENT=sqlite3 npm run reset && cross-env NODE_ENV=test NODE_CONFIG_ENV=sqlite-test mocha", "test:mysql": "cross-env-shell NODE_ENV=test NODE_CONFIG_ENV=mysql-test DB_CLIENT=mysql \"npm run reset && mocha\" ", @@ -48,6 +51,7 @@ "@types/supertest": "^6.0.3", "chai": "6.2.1", "chai-as-promised": "^8.0.2", + "copyfiles": "^2.4.1", "cross-env": "10.1.0", "jsonwebtoken": "^9.0.2", "mocha": "^11.7.5", diff --git a/registry/scripts/docker-entrypoint.sh b/registry/scripts/docker-entrypoint.sh index 84f73dcd..21e88c5e 100755 --- a/registry/scripts/docker-entrypoint.sh +++ b/registry/scripts/docker-entrypoint.sh @@ -2,8 +2,8 @@ set -e if [ "$DB_SEED" = 'true' ] && [ ! -f .seed ]; then - npm run migrate - npm run seed + npm run migrate:compiled + npm run seed:compiled touch .seed fi diff --git a/registry/server/db/migrationSource.ts b/registry/server/db/migrationSource.ts new file mode 100644 index 00000000..d090223b --- /dev/null +++ b/registry/server/db/migrationSource.ts @@ -0,0 +1,35 @@ +import fs from 'node:fs'; +import path from 'node:path'; + +import type { Knex } from 'knex'; + +export interface MigrationFile { + file: string; + directory: string; +} + +/** + * Migration source that can load pre-compiled (.js) migrations from the build output. + * Migration names are always reported with the ".ts" extension to stay compatible + * with the records knex has already stored in the "knex_migrations" table. + */ +export class PrecompiledMigrationSource implements Knex.MigrationSource { + constructor(private readonly directory: string) {} + + public async getMigrations(): Promise { + const files = await fs.promises.readdir(this.directory); + + return files + .filter((file) => (file.endsWith('.js') || file.endsWith('.ts')) && !file.endsWith('.d.ts')) + .sort() + .map((file) => ({ file, directory: this.directory })); + } + + public getMigrationName(migration: MigrationFile): string { + return migration.file.replace(/\.js$/, '.ts'); + } + + public getMigration(migration: MigrationFile): Promise { + return Promise.resolve(require(path.resolve(migration.directory, migration.file))); + } +}