From c78977b06aa20b736283c9589a177d4c0dfd459e Mon Sep 17 00:00:00 2001 From: Serhii Tatarintsev Date: Fri, 22 Sep 2023 15:44:31 +0200 Subject: [PATCH] js-adapters: Better result type (#4274) Adds convenience functions for creating result types: `err` and `ok`, similar to Rust ones. Adds `map` and `flatMap` methods on `Result` type for easier way to work with a `Result` on a happy path. Split from #4213 --- .../js/adapter-neon/src/neon.ts | 16 ++++---- .../driver-adapters/js/adapter-pg/src/pg.ts | 37 +++++++++-------- .../js/adapter-planetscale/src/planetscale.ts | 14 +++---- .../js/driver-adapter-utils/src/binder.ts | 17 ++------ .../js/driver-adapter-utils/src/index.ts | 1 + .../js/driver-adapter-utils/src/result.ts | 41 +++++++++++++++++++ .../js/driver-adapter-utils/src/types.ts | 19 +++------ 7 files changed, 88 insertions(+), 57 deletions(-) create mode 100644 query-engine/driver-adapters/js/driver-adapter-utils/src/result.ts diff --git a/query-engine/driver-adapters/js/adapter-neon/src/neon.ts b/query-engine/driver-adapters/js/adapter-neon/src/neon.ts index da47c9192094..0c915e954dba 100644 --- a/query-engine/driver-adapters/js/adapter-neon/src/neon.ts +++ b/query-engine/driver-adapters/js/adapter-neon/src/neon.ts @@ -1,5 +1,5 @@ import type neon from '@neondatabase/serverless' -import { Debug } from '@prisma/driver-adapter-utils' +import { Debug, ok, err } from '@prisma/driver-adapter-utils' import type { DriverAdapter, ResultSet, @@ -36,7 +36,7 @@ abstract class NeonQueryable implements Queryable { rows, } - return { ok: true, value: resultSet } + return ok(resultSet) } async executeRaw(query: Query): Promise> { @@ -46,7 +46,7 @@ abstract class NeonQueryable implements Queryable { const { rowCount: rowsAffected } = await this.performIO(query) // Note: `rowsAffected` can sometimes be null (e.g., when executing `"BEGIN"`) - return { ok: true, value: rowsAffected ?? 0 } + return ok(rowsAffected ?? 0) } abstract performIO(query: Query): Promise @@ -82,14 +82,14 @@ class NeonTransaction extends NeonWsQueryable implements Transa debug(`[js::commit]`) this.client.release() - return Promise.resolve({ ok: true, value: undefined }) + return Promise.resolve(ok(undefined)) } async rollback(): Promise> { debug(`[js::rollback]`) this.client.release() - return Promise.resolve({ ok: true, value: undefined }) + return Promise.resolve(ok(undefined)) } } @@ -109,7 +109,7 @@ export class PrismaNeon extends NeonWsQueryable implements DriverAdap debug(`${tag} options: %O`, options) const connection = await this.client.connect() - return { ok: true, value: new NeonTransaction(connection, options) } + return ok(new NeonTransaction(connection, options)) } async close() { @@ -117,7 +117,7 @@ export class PrismaNeon extends NeonWsQueryable implements DriverAdap await this.client.end() this.isRunning = false } - return { ok: true as const, value: undefined } + return ok(undefined) } } @@ -139,6 +139,6 @@ export class PrismaNeonHTTP extends NeonQueryable implements DriverAdapter { } async close() { - return { ok: true as const, value: undefined } + return ok(undefined) } } diff --git a/query-engine/driver-adapters/js/adapter-pg/src/pg.ts b/query-engine/driver-adapters/js/adapter-pg/src/pg.ts index bc111e84d8a1..5c574460b49b 100644 --- a/query-engine/driver-adapters/js/adapter-pg/src/pg.ts +++ b/query-engine/driver-adapters/js/adapter-pg/src/pg.ts @@ -1,6 +1,14 @@ import type pg from 'pg' -import { Debug } from '@prisma/driver-adapter-utils' -import type { DriverAdapter, Query, Queryable, Result, ResultSet, Transaction, TransactionOptions } from '@prisma/driver-adapter-utils' +import { Debug, ok } from '@prisma/driver-adapter-utils' +import type { + DriverAdapter, + Query, + Queryable, + Result, + ResultSet, + Transaction, + TransactionOptions, +} from '@prisma/driver-adapter-utils' import { fieldToColumnType } from './conversion' const debug = Debug('prisma:driver-adapter:pg') @@ -8,12 +16,10 @@ const debug = Debug('prisma:driver-adapter:pg') type StdClient = pg.Pool type TransactionClient = pg.PoolClient -class PgQueryable - implements Queryable { +class PgQueryable implements Queryable { readonly flavour = 'postgres' - constructor(protected readonly client: ClientT) { - } + constructor(protected readonly client: ClientT) {} /** * Execute a query given as SQL, interpolating the given parameters. @@ -25,14 +31,14 @@ class PgQueryable const { fields, rows } = await this.performIO(query) const columns = fields.map((field) => field.name) - const columnTypes = fields.map((field) => fieldToColumnType(field.dataTypeID)); + const columnTypes = fields.map((field) => fieldToColumnType(field.dataTypeID)) const resultSet: ResultSet = { columnNames: columns, columnTypes, rows, } - return { ok: true, value: resultSet } + return ok(resultSet) } /** @@ -45,9 +51,9 @@ class PgQueryable debug(`${tag} %O`, query) const { rowCount: rowsAffected } = await this.performIO(query) - + // Note: `rowsAffected` can sometimes be null (e.g., when executing `"BEGIN"`) - return { ok: true, value: rowsAffected ?? 0 } + return ok(rowsAffected ?? 0) } /** @@ -69,8 +75,7 @@ class PgQueryable } } -class PgTransaction extends PgQueryable - implements Transaction { +class PgTransaction extends PgQueryable implements Transaction { constructor(client: pg.PoolClient, readonly options: TransactionOptions) { super(client) } @@ -79,14 +84,14 @@ class PgTransaction extends PgQueryable debug(`[js::commit]`) this.client.release() - return Promise.resolve({ ok: true, value: undefined }) + return ok(undefined) } async rollback(): Promise> { debug(`[js::rollback]`) this.client.release() - return Promise.resolve({ ok: true, value: undefined }) + return ok(undefined) } } @@ -104,10 +109,10 @@ export class PrismaPg extends PgQueryable implements DriverAdapter { debug(`${tag} options: %O`, options) const connection = await this.client.connect() - return { ok: true, value: new PgTransaction(connection, options) } + return ok(new PgTransaction(connection, options)) } async close() { - return { ok: true as const, value: undefined } + return ok(undefined) } } diff --git a/query-engine/driver-adapters/js/adapter-planetscale/src/planetscale.ts b/query-engine/driver-adapters/js/adapter-planetscale/src/planetscale.ts index b1d640398004..b5dffb89272f 100644 --- a/query-engine/driver-adapters/js/adapter-planetscale/src/planetscale.ts +++ b/query-engine/driver-adapters/js/adapter-planetscale/src/planetscale.ts @@ -1,5 +1,5 @@ import type planetScale from '@planetscale/database' -import { Debug } from '@prisma/driver-adapter-utils' +import { Debug, ok } from '@prisma/driver-adapter-utils' import type { DriverAdapter, ResultSet, @@ -46,7 +46,7 @@ class PlanetScaleQueryable> { debug(`[js::rollback]`) this.txDeferred.reject(new RollbackError()) - return Promise.resolve({ ok: true, value: await this.txResultPromise }) + return Promise.resolve(ok(await this.txResultPromise)) } } @@ -127,7 +127,7 @@ export class PrismaPlanetScale extends PlanetScaleQueryable() const txWrapper = new PlanetScaleTransaction(tx, options, txDeferred, txResultPromise) - resolve({ ok: true, value: txWrapper }) + resolve(ok(txWrapper)) return deferredPromise }) .catch((error) => { @@ -143,6 +143,6 @@ export class PrismaPlanetScale extends PlanetScaleQueryable { const result = await startTransaction(...args) - if (result.ok) { - return { ok: true, value: bindTransaction(errorRegistry, result.value) } - } - return result + return result.map((tx) => bindTransaction(errorRegistry, tx)) }, close: wrapAsync(errorRegistry, adapter.close.bind(adapter)), } @@ -68,7 +59,7 @@ function wrapAsync( return await fn(...args) } catch (error) { const id = registry.registerNewError(error) - return { ok: false, error: { kind: 'GenericJsError', id } } + return err({ kind: 'GenericJsError', id }) } } } diff --git a/query-engine/driver-adapters/js/driver-adapter-utils/src/index.ts b/query-engine/driver-adapters/js/driver-adapter-utils/src/index.ts index ce04822473d9..ee851d6961c6 100644 --- a/query-engine/driver-adapters/js/driver-adapter-utils/src/index.ts +++ b/query-engine/driver-adapters/js/driver-adapter-utils/src/index.ts @@ -1,4 +1,5 @@ export { bindAdapter } from './binder' export { ColumnTypeEnum } from './const' export { Debug } from './debug' +export { ok, err, type Result } from './result' export type * from './types' diff --git a/query-engine/driver-adapters/js/driver-adapter-utils/src/result.ts b/query-engine/driver-adapters/js/driver-adapter-utils/src/result.ts new file mode 100644 index 000000000000..5af95db68671 --- /dev/null +++ b/query-engine/driver-adapters/js/driver-adapter-utils/src/result.ts @@ -0,0 +1,41 @@ +import { Error } from './types' +export type Result = { + // common methods + map(fn: (value: T) => U): Result + flatMap(fn: (value: T) => Result): Result +} & ( + | { + readonly ok: true + readonly value: T + } + | { + readonly ok: false + readonly error: Error + } +) + +export function ok(value: T): Result { + return { + ok: true, + value, + map(fn) { + return ok(fn(value)) + }, + flatMap(fn) { + return fn(value) + }, + } +} + +export function err(error: Error): Result { + return { + ok: false, + error, + map() { + return err(error) + }, + flatMap() { + return err(error) + }, + } +} diff --git a/query-engine/driver-adapters/js/driver-adapter-utils/src/types.ts b/query-engine/driver-adapters/js/driver-adapter-utils/src/types.ts index 0dc06f33dc9f..763a85b7be67 100644 --- a/query-engine/driver-adapters/js/driver-adapter-utils/src/types.ts +++ b/query-engine/driver-adapters/js/driver-adapter-utils/src/types.ts @@ -1,6 +1,7 @@ import { ColumnTypeEnum } from './const' +import { Result } from './result' -export type ColumnType = typeof ColumnTypeEnum[keyof typeof ColumnTypeEnum] +export type ColumnType = (typeof ColumnTypeEnum)[keyof typeof ColumnTypeEnum] export interface ResultSet { /** @@ -33,25 +34,17 @@ export type Query = { } export type Error = { - kind: 'GenericJsError', + kind: 'GenericJsError' id: number } -export type Result = { - ok: true, - value: T -} | { - ok: false, - error: Error -} - -export interface Queryable { +export interface Queryable { readonly flavour: 'mysql' | 'postgres' | 'sqlite' /** * Execute a query given as SQL, interpolating the given parameters, * and returning the type-aware result set of the query. - * + * * This is the preferred way of executing `SELECT` queries. */ queryRaw(params: Query): Promise> @@ -59,7 +52,7 @@ export interface Queryable { /** * Execute a query given as SQL, interpolating the given parameters, * and returning the number of affected rows. - * + * * This is the preferred way of executing `INSERT`, `UPDATE`, `DELETE` queries, * as well as transactional queries. */