• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

RobinTail / express-zod-api / 13444086320

20 Feb 2025 08:28PM UTC coverage: 100.0%. First build
13444086320

Pull #2425

github

web-flow
Merge 60a537c42 into 23732b2fe
Pull Request #2425: Fix: async `errorHandler` for not found case

1253 of 1279 branches covered (97.97%)

1 of 1 new or added line in 1 file covered. (100.0%)

4223 of 4223 relevant lines covered (100.0%)

210.05 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

100.0
/express-zod-api/src/common-helpers.ts
1
import { Request } from "express";
2
import * as R from "ramda";
4✔
3
import { z } from "zod";
4✔
4
import { CommonConfig, InputSource, InputSources } from "./config-type";
5
import { contentTypes } from "./content-type";
4✔
6
import { OutputValidationError } from "./errors";
4✔
7
import { metaSymbol } from "./metadata";
4✔
8
import { AuxMethod, Method } from "./method";
9

10
/** @desc this type does not allow props assignment, but it works for reading them when merged with another interface */
11
export type EmptyObject = Record<string, never>;
12
export type EmptySchema = z.ZodObject<EmptyObject, "strip">;
13
export type FlatObject = Record<string, unknown>;
14

15
/** @link https://stackoverflow.com/a/65492934 */
16
type NoNever<T, F> = [T] extends [never] ? F : T;
17

18
/**
19
 * @desc Using module augmentation approach you can specify tags as the keys of this interface
20
 * @example declare module "express-zod-api" { interface TagOverrides { users: unknown } }
21
 * @link https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation
22
 * */
23
// eslint-disable-next-line @typescript-eslint/no-empty-object-type -- augmentation
24
export interface TagOverrides {}
25
export type Tag = NoNever<keyof TagOverrides, string>;
26

27
/** @see https://expressjs.com/en/guide/routing.html */
28
export const routePathParamsRegex = /:([A-Za-z0-9_]+)/g;
4✔
29
export const getRoutePathParams = (path: string): string[] =>
4✔
30
  path.match(routePathParamsRegex)?.map((param) => param.slice(1)) || [];
460✔
31

32
const areFilesAvailable = (request: Request): boolean => {
4✔
33
  const contentType = request.header("content-type") || "";
88!
34
  const isUpload = contentType.toLowerCase().startsWith(contentTypes.upload);
88✔
35
  return "files" in request && isUpload;
88✔
36
};
88✔
37

38
export const defaultInputSources: InputSources = {
4✔
39
  get: ["query", "params"],
4✔
40
  post: ["body", "params", "files"],
4✔
41
  put: ["body", "params"],
4✔
42
  patch: ["body", "params"],
4✔
43
  delete: ["query", "params"],
4✔
44
};
4✔
45
const fallbackInputSource: InputSource[] = ["body", "query", "params"];
4✔
46

47
export const getActualMethod = (request: Request) =>
4✔
48
  request.method.toLowerCase() as Method | AuxMethod;
400✔
49

50
export const getInput = (
4✔
51
  req: Request,
252✔
52
  userDefined: CommonConfig["inputSources"] = {},
252✔
53
): FlatObject => {
252✔
54
  const method = getActualMethod(req);
252✔
55
  if (method === "options") return {};
252✔
56
  return (
240✔
57
    userDefined[method] ||
240✔
58
    defaultInputSources[method] ||
200✔
59
    fallbackInputSource
4✔
60
  )
61
    .filter((src) => (src === "files" ? areFilesAvailable(req) : true))
252✔
62
    .reduce<FlatObject>((agg, src) => Object.assign(agg, req[src]), {});
252✔
63
};
252✔
64

65
export const ensureError = (subject: unknown): Error =>
4✔
66
  subject instanceof Error ? subject : new Error(String(subject));
180✔
67

68
export const getMessageFromError = (error: Error): string => {
4✔
69
  if (error instanceof z.ZodError) {
236✔
70
    return error.issues
76✔
71
      .map(({ path, message }) =>
76✔
72
        (path.length ? [path.join("/")] : []).concat(message).join(": "),
72✔
73
      )
76✔
74
      .join("; ");
76✔
75
  }
76✔
76
  if (error instanceof OutputValidationError) {
236✔
77
    const hasFirstField = error.cause.issues[0]?.path.length > 0;
16✔
78
    return `output${hasFirstField ? "/" : ": "}${error.message}`;
16✔
79
  }
16✔
80
  return error.message;
144✔
81
};
144✔
82

83
/** Takes the original unvalidated examples from the properties of ZodObject schema shape */
84
export const pullExampleProps = <T extends z.SomeZodObject>(subject: T) =>
4✔
85
  Object.entries(subject.shape).reduce<Partial<z.input<T>>[]>(
832✔
86
    (acc, [key, schema]) => {
832✔
87
      const { _def } = schema as z.ZodType;
1,240✔
88
      return combinations(
1,240✔
89
        acc,
1,240✔
90
        (_def[metaSymbol]?.examples || []).map(R.objOf(key)),
1,240✔
91
        ([left, right]) => ({ ...left, ...right }),
1,240✔
92
      );
1,240✔
93
    },
1,240✔
94
    [],
832✔
95
  );
832✔
96

97
export const getExamples = <
4✔
98
  T extends z.ZodType,
99
  V extends "original" | "parsed" | undefined,
100
>({
4,352✔
101
  schema,
4,352✔
102
  variant = "original",
4,352✔
103
  validate = variant === "parsed",
4,352✔
104
  pullProps = false,
4,352✔
105
}: {
4,352✔
106
  schema: T;
107
  /**
108
   * @desc examples variant: original or parsed
109
   * @example "parsed" — for the case when possible schema transformations should be applied
110
   * @default "original"
111
   * @override validate: variant "parsed" activates validation as well
112
   * */
113
  variant?: V;
114
  /**
115
   * @desc filters out the examples that do not match the schema
116
   * @default variant === "parsed"
117
   * */
118
  validate?: boolean;
119
  /**
120
   * @desc should pull examples from properties — applicable to ZodObject only
121
   * @default false
122
   * */
123
  pullProps?: boolean;
124
}): ReadonlyArray<V extends "parsed" ? z.output<T> : z.input<T>> => {
4,352✔
125
  let examples = schema._def[metaSymbol]?.examples || [];
4,352✔
126
  if (!examples.length && pullProps && schema instanceof z.ZodObject)
4,352✔
127
    examples = pullExampleProps(schema);
4,352✔
128
  if (!validate && variant === "original") return examples;
4,352✔
129
  const result: Array<z.input<T> | z.output<T>> = [];
3,924✔
130
  for (const example of examples) {
4,180✔
131
    const parsedExample = schema.safeParse(example);
636✔
132
    if (parsedExample.success)
636✔
133
      result.push(variant === "parsed" ? parsedExample.data : example);
636✔
134
  }
636✔
135
  return result;
3,924✔
136
};
3,924✔
137

138
export const combinations = <T>(
4✔
139
  a: T[],
1,428✔
140
  b: T[],
1,428✔
141
  merge: (pair: [T, T]) => T,
1,428✔
142
): T[] => (a.length && b.length ? R.xprod(a, b).map(merge) : a.concat(b));
1,428✔
143

144
/**
145
 * @desc isNullable() and isOptional() validate the schema's input
146
 * @desc They always return true in case of coercion, which should be taken into account when depicting response
147
 */
148
export const hasCoercion = (schema: z.ZodTypeAny): boolean =>
4✔
149
  "coerce" in schema._def && typeof schema._def.coerce === "boolean"
3,596✔
150
    ? schema._def.coerce
844✔
151
    : false;
2,752✔
152

153
export const ucFirst = (subject: string) =>
4✔
154
  subject.charAt(0).toUpperCase() + subject.slice(1).toLowerCase();
3,040✔
155

156
export const makeCleanId = (...args: string[]) => {
4✔
157
  const byAlpha = R.chain((entry) => entry.split(/[^A-Z0-9]/gi), args);
420✔
158
  const byWord = R.chain(
420✔
159
    (entry) =>
420✔
160
      entry.replaceAll(/[A-Z]+/g, (beginning) => `/${beginning}`).split("/"),
2,356✔
161
    byAlpha,
420✔
162
  );
420✔
163
  return byWord.map(ucFirst).join("");
420✔
164
};
420✔
165

166
export const getTransformedType = R.tryCatch(
4✔
167
  <T>(schema: z.ZodEffects<z.ZodTypeAny, unknown, T>, sample: T) =>
4✔
168
    typeof schema.parse(sample),
40✔
169
  R.always(undefined),
4✔
170
);
4✔
171

172
/** @desc can still be an array, use Array.isArray() or rather R.type() to exclude that case */
173
export const isObject = (subject: unknown) =>
4✔
174
  typeof subject === "object" && subject !== null;
1,476✔
175

176
export const isProduction = R.memoizeWith(
4✔
177
  () => process.env.TSUP_STATIC as string, // eslint-disable-line no-restricted-syntax -- substituted by TSUP
4✔
178
  () => process.env.NODE_ENV === "production", // eslint-disable-line no-restricted-syntax -- memoized
4✔
179
);
4✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc