Skip to content

Commit d1f72b0

Browse files
committed
Split fields.ts into separate files
1 parent 375897e commit d1f72b0

11 files changed

+709
-686
lines changed

lib/fields/chemical.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { Merge, Simplify } from "type-fest"
2+
import * as z from "zod"
3+
4+
import { NullPartialDeep } from "types/util"
5+
6+
import { utcDateZodString } from "./utils"
7+
8+
//==== Chemical schema =====
9+
10+
export const chemicalSchema = z.object({
11+
id: z.number().int().optional(),
12+
name: z.string().trim().min(1),
13+
casNumber: z
14+
.string()
15+
.trim()
16+
.superRefine((arg, ctx) => {
17+
if (!/\d{2,7}-\d{2}-\d{1}/.test(arg)) {
18+
ctx.addIssue({
19+
code: z.ZodIssueCode.invalid_string,
20+
validation: "regex",
21+
message:
22+
"CAS number must be in the form of XXXXXXX-YY-Z, where the first part X is 2 to 7 digits long.",
23+
fatal: true,
24+
})
25+
return z.NEVER
26+
}
27+
/*
28+
The check digit is found by taking the last digit times 1,
29+
the preceding digit times 2, the preceding digit times 3 etc.,
30+
adding all these up and computing the sum modulo 10.
31+
*/
32+
const digits = Array.from(arg.replaceAll("-", "")).map(Number)
33+
const checkDigit = digits.pop()
34+
digits.reverse()
35+
const checksum =
36+
digits.reduce((sum, digit, i) => sum + digit * (i + 1), 0) % 10
37+
if (checkDigit !== checksum) {
38+
ctx.addIssue({
39+
code: z.ZodIssueCode.custom,
40+
message:
41+
"CAS number is invalid. Please double check that the CAS number was inputted correctly.",
42+
})
43+
}
44+
})
45+
.nullish(),
46+
hasNoCasNumber: z.boolean(),
47+
synonyms: z.array(z.string().trim().min(1)).nullable(),
48+
nioshTable: z.number().int().min(1).max(3).or(z.literal(-1)),
49+
nioshRevisionDate: utcDateZodString.nullable(), //TODO: Check that date is not in the future
50+
})
51+
52+
export type ChemicalFields = z.output<typeof chemicalSchema>
53+
export type ChemicalFieldsInput = z.input<typeof chemicalSchema>
54+
55+
export type NullPartialChemicalFields = Simplify<
56+
Merge<NullPartialDeep<ChemicalFieldsInput>, Pick<ChemicalFieldsInput, "id">>
57+
>

lib/fields/compound.ts

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
import { PhysicalForm } from "@prisma/client"
2+
import { Merge, Simplify } from "type-fest"
3+
import * as z from "zod"
4+
5+
import { GetElementType, NullPartialDeep } from "types/util"
6+
7+
import { refineNoDuplicates, transformStringToNumber } from "./utils"
8+
9+
//==== Compound & Ingredient schemas ====//
10+
11+
export const ingredientSchemaBase = z.object({
12+
order: z.number().int(),
13+
chemicalId: z.number().int(),
14+
sdsId: z.number().int(),
15+
physicalForm: z.nativeEnum(PhysicalForm),
16+
isCommercialProduct: z.boolean(),
17+
commercialProduct: z.object({
18+
name: z.string().trim().min(1),
19+
din: transformStringToNumber(z.number().int()).nullish(),
20+
hasNoDin: z.boolean(),
21+
hasProductMonographConcerns: z.boolean(),
22+
concernsDescription: z.string().trim().min(1).nullish(),
23+
}),
24+
})
25+
const commercialIngredientSchema = ingredientSchemaBase.extend({
26+
chemicalId: ingredientSchemaBase.shape.chemicalId.nullish(),
27+
sdsId: ingredientSchemaBase.shape.sdsId.nullish(),
28+
isCommercialProduct: z.literal(true),
29+
})
30+
const nonCommercialIngredientSchema = ingredientSchemaBase.extend({
31+
isCommercialProduct: z.literal(false),
32+
commercialProduct: z.object({
33+
name: z.undefined(),
34+
din: z.undefined(),
35+
hasNoDin: z.undefined(),
36+
hasProductMonographConcerns: z.undefined(),
37+
concernsDescription: z.undefined(),
38+
}),
39+
})
40+
41+
export const ingredientSchema = z.discriminatedUnion("isCommercialProduct", [
42+
commercialIngredientSchema,
43+
nonCommercialIngredientSchema,
44+
])
45+
46+
export type IngredientFields = Simplify<z.output<typeof ingredientSchema>>
47+
export type IngredientFieldsInput = Simplify<z.input<typeof ingredientSchema>>
48+
49+
export type NullPartialIngredientFields = Simplify<
50+
NullPartialDeep<IngredientFieldsInput, { ignoreKeys: "order" }>
51+
>
52+
const shortcutSchema = z
53+
.discriminatedUnion("hasShortcut", [
54+
z.object({
55+
hasShortcut: z.literal(false),
56+
variations: z
57+
.object({
58+
code: z.string().trim().min(1),
59+
name: z.string().trim().min(1),
60+
})
61+
.array()
62+
.max(0)
63+
.default([]),
64+
suffix: z.undefined(),
65+
}),
66+
z.object({
67+
hasShortcut: z.literal(true),
68+
//TODO: Simplify shortcut variations validation code
69+
variations: z
70+
.object({
71+
code: z
72+
.string()
73+
.trim()
74+
.transform((arg) => (arg === "" ? null : arg.toUpperCase()))
75+
.nullable(),
76+
name: z
77+
.string()
78+
.trim()
79+
.transform((arg) => (arg === "" ? null : arg))
80+
.nullable(),
81+
})
82+
.array()
83+
.transform((arr) =>
84+
arr.filter((v) => !(v.code === null && v.name === null)),
85+
)
86+
.superRefine((arg, ctx) => {
87+
for (const i in arg) {
88+
const variation = arg[i]
89+
if (!variation.code) {
90+
ctx.addIssue({
91+
code: "custom",
92+
message: "Required.",
93+
path: [i, "code"],
94+
})
95+
}
96+
97+
if (!variation.name) {
98+
ctx.addIssue({
99+
code: "custom",
100+
message: "Required.",
101+
path: [i, "name"],
102+
})
103+
}
104+
}
105+
})
106+
.superRefine((arg, ctx) =>
107+
refineNoDuplicates(
108+
arg,
109+
ctx,
110+
"code",
111+
(v: GetElementType<typeof arg>) => v.code,
112+
),
113+
)
114+
.pipe(
115+
z
116+
.object({
117+
code: z.string().trim().min(1),
118+
name: z.string().trim().min(1),
119+
})
120+
.array(),
121+
),
122+
suffix: z
123+
.string()
124+
.trim()
125+
.transform((arg) => (arg === "" ? null : arg))
126+
.nullable()
127+
.default(null),
128+
}),
129+
])
130+
.nullable()
131+
.default({ hasShortcut: false })
132+
133+
export const compoundSchema = z.object({
134+
id: z.number().int().optional(),
135+
name: z.string().trim().min(1),
136+
ingredients: z.array(ingredientSchema).min(1),
137+
shortcut: shortcutSchema,
138+
notes: z
139+
.string()
140+
.trim()
141+
.transform((arg) => (arg === "" ? null : arg))
142+
.nullable()
143+
.default(null),
144+
})
145+
146+
export type CompoundFields = z.output<typeof compoundSchema>
147+
export type CompoundFieldsInput = z.input<typeof compoundSchema>
148+
149+
export type NullPartialCompoundFields = Simplify<
150+
Merge<
151+
Merge<
152+
NullPartialDeep<CompoundFieldsInput>,
153+
Pick<CompoundFieldsInput, "id">
154+
>,
155+
{
156+
shortcut?: NullPartialDeep<
157+
z.input<typeof shortcutSchema>,
158+
{ ignoreKeys: "hasShortcut" }
159+
>
160+
ingredients: NullPartialIngredientFields[]
161+
}
162+
>
163+
>

lib/fields/errorMap.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/* eslint-disable @typescript-eslint/no-explicit-any */
2+
3+
import * as z from "zod"
4+
5+
const customErrorMap: z.ZodErrorMap = (issue, ctx) => {
6+
if (
7+
issue.code === z.ZodIssueCode.invalid_type &&
8+
["null", "nan", "undefined"].includes(issue.received)
9+
) {
10+
return { message: "Required." }
11+
}
12+
if (
13+
issue.code === z.ZodIssueCode.too_small &&
14+
issue.type === "string" &&
15+
issue.minimum === 1
16+
) {
17+
return { message: "Required." }
18+
}
19+
20+
return { message: ctx.defaultError }
21+
}
22+
23+
z.setErrorMap(customErrorMap)

0 commit comments

Comments
 (0)