diff --git a/typed-sql/package.json b/typed-sql/package.json index 40b1f10..1907df6 100644 --- a/typed-sql/package.json +++ b/typed-sql/package.json @@ -1,6 +1,6 @@ { "name": "@ty-ras-extras/typed-sql-runtypes", - "version": "2.0.0", + "version": "2.1.0", "author": { "name": "Stanislav Muhametsin", "email": "346799+stazz@users.noreply.github.com", diff --git a/typed-sql/src/__test__/output.spec.ts b/typed-sql/src/__test__/output.spec.ts index 84d4050..9231f93 100644 --- a/typed-sql/src/__test__/output.spec.ts +++ b/typed-sql/src/__test__/output.spec.ts @@ -69,3 +69,30 @@ test("Validate that validateRows invokes given validation", async (c) => { instanceOf: errors.SQLQueryOutputValidationError, }); }); + +test("Validate that validateOneRow invokes given validation", async (c) => { + c.plan(3); + const firstQueryResult = ["returnedRow"]; + const secondQueryResult = ["one", "two"]; + const thirdQueryResult = "bad-query"; + const { usingMockedClient } = common.createMockedClientProvider([ + firstQueryResult, + secondQueryResult, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + thirdQueryResult as any, + ]); + const executor = spec.validateOneRow( + input.prepareSQL`SELECT 1`(usingMockedClient), + // Validator for single row, not many rows! + t.String, + ); + c.deepEqual(await executor([]), "returnedRow"); + await c.throwsAsync(async () => await executor([]), { + instanceOf: errors.SQLQueryOutputValidationError, + message: "Expected array to contain exactly one element, but contained 2.", + }); + await c.throwsAsync(async () => await executor([]), { + instanceOf: errors.SQLQueryOutputValidationError, + message: "Expected string[], but was string", + }); +}); diff --git a/typed-sql/src/output.ts b/typed-sql/src/output.ts index 7006935..8da7087 100644 --- a/typed-sql/src/output.ts +++ b/typed-sql/src/output.ts @@ -6,29 +6,6 @@ import * as t from "runtypes"; import type * as query from "./input"; import * as errors from "./errors"; -/* eslint-disable @typescript-eslint/ban-types */ - -// Unfortunately, runtypes is not capable of transforming values. -// There is some discussion in issue https://github.com/pelotom/runtypes/issues/56 -// And PR: https://github.com/pelotom/runtypes/pull/191 -// However, this doesn't seem to be happening anytime soon, so just leave this out for now. -// /** -// * Creates `runtypes` validator which ensures that input array of rows contains exactly one row with given shape. -// * @param singleRow The `runtypes` validator for row object. -// * @returns The `runtypes` validator which takes an array of rows as input, and ensures that array contains exactly one element. That element is then validated using given validator. -// */ -// export const one = ( -// singleRow: TValidation, -// ): t.Runtype> => -// many(singleRow).withConstraint, void>( -// (array) => -// // eslint-disable-next-line @typescript-eslint/no-unsafe-return -// array.length == 1 -// ? array[0] -// : `Expected exactly 1 row, but got ${array.length}.`, -// { name: "SingleRow" }, -// ); - /** * Creates `runtypes` validator which ensures that all rows of input array match given shape. * @param singleRow The `runtypes` validator for row object. @@ -39,9 +16,9 @@ export const many = ( ): t.Runtype>> => t.Array(singleRow); /** - * Creates {@link input.SQLQueryExecutor}, which takes input from rows produced from another {@link input.SQLQueryExecutor} and validate them using the given `runtypes` validation. + * Creates {@link input.SQLQueryExecutor}, which takes input from rows produced from another {@link input.SQLQueryExecutor} and validates them using the given `runtypes` validation. * @param executor The {@link query.SQLQueryExecutor} which will provide the rows to validate. - * @param validation The `runtypes` validation, typically obtained via {@link one} or {@link many}. + * @param validation The `runtypes` validation, typically obtained via {@link many}. * @returns The {@link input.SQLQueryExecutor}, which takes input from rows produced from another {@link input.SQLQueryExecutor} and validate them using the given `runtypes` validation. */ export const validateRows = < @@ -60,13 +37,51 @@ export const validateRows = < if (!maybeResult.success) { throw new errors.SQLQueryOutputValidationError(maybeResult); } - // eslint-disable-next-line @typescript-eslint/no-unsafe-return return maybeResult.value; } retVal.sqlString = executor.sqlString; return retVal; }; +/** + * Creates {@link input.SQLQueryExecutor}, which takes input from rows produced from another {@link input.SQLQueryExecutor} and validates that only one row is present, and that the row passes the given `runtypes` validation. + * @param executor The {@link query.SQLQueryExecutor} which will provide the rows to validate. + * @param validation The `runtypes` validation for a single row. + * @returns The {@link input.SQLQueryExecutor}, which takes input from rows produced from another {@link input.SQLQueryExecutor} and validate them using the given `runtypes` validation. + */ +export const validateOneRow = < + TClient, + TParameters, + TValidation extends t.Runtype, +>( + executor: query.SQLQueryExecutor>, + validation: TValidation, +): query.SQLQueryExecutor> => { + const validateRows = many(validation); + async function retVal( + client: ClientOf, + parameters: ParametersOf, + ) { + const maybeResult = validateRows.validate( + await executor(client, parameters), + ); + if (!maybeResult.success) { + throw new errors.SQLQueryOutputValidationError(maybeResult); + } + const len = maybeResult.value.length; + if (len !== 1) { + throw new errors.SQLQueryOutputValidationError({ + success: false, + code: "CONSTRAINT_FAILED", + message: `Expected array to contain exactly one element, but contained ${len}.`, + }); + } + return maybeResult.value[0]; + } + retVal.sqlString = executor.sqlString; + return retVal; +}; + /* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unused-vars */ type ParametersOf> = TExecutor extends query.SQLQueryExecutor<