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

jeremydaly / lambda-api / #197

05 Apr 2025 05:27PM UTC coverage: 97.807%. Remained the same
#197

push

github

web-flow
chore(release): v1.2.0

756 of 772 branches covered (97.93%)

Branch coverage included in aggregate %.

671 of 687 relevant lines covered (97.67%)

155.67 hits per line

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

100.0
/lib/utils.js
1
'use strict';
2
/**
3
 * Lightweight web framework for your serverless applications
4
 * @author Jeremy Daly <jeremy@jeremydaly.com>
5
 * @license MIT
6
 */
7

8
const QS = require('querystring'); // Require the querystring library
26✔
9
const crypto = require('crypto'); // Require Node.js crypto library
26✔
10
const { FileError } = require('./errors'); // Require custom errors
26✔
11

12
const entityMap = {
26✔
13
  '&': '&amp;',
14
  '<': '&lt;',
15
  '>': '&gt;',
16
  '"': '&quot;',
17
  "'": '&#39;',
18
};
19

20
exports.escapeHtml = (html) => html.replace(/[&<>"']/g, (s) => entityMap[s]);
26✔
21

22
// From encodeurl by Douglas Christopher Wilson
23
let ENCODE_CHARS_REGEXP =
24
  /(?:[^\x21\x25\x26-\x3B\x3D\x3F-\x5B\x5D\x5F\x61-\x7A\x7E]|%(?:[^0-9A-Fa-f]|[0-9A-Fa-f][^0-9A-Fa-f]|$))+/g;
26✔
25
let UNMATCHED_SURROGATE_PAIR_REGEXP =
26
  /(^|[^\uD800-\uDBFF])[\uDC00-\uDFFF]|[\uD800-\uDBFF]([^\uDC00-\uDFFF]|$)/g;
26✔
27
let UNMATCHED_SURROGATE_PAIR_REPLACE = '$1\uFFFD$2';
26✔
28

29
exports.encodeUrl = (url) =>
26✔
30
  String(url)
10✔
31
    .replace(UNMATCHED_SURROGATE_PAIR_REGEXP, UNMATCHED_SURROGATE_PAIR_REPLACE)
32
    .replace(ENCODE_CHARS_REGEXP, encodeURI);
33

34
const encodeBody = (body, serializer) => {
26✔
35
  const encode = typeof serializer === 'function' ? serializer : JSON.stringify;
601✔
36
  return typeof body === 'object'
601✔
37
    ? encode(body)
38
    : body && typeof body !== 'string'
1,000✔
39
    ? body.toString()
40
    : body
333✔
41
    ? body
42
    : '';
43
};
44

45
exports.encodeBody = encodeBody;
26✔
46

47
exports.parsePath = (path) => {
26✔
48
  return path
789✔
49
    ? path
50
        .trim()
51
        .split('?')[0]
52
        .replace(/^\/(.*?)(\/)*$/, '$1')
53
        .split('/')
54
    : [];
55
};
56

57
exports.parseBody = (body) => {
26✔
58
  try {
28✔
59
    return JSON.parse(body);
28✔
60
  } catch (e) {
61
    return body;
20✔
62
  }
63
};
64

65
// Parses auth values into known formats
66
const parseAuthValue = (type, value) => {
26✔
67
  switch (type) {
9✔
68
    case 'Basic': {
69
      let creds = Buffer.from(value, 'base64').toString().split(':');
3✔
70
      return {
3✔
71
        type,
72
        value,
73
        username: creds[0],
74
        password: creds[1] ? creds[1] : null,
3✔
75
      };
76
    }
77
    case 'OAuth': {
78
      let params = QS.parse(
2✔
79
        value.replace(/",\s*/g, '&').replace(/"/g, '').trim()
80
      );
81
      return Object.assign({ type, value }, params);
2✔
82
    }
83
    default: {
84
      return { type, value };
4✔
85
    }
86
  }
87
};
88

89
exports.parseAuth = (authStr) => {
26✔
90
  let auth = authStr && typeof authStr === 'string' ? authStr.split(' ') : [];
577✔
91
  return auth.length > 1 &&
577✔
92
    ['Bearer', 'Basic', 'Digest', 'OAuth'].includes(auth[0])
93
    ? parseAuthValue(auth[0], auth.slice(1).join(' ').trim())
94
    : { type: 'none', value: null };
95
};
96

97
const mimeMap = require('./mimemap.js'); // MIME Map
26✔
98

99
exports.mimeLookup = (input, custom = {}) => {
26✔
100
  let type = input.trim().replace(/^\./, '');
42✔
101

102
  // If it contains a slash, return unmodified
103
  if (/.*\/.*/.test(type)) {
42✔
104
    return input.trim();
7✔
105
  } else {
106
    // Lookup mime type
107
    let mime = Object.assign(mimeMap, custom)[type];
35✔
108
    return mime ? mime : false;
35✔
109
  }
110
};
111

112
const statusCodes = require('./statusCodes.js'); // MIME Map
26✔
113

114
exports.statusLookup = (status) => {
26✔
115
  return status in statusCodes ? statusCodes[status] : 'Unknown';
13✔
116
};
117

118
// Parses routes into readable array
119
const extractRoutes = (routes, table = []) => {
26✔
120
  // Loop through all routes
121
  for (let route in routes['ROUTES']) {
104✔
122
    // Add methods
123
    for (let method in routes['ROUTES'][route]['METHODS']) {
84✔
124
      table.push([
75✔
125
        method,
126
        routes['ROUTES'][route]['METHODS'][method].path,
127
        routes['ROUTES'][route]['METHODS'][method].stack.map((x) =>
128
          x.name.trim() !== '' ? x.name : 'unnamed'
101✔
129
        ),
130
      ]);
131
    }
132
    extractRoutes(routes['ROUTES'][route], table);
84✔
133
  }
134
  return table;
104✔
135
};
136

137
exports.extractRoutes = extractRoutes;
26✔
138

139
// Generate an Etag for the supplied value
140
exports.generateEtag = (data) =>
26✔
141
  crypto
11✔
142
    .createHash('sha256')
143
    .update(encodeBody(data))
144
    .digest('hex')
145
    .substr(0, 32);
146

147
// Check if valid S3 path
148
exports.isS3 = (path) => /^s3:\/\/.+\/.+/i.test(path);
33✔
149

150
// Parse S3 path
151
exports.parseS3 = (path) => {
26✔
152
  if (!this.isS3(path)) throw new FileError('Invalid S3 path', { path });
22✔
153
  let s3object = path.replace(/^s3:\/\//i, '').split('/');
16✔
154
  return { Bucket: s3object.shift(), Key: s3object.join('/') };
16✔
155
};
156

157
// Deep Merge
158
exports.deepMerge = (a, b) => {
26✔
159
  Object.keys(b).forEach((key) => {
21✔
160
    if (key === '__proto__') return;
22✔
161
    if (typeof b[key] !== 'object') return Object.assign(a, b);
21✔
162
    return key in a ? this.deepMerge(a[key], b[key]) : Object.assign(a, b);
18✔
163
  });
164
  return a;
21✔
165
};
166

167
// Concatenate arrays when merging two objects
168
exports.mergeObjects = (obj1, obj2) =>
26✔
169
  Object.keys(Object.assign({}, obj1, obj2)).reduce((acc, key) => {
18✔
170
    if (
12✔
171
      obj1[key] &&
30✔
172
      obj2[key] &&
173
      obj1[key].every((e) => obj2[key].includes(e))
8✔
174
    ) {
175
      return Object.assign(acc, { [key]: obj1[key] });
2✔
176
    } else {
177
      return Object.assign(acc, {
10✔
178
        [key]: obj1[key]
10✔
179
          ? obj2[key]
8✔
180
            ? obj1[key].concat(obj2[key])
181
            : obj1[key]
182
          : obj2[key],
183
      });
184
    }
185
  }, {});
186

187
// Concats values from an array to ',' separated string
188
exports.fromArray = (val) =>
26✔
189
  val && val instanceof Array ? val.toString() : undefined;
1,226✔
190

191
// Stringify multi-value headers
192
exports.stringifyHeaders = (headers) =>
26✔
193
  Object.keys(headers).reduce(
5✔
194
    (acc, key) =>
195
      Object.assign(acc, {
6✔
196
        // set-cookie cannot be concatenated with a comma
197
        [key]:
198
          key === 'set-cookie'
6✔
199
            ? headers[key].slice(-1)[0]
200
            : headers[key].toString(),
201
      }),
202
    {}
203
  );
204

205
exports.streamToBuffer = (stream) =>
26✔
206
  new Promise((resolve, reject) => {
1✔
207
    const chunks = [];
1✔
208
    stream.on('data', (chunk) => chunks.push(chunk));
1✔
209
    stream.on('error', reject);
1✔
210
    stream.on('end', () => resolve(Buffer.concat(chunks)));
1✔
211
  });
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

© 2026 Coveralls, Inc