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

RobinTail / express-zod-api / 4410939844

pending completion
4410939844

Pull #880

github

GitHub
Merge 1f79a241a into 1ca06dc0d
Pull Request #880: Bump @typescript-eslint/eslint-plugin from 5.54.1 to 5.55.0

499 of 524 branches covered (95.23%)

1119 of 1119 relevant lines covered (100.0%)

385.91 hits per line

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

98.88
/src/common-helpers.ts
1
import { Request } from "express";
2
import { HttpError } from "http-errors";
200✔
3
import { z } from "zod";
200✔
4
import {
200✔
5
  CommonConfig,
6
  InputSource,
7
  InputSources,
8
  LoggerConfig,
9
  loggerLevels,
10
} from "./config-type";
11
import { InputValidationError, OutputValidationError } from "./errors";
200✔
12
import { IOSchema } from "./io-schema";
13
import { getMeta } from "./metadata";
200✔
14
import { AuxMethod, Method } from "./method";
15
import { mimeMultipart } from "./mime";
200✔
16
import { ZodUpload } from "./upload-schema";
200✔
17

18
export type FlatObject = Record<string, any>;
19

20
/** @see https://expressjs.com/en/guide/routing.html */
21
export const routePathParamsRegex = /:([A-Za-z0-9_]+)/g;
200✔
22

23
function areFilesAvailable(request: Request) {
24
  const contentType = request.header("content-type") || "";
168!
25
  const isMultipart =
26
    contentType.slice(0, mimeMultipart.length).toLowerCase() === mimeMultipart;
168✔
27
  return "files" in request && isMultipart;
168✔
28
}
29

30
export const defaultInputSources: InputSources = {
200✔
31
  get: ["query", "params"],
32
  post: ["body", "params", "files"],
33
  put: ["body", "params"],
34
  patch: ["body", "params"],
35
  delete: ["query", "params"],
36
};
37
const fallbackInputSource: InputSource[] = ["body", "query", "params"];
200✔
38

39
export const getActualMethod = (request: Request) =>
200✔
40
  request.method.toLowerCase() as Method | AuxMethod;
608✔
41

42
export function getInput(
200✔
43
  request: Request,
44
  inputAssignment: CommonConfig["inputSources"]
45
): any {
46
  const method = getActualMethod(request);
344✔
47
  if (method === "options") {
344✔
48
    return {};
24✔
49
  }
50
  let props = fallbackInputSource;
320✔
51
  if (method in defaultInputSources) {
320✔
52
    props = defaultInputSources[method];
312✔
53
  }
54
  if (inputAssignment && method in inputAssignment) {
320✔
55
    props = inputAssignment[method] || props;
72!
56
  }
57
  return props
320✔
58
    .filter((prop) => (prop === "files" ? areFilesAvailable(request) : true))
816✔
59
    .reduce(
60
      (carry, prop) => ({
656✔
61
        ...carry,
62
        ...request[prop],
63
      }),
64
      {}
65
    );
66
}
67

68
export function isLoggerConfig(logger: any): logger is LoggerConfig {
200✔
69
  return (
104✔
70
    typeof logger === "object" &&
320✔
71
    "level" in logger &&
72
    "color" in logger &&
73
    Object.keys(loggerLevels).includes(logger.level) &&
74
    typeof logger.color === "boolean"
75
  );
76
}
77

78
export function isValidDate(date: Date): boolean {
200✔
79
  return !isNaN(date.getTime());
224✔
80
}
81

82
export function makeErrorFromAnything<T extends Error>(subject: T): T;
83
export function makeErrorFromAnything(subject: any): Error;
84
export function makeErrorFromAnything<T>(subject: T): Error {
200✔
85
  return subject instanceof Error
336✔
86
    ? subject
87
    : new Error(
88
        typeof subject === "symbol" ? subject.toString() : `${subject}`
168✔
89
      );
90
}
91

92
export function getMessageFromError(error: Error): string {
200✔
93
  if (error instanceof z.ZodError) {
1,048✔
94
    return error.issues
176✔
95
      .map(({ path, message }) =>
96
        (path.length ? [path.join("/")] : []).concat(message).join(": ")
120✔
97
      )
98
      .join("; ");
99
  }
100
  if (error instanceof OutputValidationError) {
872✔
101
    const hasFirstField = error.originalError.issues[0]?.path.length > 0;
24✔
102
    return `output${hasFirstField ? "/" : ": "}${error.message}`;
24✔
103
  }
104
  return error.message;
848✔
105
}
106

107
export function getStatusCodeFromError(error: Error): number {
200✔
108
  if (error instanceof HttpError) {
176✔
109
    return error.statusCode;
24✔
110
  }
111
  if (error instanceof InputValidationError) {
152✔
112
    return 400;
64✔
113
  }
114
  return 500;
88✔
115
}
116

117
type Examples<T extends z.ZodTypeAny> = Readonly<z.input<T>[] | z.output<T>[]>;
118
export const getExamples = <T extends z.ZodTypeAny>(
200✔
119
  schema: T,
120
  parseToOutput: boolean
121
): Examples<T> => {
122
  const examples = getMeta(schema, "examples");
6,848✔
123
  if (examples === undefined) {
6,848✔
124
    return [];
5,168✔
125
  }
126
  return examples.reduce((carry, example) => {
1,680✔
127
    const parsedExample = schema.safeParse(example);
1,120✔
128
    return carry.concat(
1,120✔
129
      parsedExample.success
1,120✔
130
        ? parseToOutput
1,104✔
131
          ? parsedExample.data
132
          : example
133
        : []
134
    );
135
  }, [] as z.output<typeof schema>[]);
136
};
137

138
export const combinations = <T extends any>(
200✔
139
  a: T[],
140
  b: T[]
141
): { type: "single"; value: T[] } | { type: "tuple"; value: [T, T][] } => {
142
  if (a.length === 0) {
520✔
143
    return { type: "single", value: b };
240✔
144
  }
145
  if (b.length === 0) {
280✔
146
    return { type: "single", value: a };
32✔
147
  }
148
  const result: [T, T][] = [];
248✔
149
  for (const itemA of a) {
248✔
150
    for (const itemB of b) {
328✔
151
      result.push([itemA, itemB]);
472✔
152
    }
153
  }
154
  return { type: "tuple", value: result };
248✔
155
};
156

157
export function getRoutePathParams(path: string): string[] {
200✔
158
  const match = path.match(routePathParamsRegex);
736✔
159
  if (!match) {
736✔
160
    return [];
608✔
161
  }
162
  return match.map((param) => param.slice(1));
168✔
163
}
164

165
const reduceBool = (arr: boolean[]) =>
200✔
166
  arr.reduce((carry, bool) => carry || bool, false);
2,296✔
167

168
export function hasTopLevelTransformingEffect(schema: IOSchema): boolean {
200✔
169
  if (schema instanceof z.ZodEffects) {
2,664✔
170
    if (schema._def.effect.type !== "refinement") {
128✔
171
      return true;
56✔
172
    }
173
  }
174
  if (schema instanceof z.ZodUnion) {
2,608✔
175
    return reduceBool(schema.options.map(hasTopLevelTransformingEffect));
72✔
176
  }
177
  if (schema instanceof z.ZodIntersection) {
2,536✔
178
    return reduceBool(
240✔
179
      [schema._def.left, schema._def.right].map(hasTopLevelTransformingEffect)
180
    );
181
  }
182
  return false; // ZodObject left
2,296✔
183
}
184

185
export function hasUpload(schema: z.ZodTypeAny): boolean {
200✔
186
  if (schema instanceof ZodUpload) {
3,048✔
187
    return true;
96✔
188
  }
189
  if (schema instanceof z.ZodObject) {
2,952✔
190
    return reduceBool(Object.values<z.ZodTypeAny>(schema.shape).map(hasUpload));
1,104✔
191
  }
192
  if (schema instanceof z.ZodUnion) {
1,848✔
193
    return reduceBool(schema.options.map(hasUpload));
72✔
194
  }
195
  if (schema instanceof z.ZodIntersection) {
1,776✔
196
    return reduceBool([schema._def.left, schema._def.right].map(hasUpload));
216✔
197
  }
198
  if (schema instanceof z.ZodOptional || schema instanceof z.ZodNullable) {
1,560✔
199
    return hasUpload(schema.unwrap());
152✔
200
  }
201
  if (schema instanceof z.ZodEffects || schema instanceof z.ZodTransformer) {
1,408✔
202
    return hasUpload(schema._def.schema);
216✔
203
  }
204
  if (schema instanceof z.ZodRecord) {
1,192✔
205
    return hasUpload(schema._def.valueType);
8✔
206
  }
207
  if (schema instanceof z.ZodArray) {
1,184✔
208
    return hasUpload(schema._def.type);
32✔
209
  }
210
  if (schema instanceof z.ZodDefault) {
1,152✔
211
    return hasUpload(schema._def.innerType);
24✔
212
  }
213
  return false;
1,128✔
214
}
215

216
/**
217
 * @desc isNullable() and isOptional() validate the schema's input
218
 * @desc They always return true in case of coercion, which should be taken into account when depicting response
219
 */
220
export const hasCoercion = (schema: z.ZodTypeAny): boolean =>
200✔
221
  "coerce" in schema._def && typeof schema._def.coerce === "boolean"
2,864✔
222
    ? schema._def.coerce
223
    : false;
224

225
export const makeCleanId = (path: string, method: string, suffix?: string) => {
200✔
226
  return [method]
616✔
227
    .concat(path.split("/"))
228
    .concat(suffix || [])
1,008✔
229
    .flatMap((entry) => entry.split(/[^A-Z0-9]/gi))
2,992✔
230
    .map(
231
      (entry) => entry.slice(0, 1).toUpperCase() + entry.slice(1).toLowerCase()
3,240✔
232
    )
233
    .join("");
234
};
235

236
export const tryToTransform = ({
200✔
237
  effect,
238
  sample,
239
}: {
240
  effect: z.Effect<any> & { type: "transform" };
241
  sample: any;
242
}) => {
243
  try {
80✔
244
    return typeof effect.transform(sample, {
80✔
245
      addIssue: () => {},
246
      path: [],
247
    });
248
  } catch (e) {
249
    return undefined;
16✔
250
  }
251
};
252

253
// obtaining the private helper type from Zod
254
export type ErrMessage = Exclude<
255
  Parameters<typeof z.ZodString.prototype.email>[0],
256
  undefined
257
>;
258

259
// the copy of the private Zod errorUtil.errToObj
260
export const errToObj = (message: ErrMessage | undefined) =>
200✔
261
  typeof message === "string" ? { message } : message || {};
80✔
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