mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-21 20:18:06 +02:00
refactor(dynamic): enhance migration harnesses with Prisma, Sequelize-CLI, Laravel, Rails, Flask support; implement fallback logic and extend SQL framework integration
This commit is contained in:
parent
ed8decb510
commit
fd39304eed
7 changed files with 431 additions and 14 deletions
|
|
@ -133,15 +133,25 @@ const NODE_KNEX: &[VersionedPackage] = &[VersionedPackage {
|
||||||
name: "knex",
|
name: "knex",
|
||||||
version: "^3.1.0",
|
version: "^3.1.0",
|
||||||
}];
|
}];
|
||||||
const NODE_PRISMA: &[VersionedPackage] = &[VersionedPackage {
|
const NODE_PRISMA: &[VersionedPackage] = &[
|
||||||
name: "@prisma/client",
|
VersionedPackage {
|
||||||
version: "^5.14.0",
|
name: "@prisma/client",
|
||||||
}];
|
version: "^5.14.0",
|
||||||
|
},
|
||||||
|
VersionedPackage {
|
||||||
|
name: "prisma",
|
||||||
|
version: "^5.14.0",
|
||||||
|
},
|
||||||
|
];
|
||||||
const NODE_SEQUELIZE: &[VersionedPackage] = &[
|
const NODE_SEQUELIZE: &[VersionedPackage] = &[
|
||||||
VersionedPackage {
|
VersionedPackage {
|
||||||
name: "sequelize",
|
name: "sequelize",
|
||||||
version: "^6.37.3",
|
version: "^6.37.3",
|
||||||
},
|
},
|
||||||
|
VersionedPackage {
|
||||||
|
name: "sequelize-cli",
|
||||||
|
version: "^6.6.2",
|
||||||
|
},
|
||||||
VersionedPackage {
|
VersionedPackage {
|
||||||
name: "sqlite3",
|
name: "sqlite3",
|
||||||
version: "^5.1.7",
|
version: "^5.1.7",
|
||||||
|
|
|
||||||
|
|
@ -1582,6 +1582,11 @@ fn emit_migration(spec: &HarnessSpec, version: Option<&str>, is_typescript: bool
|
||||||
let (preamble, entry_subpath) = nyx_js_preamble(spec, is_typescript);
|
let (preamble, entry_subpath) = nyx_js_preamble(spec, is_typescript);
|
||||||
let handler = &spec.entry_name;
|
let handler = &spec.entry_name;
|
||||||
let version_repr = version.unwrap_or("<no-version>");
|
let version_repr = version.unwrap_or("<no-version>");
|
||||||
|
let framework = spec
|
||||||
|
.framework
|
||||||
|
.as_ref()
|
||||||
|
.map(|binding| binding.adapter.as_str())
|
||||||
|
.unwrap_or("");
|
||||||
let body = format!(
|
let body = format!(
|
||||||
r#"{preamble}
|
r#"{preamble}
|
||||||
// Phase 21 (Track M.3) — migration.
|
// Phase 21 (Track M.3) — migration.
|
||||||
|
|
@ -1591,6 +1596,7 @@ if (_h == null) {{
|
||||||
process.stderr.write('NYX_HANDLER_NOT_FOUND: ' + {handler:?} + '\n');
|
process.stderr.write('NYX_HANDLER_NOT_FOUND: ' + {handler:?} + '\n');
|
||||||
process.exit(78);
|
process.exit(78);
|
||||||
}}
|
}}
|
||||||
|
const _nyxFramework = {framework:?};
|
||||||
function _nyxLooksLikeSql(sql) {{
|
function _nyxLooksLikeSql(sql) {{
|
||||||
const upper = String(sql).toUpperCase();
|
const upper = String(sql).toUpperCase();
|
||||||
return ['SELECT', 'INSERT', 'UPDATE', 'DELETE', 'CREATE', 'ALTER', 'DROP'].some((k) => upper.includes(k));
|
return ['SELECT', 'INSERT', 'UPDATE', 'DELETE', 'CREATE', 'ALTER', 'DROP'].some((k) => upper.includes(k));
|
||||||
|
|
@ -1618,6 +1624,124 @@ function _nyxMigrationSqlRecord(sql, driver) {{
|
||||||
const sqliteDriver = _nyxTryExecuteSqlite(sql);
|
const sqliteDriver = _nyxTryExecuteSqlite(sql);
|
||||||
__nyx_stub_sql_record(String(sql), {{ driver: driver, source: 'migration', sqlite_driver: sqliteDriver }});
|
__nyx_stub_sql_record(String(sql), {{ driver: driver, source: 'migration', sqlite_driver: sqliteDriver }});
|
||||||
}}
|
}}
|
||||||
|
function _nyxCliBin(names) {{
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const suffix = process.platform === 'win32' ? '.cmd' : '';
|
||||||
|
for (const name of names) {{
|
||||||
|
const candidate = path.join(__dirname, 'node_modules', '.bin', name + suffix);
|
||||||
|
if (fs.existsSync(candidate)) return candidate;
|
||||||
|
}}
|
||||||
|
return null;
|
||||||
|
}}
|
||||||
|
function _nyxWriteSequelizeCliConfig(configPath) {{
|
||||||
|
const open = String.fromCharCode(123);
|
||||||
|
const close = String.fromCharCode(125);
|
||||||
|
const lines = [
|
||||||
|
"const fs = require('fs');",
|
||||||
|
"function record(sql) " + open,
|
||||||
|
" const p = process.env.NYX_SQL_LOG;",
|
||||||
|
" if (!p || !sql) return;",
|
||||||
|
" let out = '# driver: sequelize-cli\\n# source: migration-cli\\n' + String(sql);",
|
||||||
|
" if (!out.endsWith('\\n')) out += '\\n';",
|
||||||
|
" try " + open + " fs.appendFileSync(p, out); " + close + " catch (_) " + open + close,
|
||||||
|
close,
|
||||||
|
"module.exports = " + open + " nyx: " + open + " dialect: 'sqlite', storage: process.env.NYX_SQL_ENDPOINT || 'nyx.sqlite', logging: record " + close + " " + close + ";",
|
||||||
|
];
|
||||||
|
require('fs').writeFileSync(configPath, lines.join('\n'));
|
||||||
|
}}
|
||||||
|
function _nyxTrySequelizeCli() {{
|
||||||
|
if (_nyxFramework !== 'migration-sequelize') return false;
|
||||||
|
try {{
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const cp = require('child_process');
|
||||||
|
const bin = _nyxCliBin(['sequelize', 'sequelize-cli']);
|
||||||
|
if (!bin) return false;
|
||||||
|
const migrationsDir = path.join(__dirname, 'migrations');
|
||||||
|
fs.mkdirSync(migrationsDir, {{ recursive: true }});
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(migrationsDir, '00000000000000-nyx-migration.js'),
|
||||||
|
"module.exports = require('../" + {entry_subpath:?} + "');\n"
|
||||||
|
);
|
||||||
|
const configPath = path.join(__dirname, 'nyx-sequelize-config.cjs');
|
||||||
|
_nyxWriteSequelizeCliConfig(configPath);
|
||||||
|
const result = cp.spawnSync(bin, [
|
||||||
|
'db:migrate',
|
||||||
|
'--migrations-path', migrationsDir,
|
||||||
|
'--config', configPath,
|
||||||
|
'--env', 'nyx',
|
||||||
|
], {{
|
||||||
|
cwd: __dirname,
|
||||||
|
env: process.env,
|
||||||
|
encoding: 'utf8',
|
||||||
|
}});
|
||||||
|
if (result.stdout) process.stdout.write(result.stdout);
|
||||||
|
if (result.stderr) process.stderr.write(result.stderr);
|
||||||
|
return result.status === 0;
|
||||||
|
}} catch (e) {{
|
||||||
|
process.stderr.write('NYX_SEQUELIZE_CLI_FALLBACK: ' + (e && e.message ? e.message : String(e)) + '\n');
|
||||||
|
return false;
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
function _nyxRecordPrismaMigrationFiles(schemaPath) {{
|
||||||
|
try {{
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const root = path.dirname(schemaPath);
|
||||||
|
const migrations = path.join(root, 'migrations');
|
||||||
|
if (!fs.existsSync(migrations)) return;
|
||||||
|
for (const dirent of fs.readdirSync(migrations, {{ withFileTypes: true }})) {{
|
||||||
|
if (!dirent.isDirectory()) continue;
|
||||||
|
const sqlPath = path.join(migrations, dirent.name, 'migration.sql');
|
||||||
|
if (!fs.existsSync(sqlPath)) continue;
|
||||||
|
const sql = fs.readFileSync(sqlPath, 'utf8');
|
||||||
|
_nyxMigrationSqlRecord(sql, 'prisma-cli');
|
||||||
|
}}
|
||||||
|
}} catch (e) {{
|
||||||
|
process.stderr.write('NYX_PRISMA_CLI_SQLLOG_FALLBACK: ' + (e && e.message ? e.message : String(e)) + '\n');
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
function _nyxTryPrismaCli() {{
|
||||||
|
if (_nyxFramework !== 'migration-prisma') return false;
|
||||||
|
try {{
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const cp = require('child_process');
|
||||||
|
const bin = _nyxCliBin(['prisma']);
|
||||||
|
if (!bin) return false;
|
||||||
|
const candidates = [
|
||||||
|
path.join(__dirname, 'prisma', 'schema.prisma'),
|
||||||
|
path.join(__dirname, 'schema.prisma'),
|
||||||
|
];
|
||||||
|
const schemaPath = candidates.find((p) => fs.existsSync(p));
|
||||||
|
if (!schemaPath) return false;
|
||||||
|
if (process.env.NYX_SQL_ENDPOINT && !process.env.DATABASE_URL) {{
|
||||||
|
process.env.DATABASE_URL = 'file:' + process.env.NYX_SQL_ENDPOINT;
|
||||||
|
}}
|
||||||
|
let result = cp.spawnSync(bin, ['migrate', 'deploy', '--schema', schemaPath], {{
|
||||||
|
cwd: __dirname,
|
||||||
|
env: process.env,
|
||||||
|
encoding: 'utf8',
|
||||||
|
}});
|
||||||
|
if (result.status !== 0) {{
|
||||||
|
process.stderr.write('NYX_PRISMA_CLI_DEPLOY_FALLBACK: ' + (result.stderr || result.stdout || '') + '\n');
|
||||||
|
result = cp.spawnSync(bin, ['db', 'push', '--skip-generate', '--schema', schemaPath], {{
|
||||||
|
cwd: __dirname,
|
||||||
|
env: process.env,
|
||||||
|
encoding: 'utf8',
|
||||||
|
}});
|
||||||
|
}}
|
||||||
|
if (result.stdout) process.stdout.write(result.stdout);
|
||||||
|
if (result.stderr) process.stderr.write(result.stderr);
|
||||||
|
if (result.status !== 0) return false;
|
||||||
|
_nyxRecordPrismaMigrationFiles(schemaPath);
|
||||||
|
return true;
|
||||||
|
}} catch (e) {{
|
||||||
|
process.stderr.write('NYX_PRISMA_CLI_FALLBACK: ' + (e && e.message ? e.message : String(e)) + '\n');
|
||||||
|
return false;
|
||||||
|
}}
|
||||||
|
}}
|
||||||
function _nyxTryRealSequelize() {{
|
function _nyxTryRealSequelize() {{
|
||||||
try {{
|
try {{
|
||||||
const SequelizeLib = require('sequelize');
|
const SequelizeLib = require('sequelize');
|
||||||
|
|
@ -1732,6 +1856,8 @@ async function _nyxTryRealPrismaClient() {{
|
||||||
}}
|
}}
|
||||||
(async () => {{
|
(async () => {{
|
||||||
try {{
|
try {{
|
||||||
|
if (_nyxTryPrismaCli()) return;
|
||||||
|
if (_nyxTrySequelizeCli()) return;
|
||||||
_realPrisma = await _nyxTryRealPrismaClient();
|
_realPrisma = await _nyxTryRealPrismaClient();
|
||||||
if (_realPrisma && _realPrisma.prisma) {{
|
if (_realPrisma && _realPrisma.prisma) {{
|
||||||
_prisma = _realPrisma.prisma;
|
_prisma = _realPrisma.prisma;
|
||||||
|
|
@ -1769,6 +1895,8 @@ async function _nyxTryRealPrismaClient() {{
|
||||||
preamble = preamble,
|
preamble = preamble,
|
||||||
handler = handler,
|
handler = handler,
|
||||||
version = version_repr,
|
version = version_repr,
|
||||||
|
framework = framework,
|
||||||
|
entry_subpath = entry_subpath,
|
||||||
);
|
);
|
||||||
HarnessSource {
|
HarnessSource {
|
||||||
source: body,
|
source: body,
|
||||||
|
|
@ -3105,7 +3233,7 @@ fn message_handler_dependency_files(spec: &HarnessSpec) -> Vec<(String, String)>
|
||||||
}
|
}
|
||||||
|
|
||||||
fn framework_dependency_files(spec: &HarnessSpec) -> Vec<(String, String)> {
|
fn framework_dependency_files(spec: &HarnessSpec) -> Vec<(String, String)> {
|
||||||
if spec.expected_cap != crate::labels::Cap::CODE_EXEC {
|
if !should_stage_framework_dependency_files(spec) {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
}
|
}
|
||||||
let Some(adapter) = spec.framework.as_ref().map(|b| b.adapter.as_str()) else {
|
let Some(adapter) = spec.framework.as_ref().map(|b| b.adapter.as_str()) else {
|
||||||
|
|
@ -3134,6 +3262,14 @@ fn framework_dependency_files(spec: &HarnessSpec) -> Vec<(String, String)> {
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn should_stage_framework_dependency_files(spec: &HarnessSpec) -> bool {
|
||||||
|
spec.expected_cap == crate::labels::Cap::CODE_EXEC
|
||||||
|
|| matches!(
|
||||||
|
&spec.entry_kind,
|
||||||
|
crate::evidence::EntryKind::Migration { .. }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fn js_message_handler_deps(source: &str) -> Vec<(&'static str, &'static str)> {
|
fn js_message_handler_deps(source: &str) -> Vec<(&'static str, &'static str)> {
|
||||||
let mut deps = Vec::new();
|
let mut deps = Vec::new();
|
||||||
for raw_line in source.lines() {
|
for raw_line in source.lines() {
|
||||||
|
|
|
||||||
|
|
@ -3135,7 +3135,28 @@ function __nyx_make_middleware_request(string $payload) {{
|
||||||
$req = __nyx_make_middleware_request($payload);
|
$req = __nyx_make_middleware_request($payload);
|
||||||
$next = function ($r) {{ return $r; }};
|
$next = function ($r) {{ return $r; }};
|
||||||
|
|
||||||
|
function __nyx_try_laravel_pipeline(string $handler, $req, callable $next): bool {{
|
||||||
|
if (!class_exists('Illuminate\\Pipeline\\Pipeline')) return false;
|
||||||
|
try {{
|
||||||
|
$container = class_exists('Illuminate\\Container\\Container')
|
||||||
|
? new Illuminate\Container\Container()
|
||||||
|
: null;
|
||||||
|
$pipeline = $container === null
|
||||||
|
? new Illuminate\Pipeline\Pipeline()
|
||||||
|
: new Illuminate\Pipeline\Pipeline($container);
|
||||||
|
$result = $pipeline->send($req)->through([$handler])->then($next);
|
||||||
|
if ($result !== null) echo (string)$result . "\n";
|
||||||
|
return true;
|
||||||
|
}} catch (Throwable $e) {{
|
||||||
|
fwrite(STDERR, 'NYX_LARAVEL_PIPELINE_FALLBACK: ' . get_class($e) . ': ' . $e->getMessage() . "\n");
|
||||||
|
return false;
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
|
||||||
if (class_exists({handler:?})) {{
|
if (class_exists({handler:?})) {{
|
||||||
|
if (__nyx_try_laravel_pipeline({handler:?}, $req, $next)) {{
|
||||||
|
exit(0);
|
||||||
|
}}
|
||||||
$inst = new {handler}();
|
$inst = new {handler}();
|
||||||
if (method_exists($inst, 'handle')) {{
|
if (method_exists($inst, 'handle')) {{
|
||||||
try {{
|
try {{
|
||||||
|
|
@ -3217,7 +3238,88 @@ function __nyx_try_execute_migration_sqlite($value): string {{
|
||||||
}}
|
}}
|
||||||
}}
|
}}
|
||||||
|
|
||||||
|
function __nyx_snake_migration_name(string $class): string {{
|
||||||
|
$base = preg_replace('/[^A-Za-z0-9]+/', '_', $class);
|
||||||
|
$snake = strtolower(preg_replace('/(?<!^)[A-Z]/', '_$0', $base));
|
||||||
|
return trim($snake, '_') ?: 'nyx_migration';
|
||||||
|
}}
|
||||||
|
|
||||||
|
function __nyx_boot_laravel_database(): bool {{
|
||||||
|
if (!class_exists('Illuminate\\Database\\Capsule\\Manager')) return false;
|
||||||
|
try {{
|
||||||
|
$endpoint = getenv('NYX_SQL_ENDPOINT');
|
||||||
|
if ($endpoint === false || $endpoint === '') $endpoint = ':memory:';
|
||||||
|
$capsule = new Illuminate\Database\Capsule\Manager();
|
||||||
|
$capsule->addConnection([
|
||||||
|
'driver' => 'sqlite',
|
||||||
|
'database' => $endpoint,
|
||||||
|
'prefix' => '',
|
||||||
|
]);
|
||||||
|
$capsule->setAsGlobal();
|
||||||
|
$capsule->bootEloquent();
|
||||||
|
$db = $capsule->getDatabaseManager();
|
||||||
|
$GLOBALS['__nyx_laravel_db'] = $db;
|
||||||
|
if (class_exists('Illuminate\\Container\\Container')) {{
|
||||||
|
$container = new Illuminate\Container\Container();
|
||||||
|
$container->instance('db', $db);
|
||||||
|
$container->instance('db.connection', $db->connection());
|
||||||
|
if (class_exists('Illuminate\\Support\\Facades\\Facade')) {{
|
||||||
|
Illuminate\Support\Facades\Facade::setFacadeApplication($container);
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
try {{
|
||||||
|
$db->connection()->listen(function ($query) {{
|
||||||
|
$sql = is_object($query) && isset($query->sql) ? $query->sql : (string)$query;
|
||||||
|
__nyx_record_migration_result($sql, 'laravel-listener');
|
||||||
|
}});
|
||||||
|
}} catch (Throwable $e) {{}}
|
||||||
|
return true;
|
||||||
|
}} catch (Throwable $e) {{
|
||||||
|
fwrite(STDERR, 'NYX_LARAVEL_DB_BOOTSTRAP_FALLBACK: ' . get_class($e) . ': ' . $e->getMessage() . "\n");
|
||||||
|
return false;
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
|
||||||
|
function __nyx_try_laravel_migrator(string $class): bool {{
|
||||||
|
if (!class_exists($class)) return false;
|
||||||
|
if (!class_exists('Illuminate\\Database\\Migrations\\Migrator')) return false;
|
||||||
|
if (!class_exists('Illuminate\\Database\\Migrations\\DatabaseMigrationRepository')) return false;
|
||||||
|
if (!class_exists('Illuminate\\Filesystem\\Filesystem')) return false;
|
||||||
|
if (!__nyx_boot_laravel_database()) return false;
|
||||||
|
try {{
|
||||||
|
$db = $GLOBALS['__nyx_laravel_db'] ?? null;
|
||||||
|
if (!$db) return false;
|
||||||
|
}} catch (Throwable $e) {{
|
||||||
|
try {{
|
||||||
|
$db = Illuminate\Support\Facades\DB::getFacadeRoot();
|
||||||
|
}} catch (Throwable $inner) {{
|
||||||
|
fwrite(STDERR, 'NYX_LARAVEL_MIGRATOR_DB_FALLBACK: ' . get_class($inner) . ': ' . $inner->getMessage() . "\n");
|
||||||
|
return false;
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
try {{
|
||||||
|
$repo = new Illuminate\Database\Migrations\DatabaseMigrationRepository($db, 'migrations');
|
||||||
|
if (!$repo->repositoryExists()) {{
|
||||||
|
$repo->createRepository();
|
||||||
|
}}
|
||||||
|
$files = new Illuminate\Filesystem\Filesystem();
|
||||||
|
$migrator = new Illuminate\Database\Migrations\Migrator($repo, $db, $files);
|
||||||
|
$dir = __DIR__ . '/nyx_laravel_migrations';
|
||||||
|
if (!is_dir($dir)) mkdir($dir, 0777, true);
|
||||||
|
$file = $dir . '/2026_01_01_000000_' . __nyx_snake_migration_name($class) . '.php';
|
||||||
|
file_put_contents($file, "<?php\nrequire_once __DIR__ . '/../entry.php';\n");
|
||||||
|
$migrator->run([$dir], ['pretend' => false]);
|
||||||
|
return true;
|
||||||
|
}} catch (Throwable $e) {{
|
||||||
|
fwrite(STDERR, 'NYX_LARAVEL_MIGRATOR_FALLBACK: ' . get_class($e) . ': ' . $e->getMessage() . "\n");
|
||||||
|
return false;
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
|
||||||
if (class_exists({handler:?})) {{
|
if (class_exists({handler:?})) {{
|
||||||
|
if (__nyx_try_laravel_migrator({handler:?})) {{
|
||||||
|
exit(0);
|
||||||
|
}}
|
||||||
$inst = new {handler}();
|
$inst = new {handler}();
|
||||||
if (method_exists($inst, 'up')) {{
|
if (method_exists($inst, 'up')) {{
|
||||||
try {{
|
try {{
|
||||||
|
|
@ -3258,7 +3360,7 @@ if (class_exists({handler:?})) {{
|
||||||
}
|
}
|
||||||
|
|
||||||
fn framework_dependency_files(spec: &HarnessSpec) -> Vec<(String, String)> {
|
fn framework_dependency_files(spec: &HarnessSpec) -> Vec<(String, String)> {
|
||||||
if spec.expected_cap != crate::labels::Cap::CODE_EXEC {
|
if !should_stage_framework_dependency_files(spec) {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
}
|
}
|
||||||
let Some(adapter) = spec.framework.as_ref().map(|b| b.adapter.as_str()) else {
|
let Some(adapter) = spec.framework.as_ref().map(|b| b.adapter.as_str()) else {
|
||||||
|
|
@ -3281,6 +3383,14 @@ fn framework_dependency_files(spec: &HarnessSpec) -> Vec<(String, String)> {
|
||||||
vec![("composer.json".to_owned(), body)]
|
vec![("composer.json".to_owned(), body)]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn should_stage_framework_dependency_files(spec: &HarnessSpec) -> bool {
|
||||||
|
spec.expected_cap == crate::labels::Cap::CODE_EXEC
|
||||||
|
|| matches!(
|
||||||
|
&spec.entry_kind,
|
||||||
|
crate::evidence::EntryKind::Migration { .. }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fn build_call_expr(spec: &HarnessSpec, shape: PhpShape, func: &str) -> String {
|
fn build_call_expr(spec: &HarnessSpec, shape: PhpShape, func: &str) -> String {
|
||||||
match shape {
|
match shape {
|
||||||
PhpShape::TopLevelScript => "null".to_owned(),
|
PhpShape::TopLevelScript => "null".to_owned(),
|
||||||
|
|
|
||||||
|
|
@ -4026,7 +4026,7 @@ fn message_handler_dependency_files(spec: &HarnessSpec) -> Vec<(String, String)>
|
||||||
}
|
}
|
||||||
|
|
||||||
fn framework_dependency_files(spec: &HarnessSpec) -> Vec<(String, String)> {
|
fn framework_dependency_files(spec: &HarnessSpec) -> Vec<(String, String)> {
|
||||||
if spec.expected_cap != crate::labels::Cap::CODE_EXEC {
|
if !should_stage_framework_dependency_files(spec) {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
}
|
}
|
||||||
let Some(adapter) = spec.framework.as_ref().map(|b| b.adapter.as_str()) else {
|
let Some(adapter) = spec.framework.as_ref().map(|b| b.adapter.as_str()) else {
|
||||||
|
|
@ -4049,6 +4049,14 @@ fn framework_dependency_files(spec: &HarnessSpec) -> Vec<(String, String)> {
|
||||||
vec![("requirements.txt".to_owned(), body)]
|
vec![("requirements.txt".to_owned(), body)]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn should_stage_framework_dependency_files(spec: &HarnessSpec) -> bool {
|
||||||
|
spec.expected_cap == crate::labels::Cap::CODE_EXEC
|
||||||
|
|| matches!(
|
||||||
|
&spec.entry_kind,
|
||||||
|
crate::evidence::EntryKind::Migration { .. }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fn python_message_handler_deps(source: &str) -> Vec<&'static str> {
|
fn python_message_handler_deps(source: &str) -> Vec<&'static str> {
|
||||||
let mut deps = Vec::new();
|
let mut deps = Vec::new();
|
||||||
for raw_line in source.lines() {
|
for raw_line in source.lines() {
|
||||||
|
|
|
||||||
|
|
@ -1054,6 +1054,51 @@ def __nyx_patch_active_record_sql_recording
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def __nyx_try_rails_migration_context(cls, version)
|
||||||
|
return false unless defined?(Rails)
|
||||||
|
return false unless defined?(ActiveRecord::MigrationContext)
|
||||||
|
return false unless Rails.respond_to?(:application) && Rails.application
|
||||||
|
begin
|
||||||
|
paths = []
|
||||||
|
begin
|
||||||
|
app_paths = Rails.application.paths if Rails.application.respond_to?(:paths)
|
||||||
|
if app_paths && app_paths['db/migrate']
|
||||||
|
paths = Array(app_paths['db/migrate'].respond_to?(:existent) ? app_paths['db/migrate'].existent : app_paths['db/migrate'])
|
||||||
|
end
|
||||||
|
rescue StandardError
|
||||||
|
paths = []
|
||||||
|
end
|
||||||
|
paths = [File.dirname(File.expand_path(__FILE__))] if paths.empty?
|
||||||
|
__nyx_patch_active_record_sql_recording
|
||||||
|
context = begin
|
||||||
|
if defined?(ActiveRecord::SchemaMigration)
|
||||||
|
ActiveRecord::MigrationContext.new(paths, ActiveRecord::SchemaMigration)
|
||||||
|
else
|
||||||
|
ActiveRecord::MigrationContext.new(paths)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
target = nil
|
||||||
|
if version && version != '<no-version>'
|
||||||
|
begin
|
||||||
|
target = Integer(version)
|
||||||
|
rescue ArgumentError
|
||||||
|
target = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if target && target > 0 && context.respond_to?(:run)
|
||||||
|
context.run(:up, target)
|
||||||
|
elsif context.respond_to?(:up)
|
||||||
|
context.up
|
||||||
|
else
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
true
|
||||||
|
rescue StandardError => e
|
||||||
|
STDERR.puts("NYX_RAILS_MIGRATION_CONTEXT_FALLBACK: #{{e.class.name}}: #{{e.message}}")
|
||||||
|
false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# ActiveRecord migrations expose `up` / `down` / `change` on a subclass.
|
# ActiveRecord migrations expose `up` / `down` / `change` on a subclass.
|
||||||
if Object.const_defined?({handler:?})
|
if Object.const_defined?({handler:?})
|
||||||
cls = Object.const_get({handler:?})
|
cls = Object.const_get({handler:?})
|
||||||
|
|
@ -1061,6 +1106,9 @@ if Object.const_defined?({handler:?})
|
||||||
if defined?(ActiveRecord::Migration) && cls.is_a?(Class) && cls < ActiveRecord::Migration
|
if defined?(ActiveRecord::Migration) && cls.is_a?(Class) && cls < ActiveRecord::Migration
|
||||||
begin
|
begin
|
||||||
__nyx_patch_active_record_sql_recording
|
__nyx_patch_active_record_sql_recording
|
||||||
|
if __nyx_try_rails_migration_context(cls, {ver:?})
|
||||||
|
exit 0
|
||||||
|
end
|
||||||
cls.migrate(:up)
|
cls.migrate(:up)
|
||||||
exit 0
|
exit 0
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
|
|
@ -1125,7 +1173,7 @@ end
|
||||||
}
|
}
|
||||||
|
|
||||||
fn framework_dependency_files(spec: &HarnessSpec) -> Vec<(String, String)> {
|
fn framework_dependency_files(spec: &HarnessSpec) -> Vec<(String, String)> {
|
||||||
if spec.expected_cap != crate::labels::Cap::CODE_EXEC {
|
if !should_stage_framework_dependency_files(spec) {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
}
|
}
|
||||||
let Some(adapter) = spec.framework.as_ref().map(|b| b.adapter.as_str()) else {
|
let Some(adapter) = spec.framework.as_ref().map(|b| b.adapter.as_str()) else {
|
||||||
|
|
@ -1147,6 +1195,14 @@ fn framework_dependency_files(spec: &HarnessSpec) -> Vec<(String, String)> {
|
||||||
vec![("Gemfile".to_owned(), body)]
|
vec![("Gemfile".to_owned(), body)]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn should_stage_framework_dependency_files(spec: &HarnessSpec) -> bool {
|
||||||
|
spec.expected_cap == crate::labels::Cap::CODE_EXEC
|
||||||
|
|| matches!(
|
||||||
|
&spec.entry_kind,
|
||||||
|
crate::evidence::EntryKind::Migration { .. }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/// Phase 03 — Track J.1 deserialize harness for Ruby.
|
/// Phase 03 — Track J.1 deserialize harness for Ruby.
|
||||||
///
|
///
|
||||||
/// Wraps a call to `Marshal.load(input)` with a const-lookup
|
/// Wraps a call to `Marshal.load(input)` with a const-lookup
|
||||||
|
|
|
||||||
|
|
@ -698,14 +698,14 @@ fn kafka_parse_produce_request(version: i16, body: &[u8]) -> Vec<(String, i32, S
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
};
|
};
|
||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
for _ in 0..topic_len.max(0).min(256) {
|
for _ in 0..topic_len.clamp(0, 256) {
|
||||||
let Some(topic) = reader.string() else {
|
let Some(topic) = reader.string() else {
|
||||||
break;
|
break;
|
||||||
};
|
};
|
||||||
let Some(partition_len) = reader.array_len() else {
|
let Some(partition_len) = reader.array_len() else {
|
||||||
break;
|
break;
|
||||||
};
|
};
|
||||||
for _ in 0..partition_len.max(0).min(256) {
|
for _ in 0..partition_len.clamp(0, 256) {
|
||||||
let Some(partition) = reader.i32() else {
|
let Some(partition) = reader.i32() else {
|
||||||
break;
|
break;
|
||||||
};
|
};
|
||||||
|
|
@ -783,7 +783,7 @@ fn kafka_parse_fetch_request(version: i16, body: &[u8]) -> BTreeMap<String, Vec<
|
||||||
let Some(topic_len) = reader.array_len() else {
|
let Some(topic_len) = reader.array_len() else {
|
||||||
return out;
|
return out;
|
||||||
};
|
};
|
||||||
for _ in 0..topic_len.max(0).min(256) {
|
for _ in 0..topic_len.clamp(0, 256) {
|
||||||
let Some(topic) = reader.string() else {
|
let Some(topic) = reader.string() else {
|
||||||
break;
|
break;
|
||||||
};
|
};
|
||||||
|
|
@ -791,7 +791,7 @@ fn kafka_parse_fetch_request(version: i16, body: &[u8]) -> BTreeMap<String, Vec<
|
||||||
break;
|
break;
|
||||||
};
|
};
|
||||||
let mut partitions = Vec::new();
|
let mut partitions = Vec::new();
|
||||||
for _ in 0..partition_len.max(0).min(256) {
|
for _ in 0..partition_len.clamp(0, 256) {
|
||||||
let Some(partition) = reader.i32() else {
|
let Some(partition) = reader.i32() else {
|
||||||
break;
|
break;
|
||||||
};
|
};
|
||||||
|
|
@ -840,7 +840,7 @@ fn kafka_parse_list_offsets_request(body: &[u8]) -> BTreeMap<String, Vec<(i32, i
|
||||||
let Some(topic_len) = reader.array_len() else {
|
let Some(topic_len) = reader.array_len() else {
|
||||||
return out;
|
return out;
|
||||||
};
|
};
|
||||||
for _ in 0..topic_len.max(0).min(256) {
|
for _ in 0..topic_len.clamp(0, 256) {
|
||||||
let Some(topic) = reader.string() else {
|
let Some(topic) = reader.string() else {
|
||||||
break;
|
break;
|
||||||
};
|
};
|
||||||
|
|
@ -848,7 +848,7 @@ fn kafka_parse_list_offsets_request(body: &[u8]) -> BTreeMap<String, Vec<(i32, i
|
||||||
break;
|
break;
|
||||||
};
|
};
|
||||||
let mut partitions = Vec::new();
|
let mut partitions = Vec::new();
|
||||||
for _ in 0..partition_len.max(0).min(256) {
|
for _ in 0..partition_len.clamp(0, 256) {
|
||||||
let Some(partition) = reader.i32() else {
|
let Some(partition) = reader.i32() else {
|
||||||
break;
|
break;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -119,6 +119,19 @@ fn framework_bound_spec(
|
||||||
spec
|
spec
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn framework_bound_sql_spec(
|
||||||
|
lang: Lang,
|
||||||
|
kind: EvEntryKind,
|
||||||
|
entry_name: &str,
|
||||||
|
entry_file: &str,
|
||||||
|
adapter: &str,
|
||||||
|
) -> HarnessSpec {
|
||||||
|
let mut spec = framework_bound_spec(lang, kind, entry_name, entry_file, adapter);
|
||||||
|
spec.expected_cap = Cap::SQL_QUERY;
|
||||||
|
spec.stubs_required = StubKind::for_cap(Cap::SQL_QUERY);
|
||||||
|
spec
|
||||||
|
}
|
||||||
|
|
||||||
fn extra_file_content<'a>(files: &'a [(String, String)], rel: &str) -> &'a str {
|
fn extra_file_content<'a>(files: &'a [(String, String)], rel: &str) -> &'a str {
|
||||||
files
|
files
|
||||||
.iter()
|
.iter()
|
||||||
|
|
@ -1001,6 +1014,7 @@ fn middleware_php_harness_carries_sentinel_and_handler() {
|
||||||
assert!(h.source.contains("__NYX_MIDDLEWARE__"));
|
assert!(h.source.contains("__NYX_MIDDLEWARE__"));
|
||||||
assert!(h.source.contains("Audit"));
|
assert!(h.source.contains("Audit"));
|
||||||
assert!(h.source.contains("Illuminate\\Http\\Request"));
|
assert!(h.source.contains("Illuminate\\Http\\Request"));
|
||||||
|
assert!(h.source.contains("Illuminate\\Pipeline\\Pipeline"));
|
||||||
assert!(h.source.contains("__nyx_make_middleware_request"));
|
assert!(h.source.contains("__nyx_make_middleware_request"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1044,6 +1058,10 @@ fn migration_js_harness_carries_sentinel_and_handler() {
|
||||||
assert!(h.source.contains("global.__nyx_prisma"));
|
assert!(h.source.contains("global.__nyx_prisma"));
|
||||||
assert!(h.source.contains("require('@prisma/client')"));
|
assert!(h.source.contains("require('@prisma/client')"));
|
||||||
assert!(h.source.contains("_nyxTryRealPrismaClient"));
|
assert!(h.source.contains("_nyxTryRealPrismaClient"));
|
||||||
|
assert!(h.source.contains("_nyxTrySequelizeCli"));
|
||||||
|
assert!(h.source.contains("_nyxTryPrismaCli"));
|
||||||
|
assert!(h.source.contains("sequelize-cli"));
|
||||||
|
assert!(h.source.contains("'migrate', 'deploy'"));
|
||||||
assert!(h.source.contains("NYX_PRISMA_CLIENT_SQL"));
|
assert!(h.source.contains("NYX_PRISMA_CLIENT_SQL"));
|
||||||
assert!(h.source.contains("$disconnect"));
|
assert!(h.source.contains("$disconnect"));
|
||||||
assert!(h.source.contains("node:sqlite"));
|
assert!(h.source.contains("node:sqlite"));
|
||||||
|
|
@ -1063,6 +1081,8 @@ fn migration_ruby_harness_carries_sentinel_and_handler() {
|
||||||
assert!(h.source.contains("AddIndex"));
|
assert!(h.source.contains("AddIndex"));
|
||||||
assert!(h.source.contains("__nyx_stub_sql_record"));
|
assert!(h.source.contains("__nyx_stub_sql_record"));
|
||||||
assert!(h.source.contains("ActiveRecord::Base.establish_connection"));
|
assert!(h.source.contains("ActiveRecord::Base.establish_connection"));
|
||||||
|
assert!(h.source.contains("ActiveRecord::MigrationContext"));
|
||||||
|
assert!(h.source.contains("__nyx_try_rails_migration_context"));
|
||||||
assert!(h.source.contains("cls.migrate(:up)"));
|
assert!(h.source.contains("cls.migrate(:up)"));
|
||||||
assert!(h.source.contains("SQLite3::Database"));
|
assert!(h.source.contains("SQLite3::Database"));
|
||||||
assert!(h.source.contains("NYX_SQL_ENDPOINT"));
|
assert!(h.source.contains("NYX_SQL_ENDPOINT"));
|
||||||
|
|
@ -1081,10 +1101,87 @@ fn migration_php_harness_carries_sentinel_and_handler() {
|
||||||
assert!(h.source.contains("AddUsers"));
|
assert!(h.source.contains("AddUsers"));
|
||||||
assert!(h.source.contains("__nyx_stub_sql_record"));
|
assert!(h.source.contains("__nyx_stub_sql_record"));
|
||||||
assert!(h.source.contains("vendor/autoload.php"));
|
assert!(h.source.contains("vendor/autoload.php"));
|
||||||
|
assert!(
|
||||||
|
h.source
|
||||||
|
.contains("Illuminate\\Database\\Migrations\\Migrator")
|
||||||
|
);
|
||||||
|
assert!(h.source.contains("Illuminate\\Database\\Capsule\\Manager"));
|
||||||
assert!(h.source.contains("new SQLite3"));
|
assert!(h.source.contains("new SQLite3"));
|
||||||
assert!(h.source.contains("NYX_SQL_ENDPOINT"));
|
assert!(h.source.contains("NYX_SQL_ENDPOINT"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn migration_harnesses_stage_framework_deps_for_sql_specs() {
|
||||||
|
let cases = [
|
||||||
|
(
|
||||||
|
framework_bound_sql_spec(
|
||||||
|
Lang::Python,
|
||||||
|
EvEntryKind::Migration { version: None },
|
||||||
|
"upgrade",
|
||||||
|
"tests/dynamic_fixtures/migration/flask/vuln.py",
|
||||||
|
"migration-flask",
|
||||||
|
),
|
||||||
|
"requirements.txt",
|
||||||
|
vec!["alembic", "Flask-Migrate"],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
framework_bound_sql_spec(
|
||||||
|
Lang::JavaScript,
|
||||||
|
EvEntryKind::Migration { version: None },
|
||||||
|
"up",
|
||||||
|
"tests/dynamic_fixtures/migration/sequelize/vuln.js",
|
||||||
|
"migration-sequelize",
|
||||||
|
),
|
||||||
|
"package.json",
|
||||||
|
vec!["sequelize", "sequelize-cli", "sqlite3"],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
framework_bound_sql_spec(
|
||||||
|
Lang::JavaScript,
|
||||||
|
EvEntryKind::Migration { version: None },
|
||||||
|
"up",
|
||||||
|
"tests/dynamic_fixtures/migration/prisma/vuln.js",
|
||||||
|
"migration-prisma",
|
||||||
|
),
|
||||||
|
"package.json",
|
||||||
|
vec!["@prisma/client", "\"prisma\""],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
framework_bound_sql_spec(
|
||||||
|
Lang::Ruby,
|
||||||
|
EvEntryKind::Migration { version: None },
|
||||||
|
"AddIndex",
|
||||||
|
"tests/dynamic_fixtures/migration/rails/vuln.rb",
|
||||||
|
"migration-rails",
|
||||||
|
),
|
||||||
|
"Gemfile",
|
||||||
|
vec!["rails"],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
framework_bound_sql_spec(
|
||||||
|
Lang::Php,
|
||||||
|
EvEntryKind::Migration { version: None },
|
||||||
|
"AddUsers",
|
||||||
|
"tests/dynamic_fixtures/migration/laravel/vuln.php",
|
||||||
|
"migration-laravel",
|
||||||
|
),
|
||||||
|
"composer.json",
|
||||||
|
vec!["laravel/framework"],
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (spec, manifest, needles) in cases {
|
||||||
|
let harness = lang::emit(&spec).expect("emit ok");
|
||||||
|
let manifest_content = extra_file_content(&harness.extra_files, manifest);
|
||||||
|
for needle in needles {
|
||||||
|
assert!(
|
||||||
|
manifest_content.contains(needle),
|
||||||
|
"{manifest} missing {needle}: {manifest_content}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn phase21_harness_emitters_stage_framework_dependency_manifests() {
|
fn phase21_harness_emitters_stage_framework_dependency_manifests() {
|
||||||
let cases = [
|
let cases = [
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue