diff --git a/.env.example b/.env.example index c82051095f..f6731270ec 100644 --- a/.env.example +++ b/.env.example @@ -52,7 +52,7 @@ DEL_INSTANCE=false # Provider: postgresql | mysql | psql_bouncer DATABASE_PROVIDER=postgresql -DATABASE_CONNECTION_URI='postgresql://user:pass@evolution-postgres:5432/evolution_db?schema=evolution_api' +DATABASE_CONNECTION_URI=postgresql://user:pass@evolution-postgres:5432/evolution_db # Postgres container settings (used by docker-compose) POSTGRES_DATABASE=evolution_db diff --git a/Docker/scripts/create_database.js b/Docker/scripts/create_database.js new file mode 100644 index 0000000000..ca5c77ed3d --- /dev/null +++ b/Docker/scripts/create_database.js @@ -0,0 +1,112 @@ +#!/usr/bin/env node +/** + * Ensures the target database exists before running migrations. + * Connects to the server's default admin database, checks if the target + * database is present, and creates it if not. + * + * Runs as a non-fatal pre-migration step: if the user lacks CREATE DATABASE + * permission (managed DB, read-only replica, etc.) the error is logged as a + * warning and the process exits 0 so the migration attempt still proceeds. + * + * Supported providers: postgresql, psql_bouncer, mysql + */ + +'use strict'; + +const provider = process.env.DATABASE_PROVIDER ?? 'postgresql'; +const uri = process.env.DATABASE_CONNECTION_URI; + +if (!uri) { + console.error('[create_database] DATABASE_CONNECTION_URI is not set — skipping auto-create.'); + process.exit(0); +} + +async function ensurePostgresDatabase() { + const { Pool } = require('pg'); + + const url = new URL(uri); + const dbName = decodeURIComponent(url.pathname.slice(1)); + + if (!dbName) { + console.warn('[create_database] Could not parse database name from URI — skipping.'); + return; + } + + // Connect to the server admin database to check / create the target DB + const adminUrl = new URL(uri); + adminUrl.pathname = '/postgres'; + // Strip any search params that are schema-specific (not relevant for admin conn) + adminUrl.search = ''; + + const pool = new Pool({ connectionString: adminUrl.toString(), connectionTimeoutMillis: 10000 }); + + try { + const result = await pool.query('SELECT 1 FROM pg_database WHERE datname = $1', [dbName]); + + if (result.rowCount === 0) { + console.log(`[create_database] Database "${dbName}" not found. Creating...`); + // Identifier cannot be parameterized — validate it is a plain name first + if (!/^[\w-]+$/.test(dbName)) { + throw new Error(`Refusing to CREATE DATABASE: name "${dbName}" contains unsafe characters.`); + } + await pool.query(`CREATE DATABASE "${dbName}"`); + console.log(`[create_database] Database "${dbName}" created successfully.`); + } else { + console.log(`[create_database] Database "${dbName}" already exists — skipping creation.`); + } + } finally { + await pool.end(); + } +} + +async function ensureMysqlDatabase() { + // mysql2 is a transitive dep via @prisma/adapter-mariadb; attempt to load it + let mysql; + try { + mysql = require('mysql2/promise'); + } catch { + console.warn('[create_database] mysql2 not available — skipping auto-create for MySQL.'); + return; + } + + const url = new URL(uri); + const dbName = decodeURIComponent(url.pathname.slice(1)); + + if (!dbName) { + console.warn('[create_database] Could not parse database name from URI — skipping.'); + return; + } + + if (!/^[\w-]+$/.test(dbName)) { + throw new Error(`Refusing to CREATE DATABASE: name "${dbName}" contains unsafe characters.`); + } + + const connection = await mysql.createConnection({ + host: url.hostname, + port: Number(url.port) || 3306, + user: decodeURIComponent(url.username), + password: decodeURIComponent(url.password), + connectTimeout: 10000, + }); + + try { + await connection.execute(`CREATE DATABASE IF NOT EXISTS \`${dbName}\``); + console.log(`[create_database] Database "${dbName}" ensured.`); + } finally { + await connection.end(); + } +} + +(async () => { + try { + if (provider === 'mysql') { + await ensureMysqlDatabase(); + } else { + await ensurePostgresDatabase(); + } + } catch (err) { + console.warn(`[create_database] Warning: could not auto-create database: ${err.message}`); + console.warn('[create_database] Proceeding — migration will fail with a clear error if the DB is truly missing.'); + process.exit(0); + } +})(); diff --git a/Docker/scripts/deploy_database.sh b/Docker/scripts/deploy_database.sh index fea58ff850..60ae9688cd 100755 --- a/Docker/scripts/deploy_database.sh +++ b/Docker/scripts/deploy_database.sh @@ -8,6 +8,8 @@ fi if [[ "$DATABASE_PROVIDER" == "postgresql" || "$DATABASE_PROVIDER" == "mysql" || "$DATABASE_PROVIDER" == "psql_bouncer" ]]; then export DATABASE_URL + echo "Ensuring database exists for $DATABASE_PROVIDER..." + node ./Docker/scripts/create_database.js echo "Deploying migrations for $DATABASE_PROVIDER" echo "Database URL: $DATABASE_URL" # rm -rf ./prisma/migrations diff --git a/Dockerfile b/Dockerfile index 81953db4ff..2bfdfb0f5e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,6 +24,7 @@ COPY ./prisma ./prisma COPY ./manager ./manager COPY ./.env.example ./.env COPY ./runWithProvider.js ./ +COPY ./prisma.config.ts ./ COPY ./Docker ./Docker @@ -65,6 +66,7 @@ COPY --from=builder /evolution/.env ./.env COPY --from=builder /evolution/Docker ./Docker COPY --from=builder /evolution/runWithProvider.js ./runWithProvider.js COPY --from=builder /evolution/tsup.config.ts ./tsup.config.ts +COPY --from=builder /evolution/prisma.config.ts ./prisma.config.ts ENV DOCKER_ENV=true diff --git a/patches/.gitkeep b/patches/.gitkeep new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/patches/.gitkeep @@ -0,0 +1 @@ + diff --git a/src/api/abstract/abstract.router.ts b/src/api/abstract/abstract.router.ts index 3ff633d58b..b991c9cb74 100644 --- a/src/api/abstract/abstract.router.ts +++ b/src/api/abstract/abstract.router.ts @@ -51,7 +51,12 @@ export abstract class RouterBroker { } if (request.originalUrl.includes('/instance/create')) { - Object.assign(instance, sanitizeUntrustedInput(body)); + const sanitized = sanitizeUntrustedInput(body); + // instanceName must come from the body on create — there is no URL param on this route + if (body?.instanceName !== undefined) { + sanitized.instanceName = body.instanceName; + } + Object.assign(instance, sanitized); } Object.assign(ref, body); diff --git a/src/api/integrations/channel/evohub/evohub.controlplane.router.ts b/src/api/integrations/channel/evohub/evohub.controlplane.router.ts index f87b335259..022a821a6a 100644 --- a/src/api/integrations/channel/evohub/evohub.controlplane.router.ts +++ b/src/api/integrations/channel/evohub/evohub.controlplane.router.ts @@ -37,7 +37,7 @@ export class EvoHubControlPlaneRouter extends RouterBroker { }); this.router.get('/evohub/channels/:id', guard, async (req, res) => { - res.json(await evoHubClient.getChannel(req.params.id)); + res.json(await evoHubClient.getChannel(req.params.id as string)); }); this.router.get('/evohub/available-channels', guard, async (_req, res) => { @@ -114,7 +114,7 @@ export class EvoHubControlPlaneRouter extends RouterBroker { }); this.router.post('/evohub/channels/:id/meta-connect', guard, async (req, res) => { - res.json(await evoHubClient.connectToMeta(req.params.id, req.body)); + res.json(await evoHubClient.connectToMeta(req.params.id as string, req.body)); }); } } diff --git a/src/api/services/monitor.service.ts b/src/api/services/monitor.service.ts index f327e4a7c9..f78ccf8308 100644 --- a/src/api/services/monitor.service.ts +++ b/src/api/services/monitor.service.ts @@ -257,8 +257,25 @@ export class WAMonitoringService { businessId: data.businessId, }, }); + // Ensure Setting record exists immediately after Instance creation. + // Prevents FK constraint violations (Setting_instanceId_fkey) in chatbot + // integrations that write to IntegrationSession before setSettings() runs. + await this.prismaRepository.setting.upsert({ + where: { instanceId: data.instanceId }, + update: {}, + create: { + instanceId: data.instanceId, + rejectCall: false, + groupsIgnore: false, + alwaysOnline: false, + readMessages: false, + readStatus: false, + syncFullHistory: false, + }, + }); } catch (error) { this.logger.error(error); + throw error; } } diff --git a/tsup.config.ts b/tsup.config.ts index 23e9208362..7a36420dd1 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -26,6 +26,10 @@ export default defineConfig({ // Apenas o nome exato @prisma/client é bundlado (redirecionado pelo alias para o client // gerado). Subpaths como @prisma/client/runtime/* permanecem externos (node_modules). noExternal: [/^@prisma\/client$/], + define: { + __LICENSE_ENDPOINT_ENCODED__: licenseEndpointEncoded, + __LICENSE_ENDPOINT_XOR_KEY__: licenseEndpointXorKey, + }, esbuildOptions(options) { // platform node: garante o shim de import.meta.url no bundle CJS (client Prisma 7 usa import.meta) options.platform = 'node'; @@ -33,9 +37,6 @@ export default defineConfig({ ...(options.alias ?? {}), '@prisma/client': path.resolve(process.cwd(), 'prisma/generated/client/client.ts'), }; - define: { - __LICENSE_ENDPOINT_ENCODED__: licenseEndpointEncoded, - __LICENSE_ENDPOINT_XOR_KEY__: licenseEndpointXorKey, }, onSuccess: async () => { cpSync('src/utils/translations', 'dist/translations', { recursive: true });