From 1b47be09de29e1abe9398bdf611a0368553b704b Mon Sep 17 00:00:00 2001 From: Mikayla Dobson Date: Tue, 9 Apr 2024 08:56:28 -0500 Subject: [PATCH] added unique constraint assertion tools --- package.json | 11 +++++++--- pkg/index.ts | 10 +++++++++ pkg/unique.ts | 22 +++++++++++++++++++ tests/unique.test.ts | 50 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 90 insertions(+), 3 deletions(-) create mode 100644 pkg/index.ts create mode 100644 pkg/unique.ts create mode 100644 tests/unique.test.ts diff --git a/package.json b/package.json index cb1ae72..c760e32 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,18 @@ { "name": "utility-closet", - "version": "0.0.1", - "main": "index.js", + "version": "0.0.2", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "repository": { + "type": "git", + "url": "git+https://github.com/innocuous-symmetry/utility-closet.git" + }, "scripts": { "build": "tsc", "test": "vitest" }, "keywords": [], - "author": "", + "author": "Mikayla Dobson", "license": "ISC", "dependencies": { "csv-parse": "^5.5.5", diff --git a/pkg/index.ts b/pkg/index.ts new file mode 100644 index 0000000..9ab0835 --- /dev/null +++ b/pkg/index.ts @@ -0,0 +1,10 @@ +export * from "./async"; +export * from "./csv"; +export * from "./dom"; +export * from "./logger"; +export * from "./obj"; +export * from "./queue"; +export * from "./time"; +export * from "./types"; +export * from "./unique"; +export * from "./validators"; diff --git a/pkg/unique.ts b/pkg/unique.ts new file mode 100644 index 0000000..84d462a --- /dev/null +++ b/pkg/unique.ts @@ -0,0 +1,22 @@ +import { z } from "zod"; + +export type AssertUniqueType> = z.ZodEffects>, T[], unknown>; + +export function generateUniqueValidator>(validator: z.ZodType, ...keys: (keyof T)[]) { + return validator.array().refine((val) => { + for (const key of keys ?? []) { + const values = val.map((item) => item[key as keyof T]); + return values.length === new Set(values).size; + } + }, { + message: "Encountered duplicate values in unique field" + }) satisfies AssertUniqueType; +} + +export function assertUniqueKeys>(data: T[], validator: z.ZodType, ...keys: (keyof T)[]) { + return generateUniqueValidator(validator, ...keys).parse(data) satisfies T[]; +} + +export async function assertUniqueKeysAsync>(data: T[], validator: z.ZodType, ...keys: (keyof T)[]) { + return generateUniqueValidator(validator, ...keys).parseAsync(data) satisfies Promise; +} diff --git a/tests/unique.test.ts b/tests/unique.test.ts new file mode 100644 index 0000000..022d042 --- /dev/null +++ b/tests/unique.test.ts @@ -0,0 +1,50 @@ +import { describe, it, expect } from 'vitest'; +import { z } from 'zod'; +import { assertUniqueKeys } from "../pkg/unique"; + +describe("assert unique", () => { + it("should assert unique keys in a homogenous array", () => { + const data = [ + { id: 1, name: "John" }, + { id: 2, name: "Doe" }, + { id: 3, name: "Jane" }, + ]; + const validator = z.object({ + id: z.number(), + name: z.string(), + }); + + expect( + () => assertUniqueKeys(data, validator, "id") + ).not.toThrow(); + }) + + it("should reject arrays violating unique key constraints", () => { + const data = [ + { id: 1, name: "John" }, + { id: 1, name: "Doe" }, + { id: 1, name: "Jane" }, + ]; + const validator = z.object({ + id: z.number(), + name: z.string(), + }); + + expect( + () => assertUniqueKeys(data, validator, "id") + ).toThrowError("Encountered duplicate values in unique field"); + }) + + it("should only allow record types", () => { + const data = [ + 2, 3, 4, 5, "foo", "bar", true + ]; + + const validator = z.union([z.number(), z.string(), z.boolean()]); + + expect( + // @ts-expect-error + () => assertUniqueKeys(data, validator) + ).toThrow(); + }) +})