diff --git a/README.md b/README.md index de16572..53df3ef 100644 --- a/README.md +++ b/README.md @@ -165,7 +165,11 @@ MongoDB is a decent database, yet it has issues around [ACID-complaince](https:/ - operate **easier than crafting native PostgreSQL JSON queries**. They're powerful but not the most accessible, - get **performance boost** with [JSONB indexing capabilities](https://pganalyze.com/blog/gin-index#postgresql-jsonb-and-gin-indexes), - **benefit from PostgreSQL advanced capabilities** like [partitioning](https://www.postgresql.fastware.com/postgresql-insider-prt-ove), [logical replication](https://event-driven.io/en/push_based_outbox_pattern_with_postgres_logical_replication/) and [other PostgreSQL superpowers](https://event-driven.io/en/postgres_superpowers/) -- **seamless integration with Cloud RDSes** and solutions like [CockroachDB](https://www.cockroachlabs.com/docs/stable/why-cockroachdb), [Supabase](https://supabase.com/), [Vercel Postgres](https://vercel.com/docs/storage/vercel-postgres). +- **seamless integration with Cloud RDSes** and solutions like [Supabase](https://supabase.com/), [Vercel Postgres](https://vercel.com/docs/storage/vercel-postgres), [YugabyteDB](https://www.yugabyte.com/yugabytedb/), [CockroachDB](https://www.cockroachlabs.com/docs/stable/why-cockroachdb), . + +Watch also more in: + +[![](https://i.ytimg.com/vi/P4r19rv4vOg/hqdefault.jpg?sqp=-oaymwEcCNACELwBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLA5xOyPKh2X4f4ghHcapr5n69GvvA)](https://www.youtube.com/watch?v=P4r19rv4vOg) ## Storage diff --git a/src/docs/getting-started.md b/src/docs/getting-started.md index a5c4aab..03e9d5a 100644 --- a/src/docs/getting-started.md +++ b/src/docs/getting-started.md @@ -163,7 +163,11 @@ MongoDB is a decent database, yet it has issues around [ACID-complaince](https:/ - operate **easier than crafting native PostgreSQL JSON queries**. They're powerful but not the most accessible, - get **performance boost** with [JSONB indexing capabilities](https://pganalyze.com/blog/gin-index#postgresql-jsonb-and-gin-indexes), - **benefit from PostgreSQL advanced capabilities** like [partitioning](https://www.postgresql.fastware.com/postgresql-insider-prt-ove), [logical replication](https://event-driven.io/en/push_based_outbox_pattern_with_postgres_logical_replication/) and [other PostgreSQL superpowers](https://event-driven.io/en/postgres_superpowers/) -- **seamless integration with Cloud RDSes** and solutions like [CockroachDB](https://www.cockroachlabs.com/docs/stable/why-cockroachdb), [Supabase](https://supabase.com/), [Vercel Postgres](https://vercel.com/docs/storage/vercel-postgres). +- **seamless integration with Cloud RDSes** and solutions like [Supabase](https://supabase.com/), [Vercel Postgres](https://vercel.com/docs/storage/vercel-postgres), [YugabyteDB](https://www.yugabyte.com/yugabytedb/), [CockroachDB](https://www.cockroachlabs.com/docs/stable/why-cockroachdb), . + +Watch also more in: + +[![](https://i.ytimg.com/vi/P4r19rv4vOg/hqdefault.jpg?sqp=-oaymwEcCNACELwBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLA5xOyPKh2X4f4ghHcapr5n69GvvA)](https://www.youtube.com/watch?v=P4r19rv4vOg) ## Storage diff --git a/src/package-lock.json b/src/package-lock.json index 679ce6b..a909504 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.9", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@event-driven-io/pongo-core", - "version": "0.15.3", + "version": "0.16.0-alpha.9", "workspaces": [ "packages/dumbo", "packages/pongo" @@ -8660,7 +8660,7 @@ }, "packages/dumbo": { "name": "@event-driven-io/dumbo", - "version": "0.11.1", + "version": "0.12.0-alpha.9", "devDependencies": { "@types/node": "22.4.1" }, @@ -8674,7 +8674,7 @@ }, "packages/pongo": { "name": "@event-driven-io/pongo", - "version": "0.15.3", + "version": "0.16.0-alpha.9", "bin": { "pongo": "dist/cli.js" }, @@ -8682,7 +8682,7 @@ "@types/node": "22.4.1" }, "peerDependencies": { - "@event-driven-io/dumbo": "0.11.1", + "@event-driven-io/dumbo": "0.12.0-alpha.9", "@types/mongodb": "^4.0.7", "@types/pg": "^8.11.6", "@types/uuid": "^10.0.0", diff --git a/src/package.json b/src/package.json index e331183..215a729 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.11", "description": "Pongo - Mongo with strong consistency on top of Postgres", "type": "module", "engines": { diff --git a/src/packages/dumbo/package.json b/src/packages/dumbo/package.json index f242af8..941ff68 100644 --- a/src/packages/dumbo/package.json +++ b/src/packages/dumbo/package.json @@ -1,6 +1,6 @@ { "name": "@event-driven-io/dumbo", - "version": "0.11.1", + "version": "0.12.0-alpha.11", "description": "Dumbo - tools for dealing with PostgreSQL", "type": "module", "scripts": { 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/sql/index.ts b/src/packages/dumbo/src/core/sql/index.ts index d171637..7b4dfc8 100644 --- a/src/packages/dumbo/src/core/sql/index.ts +++ b/src/packages/dumbo/src/core/sql/index.ts @@ -35,6 +35,8 @@ const defaultFormat = (value: unknown) => { return format('%L', value); } else if (Array.isArray(value)) { return format('(%L)', value); + } else if (typeof value === 'object') { + return format('%s', JSON.stringify(value)); } else { return format('%L', value); } diff --git a/src/packages/dumbo/src/core/tracing/index.ts b/src/packages/dumbo/src/core/tracing/index.ts index 6f2848d..80b4b28 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; @@ -37,7 +42,9 @@ const shouldLog = (logLevel: LogLevel): boolean => { if ( definedLogLevel === LogLevel.INFO && - [LogLevel.ERROR, LogLevel.WARN, LogLevel.INFO].includes(logLevel) + [LogLevel.ERROR, LogLevel.WARN, LogLevel.LOG, LogLevel.INFO].includes( + logLevel, + ) ) return true; @@ -59,7 +66,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..29904b3 100644 --- a/src/packages/dumbo/src/core/tracing/printing/pretty.ts +++ b/src/packages/dumbo/src/core/tracing/printing/pretty.ts @@ -4,9 +4,9 @@ const TWO_SPACES = ' '; const COLOR_STRING = chalk.hex('#98c379'); // Soft green for strings const COLOR_KEY = chalk.hex('#61afef'); // Muted cyan for keys -const COLOR_NUMBER = chalk.hex('#d19a66'); // Light orange for numbers +const COLOR_NUMBER_OR_DATE = chalk.hex('#d19a66'); // Light orange for numbers const COLOR_BOOLEAN = chalk.hex('#c678dd'); // Light purple for booleans -const COLOR_NULL = chalk.hex('#c678dd'); // Light purple for null +const COLOR_NULL_OR_UNDEFINED = chalk.hex('#c678dd'); // Light purple for null const COLOR_BRACKETS = chalk.hex('#abb2bf'); // Soft white for object and array brackets const processString = ( @@ -31,7 +31,10 @@ const processString = ( return COLOR_STRING(`"${str}"`); }; -// Function to format and colorize JSON by traversing it +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const shouldPrint = (obj: any): boolean => + typeof obj !== 'function' && typeof obj !== 'symbol'; + const formatJson = ( // eslint-disable-next-line @typescript-eslint/no-explicit-any obj: any, @@ -40,13 +43,34 @@ const formatJson = ( ): string => { const indent = TWO_SPACES.repeat(indentLevel); - if (obj === null) return COLOR_NULL('null'); + if (obj === null) return COLOR_NULL_OR_UNDEFINED('null'); + + if (obj === undefined) return COLOR_NULL_OR_UNDEFINED('undefined'); + if (typeof obj === 'string') return processString(obj, indent, handleMultiline); - if (typeof obj === 'number') return COLOR_NUMBER(String(obj)); + if (typeof obj === 'number' || typeof obj === 'bigint' || obj instanceof Date) + return COLOR_NUMBER_OR_DATE(String(obj)); if (typeof obj === 'boolean') return COLOR_BOOLEAN(String(obj)); - // Handle arrays + if (obj instanceof Error) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const errorObj: Record = {}; + + const propNames = Object.getOwnPropertyNames(obj); + + propNames.forEach((key) => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access + errorObj[key] = (obj as any)[key]; + }); + + return formatJson(errorObj, indentLevel, handleMultiline); + } + + if (obj instanceof Promise) { + return COLOR_STRING('Promise {pending}'); + } + if (Array.isArray(obj)) { const arrayItems = obj.map((item) => formatJson(item, indentLevel + 1, handleMultiline), @@ -57,20 +81,22 @@ const formatJson = ( } // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - const entries = Object.entries(obj).map( - ([key, value]) => - `${COLOR_KEY(`"${key}"`)}: ${formatJson( - value, - indentLevel + 1, - handleMultiline, - )}`, - ); + const entries = Object.entries(obj) + .filter(([_, value]) => shouldPrint(value)) + .map( + ([key, value]) => + `${COLOR_KEY(`"${key}"`)}: ${formatJson( + value, + indentLevel + 1, + handleMultiline, + )}`, + ); return `${COLOR_BRACKETS('{')}\n${indent} ${entries.join( `,\n${indent} `, )}\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..f8f604b 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,9 +1,8 @@ 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', () => { // Turn off chalk colorization during tests for easy comparison chalk.level = 0; @@ -19,7 +18,7 @@ void describe('prettyPrintJson', () => { "age": 30 }`; - const output = prettyPrintJson(input, false); // Multiline handling off + const output = prettyJson(input, { handleMultiline: false }); assert.strictEqual(output, expectedOutput); }); @@ -37,7 +36,7 @@ void describe('prettyPrintJson', () => { " }`; - const output = prettyPrintJson(input, true); // Multiline handling on + const output = prettyJson(input, { handleMultiline: true }); assert.strictEqual(output, expectedOutput); }); @@ -64,7 +63,7 @@ void describe('prettyPrintJson', () => { } }`; - const output = prettyPrintJson(input, false); // Multiline handling off + const output = prettyJson(input, { handleMultiline: false }); assert.strictEqual(output, expectedOutput); }); @@ -85,7 +84,7 @@ void describe('prettyPrintJson', () => { "active": true }`; - const output = prettyPrintJson(input, false); // Multiline handling off + const output = prettyJson(input, { handleMultiline: false }); assert.strictEqual(output, expectedOutput); }); @@ -102,7 +101,7 @@ void describe('prettyPrintJson', () => { "tags": null }`; - const output = prettyPrintJson(input, false); // Multiline handling off + const output = prettyJson(input, { handleMultiline: false }); assert.strictEqual(output, expectedOutput); }); @@ -121,8 +120,7 @@ void describe('prettyPrintJson', () => { " }`; - const output = prettyPrintJson(input, true); // Multiline handling on - console.log(output); + const output = prettyJson(input, { handleMultiline: true }); 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..dddfe8a 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.11", "description": "Pongo - Mongo with strong consistency on top of Postgres", "type": "module", "scripts": { @@ -87,7 +87,7 @@ "pongo": "./dist/cli.js" }, "peerDependencies": { - "@event-driven-io/dumbo": "0.11.1", + "@event-driven-io/dumbo": "0.12.0-alpha.11", "@types/mongodb": "^4.0.7", "@types/pg": "^8.11.6", "@types/uuid": "^10.0.0", diff --git a/src/packages/pongo/src/commandLine/configFile.ts b/src/packages/pongo/src/commandLine/configFile.ts index b229d8d..19def9f 100644 --- a/src/packages/pongo/src/commandLine/configFile.ts +++ b/src/packages/pongo/src/commandLine/configFile.ts @@ -25,7 +25,7 @@ const sampleConfig = (collectionNames: string[] = ['users']) => { const types = collectionNames .map( (name) => - `type ${formatTypeName(name)} = { name: string; description: string; date: Date }`, + `export type ${formatTypeName(name)} = { name: string; description: string; date: Date }`, ) .join('\n'); diff --git a/src/packages/pongo/src/commandLine/migrate.ts b/src/packages/pongo/src/commandLine/migrate.ts index 3d8a9b1..f04f360 100644 --- a/src/packages/pongo/src/commandLine/migrate.ts +++ b/src/packages/pongo/src/commandLine/migrate.ts @@ -18,6 +18,7 @@ interface MigrateRunOptions { interface MigrateSqlOptions { print?: boolean; write?: string; + config?: string; collection: string[]; } @@ -29,7 +30,7 @@ migrateCommand .command('run') .description('Run database migrations') .option( - '-cs, --connectionString ', + '-cs, --connection-string ', 'Connection string for the database', ) .option( @@ -51,7 +52,8 @@ migrateCommand if (!connectionString) { console.error( - 'Error: Connection string is required. Provide it either as a "--connectionString" parameter or through the DB_CONNECTION_STRING environment variable.', + 'Error: Connection string is required. Provide it either as a "--connection-string" parameter or through the DB_CONNECTION_STRING environment variable.' + + '\nFor instance: --connection-string postgresql://postgres:postgres@localhost:5432/postgres', ); process.exit(1); } @@ -94,24 +96,34 @@ migrateCommand }, [] as string[], ) + .option('-f, --config ', 'Path to configuration file with Pongo config') .option('--print', 'Print the SQL to the console (default)', true) //.option('--write ', 'Write the SQL to a specified file') - .action((options: MigrateSqlOptions) => { + .action(async (options: MigrateSqlOptions) => { const { collection } = options; - if (!collection) { + let collectionNames: string[]; + + if (options.config) { + const config = await loadConfigFile(options.config); + + collectionNames = config.collections.map((c) => c.name); + } else if (collection) { + collectionNames = collection; + } else { console.error( - 'Error: You need to provide at least one collection name is required. Provide it either as a "col" parameter.', + 'Error: You need to provide at least one collection name. Provide it either through "--config" file or as a "--collection" parameter.', ); process.exit(1); } + const coreMigrations = migrationTableSchemaComponent.migrations({ connector: 'PostgreSQL:pg', }); const migrations = [ ...coreMigrations, - ...collection.flatMap((collectionsName) => - pongoCollectionSchemaComponent(collectionsName).migrations({ + ...collectionNames.flatMap((collectionName) => + pongoCollectionSchemaComponent(collectionName).migrations({ connector: 'PostgreSQL:pg', // TODO: Provide connector here }), ), diff --git a/src/packages/pongo/src/commandLine/shell.ts b/src/packages/pongo/src/commandLine/shell.ts index cb9a6f2..f49c0b0 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 @@ -38,8 +49,13 @@ const displayResultsAsTable = (results: any[]): string => { return chalk.yellow('No documents found.'); } - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - const columnNames = Object.keys(results[0]); + const columnNames = results + + .flatMap((result) => + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + typeof result === 'object' ? Object.keys(result) : typeof result, + ) + .filter((value, index, array) => array.indexOf(value) === index); const columnWidths = calculateColumnWidths(results, columnNames); @@ -52,7 +68,18 @@ const displayResultsAsTable = (results: any[]): string => { table.push( columnNames.map((col) => // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - result[col] !== undefined ? String(result[col]) : '', + result[col] !== undefined + ? // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + Array.isArray(result[col]) + ? // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + displayResultsAsTable(result[col]) + : // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + prettyJson(result[col]) + : typeof result === 'object' + ? '' + : result != undefined && result != undefined + ? prettyJson(result) + : '', ), ); }); @@ -60,51 +87,138 @@ 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({ + console.log(JSON.stringify(options)); + // 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.11)')); + + 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.red('Exiting Pongo Shell...')); + process.exit(); + } + + 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) { + collectionsSchema[collectionName] = + pongoSchema.collection(collectionName); + } + + const schema = pongoSchema.client({ + database: pongoSchema.db(options.schema.database, collectionsSchema), + }); + + const typedClient = pongoClient(connectionString, { + schema: { + definition: schema, + autoMigration: options.schema.autoMigration, + }, + }); + + db = typedClient.database; + + for (const collectionName of options.schema.collections) { + shell.context[collectionName] = typedClient.database[collectionName]; + } + + pongo = typedClient; + } else { + pongo = pongoClient(connectionString, { + schema: { autoMigration: options.schema.autoMigration }, + }); + + db = pongo.db(options.schema.database); + } - // 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); + shell.context.pongo = pongo; + shell.context.db = db; - // 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; + // 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 +235,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 +247,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 +258,40 @@ 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) => { + console.log(JSON.stringify(options)); 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, }); }); diff --git a/src/packages/pongo/src/core/collection/pongoCollection.ts b/src/packages/pongo/src/core/collection/pongoCollection.ts index 4846d0f..3b58407 100644 --- a/src/packages/pongo/src/core/collection/pongoCollection.ts +++ b/src/packages/pongo/src/core/collection/pongoCollection.ts @@ -66,7 +66,7 @@ const enlistIntoTransactionIfActive = async < return await transaction.enlistDatabase(db); }; -const transactionExecutorOrDefault = async < +export const transactionExecutorOrDefault = async < ConnectorType extends string = string, >( db: PongoDb, @@ -197,10 +197,12 @@ export const pongoCollection = < return operationResult( { - successful: result.rows[0]!.modified === result.rows[0]!.matched, - modifiedCount: Number(result.rows[0]!.modified), - matchedCount: Number(result.rows[0]!.matched), - nextExpectedVersion: result.rows[0]!.version, + successful: + result.rows.length > 0 && + result.rows[0]!.modified === result.rows[0]!.matched, + modifiedCount: Number(result.rows[0]?.modified ?? 0), + matchedCount: Number(result.rows[0]?.matched ?? 0), + nextExpectedVersion: result.rows[0]?.version ?? 0n, }, { operationName: 'updateOne', collectionName, errors }, ); @@ -218,10 +220,10 @@ export const pongoCollection = < ); return operationResult( { - successful: result.rows[0]!.modified > 0, - modifiedCount: Number(result.rows[0]!.modified), - matchedCount: Number(result.rows[0]!.matched), - nextExpectedVersion: result.rows[0]!.version, + successful: result.rows.length > 0 && result.rows[0]!.modified > 0, + modifiedCount: Number(result.rows[0]?.modified ?? 0), + matchedCount: Number(result.rows[0]?.matched ?? 0), + nextExpectedVersion: result.rows[0]?.version ?? 0n, }, { operationName: 'replaceOne', collectionName, errors }, ); @@ -256,9 +258,9 @@ export const pongoCollection = < ); return operationResult( { - successful: result.rows[0]!.deleted! > 0, - deletedCount: Number(result.rows[0]!.deleted!), - matchedCount: Number(result.rows[0]!.matched!), + successful: result.rows.length > 0 && result.rows[0]!.deleted! > 0, + deletedCount: Number(result.rows[0]?.deleted ?? 0), + matchedCount: Number(result.rows[0]?.matched ?? 0), }, { operationName: 'deleteOne', collectionName, errors }, ); @@ -467,7 +469,7 @@ export const pongoCollection = < ): Promise { await ensureCollectionCreated(options); - const result = await query(sql); + const result = await query(sql, options); return result.rows; }, async command( @@ -476,7 +478,7 @@ export const pongoCollection = < ): Promise> { await ensureCollectionCreated(options); - return command(sql); + return command(sql, options); }, }, schema: { diff --git a/src/packages/pongo/src/core/typing/operations.ts b/src/packages/pongo/src/core/typing/operations.ts index 718a4d9..32713ff 100644 --- a/src/packages/pongo/src/core/typing/operations.ts +++ b/src/packages/pongo/src/core/typing/operations.ts @@ -67,10 +67,21 @@ export interface PongoDb connect(): Promise; close(): Promise; collection(name: string): PongoCollection; + collections(): ReadonlyArray>; readonly schema: Readonly<{ component: SchemaComponent; migrate(): Promise; }>; + sql: { + query( + sql: SQL, + options?: CollectionOperationOptions, + ): Promise; + command( + sql: SQL, + options?: CollectionOperationOptions, + ): Promise>; + }; } export type CollectionOperationOptions = { diff --git a/src/packages/pongo/src/e2e/postgres.e2e.spec.ts b/src/packages/pongo/src/e2e/postgres.e2e.spec.ts index 3b629fb..70997b4 100644 --- a/src/packages/pongo/src/e2e/postgres.e2e.spec.ts +++ b/src/packages/pongo/src/e2e/postgres.e2e.spec.ts @@ -217,6 +217,43 @@ void describe('MongoDB Compatibility Tests', () => { ); }); + void it('should NOT update a non-existing document', async () => { + const pongoCollection = pongoDb.collection('updateOne'); + const mongoCollection = mongoDb.collection('shimupdateOne'); + const nonExistingId = 'non-existing'; + + const update = { $set: { age: 31 } }; + + const updateResult = await pongoCollection.updateOne( + { _id: nonExistingId }, + update, + ); + const shimUpdateResult = await mongoCollection.updateOne( + { _id: nonExistingId }, + update, + ); + + assert(updateResult); + assert(updateResult.successful === false); + assert(updateResult.matchedCount === 0); + assert(updateResult.modifiedCount === 0); + assert(updateResult.nextExpectedVersion === 0n); + + assert(shimUpdateResult); + assert(updateResult.matchedCount === 0); + assert(updateResult.modifiedCount === 0); + + const pongoDoc = await pongoCollection.findOne({ + _id: nonExistingId, + }); + const mongoDoc = await mongoCollection.findOne({ + _id: nonExistingId, + }); + + assert(pongoDoc === null); + assert(mongoDoc === null); + }); + void it('should update a multiple properties in document', async () => { const pongoCollection = pongoDb.collection('updateOneMultiple'); const mongoCollection = mongoDb.collection('shimupdateOneMultiple'); @@ -225,7 +262,7 @@ void describe('MongoDB Compatibility Tests', () => { const pongoInsertResult = await pongoCollection.insertOne(doc); const mongoInsertResult = await mongoCollection.insertOne(doc); - const update = { $set: { age: 31, tags: [] } }; + const update = { $set: { age: 31, tags: ['t', 'a', 'g'] } }; await pongoCollection.updateOne( { _id: pongoInsertResult.insertedId! }, @@ -244,7 +281,7 @@ void describe('MongoDB Compatibility Tests', () => { }); assert.equal(mongoDoc?.age, 31); - assert.deepEqual(mongoDoc?.tags, []); + assert.deepEqual(mongoDoc?.tags, ['t', 'a', 'g']); assert.deepStrictEqual( { name: pongoDoc!.name, diff --git a/src/packages/pongo/src/postgres/dbClient.ts b/src/packages/pongo/src/postgres/dbClient.ts index 9ee3f1e..8b883e1 100644 --- a/src/packages/pongo/src/postgres/dbClient.ts +++ b/src/packages/pongo/src/postgres/dbClient.ts @@ -4,8 +4,11 @@ import { NodePostgresConnectorType, runPostgreSQLMigrations, schemaComponent, + SQL, type PostgresConnector, type PostgresPoolOptions, + type QueryResult, + type QueryResultRow, type SchemaComponent, } from '@event-driven-io/dumbo'; import type { Document } from 'mongodb'; @@ -14,6 +17,8 @@ import { pongoCollection, pongoCollectionSchemaComponent, proxyPongoDbWithSchema, + transactionExecutorOrDefault, + type CollectionOperationOptions, type PongoCollection, type PongoDb, type PongoDbClientOptions, @@ -40,19 +45,37 @@ export const postgresDb = ( const collections = new Map>(); + const command = async ( + sql: SQL, + options?: CollectionOperationOptions, + ) => + ( + await transactionExecutorOrDefault(db, options, pool.execute) + ).command(sql); + + const query = async ( + sql: SQL, + options?: CollectionOperationOptions, + ) => + (await transactionExecutorOrDefault(db, options, pool.execute)).query( + sql, + ); + const db: PongoDb = { connectorType: options.connectorType, databaseName, connect: () => Promise.resolve(), close: () => pool.close(), + + collections: () => [...collections.values()], collection: (collectionName) => pongoCollection({ collectionName, db, pool, sqlBuilder: postgresSQLBuilder(collectionName), - ...(options.schema ? options.schema : {}), - ...(options.errors ? options.errors : {}), + schema: options.schema ? options.schema : {}, + errors: options.errors ? options.errors : {}, }), transaction: () => pool.transaction(), withTransaction: (handle) => pool.withTransaction(handle), @@ -72,6 +95,22 @@ export const postgresDb = ( ), ), }, + + sql: { + async query( + sql: SQL, + options?: CollectionOperationOptions, + ): Promise { + const result = await query(sql, options); + return result.rows; + }, + async command( + sql: SQL, + options?: CollectionOperationOptions, + ): Promise> { + return command(sql, options); + }, + }, }; const dbsSchema = options?.schema?.definition?.dbs;