diff --git a/src/package-lock.json b/src/package-lock.json index 679ce6b..45d41b0 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -1,12 +1,12 @@ { "name": "@event-driven-io/pongo-core", - "version": "0.15.3", + "version": "0.16.0-alpha.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@event-driven-io/pongo-core", - "version": "0.15.3", + "version": "0.16.0-alpha.1", "workspaces": [ "packages/dumbo", "packages/pongo" @@ -8674,7 +8674,7 @@ }, "packages/pongo": { "name": "@event-driven-io/pongo", - "version": "0.15.3", + "version": "0.16.0-alpha.1", "bin": { "pongo": "dist/cli.js" }, diff --git a/src/package.json b/src/package.json index e331183..e258770 100644 --- a/src/package.json +++ b/src/package.json @@ -1,6 +1,6 @@ { "name": "@event-driven-io/pongo-core", - "version": "0.15.3", + "version": "0.16.0-alpha.1", "description": "Pongo - Mongo with strong consistency on top of Postgres", "type": "module", "engines": { diff --git a/src/packages/dumbo/src/core/schema/migrations.ts b/src/packages/dumbo/src/core/schema/migrations.ts index 570ddb4..455143a 100644 --- a/src/packages/dumbo/src/core/schema/migrations.ts +++ b/src/packages/dumbo/src/core/schema/migrations.ts @@ -3,6 +3,7 @@ import { rawSql, singleOrNull, sql, + tracer, type SchemaComponent, type SQLExecutor, } from '..'; @@ -101,7 +102,10 @@ const runSQLMigration = async ( await recordMigration(execute, newMigration); // console.log(`Migration "${newMigration.name}" applied successfully.`); } catch (error) { - console.error(`Failed to apply migration "${migration.name}":`, error); + tracer.error('migration-error', { + migationName: migration.name, + error: error, + }); throw error; } }; diff --git a/src/packages/dumbo/src/core/tracing/index.ts b/src/packages/dumbo/src/core/tracing/index.ts index 6f2848d..a6ea668 100644 --- a/src/packages/dumbo/src/core/tracing/index.ts +++ b/src/packages/dumbo/src/core/tracing/index.ts @@ -1,14 +1,10 @@ import { JSONSerializer } from '../serializer'; -import { prettyPrintJson } from './printing'; +import { prettyJson } from './printing'; export const tracer = () => {}; export type LogLevel = 'DISABLED' | 'INFO' | 'LOG' | 'WARN' | 'ERROR'; -export type LogType = 'CONSOLE'; - -export type LogStyle = 'RAW' | 'PRETTY'; - export const LogLevel = { DISABLED: 'DISABLED' as LogLevel, INFO: 'INFO' as LogLevel, @@ -17,6 +13,15 @@ export const LogLevel = { ERROR: 'ERROR' as LogLevel, }; +export type LogType = 'CONSOLE'; + +export type LogStyle = 'RAW' | 'PRETTY'; + +export const LogStyle = { + RAW: 'RAW' as LogStyle, + PRETTY: 'PRETTY' as LogStyle, +}; + const shouldLog = (logLevel: LogLevel): boolean => { const definedLogLevel = process.env.DUMBO_LOG_LEVEL ?? LogLevel.DISABLED; @@ -59,7 +64,7 @@ const getTraceEventFormatter = case 'RAW': return JSONSerializer.serialize(event); case 'PRETTY': - return prettyPrintJson(event, true); + return prettyJson(event, { handleMultiline: true }); } }; diff --git a/src/packages/dumbo/src/core/tracing/printing/pretty.ts b/src/packages/dumbo/src/core/tracing/printing/pretty.ts index b3dcfeb..9db395d 100644 --- a/src/packages/dumbo/src/core/tracing/printing/pretty.ts +++ b/src/packages/dumbo/src/core/tracing/printing/pretty.ts @@ -31,7 +31,6 @@ const processString = ( return COLOR_STRING(`"${str}"`); }; -// Function to format and colorize JSON by traversing it const formatJson = ( // eslint-disable-next-line @typescript-eslint/no-explicit-any obj: any, @@ -46,7 +45,6 @@ const formatJson = ( if (typeof obj === 'number') return COLOR_NUMBER(String(obj)); if (typeof obj === 'boolean') return COLOR_BOOLEAN(String(obj)); - // Handle arrays if (Array.isArray(obj)) { const arrayItems = obj.map((item) => formatJson(item, indentLevel + 1, handleMultiline), @@ -70,7 +68,7 @@ const formatJson = ( )}\n${indent}${COLOR_BRACKETS('}')}`; }; -export const prettyPrintJson = ( +export const prettyJson = ( obj: unknown, - handleMultiline: boolean = false, -): string => formatJson(obj, 0, handleMultiline); + options?: { handleMultiline?: boolean }, +): string => formatJson(obj, 0, options?.handleMultiline); diff --git a/src/packages/dumbo/src/core/tracing/printing/pretty.unit.spec.ts b/src/packages/dumbo/src/core/tracing/printing/pretty.unit.spec.ts index a9cd273..d550755 100644 --- a/src/packages/dumbo/src/core/tracing/printing/pretty.unit.spec.ts +++ b/src/packages/dumbo/src/core/tracing/printing/pretty.unit.spec.ts @@ -1,7 +1,7 @@ import assert from 'assert'; import chalk from 'chalk'; import { describe, it } from 'node:test'; -import { prettyPrintJson } from './pretty'; +import { prettyJson } from './pretty'; // Define a basic test suite void describe('prettyPrintJson', () => { @@ -19,7 +19,7 @@ void describe('prettyPrintJson', () => { "age": 30 }`; - const output = prettyPrintJson(input, false); // Multiline handling off + const output = prettyJson(input, { handleMultiline: false }); // Multiline handling off assert.strictEqual(output, expectedOutput); }); @@ -37,7 +37,7 @@ void describe('prettyPrintJson', () => { " }`; - const output = prettyPrintJson(input, true); // Multiline handling on + const output = prettyJson(input, { handleMultiline: true }); // Multiline handling on assert.strictEqual(output, expectedOutput); }); @@ -64,7 +64,7 @@ void describe('prettyPrintJson', () => { } }`; - const output = prettyPrintJson(input, false); // Multiline handling off + const output = prettyJson(input, { handleMultiline: false }); // Multiline handling off assert.strictEqual(output, expectedOutput); }); @@ -85,7 +85,7 @@ void describe('prettyPrintJson', () => { "active": true }`; - const output = prettyPrintJson(input, false); // Multiline handling off + const output = prettyJson(input, { handleMultiline: false }); // Multiline handling off assert.strictEqual(output, expectedOutput); }); @@ -102,7 +102,7 @@ void describe('prettyPrintJson', () => { "tags": null }`; - const output = prettyPrintJson(input, false); // Multiline handling off + const output = prettyJson(input, { handleMultiline: false }); // Multiline handling off assert.strictEqual(output, expectedOutput); }); @@ -121,7 +121,7 @@ void describe('prettyPrintJson', () => { " }`; - const output = prettyPrintJson(input, true); // Multiline handling on + const output = prettyJson(input, { handleMultiline: true }); // Multiline handling on console.log(output); assert.strictEqual(output, expectedOutput); }); diff --git a/src/packages/dumbo/src/postgres/pg/connections/connection.ts b/src/packages/dumbo/src/postgres/pg/connections/connection.ts index 26d49e2..6ef9dd4 100644 --- a/src/packages/dumbo/src/postgres/pg/connections/connection.ts +++ b/src/packages/dumbo/src/postgres/pg/connections/connection.ts @@ -77,3 +77,47 @@ export function nodePostgresConnection( ? nodePostgresClientConnection(options) : nodePostgresPoolClientConnection(options); } + +export type ConnectionCheckResult = + | { successful: true } + | { + successful: false; + code: string | undefined; + errorType: 'ConnectionRefused' | 'Authentication' | 'Unknown'; + error: unknown; + }; + +export const checkConnection = async ( + connectionString: string, +): Promise => { + const client = new pg.Client({ + connectionString: connectionString, + }); + + try { + await client.connect(); + return { successful: true }; + } catch (error) { + const code = + error instanceof Error && + 'code' in error && + typeof error.code === 'string' + ? error.code + : undefined; + + return { + successful: false, + errorType: + code === 'ECONNREFUSED' + ? 'ConnectionRefused' + : code === '28P01' + ? 'Authentication' + : 'Unknown', + code, + error, + }; + } finally { + // Ensure the client is closed properly if connected + await client.end(); + } +}; diff --git a/src/packages/dumbo/src/postgres/pg/connections/pool.ts b/src/packages/dumbo/src/postgres/pg/connections/pool.ts index 5a1d5ee..d6f6429 100644 --- a/src/packages/dumbo/src/postgres/pg/connections/pool.ts +++ b/src/packages/dumbo/src/postgres/pg/connections/pool.ts @@ -2,12 +2,14 @@ import pg from 'pg'; import { createConnectionPool, JSONSerializer, + tracer, type ConnectionPool, } from '../../../core'; import { defaultPostgreSqlDatabase, getDatabaseNameOrDefault, } from '../../core'; +import { setNodePostgresTypeParser } from '../serialization'; import { nodePostgresConnection, NodePostgresConnectorType, @@ -15,7 +17,6 @@ import { type NodePostgresConnector, type NodePostgresPoolClientConnection, } from './connection'; -import { setNodePostgresTypeParser } from '../serialization'; export type NodePostgresNativePool = ConnectionPool; @@ -291,8 +292,7 @@ export const onEndPool = async (lookupKey: string, pool: pg.Pool) => { try { await pool.end(); } catch (error) { - console.log(`Error while closing the connection pool: ${lookupKey}`); - console.log(error); + tracer.error('connection-closing-error', { lookupKey, error }); } pools.delete(lookupKey); }; diff --git a/src/packages/pongo/package.json b/src/packages/pongo/package.json index 5a63b60..951acff 100644 --- a/src/packages/pongo/package.json +++ b/src/packages/pongo/package.json @@ -1,6 +1,6 @@ { "name": "@event-driven-io/pongo", - "version": "0.15.3", + "version": "0.16.0-alpha.1", "description": "Pongo - Mongo with strong consistency on top of Postgres", "type": "module", "scripts": { diff --git a/src/packages/pongo/src/commandLine/shell.ts b/src/packages/pongo/src/commandLine/shell.ts index cb9a6f2..7c320b3 100644 --- a/src/packages/pongo/src/commandLine/shell.ts +++ b/src/packages/pongo/src/commandLine/shell.ts @@ -1,9 +1,22 @@ -import { JSONSerializer, SQL } from '@event-driven-io/dumbo'; +import { + checkConnection, + LogLevel, + LogStyle, + prettyJson, + SQL, + type MigrationStyle, +} from '@event-driven-io/dumbo'; import chalk from 'chalk'; import Table from 'cli-table3'; import { Command } from 'commander'; import repl from 'node:repl'; -import { pongoClient, pongoSchema, type PongoClient } from '../core'; +import { + pongoClient, + pongoSchema, + type PongoClient, + type PongoCollectionSchema, + type PongoDb, +} from '../core'; let pongo: PongoClient; @@ -27,9 +40,7 @@ const calculateColumnWidths = ( // eslint-disable-next-line @typescript-eslint/no-explicit-any const printOutput = (obj: any): string => { - return Array.isArray(obj) - ? displayResultsAsTable(obj) - : JSONSerializer.serialize(obj); + return Array.isArray(obj) ? displayResultsAsTable(obj) : prettyJson(obj); }; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -60,51 +71,129 @@ const displayResultsAsTable = (results: any[]): string => { return table.toString(); }; -const startRepl = (options: { +const setLogLevel = (logLevel: string) => { + process.env.DUMBO_LOG_LEVEL = logLevel; +}; + +const setLogStyle = (logLevel: string) => { + process.env.DUMBO_LOG_STYLE = logLevel; +}; + +const prettifyLogs = (logLevel?: string) => { + if (logLevel !== undefined) setLogLevel(logLevel); + setLogStyle(LogStyle.PRETTY); +}; + +const startRepl = async (options: { + logging: { + logLevel: LogLevel; + logStyle: LogStyle; + }; schema: { database: string; collections: string[]; + autoMigration: MigrationStyle; }; - connectionString: string; + connectionString: string | undefined; }) => { - const r = repl.start({ + // TODO: This will change when we have proper tracing and logging config + // For now, that's enough + setLogLevel(process.env.DUMBO_LOG_LEVEL ?? options.logging.logLevel); + setLogStyle(process.env.DUMBO_LOG_STYLE ?? options.logging.logStyle); + + console.log(chalk.green('Starting Pongo Shell (version: 0.16.0-alpha.1)')); + + const connectionString = + options.connectionString ?? + process.env.DB_CONNECTION_STRING ?? + 'postgresql://postgres:postgres@localhost:5432/postgres'; + + if (!(options.connectionString ?? process.env.DB_CONNECTION_STRING)) { + console.log( + chalk.yellow( + `No connection string provided, using: 'postgresql://postgres:postgres@localhost:5432/postgres'`, + ), + ); + } + + const connectionCheck = await checkConnection(connectionString); + + if (!connectionCheck.successful) { + if (connectionCheck.errorType === 'ConnectionRefused') { + console.error( + chalk.red( + `Connection was refused. Check if the PostgreSQL server is running and accessible.`, + ), + ); + } else if (connectionCheck.errorType === 'Authentication') { + console.error( + chalk.red( + `Authentication failed. Check the username and password in the connection string.`, + ), + ); + } else { + console.error(chalk.red('Error connecting to PostgreSQL server')); + } + } + + console.log(chalk.green(`Successfully connected`)); + console.log(chalk.green('Use db..() to query.')); + + const shell = repl.start({ prompt: chalk.green('pongo> '), useGlobal: true, breakEvalOnSigint: true, writer: printOutput, }); - const schema = - options.schema.collections.length > 0 - ? pongoSchema.client({ - database: pongoSchema.db({ - users: pongoSchema.collection(options.schema.database), - }), - }) - : undefined; - - pongo = pongoClient(options.connectionString, { - ...(schema ? { schema: { definition: schema } } : {}), - }); + let db: PongoDb; + + if (options.schema.collections.length > 0) { + const collectionsSchema: Record = {}; + + for (const collectionName of options.schema.collections) { + const collection = pongoSchema.collection(collectionName); + collectionsSchema[collectionName] = shell.context[collectionName] = + collection; + } + + const schema = pongoSchema.client({ + database: pongoSchema.db(options.schema.database, collectionsSchema), + }); - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const db = schema - ? // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any - (pongo as any).database - : pongo.db(options.schema.database); + const typedClient = pongoClient(connectionString, { + schema: { definition: schema }, + }); + + db = typedClient.database; - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - r.context.db = db; - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - r.context.SQL = SQL; + pongo = typedClient; + } else { + pongo = pongoClient(connectionString, { + schema: { autoMigration: options.schema.autoMigration }, + }); + + db = pongo.db(options.schema.database); + } + + shell.context.pongo = pongo; + shell.context.db = db; + + // helpers + shell.context.SQL = SQL; + shell.context.setLogLevel = setLogLevel; + shell.context.setLogStyle = setLogStyle; + shell.context.prettifyLogs = prettifyLogs; + shell.context.LogStyle = LogStyle; + shell.context.LogLevel = LogLevel; // Intercept REPL output to display results as a table if they are arrays - r.on('exit', async () => { + shell.on('exit', async () => { await teardown(); process.exit(); }); - r.on('SIGINT', async () => { + shell.on('SIGINT', async () => { await teardown(); process.exit(); }); @@ -121,7 +210,11 @@ process.on('SIGINT', teardown); interface ShellOptions { database: string; collection: string[]; - connectionString: string; + connectionString?: string; + disableAutoMigrations: boolean; + logStyle?: string; + logLevel?: string; + prettyLog?: boolean; } const shellCommand = new Command('shell') @@ -129,7 +222,6 @@ const shellCommand = new Command('shell') .option( '-cs, --connectionString ', 'Connection string for the database', - 'postgresql://postgres:postgres@localhost:5432/postgres', ) .option('-db, --database ', 'Database name to connect', 'postgres') .option( @@ -141,18 +233,39 @@ const shellCommand = new Command('shell') }, [] as string[], ) - .action((options: ShellOptions) => { + .option( + '-no-migrations, --disable-auto-migrations', + 'Disable automatic migrations', + ) + .option( + '-ll, --log-level ', + 'Log level: DISABLED, INFO, LOG, WARN, ERROR', + 'DISABLED', + ) + .option('-ls, --log-style', 'Log style: RAW, PRETTY', 'RAW') + .option('-p, --pretty-log', 'Turn on logging with prettified output') + .action(async (options: ShellOptions) => { const { collection, database } = options; - const connectionString = - options.connectionString ?? process.env.DB_CONNECTION_STRING; + const connectionString = options.connectionString; - console.log( - chalk.green( - 'Starting Pongo Shell. Use db..() to query.', - ), - ); - startRepl({ - schema: { collections: collection, database }, + await startRepl({ + logging: { + logStyle: options.prettyLog + ? LogStyle.PRETTY + : ((options.logStyle as LogStyle | undefined) ?? LogStyle.RAW), + logLevel: options.logLevel + ? (options.logLevel as LogLevel) + : options.prettyLog + ? LogLevel.INFO + : LogLevel.DISABLED, + }, + schema: { + collections: collection, + database, + autoMigration: options.disableAutoMigrations + ? 'None' + : 'CreateOrUpdate', + }, connectionString, }); });