UNPKG

@getanthill/datastore

Version:

Event-Sourced Datastore

229 lines (190 loc) 6.05 kB
import type { ClientConfig, QueryResult } from 'pg'; import { Client } from 'pg'; import { ModelConfig } from '../typings'; import { cloneDeep, unset } from 'lodash'; export interface PostgreSQLClientConfig { client: ClientConfig; namespace: string; } export default class PostgreSQLClient { static ERRORS = {}; private _config: PostgreSQLClientConfig; private _client: Client; constructor(config: PostgreSQLClientConfig) { this._config = config; this._client = new Client(config.client); } static async init(config: PostgreSQLClientConfig) { const url = new URL(config.client.connectionString!); const dbName: string = url.pathname.slice(1); const client = new Client({ ...config.client, connectionString: config.client.connectionString!.replace( `/${dbName}`, '', ), }); try { await client.connect(); await client.query(`CREATE DATABASE ${dbName}`); } catch (err) { // ... } finally { await client.end(); } } connect() { return this._client.connect(); } disconnect() { return this._client.end(); } static getTableBaseName( modelConfig: Partial<ModelConfig>, namespace?: string, ): string { const modelName = modelConfig.name; return `${namespace ? namespace + '_' : ''}${modelName}`; } static getSqlSchemaForModelDeletion( modelConfig: Partial<ModelConfig>, namespace?: string, ): string[] { const tableBaseName = PostgreSQLClient.getTableBaseName( modelConfig, namespace, ); return [ `DROP TABLE IF EXISTS "${tableBaseName}";`, `DROP TABLE IF EXISTS "${tableBaseName}_events";`, `DROP TABLE IF EXISTS "${tableBaseName}_snapshots";`, ]; } static getSqlSchemaForModel( modelConfig: Partial<ModelConfig>, namespace?: string, ): string[] { const sql: string[] = []; const correlationField = modelConfig.correlation_field; const tableBaseName = PostgreSQLClient.getTableBaseName( modelConfig, namespace, ); const modelDescription = modelConfig.description ?? ''; sql.push(`CREATE TABLE IF NOT EXISTS "${tableBaseName}" ( "${correlationField}" TEXT, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL, "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL, "version" BIGINT, "entity" JSON, PRIMARY KEY ("${correlationField}") );`); sql.push(`CREATE TABLE IF NOT EXISTS "${tableBaseName}_events" ( "${correlationField}" TEXT, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL, "version" BIGINT, "type" VARCHAR(256), "event" JSON, PRIMARY KEY ("${correlationField}", "version") );`); sql.push(`CREATE TABLE IF NOT EXISTS "${tableBaseName}_snapshots" ( "${correlationField}" TEXT, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL, "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL, "version" BIGINT, "entity" JSON, PRIMARY KEY ("${correlationField}") );`); sql.push( `COMMENT ON TABLE "${tableBaseName}" IS ${ modelDescription ? "'[entities] " + modelDescription + "'" : 'NULL' }`, ); sql.push( `COMMENT ON TABLE "${tableBaseName}_events" IS ${ modelDescription ? "'[events] " + modelDescription + "'" : 'NULL' }`, ); sql.push( `COMMENT ON TABLE "${tableBaseName}_snapshots" IS ${ modelDescription ? "'[snapshots] " + modelDescription + "'" : 'NULL' }`, ); return sql; } static getSqlSchemaForModels( modelConfigs: Partial<ModelConfig>[], mustClean = false, namespace?: string, ): string[] { const sql: string[] = []; for (const modelConfig of modelConfigs) { sql.push( ...(mustClean === true ? PostgreSQLClient.getSqlSchemaForModelDeletion( modelConfig, namespace, ) : []), ...PostgreSQLClient.getSqlSchemaForModel(modelConfig, namespace), ); } return sql; } query(q: string): Promise<QueryResult<any>> { return this._client.query(q); } async queryAll(queries: string[]): Promise<QueryResult<any>[]> { const results: QueryResult<any>[] = []; for (const q of queries) { const result = await this.query(q); results.push(result); } return results; } insert( modelConfig: Partial<ModelConfig>, source: 'events' | 'entities', data: any, options?: { with_encrypted_data: boolean; }, ): Promise<QueryResult<any>> { let safeData = data; if (options?.with_encrypted_data !== true) { safeData = cloneDeep(data); (modelConfig.encrypted_fields ?? []).forEach((path) => unset(safeData, `${path}.encrypted`), ); } const json = JSON.stringify(safeData).replace(/'/g, "''"); const tableBaseName = PostgreSQLClient.getTableBaseName( modelConfig, this._config.namespace, ); const tableName = `${tableBaseName}${source === 'events' ? '_events' : ''}`; const sql = `INSERT INTO "${tableName}" ( ${modelConfig.correlation_field!}, created_at,${source === 'entities' ? ' updated_at,' : ''} version, ${source === 'events' ? ' type,' : ''} ${source === 'events' ? 'event' : 'entity'} ) VALUES ( '${safeData[modelConfig.correlation_field!]}', '${safeData.created_at}',${ source === 'entities' ? "'" + safeData.updated_at + "'," : '' } ${safeData.version},${ source === 'events' ? "'" + safeData.type + "'," : '' } '${json}' ) ON CONFLICT ON CONSTRAINT ${tableName}_pkey DO UPDATE SET "version" = ${safeData.version}, ${ source === 'entities' ? '"updated_at" = \'' + safeData.updated_at + "'," : '"created_at" = \'' + safeData.created_at + "'," } "${source === 'events' ? 'event' : 'entity'}" = '${json}'`; return this.query(sql); } }