diff --git a/packages/common/lib/api/filtering/Filter.ts b/packages/common/lib/api/filtering/Filter.ts index 2c322f9a..6c61b2f1 100644 --- a/packages/common/lib/api/filtering/Filter.ts +++ b/packages/common/lib/api/filtering/Filter.ts @@ -19,10 +19,18 @@ export const SingleTargetOperators = { GREATER_THAN_OR_EQUAL_TO: "GREATER_THAN_OR_EQUAL_TO", LESS_THAN: "LESS_THAN", LESS_THAN_OR_EQUAL_TO: "LESS_THAN_OR_EQUAL_TO", - LIKE: "LIKE", - NOT_LIKE: "NOT_LIKE", - ILIKE: "ILIKE", - NOT_ILIKE: "NOT_ILIKE", + CONTAINS: "CONTAINS", + NOT_CONTAINS: "NOT_CONTAINS", + INSENSITIVE_CONTAINS: "INSENSITIVE_CONTAINS", + INSENSITIVE_NOT_CONTAINS: "INSENSITIVE_NOT_CONTAINS", + STARTS_WITH: "STARTS_WITH", + NOT_STARTS_WITH: "NOT_STARTS_WITH", + INSENSITIVE_STARTS_WITH: "INSENSITIVE_STARTS_WITH", + INSENSITIVE_NOT_STARTS_WITH: "INSENSITIVE_NOT_STARTS_WITH", + ENDS_WITH: "ENDS_WITH", + NOT_ENDS_WITH: "NOT_ENDS_WITH", + INSENSITIVE_ENDS_WITH: "INSENSITIVE_ENDS_WITH", + INSENSITIVE_NOT_ENDS_WITH: "INSENSITIVE_NOT_ENDS_WITH", } as const; export type SingleTargetOperators = (typeof SingleTargetOperators)[keyof typeof SingleTargetOperators]; diff --git a/packages/server/src/lib/queryFromArgs.ts b/packages/server/src/lib/queryFromArgs.ts index a2c06e03..a9c82b8a 100644 --- a/packages/server/src/lib/queryFromArgs.ts +++ b/packages/server/src/lib/queryFromArgs.ts @@ -1,3 +1,5 @@ +import type { Prisma, PrismaClient } from "@prisma/client"; +import type { Args } from "@prisma/client/runtime/library"; import type { AbstractFilterGroup, AbstractFilterItem, @@ -12,40 +14,35 @@ import { TwoTargetOperators, } from "@ukdanceblue/common"; import { InvariantError } from "@ukdanceblue/common/error"; -import type { SQL } from "drizzle-orm"; -import { - and, - asc, - between, - desc, - eq, - gt, - gte, - ilike, - inArray, - isNotNull, - isNull, - like, - lt, - lte, - ne, - not, - notBetween, - notIlike, - notInArray, - notLike, - or, -} from "drizzle-orm"; -import type { AnyPgColumn } from "drizzle-orm/pg-core"; import { Result } from "ts-results-es"; import { Err, Ok } from "ts-results-es"; -function buildWhereFromItemWithoutNegate( +export type GetWhereFn = ( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + placeholder: any +) => Args["where"]; + +function buildWhereFromItemWithoutNegate( filter: AbstractFilterItem, - fieldLookup: Record -): Result { - const field = fieldLookup[filter.field]; + fieldLookup: Record< + Field, + { getWhere: GetWhereFn; orderBy: Args["orderBy"] } + > +): Result< + { + where: Args["where"]; + orderObj: Args["orderBy"]; + }, + InvariantError +> { + return getPrismaFilterFor(filter).map((prismaFilter) => + fieldLookup[filter.field].getWhere(prismaFilter) + ); +} +function getPrismaFilterFor( + filter: AbstractFilterItem +): Result { const { arrayBooleanFilter, arrayDateFilter, @@ -63,10 +60,12 @@ function buildWhereFromItemWithoutNegate( if (nullFilter) { switch (nullFilter.comparison) { case NoTargetOperators.IS_NULL: { - return Ok(isNull(field)); + return Ok(null); } case NoTargetOperators.IS_NOT_NULL: { - return Ok(isNotNull(field)); + return Ok({ + not: null, + } satisfies Prisma.IntNullableFilter); } default: { nullFilter.comparison satisfies never; @@ -80,34 +79,112 @@ function buildWhereFromItemWithoutNegate( } else if (singleStringFilter) { switch (singleStringFilter.comparison) { case SingleTargetOperators.EQUALS: { - return Ok(eq(field as AnyPgColumn, singleStringFilter.value)); + return Ok({ + equals: singleStringFilter.value, + } satisfies Prisma.StringFilter); } case SingleTargetOperators.NOT_EQUALS: { - return Ok(ne(field as AnyPgColumn, singleStringFilter.value)); + return Ok({ + not: singleStringFilter.value, + } satisfies Prisma.StringFilter); } case SingleTargetOperators.GREATER_THAN: { - return Ok(gt(field as AnyPgColumn, singleStringFilter.value)); + return Ok({ + gt: singleStringFilter.value, + } satisfies Prisma.StringFilter); } case SingleTargetOperators.GREATER_THAN_OR_EQUAL_TO: { - return Ok(gte(field as AnyPgColumn, singleStringFilter.value)); + return Ok({ + gte: singleStringFilter.value, + } satisfies Prisma.StringFilter); } case SingleTargetOperators.LESS_THAN: { - return Ok(lt(field as AnyPgColumn, singleStringFilter.value)); + return Ok({ + lt: singleStringFilter.value, + } satisfies Prisma.StringFilter); } case SingleTargetOperators.LESS_THAN_OR_EQUAL_TO: { - return Ok(lte(field as AnyPgColumn, singleStringFilter.value)); - } - case SingleTargetOperators.LIKE: { - return Ok(like(field, singleStringFilter.value)); - } - case SingleTargetOperators.NOT_LIKE: { - return Ok(notLike(field, singleStringFilter.value)); - } - case SingleTargetOperators.ILIKE: { - return Ok(ilike(field, singleStringFilter.value)); - } - case SingleTargetOperators.NOT_ILIKE: { - return Ok(notIlike(field, singleStringFilter.value)); + return Ok({ + lte: singleStringFilter.value, + } satisfies Prisma.StringFilter); + } + case SingleTargetOperators.CONTAINS: { + return Ok({ + contains: singleStringFilter.value, + } satisfies Prisma.StringFilter); + } + case SingleTargetOperators.NOT_CONTAINS: { + return Ok({ + not: { + contains: singleStringFilter.value, + }, + } satisfies Prisma.StringFilter); + } + case SingleTargetOperators.INSENSITIVE_CONTAINS: { + return Ok({ + contains: singleStringFilter.value, + mode: "insensitive", + } satisfies Prisma.StringFilter); + } + case SingleTargetOperators.INSENSITIVE_NOT_CONTAINS: { + return Ok({ + not: { + contains: singleStringFilter.value, + }, + mode: "insensitive", + } satisfies Prisma.StringFilter); + } + case SingleTargetOperators.STARTS_WITH: { + return Ok({ + startsWith: singleStringFilter.value, + } satisfies Prisma.StringFilter); + } + case SingleTargetOperators.NOT_STARTS_WITH: { + return Ok({ + not: { + startsWith: singleStringFilter.value, + }, + } satisfies Prisma.StringFilter); + } + case SingleTargetOperators.INSENSITIVE_STARTS_WITH: { + return Ok({ + startsWith: singleStringFilter.value, + mode: "insensitive", + } satisfies Prisma.StringFilter); + } + case SingleTargetOperators.INSENSITIVE_NOT_STARTS_WITH: { + return Ok({ + not: { + startsWith: singleStringFilter.value, + }, + mode: "insensitive", + } satisfies Prisma.StringFilter); + } + case SingleTargetOperators.ENDS_WITH: { + return Ok({ + endsWith: singleStringFilter.value, + } satisfies Prisma.StringFilter); + } + case SingleTargetOperators.NOT_ENDS_WITH: { + return Ok({ + not: { + endsWith: singleStringFilter.value, + }, + } satisfies Prisma.StringFilter); + } + case SingleTargetOperators.INSENSITIVE_ENDS_WITH: { + return Ok({ + endsWith: singleStringFilter.value, + mode: "insensitive", + } satisfies Prisma.StringFilter); + } + case SingleTargetOperators.INSENSITIVE_NOT_ENDS_WITH: { + return Ok({ + not: { + endsWith: singleStringFilter.value, + }, + mode: "insensitive", + } satisfies Prisma.StringFilter); } default: { singleStringFilter.comparison satisfies never; @@ -121,27 +198,47 @@ function buildWhereFromItemWithoutNegate( } else if (singleNumberFilter) { switch (singleNumberFilter.comparison) { case SingleTargetOperators.EQUALS: { - return Ok(eq(field as AnyPgColumn, singleNumberFilter.value)); + return Ok({ + equals: singleNumberFilter.value, + } satisfies Prisma.FloatFilter); } case SingleTargetOperators.NOT_EQUALS: { - return Ok(ne(field as AnyPgColumn, singleNumberFilter.value)); + return Ok({ + not: singleNumberFilter.value, + } satisfies Prisma.FloatFilter); } case SingleTargetOperators.GREATER_THAN: { - return Ok(gt(field as AnyPgColumn, singleNumberFilter.value)); + return Ok({ + gt: singleNumberFilter.value, + } satisfies Prisma.FloatFilter); } case SingleTargetOperators.GREATER_THAN_OR_EQUAL_TO: { - return Ok(gte(field as AnyPgColumn, singleNumberFilter.value)); + return Ok({ + gte: singleNumberFilter.value, + } satisfies Prisma.FloatFilter); } case SingleTargetOperators.LESS_THAN: { - return Ok(lt(field as AnyPgColumn, singleNumberFilter.value)); + return Ok({ + lt: singleNumberFilter.value, + } satisfies Prisma.FloatFilter); } case SingleTargetOperators.LESS_THAN_OR_EQUAL_TO: { - return Ok(lte(field as AnyPgColumn, singleNumberFilter.value)); - } - case SingleTargetOperators.LIKE: - case SingleTargetOperators.NOT_LIKE: - case SingleTargetOperators.ILIKE: - case SingleTargetOperators.NOT_ILIKE: { + return Ok({ + lte: singleNumberFilter.value, + } satisfies Prisma.FloatFilter); + } + case SingleTargetOperators.CONTAINS: + case SingleTargetOperators.NOT_CONTAINS: + case SingleTargetOperators.INSENSITIVE_CONTAINS: + case SingleTargetOperators.INSENSITIVE_NOT_CONTAINS: + case SingleTargetOperators.STARTS_WITH: + case SingleTargetOperators.NOT_STARTS_WITH: + case SingleTargetOperators.INSENSITIVE_STARTS_WITH: + case SingleTargetOperators.INSENSITIVE_NOT_STARTS_WITH: + case SingleTargetOperators.ENDS_WITH: + case SingleTargetOperators.NOT_ENDS_WITH: + case SingleTargetOperators.INSENSITIVE_ENDS_WITH: + case SingleTargetOperators.INSENSITIVE_NOT_ENDS_WITH: { return Err( new InvariantError( `Unsupported single number filter comparison: ${String(singleNumberFilter.comparison)}` @@ -160,19 +257,31 @@ function buildWhereFromItemWithoutNegate( } else if (singleBooleanFilter) { switch (singleBooleanFilter.comparison) { case SingleTargetOperators.EQUALS: { - return Ok(eq(field as AnyPgColumn, singleBooleanFilter.value)); + return Ok({ + equals: singleBooleanFilter.value, + } satisfies Prisma.BoolFilter); } case SingleTargetOperators.NOT_EQUALS: { - return Ok(ne(field as AnyPgColumn, singleBooleanFilter.value)); + return Ok({ + not: singleBooleanFilter.value, + } satisfies Prisma.BoolFilter); } case SingleTargetOperators.GREATER_THAN: case SingleTargetOperators.GREATER_THAN_OR_EQUAL_TO: case SingleTargetOperators.LESS_THAN: case SingleTargetOperators.LESS_THAN_OR_EQUAL_TO: - case SingleTargetOperators.LIKE: - case SingleTargetOperators.NOT_LIKE: - case SingleTargetOperators.ILIKE: - case SingleTargetOperators.NOT_ILIKE: { + case SingleTargetOperators.CONTAINS: + case SingleTargetOperators.NOT_CONTAINS: + case SingleTargetOperators.INSENSITIVE_CONTAINS: + case SingleTargetOperators.INSENSITIVE_NOT_CONTAINS: + case SingleTargetOperators.STARTS_WITH: + case SingleTargetOperators.NOT_STARTS_WITH: + case SingleTargetOperators.INSENSITIVE_STARTS_WITH: + case SingleTargetOperators.INSENSITIVE_NOT_STARTS_WITH: + case SingleTargetOperators.ENDS_WITH: + case SingleTargetOperators.NOT_ENDS_WITH: + case SingleTargetOperators.INSENSITIVE_ENDS_WITH: + case SingleTargetOperators.INSENSITIVE_NOT_ENDS_WITH: { return Err( new InvariantError( `Unsupported single boolean filter comparison: ${String(singleBooleanFilter.comparison)}` @@ -191,10 +300,16 @@ function buildWhereFromItemWithoutNegate( } else if (arrayStringFilter) { switch (arrayStringFilter.comparison) { case ArrayOperators.IN: { - return Ok(inArray(field as AnyPgColumn, arrayStringFilter.value)); + return Ok({ + in: arrayStringFilter.value, + } satisfies Prisma.StringFilter); } case ArrayOperators.NOT_IN: { - return Ok(notInArray(field as AnyPgColumn, arrayStringFilter.value)); + return Ok({ + not: { + in: arrayStringFilter.value, + }, + } satisfies Prisma.StringFilter); } default: { arrayStringFilter.comparison satisfies never; @@ -208,10 +323,16 @@ function buildWhereFromItemWithoutNegate( } else if (arrayNumberFilter) { switch (arrayNumberFilter.comparison) { case ArrayOperators.IN: { - return Ok(inArray(field as AnyPgColumn, arrayNumberFilter.value)); + return Ok({ + in: arrayNumberFilter.value, + } satisfies Prisma.FloatFilter); } case ArrayOperators.NOT_IN: { - return Ok(notInArray(field as AnyPgColumn, arrayNumberFilter.value)); + return Ok({ + not: { + in: arrayNumberFilter.value, + }, + } satisfies Prisma.FloatFilter); } default: { arrayNumberFilter.comparison satisfies never; @@ -223,29 +344,20 @@ function buildWhereFromItemWithoutNegate( } } } else if (arrayBooleanFilter) { - switch (arrayBooleanFilter.comparison) { - case ArrayOperators.IN: { - return Ok(inArray(field as AnyPgColumn, arrayBooleanFilter.value)); - } - case ArrayOperators.NOT_IN: { - return Ok(notInArray(field as AnyPgColumn, arrayBooleanFilter.value)); - } - default: { - arrayBooleanFilter.comparison satisfies never; - return Err( - new InvariantError( - `Unsupported array boolean filter comparison: ${String(arrayBooleanFilter.comparison)}` - ) - ); - } - } + throw new InvariantError("Array boolean filters are not supported"); // Why would you even want this? } else if (arrayDateFilter) { switch (arrayDateFilter.comparison) { case ArrayOperators.IN: { - return Ok(inArray(field as AnyPgColumn, arrayDateFilter.value)); + return Ok({ + in: arrayDateFilter.value.map((v) => v.toJSDate()), + } satisfies Prisma.DateTimeFilter); } case ArrayOperators.NOT_IN: { - return Ok(notInArray(field as AnyPgColumn, arrayDateFilter.value)); + return Ok({ + not: { + in: arrayDateFilter.value.map((v) => v.toJSDate()), + }, + } satisfies Prisma.DateTimeFilter); } default: { arrayDateFilter.comparison satisfies never; @@ -259,27 +371,49 @@ function buildWhereFromItemWithoutNegate( } else if (singleDateFilter) { switch (singleDateFilter.comparison) { case SingleTargetOperators.EQUALS: { - return Ok(eq(field as AnyPgColumn, singleDateFilter.value)); + return Ok({ + equals: singleDateFilter.value.toJSDate(), + } satisfies Prisma.DateTimeFilter); } case SingleTargetOperators.NOT_EQUALS: { - return Ok(ne(field as AnyPgColumn, singleDateFilter.value)); + return Ok({ + not: { + equals: singleDateFilter.value.toJSDate(), + }, + } satisfies Prisma.DateTimeFilter); } case SingleTargetOperators.GREATER_THAN: { - return Ok(gt(field as AnyPgColumn, singleDateFilter.value)); + return Ok({ + gt: singleDateFilter.value.toJSDate(), + } satisfies Prisma.DateTimeFilter); } case SingleTargetOperators.GREATER_THAN_OR_EQUAL_TO: { - return Ok(gte(field as AnyPgColumn, singleDateFilter.value)); + return Ok({ + gte: singleDateFilter.value.toJSDate(), + } satisfies Prisma.DateTimeFilter); } case SingleTargetOperators.LESS_THAN: { - return Ok(lt(field as AnyPgColumn, singleDateFilter.value)); + return Ok({ + lt: singleDateFilter.value.toJSDate(), + } satisfies Prisma.DateTimeFilter); } case SingleTargetOperators.LESS_THAN_OR_EQUAL_TO: { - return Ok(lte(field as AnyPgColumn, singleDateFilter.value)); - } - case SingleTargetOperators.LIKE: - case SingleTargetOperators.NOT_LIKE: - case SingleTargetOperators.ILIKE: - case SingleTargetOperators.NOT_ILIKE: { + return Ok({ + lte: singleDateFilter.value.toJSDate(), + } satisfies Prisma.DateTimeFilter); + } + case SingleTargetOperators.CONTAINS: + case SingleTargetOperators.NOT_CONTAINS: + case SingleTargetOperators.INSENSITIVE_CONTAINS: + case SingleTargetOperators.INSENSITIVE_NOT_CONTAINS: + case SingleTargetOperators.STARTS_WITH: + case SingleTargetOperators.NOT_STARTS_WITH: + case SingleTargetOperators.INSENSITIVE_STARTS_WITH: + case SingleTargetOperators.INSENSITIVE_NOT_STARTS_WITH: + case SingleTargetOperators.ENDS_WITH: + case SingleTargetOperators.NOT_ENDS_WITH: + case SingleTargetOperators.INSENSITIVE_ENDS_WITH: + case SingleTargetOperators.INSENSITIVE_NOT_ENDS_WITH: { return Err( new InvariantError( `Unsupported single date filter comparison: ${String(singleDateFilter.comparison)}` @@ -298,22 +432,18 @@ function buildWhereFromItemWithoutNegate( } else if (twoNumberFilter) { switch (twoNumberFilter.comparison) { case TwoTargetOperators.BETWEEN: { - return Ok( - between( - field as AnyPgColumn, - twoNumberFilter.lower, - twoNumberFilter.upper - ) - ); + return Ok({ + gte: twoNumberFilter.lower, + lte: twoNumberFilter.upper, + } satisfies Prisma.FloatFilter); } case TwoTargetOperators.NOT_BETWEEN: { - return Ok( - notBetween( - field as AnyPgColumn, - twoNumberFilter.lower, - twoNumberFilter.upper - ) - ); + return Ok({ + not: { + gt: twoNumberFilter.lower, + lt: twoNumberFilter.upper, + }, + } satisfies Prisma.FloatFilter); } default: { twoNumberFilter.comparison satisfies never; @@ -327,22 +457,18 @@ function buildWhereFromItemWithoutNegate( } else if (twoDateFilter) { switch (twoDateFilter.comparison) { case TwoTargetOperators.BETWEEN: { - return Ok( - between( - field as AnyPgColumn, - twoDateFilter.lower, - twoDateFilter.upper - ) - ); + return Ok({ + gte: twoDateFilter.lower.toJSDate(), + lte: twoDateFilter.upper.toJSDate(), + } satisfies Prisma.DateTimeFilter); } case TwoTargetOperators.NOT_BETWEEN: { - return Ok( - notBetween( - field as AnyPgColumn, - twoDateFilter.lower, - twoDateFilter.upper - ) - ); + return Ok({ + not: { + gte: twoDateFilter.lower.toJSDate(), + lte: twoDateFilter.upper.toJSDate(), + }, + } satisfies Prisma.DateTimeFilter); } default: { twoDateFilter.comparison satisfies never; @@ -358,13 +484,18 @@ function buildWhereFromItemWithoutNegate( } } -export function buildWhereFromGroup( +export function buildWhereFromGroup( group: AbstractFilterGroup, - fieldLookup: Record -): Result { + fieldLookup: Record< + Field, + { getWhere: GetWhereFn; orderBy: Args["orderBy"] } + > +): Result["where"], InvariantError> { return Result.all( group.filters.map((filter) => - buildWhereFromItemWithoutNegate(filter, fieldLookup).map(not) + buildWhereFromItemWithoutNegate(filter, fieldLookup).map((where) => ({ + NOT: where, + })) ) ) .andThen((filters) => { @@ -373,10 +504,10 @@ export function buildWhereFromGroup( ).andThen((children) => { switch (group.operator) { case FilterGroupOperator.AND: { - return Ok(and(...filters, ...children)); + return Ok({ AND: [...filters, ...children] }); } case FilterGroupOperator.OR: { - return Ok(or(...filters, ...children)); + return Ok({ OR: [...filters, ...children] }); } default: { group.operator satisfies never; diff --git a/packages/server/src/repositories/Default.ts b/packages/server/src/repositories/Default.ts new file mode 100644 index 00000000..45724419 --- /dev/null +++ b/packages/server/src/repositories/Default.ts @@ -0,0 +1,236 @@ +import { Container } from "@freshgum/typedi"; +import type { PrismaClient } from "@prisma/client"; +import type { + Args, + Result as PrismaResult, +} from "@prisma/client/runtime/library"; +import type * as runtime from "@prisma/client/runtime/library"; +import type { BasicError } from "@ukdanceblue/common/error"; +import { InvariantError, NotFoundError } from "@ukdanceblue/common/error"; +import { AsyncResult } from "ts-results-es"; +import { Result } from "ts-results-es"; +import { Err, Ok } from "ts-results-es"; +import { isPromise } from "util/types"; + +import { PostgresError } from "#error/postgres.js"; +import { + type FindManyParams, + type GetWhereFn, + parseFindManyParams as parseFindManyParamsFunc, +} from "#lib/queryFromArgs.js"; + +import { handleRepositoryError, type RepositoryError } from "./shared.js"; + +type Transaction = Omit; + +interface BaseRepository { + uniqueToWhere(by: UniqueParam): Args["where"]; + + findOne?({ + by, + tx, + }: { + by: UniqueParam; + tx?: Transaction; + }): AsyncResult< + PrismaResult, "findUnique">, + RepositoryError + >; + + findAndCount?({ + param, + tx, + }: { + param: FindManyParams; + tx?: Transaction; + }): AsyncResult< + { + total: number; + selectedRows: PrismaResult, "findMany">[]; + }, + RepositoryError + >; + + findAll?({ + tx, + }: { + tx?: Transaction; + }): AsyncResult< + PrismaResult, "findMany">[], + RepositoryError + >; + + create?({ + init, + tx, + }: { + init: Args["data"]; + tx?: Transaction; + }): AsyncResult< + PrismaResult, "create">, + RepositoryError + >; + + update?({ + by, + init, + tx, + }: { + by: UniqueParam; + init: Args["data"]; + tx?: Transaction; + }): AsyncResult< + PrismaResult, "update">, + RepositoryError + >; + + delete?({ + by, + tx, + }: { + by: UniqueParam; + tx?: Transaction; + }): AsyncResult< + PrismaResult, "delete">, + RepositoryError + >; + + createMultiple?({ + data, + tx, + }: { + data: { + init: Args["data"]; + }[]; + tx?: Transaction; + }): AsyncResult< + PrismaResult, "createMany">[], + RepositoryError + >; + + deleteMultiple?({ + data, + tx, + }: { + data: { + by: UniqueParam; + }[]; + tx?: Transaction; + }): AsyncResult< + PrismaResult, "deleteMany">[], + RepositoryError + >; +} + +export function buildDefaultRepository( + tableName: string, + fieldLookup: Record> +) { + // eslint-disable-next-line @typescript-eslint/no-empty-object-type, @typescript-eslint/no-unsafe-declaration-merging + interface DefaultRepository extends BaseRepository {} + // eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging + abstract class DefaultRepository { + public static readonly fields: Field[] = Object.keys( + fieldLookup + ) as Field[]; + + constructor(protected readonly prisma: PrismaClient) { + Container.setValue(`${tableName}Repository`, this); + } + + protected handleQueryError( + promise: Promise, + handleNotFound?: false + ): AsyncResult; + protected handleQueryError( + promise: Promise, + handleNotFound: ConstructorParameters[0] + ): AsyncResult, PostgresError | BasicError | NotFoundError>; + protected handleQueryError( + promise: Promise, + handleNotFound: + | false + | ConstructorParameters[0] = false + ): AsyncResult { + return handleNotFound + ? this.mapToNotFound(this.promiseToAsyncResult(promise), handleNotFound) + : this.promiseToAsyncResult(promise); + } + + protected async handleTransactionError( + callback: (tx: Transaction) => Promise> + ): Promise> { + let result: Result = Err( + new InvariantError("Transaction not completed") + ); + await this.prisma.$transaction(async (tx) => { + try { + result = await callback(tx); + if (result.isErr()) { + throw new Error("Rollback"); + } + } catch (error) { + result = handleRepositoryError(error); + throw error; + } + }); + return result; + } + + protected promiseToAsyncResult( + promise: Promise + ): AsyncResult { + return new AsyncResult( + promise.then( + (v) => Ok(v), + (error) => Err(PostgresError.fromUnknown(error)) + ) + ); + } + + protected resultToAsyncResult( + val: Result | Promise> | AsyncResult + ): AsyncResult { + if (isPromise(val)) { + val = new AsyncResult(val); + } + if (Result.isResult(val)) { + val = val.toAsyncResult(); + } + return val; + } + + protected mapToNotFound( + val: + | Result + | Promise> + | AsyncResult, + params: ConstructorParameters[0] + ): AsyncResult { + return this.resultToAsyncResult(val).andThen((v) => + v + ? Ok(v) + : Err( + new NotFoundError({ + what: "field", + where: `${tableName}Repository`, + sensitive: false, + ...params, + }) + ) + ); + } + + protected parseFindManyParams( + param: FindManyParams<(typeof DefaultRepository.fields)[number]> + ) { + return parseFindManyParamsFunc(param, fieldLookup); + } + + public abstract uniqueToWhere( + by: UniqueParam + ): Args["where"]; + } + + return DefaultRepository; +} diff --git a/packages/server/src/repositories/shared.ts b/packages/server/src/repositories/shared.ts index ed1181be..69e5524a 100644 --- a/packages/server/src/repositories/shared.ts +++ b/packages/server/src/repositories/shared.ts @@ -1,4 +1,9 @@ -import type { BasicError, NotFoundError } from "@ukdanceblue/common/error"; +import type { + ActionDeniedError, + BasicError, + InvariantError, + NotFoundError, +} from "@ukdanceblue/common/error"; import { toBasicError } from "@ukdanceblue/common/error"; import { Err, Some } from "ts-results-es"; @@ -12,14 +17,21 @@ export type SimpleUniqueParam = { id: number } | { uuid: string }; /** * The error types that can be returned by most repository functions */ -export type RepositoryError = SomePrismaError | BasicError | NotFoundError; +export type RepositoryError = + | SomePrismaError + | BasicError + | NotFoundError + | ActionDeniedError + | InvariantError; /** * Takes in an arbitrary error and returns a PrismaError subclass if it is a Prisma error, or a BasicError if it is not */ export function unwrapRepositoryError(error: unknown): RepositoryError { const prismaError = toPrismaError(error); - return prismaError.orElse(() => Some(toBasicError(error))).unwrap(); + return prismaError + .orElse(() => Some(toBasicError(error))) + .expect("Error should be a BasicError"); } /**