Created
January 12, 2022 23:46
-
-
Save moroz/8455d8726048ba5faf8b4ce44cb95587 to your computer and use it in GitHub Desktop.
Changeset for Node
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { Prisma } from "@prisma/client"; | |
| import { Buffer } from "buffer"; | |
| import _ from "lodash"; | |
| export interface CastOptions { | |
| trimStrings?: boolean; | |
| } | |
| export enum Types { | |
| Integer, | |
| Binary, | |
| Float, | |
| Boolean, | |
| String, | |
| Decimal | |
| } | |
| export interface SchemaFieldOptions { | |
| default?: any; | |
| } | |
| export type SchemaFieldTuple = [Types, SchemaFieldOptions]; | |
| export type SchemaField = Types | SchemaFieldTuple; | |
| export type Schema = Record<string, SchemaField>; | |
| export type CastFunction = (value: any) => any; | |
| export type Struct = Record<string, any>; | |
| export type ChangesetChange = Record<string, any>; | |
| export interface ChangesetError { | |
| field: string; | |
| message: string; | |
| } | |
| export const TypeMapper: Record<Types, CastFunction> = { | |
| [Types.Integer]: Number, | |
| [Types.Float]: Number, | |
| [Types.String]: String, | |
| [Types.Binary]: Buffer.from, | |
| [Types.Boolean]: Boolean, | |
| [Types.Decimal]: (value: any) => { | |
| if (![undefined, "", null].includes(value)) { | |
| return new Prisma.Decimal(Number(value)); | |
| } | |
| } | |
| }; | |
| export const NumberValidators = { | |
| lessThan: ( | |
| value: number | Prisma.Decimal, | |
| expected: number | Prisma.Decimal | |
| ) => { | |
| if (value instanceof Prisma.Decimal) { | |
| return value.lessThan(expected); | |
| } else { | |
| return value < expected; | |
| } | |
| }, | |
| greaterThan: ( | |
| value: number | Prisma.Decimal, | |
| expected: number | Prisma.Decimal | |
| ) => { | |
| if (value instanceof Prisma.Decimal) { | |
| return value.greaterThan(expected); | |
| } else { | |
| return value > expected; | |
| } | |
| }, | |
| greaterThanOrEqualTo: ( | |
| value: number | Prisma.Decimal, | |
| expected: number | Prisma.Decimal | |
| ) => { | |
| if (value instanceof Prisma.Decimal) { | |
| return value.greaterThanOrEqualTo(expected); | |
| } else { | |
| return value >= expected; | |
| } | |
| }, | |
| lessThanOrEqualTo: ( | |
| value: number | Prisma.Decimal, | |
| expected: number | Prisma.Decimal | |
| ) => { | |
| if (value instanceof Prisma.Decimal) { | |
| return value.lessThanOrEqualTo(expected); | |
| } else { | |
| return value >= expected; | |
| } | |
| } | |
| }; | |
| const LengthValidators = { | |
| min: (value: string, length: number) => { | |
| return value.length >= length; | |
| }, | |
| max: (value: string, length: number) => { | |
| return value.length <= length; | |
| }, | |
| is: (value: string, length: number) => { | |
| return value.length === length; | |
| } | |
| }; | |
| export class SchemaBuilder { | |
| fields: Schema = {}; | |
| build() { | |
| return this.fields; | |
| } | |
| addField(name: string, type: Types, opts?: SchemaFieldOptions) { | |
| if (opts) { | |
| this.fields[name] = [type, opts]; | |
| } else { | |
| this.fields[name] = type; | |
| } | |
| } | |
| integer(name: string, opts?: SchemaFieldOptions) { | |
| this.addField(name, Types.Integer, opts); | |
| return this; | |
| } | |
| float(name: string, opts?: SchemaFieldOptions) { | |
| this.addField(name, Types.Float, opts); | |
| return this; | |
| } | |
| string(name: string, opts?: SchemaFieldOptions) { | |
| this.addField(name, Types.String, opts); | |
| return this; | |
| } | |
| boolean(name: string, opts?: SchemaFieldOptions) { | |
| this.addField(name, Types.Boolean, opts); | |
| return this; | |
| } | |
| binary(name: string, opts?: SchemaFieldOptions) { | |
| this.addField(name, Types.Binary, opts); | |
| return this; | |
| } | |
| decimal(name: string, opts?: SchemaFieldOptions) { | |
| this.addField(name, Types.Decimal, opts); | |
| return this; | |
| } | |
| } | |
| export class Changeset<T extends Struct = any> { | |
| data: Struct; | |
| schema: Schema; | |
| changes: ChangesetChange[] = []; | |
| errors: ChangesetError[] = []; | |
| constructor(data: Struct, schema: Schema) { | |
| this.data = data; | |
| this.schema = schema; | |
| } | |
| private getCastFunction(field: keyof typeof this.schema) { | |
| const type = this.schema[field]; | |
| if (!type === undefined) { | |
| throw new Error(`unknown field ${field} given to cast.`); | |
| } | |
| if (Array.isArray(type)) { | |
| return TypeMapper[type[0]]; | |
| } | |
| return TypeMapper[type]; | |
| } | |
| transformErrors() { | |
| return this.errors.reduce((acc, { field, message }) => { | |
| (acc[field] || (acc[field] = [])).push(message); | |
| return acc; | |
| }, {} as Record<string, string[]>); | |
| } | |
| cast( | |
| params: Record<string, any>, | |
| permitted: string[], | |
| { trimStrings = true }: CastOptions = {} | |
| ) { | |
| const changes = Object.entries(params).reduce((acc, [key, value]) => { | |
| if (!permitted.includes(key)) return acc; | |
| const castFunction = this.getCastFunction(key); | |
| let castValue = castFunction(value); | |
| if (trimStrings && typeof castValue === "string") | |
| castValue = castValue.trim(); | |
| // there is no change if the values are equal | |
| if ( | |
| ((castValue as any) instanceof Prisma.Decimal && | |
| this.data[key] instanceof Prisma.Decimal && | |
| (castValue as Prisma.Decimal).eq(this.data[key])) || | |
| castValue === this.data[key] | |
| ) | |
| return acc; | |
| return { | |
| ...acc, | |
| [key]: castValue || null | |
| }; | |
| }, {}); | |
| this.changes = { ...this.changes, ...changes }; | |
| return this; | |
| } | |
| validateRequired(fields: string[]) { | |
| fields.forEach((field) => { | |
| const value = this.getField(field); | |
| if (["", null, undefined].includes(value)) { | |
| this.errors.push({ field, message: "can't be blank" }); | |
| } | |
| }); | |
| return this; | |
| } | |
| validateNumber( | |
| field: string, | |
| opts: Partial< | |
| Record<keyof typeof NumberValidators, number | Prisma.Decimal> | |
| > | |
| ) { | |
| const value = this.getField(field); | |
| if ( | |
| typeof value !== "number" && | |
| !((value as any) instanceof Prisma.Decimal) | |
| ) | |
| return this; | |
| Object.entries(opts).forEach(([validatorKey, expected]) => { | |
| const validator = | |
| NumberValidators[validatorKey as keyof typeof NumberValidators]; | |
| if (!validator || validator(value, expected)) return; | |
| const errorType = _.snakeCase(validatorKey).replace(/_/g, " "); | |
| this.errors.push({ | |
| field, | |
| message: `must be ${errorType} ${expected}` | |
| }); | |
| }); | |
| return this; | |
| } | |
| validateLength( | |
| field: string, | |
| opts: Partial<Record<keyof typeof LengthValidators, number>> | |
| ) { | |
| const value = this.getField(field); | |
| if (typeof value !== "string") { | |
| return this; | |
| } | |
| Object.entries(opts).forEach(([validatorKey, expected]) => { | |
| const validator = | |
| LengthValidators[validatorKey as keyof typeof LengthValidators]; | |
| if (!validator || validator(value, expected)) return; | |
| this.errors.push({ | |
| field, | |
| message: `length is invalid, expected: ${validatorKey}: ${expected}` | |
| }); | |
| }); | |
| return this; | |
| } | |
| getField(field: string) { | |
| const change = this.changes[field as any]; | |
| if (change === undefined) return this.data[field as any]; | |
| return change; | |
| } | |
| get valid(): boolean { | |
| return this.errors.length === 0; | |
| } | |
| tap(fn: (changeset: typeof this) => typeof this) { | |
| return fn(this); | |
| } | |
| applyChanges() { | |
| return { | |
| ...this.data, | |
| ...this.changes | |
| } as Record<string, any>; | |
| } | |
| toPrismaParams() { | |
| return Object.entries(this.changes).reduce((acc, [key, value]) => { | |
| if (key.match(/Id$/)) { | |
| const relation = key.replace(/Id$/, ""); | |
| return { ...acc, [relation]: { connect: { id: value } } }; | |
| } | |
| return { ...acc, [key]: value }; | |
| }, {}) as T; | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment