Files
aliasvault/core/vault/scripts/generate-vault-sql.cjs
2026-01-14 12:22:11 +01:00

463 lines
14 KiB
JavaScript

#!/usr/bin/env node
/**
* Generates VaultVersions and VaultSql for Swift and Kotlin from TypeScript source.
* This allows native iOS and Android code to create vaults with the correct SQL schema.
*/
const fs = require('fs');
const path = require('path');
// Paths
const REPO_ROOT = path.join(__dirname, '../../..');
const TS_VERSIONS_SOURCE = path.join(REPO_ROOT, 'core/vault/src/sql/VaultVersions.ts');
const TS_SQL_SOURCE = path.join(REPO_ROOT, 'core/vault/src/sql/SqlConstants.ts');
// Output paths
// Note: VaultModels is a framework that can be imported by other targets (including AliasVaultUITests)
const SWIFT_OUTPUT_DIR = path.join(REPO_ROOT, 'apps/mobile-app/ios/VaultModels');
const KOTLIN_OUTPUT_DIR = path.join(REPO_ROOT, 'apps/mobile-app/android/app/src/main/java/net/aliasvault/app/vaultstore/models');
/**
* Parse VaultVersions from TypeScript source
*/
function parseVaultVersions(tsContent) {
const versions = [];
// Find the VAULT_VERSIONS array
const match = tsContent.match(/export const VAULT_VERSIONS[^=]*=\s*\[([\s\S]*?)\];/);
if (!match) {
throw new Error('Could not find VAULT_VERSIONS in source');
}
const body = match[1];
// Match each version object
const versionRegex = /\{\s*revision:\s*(\d+),\s*version:\s*'([^']+)',\s*description:\s*'([^']+)',\s*releaseVersion:\s*'([^']+)',\s*compatibleUpToVersion:\s*'([^']+)',?\s*\}/g;
let versionMatch;
while ((versionMatch = versionRegex.exec(body)) !== null) {
versions.push({
revision: parseInt(versionMatch[1], 10),
version: versionMatch[2],
description: versionMatch[3],
releaseVersion: versionMatch[4],
compatibleUpToVersion: versionMatch[5]
});
}
return versions;
}
/**
* Parse COMPLETE_SCHEMA_SQL from TypeScript source
*/
function parseCompleteSchemaSql(tsContent) {
// Find the COMPLETE_SCHEMA_SQL constant
const match = tsContent.match(/export const COMPLETE_SCHEMA_SQL\s*=\s*`([\s\S]*?)`;/);
if (!match) {
throw new Error('Could not find COMPLETE_SCHEMA_SQL in source');
}
return match[1].trim();
}
/**
* Parse MIGRATION_SCRIPTS from TypeScript source
*/
function parseMigrationScripts(tsContent) {
const migrations = {};
// Find the MIGRATION_SCRIPTS object
const match = tsContent.match(/export const MIGRATION_SCRIPTS:\s*Record<number,\s*string>\s*=\s*\{([\s\S]*?)\n\};/);
if (!match) {
// MIGRATION_SCRIPTS might be optional or have different format
console.log('Note: MIGRATION_SCRIPTS not found or has different format');
return migrations;
}
const body = match[1];
// Match each migration: number: `sql`
const migrationRegex = /(\d+):\s*`([\s\S]*?)`/g;
let migrationMatch;
while ((migrationMatch = migrationRegex.exec(body)) !== null) {
migrations[parseInt(migrationMatch[1], 10)] = migrationMatch[2].trim();
}
return migrations;
}
/**
* Escape string for Swift multiline string literal
*/
function escapeSwiftString(str) {
// Swift multiline strings handle most escaping, but we need to escape backslashes and quotes
return str
.replace(/\\/g, '\\\\')
.replace(/"/g, '\\"');
}
/**
* Escape string for Kotlin multiline string literal (trimIndent)
*/
function escapeKotlinString(str) {
// Kotlin raw strings don't need much escaping, but $ needs escaping
return str.replace(/\$/g, '\\$');
}
/**
* Generate Swift VaultVersions struct
*/
function generateSwiftVaultVersions(versions) {
const latestVersion = versions[versions.length - 1];
const versionsArray = versions.map(v => {
return ` VaultVersion(
revision: ${v.revision},
version: "${v.version}",
description: "${v.description}",
releaseVersion: "${v.releaseVersion}",
compatibleUpToVersion: "${v.compatibleUpToVersion}"
)`;
}).join(',\n');
return `// <auto-generated />
// This file is auto-generated from core/vault/src/sql/VaultVersions.ts
// Do not edit this file directly. Run './build.sh' in core/vault to regenerate.
// swiftlint:disable all
import Foundation
/// Vault database version information.
public struct VaultVersion {
/// The migration revision number.
public let revision: Int
/// The internal migration version number that equals the AliasClientDb database version (e.g., "1.5.0").
public let version: String
/// Description of changes in this version.
public let description: String
/// The AliasVault release that this vault version was introduced in (e.g., "0.14.0").
public let releaseVersion: String
/// The last AliasVault release version that the vault was compatible with before requiring this migration.
public let compatibleUpToVersion: String
}
/// Registry of all vault versions.
public struct VaultVersions {
/// All vault migrations/versions in chronological order.
public static let all: [VaultVersion] = [
${versionsArray}
]
/// Get the latest vault version.
public static var latest: VaultVersion {
return all.last!
}
/// Get the latest vault version string (e.g., "1.7.0").
public static var latestVersion: String {
return latest.version
}
/// Get the latest vault revision number.
public static var latestRevision: Int {
return latest.revision
}
/// Find a vault version by revision number.
public static func findByRevision(_ revision: Int) -> VaultVersion? {
return all.first { $0.revision == revision }
}
/// Find a vault version by version string.
public static func findByVersion(_ version: String) -> VaultVersion? {
return all.first { $0.version == version }
}
}
// swiftlint:enable all
`;
}
/**
* Generate Kotlin VaultVersions object
*/
function generateKotlinVaultVersions(versions) {
const versionsArray = versions.map(v => {
return ` VaultVersion(
revision = ${v.revision},
version = "${v.version}",
description = "${v.description}",
releaseVersion = "${v.releaseVersion}",
compatibleUpToVersion = "${v.compatibleUpToVersion}",
)`;
}).join(',\n');
return `// <auto-generated />
// This file is auto-generated from core/vault/src/sql/VaultVersions.ts
// Do not edit this file directly. Run './build.sh' in core/vault to regenerate.
@file:Suppress("all")
package net.aliasvault.app.vaultstore.models
/**
* Vault database version information.
*/
data class VaultVersion(
/** The migration revision number. */
val revision: Int,
/** The internal migration version number that equals the AliasClientDb database version (e.g., "1.5.0"). */
val version: String,
/** Description of changes in this version. */
val description: String,
/** The AliasVault release that this vault version was introduced in (e.g., "0.14.0"). */
val releaseVersion: String,
/** The last AliasVault release version that the vault was compatible with before requiring this migration. */
val compatibleUpToVersion: String,
)
/**
* Registry of all vault versions.
*/
object VaultVersions {
/**
* All vault migrations/versions in chronological order.
*/
val all: List<VaultVersion> = listOf(
${versionsArray},
)
/**
* Get the latest vault version.
*/
val latest: VaultVersion
get() = all.last()
/**
* Get the latest vault version string (e.g., "1.7.0").
*/
val latestVersion: String
get() = latest.version
/**
* Get the latest vault revision number.
*/
val latestRevision: Int
get() = latest.revision
/**
* Find a vault version by revision number.
*/
fun findByRevision(revision: Int): VaultVersion? {
return all.find { it.revision == revision }
}
/**
* Find a vault version by version string.
*/
fun findByVersion(version: String): VaultVersion? {
return all.find { it.version == version }
}
}
`;
}
/**
* Indent each line of a multi-line string for Swift multi-line string literals
*/
function indentSwiftMultilineString(str, spaces = 8) {
const indent = ' '.repeat(spaces);
return str.split('\n').map(line => indent + line).join('\n');
}
/**
* Generate Swift VaultSql struct
*/
function generateSwiftVaultSql(completeSchemaSql, migrations) {
const escapedSql = escapeSwiftString(completeSchemaSql);
const indentedSql = indentSwiftMultilineString(escapedSql);
// Generate migration entries
const migrationEntries = Object.entries(migrations).map(([key, sql]) => {
const escapedMigrationSql = escapeSwiftString(sql);
const indentedMigrationSql = indentSwiftMultilineString(escapedMigrationSql, 12);
return ` ${key}: """
${indentedMigrationSql}
"""`;
}).join(',\n');
const migrationsDict = Object.keys(migrations).length > 0
? `
/// Migration SQL scripts indexed by migration number.
/// Key is the source migration number (migration FROM this version).
public static let migrations: [Int: String] = [
${migrationEntries}
]`
: `
/// Migration SQL scripts indexed by migration number.
public static let migrations: [Int: String] = [:]`;
return `// <auto-generated />
// This file is auto-generated from core/vault/src/sql/SqlConstants.ts
// Do not edit this file directly. Run './build.sh' in core/vault to regenerate.
// swiftlint:disable all
import Foundation
/// SQL statements for vault creation and migration.
public struct VaultSql {
/// Complete database schema SQL for creating a new vault with the latest schema.
public static let completeSchema = """
${indentedSql}
"""
${migrationsDict}
/// Get migration SQL for upgrading from a specific revision.
public static func getMigrationSql(fromRevision: Int) -> String? {
return migrations[fromRevision]
}
}
// swiftlint:enable all
`;
}
/**
* Generate Kotlin VaultSql object
*/
function generateKotlinVaultSql(completeSchemaSql, migrations) {
const escapedSql = escapeKotlinString(completeSchemaSql);
// Generate migration entries
const migrationEntries = Object.entries(migrations).map(([key, sql]) => {
const escapedMigrationSql = escapeKotlinString(sql);
return ` ${key} to """
${escapedMigrationSql}
""".trimIndent()`;
}).join(',\n');
const migrationsMap = Object.keys(migrations).length > 0
? `
/**
* Migration SQL scripts indexed by migration number.
* Key is the source migration number (migration FROM this version).
*/
val migrations: Map<Int, String> = mapOf(
${migrationEntries},
)`
: `
/**
* Migration SQL scripts indexed by migration number.
*/
val migrations: Map<Int, String> = emptyMap()`;
return `// <auto-generated />
// This file is auto-generated from core/vault/src/sql/SqlConstants.ts
// Do not edit this file directly. Run './build.sh' in core/vault to regenerate.
@file:Suppress("all")
package net.aliasvault.app.vaultstore.models
/**
* SQL statements for vault creation and migration.
*/
object VaultSql {
/**
* Complete database schema SQL for creating a new vault with the latest schema.
*/
val completeSchema = """
${escapedSql}
""".trimIndent()
${migrationsMap}
/**
* Get migration SQL for upgrading from a specific revision.
*/
fun getMigrationSql(fromRevision: Int): String? {
return migrations[fromRevision]
}
}
`;
}
/**
* Ensure directory exists
*/
function ensureDir(filePath) {
const dir = path.dirname(filePath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
}
/**
* Main execution
*/
function main() {
console.log('Generating vault SQL for Swift and Kotlin...\n');
// Read TypeScript VaultVersions source
if (!fs.existsSync(TS_VERSIONS_SOURCE)) {
throw new Error(`Source file not found: ${TS_VERSIONS_SOURCE}`);
}
const tsVersionsContent = fs.readFileSync(TS_VERSIONS_SOURCE, 'utf8');
const versions = parseVaultVersions(tsVersionsContent);
if (versions.length === 0) {
throw new Error('No vault versions found in source file');
}
console.log(`Parsed ${versions.length} vault versions`);
console.log(`Latest version: ${versions[versions.length - 1].version} (revision ${versions[versions.length - 1].revision})`);
// Read TypeScript SqlConstants source
if (!fs.existsSync(TS_SQL_SOURCE)) {
throw new Error(`Source file not found: ${TS_SQL_SOURCE}`);
}
const tsSqlContent = fs.readFileSync(TS_SQL_SOURCE, 'utf8');
const completeSchemaSql = parseCompleteSchemaSql(tsSqlContent);
const migrations = parseMigrationScripts(tsSqlContent);
console.log(`Parsed complete schema SQL (${completeSchemaSql.length} chars)`);
console.log(`Parsed ${Object.keys(migrations).length} migration scripts`);
// Generate Swift VaultVersions
const swiftVersionsPath = path.join(SWIFT_OUTPUT_DIR, 'VaultVersions.swift');
ensureDir(swiftVersionsPath);
const swiftVersionsContent = generateSwiftVaultVersions(versions);
fs.writeFileSync(swiftVersionsPath, swiftVersionsContent, 'utf8');
console.log(`Generated: ${swiftVersionsPath}`);
// Generate Swift VaultSql
const swiftSqlPath = path.join(SWIFT_OUTPUT_DIR, 'VaultSql.swift');
ensureDir(swiftSqlPath);
const swiftSqlContent = generateSwiftVaultSql(completeSchemaSql, migrations);
fs.writeFileSync(swiftSqlPath, swiftSqlContent, 'utf8');
console.log(`Generated: ${swiftSqlPath}`);
// Generate Kotlin VaultVersions
const kotlinVersionsPath = path.join(KOTLIN_OUTPUT_DIR, 'VaultVersions.kt');
ensureDir(kotlinVersionsPath);
const kotlinVersionsContent = generateKotlinVaultVersions(versions);
fs.writeFileSync(kotlinVersionsPath, kotlinVersionsContent, 'utf8');
console.log(`Generated: ${kotlinVersionsPath}`);
// Generate Kotlin VaultSql
const kotlinSqlPath = path.join(KOTLIN_OUTPUT_DIR, 'VaultSql.kt');
ensureDir(kotlinSqlPath);
const kotlinSqlContent = generateKotlinVaultSql(completeSchemaSql, migrations);
fs.writeFileSync(kotlinSqlPath, kotlinSqlContent, 'utf8');
console.log(`Generated: ${kotlinSqlPath}`);
console.log('\nVault SQL generation complete!');
}
main();