@jaypie/express 1.2.4 → 1.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,6 +8,7 @@ See [Jaypie](https://github.com/finlaysonstudio/jaypie) for usage.
8
8
 
9
9
  | Date | Version | Summary |
10
10
  | ---------- | ------- | -------------- |
11
+ | 1/23/2026 | 1.2.5 | Fix array query params with bracket notation in API Gateway v1 |
11
12
  | 5/21/2024 | 1.0.0 | Initial release |
12
13
  | 5/19/2024 | 0.1.0 | Initial deploy |
13
14
  | 5/12/2024 | 0.0.1 | Initial commit |
@@ -8,6 +8,7 @@ interface LambdaRequestOptions {
8
8
  lambdaEvent: LambdaEvent;
9
9
  method: string;
10
10
  protocol: string;
11
+ query?: Record<string, string | string[]>;
11
12
  remoteAddress: string;
12
13
  url: string;
13
14
  }
@@ -34,13 +34,15 @@ class LambdaRequest extends node_stream.Readable {
34
34
  this.path = options.url.split("?")[0];
35
35
  this.headers = this.normalizeHeaders(options.headers);
36
36
  this.bodyBuffer = options.body ?? null;
37
- // Parse query string from URL
38
- const queryIndex = options.url.indexOf("?");
39
- if (queryIndex !== -1) {
40
- const queryString = options.url.slice(queryIndex + 1);
41
- const params = new URLSearchParams(queryString);
42
- for (const [key, value] of params) {
43
- this.query[key] = value;
37
+ // Use pre-parsed query if provided, otherwise parse from URL
38
+ if (options.query) {
39
+ this.query = options.query;
40
+ }
41
+ else {
42
+ const queryIndex = options.url.indexOf("?");
43
+ if (queryIndex !== -1) {
44
+ const queryString = options.url.slice(queryIndex + 1);
45
+ this.query = parseQueryString(queryString);
44
46
  }
45
47
  }
46
48
  // Store Lambda context
@@ -103,6 +105,67 @@ class LambdaRequest extends node_stream.Readable {
103
105
  }
104
106
  //
105
107
  //
108
+ // Helper Functions
109
+ //
110
+ /**
111
+ * Normalize bracket notation in query parameter key.
112
+ * Removes trailing `[]` from keys (e.g., `filterByStatus[]` → `filterByStatus`).
113
+ */
114
+ function normalizeQueryKey(key) {
115
+ return key.endsWith("[]") ? key.slice(0, -2) : key;
116
+ }
117
+ /**
118
+ * Parse a query string into a record with proper array handling.
119
+ * Handles bracket notation (e.g., `param[]`) and multi-value parameters.
120
+ */
121
+ function parseQueryString(queryString) {
122
+ const result = {};
123
+ const params = new URLSearchParams(queryString);
124
+ for (const [rawKey, value] of params) {
125
+ const key = normalizeQueryKey(rawKey);
126
+ const existing = result[key];
127
+ if (existing === undefined) {
128
+ // First occurrence - check if it's bracket notation to determine if it should be an array
129
+ result[key] = rawKey.endsWith("[]") ? [value] : value;
130
+ }
131
+ else if (Array.isArray(existing)) {
132
+ existing.push(value);
133
+ }
134
+ else {
135
+ // Convert to array when we encounter a second value
136
+ result[key] = [existing, value];
137
+ }
138
+ }
139
+ return result;
140
+ }
141
+ /**
142
+ * Build query object from API Gateway v1 multiValueQueryStringParameters.
143
+ * Normalizes bracket notation and preserves array values.
144
+ */
145
+ function buildQueryFromMultiValue(multiValueParams) {
146
+ const result = {};
147
+ if (!multiValueParams)
148
+ return result;
149
+ for (const [rawKey, values] of Object.entries(multiValueParams)) {
150
+ const key = normalizeQueryKey(rawKey);
151
+ const existingValues = result[key];
152
+ if (existingValues === undefined) {
153
+ // First occurrence - use array if multiple values or bracket notation
154
+ result[key] =
155
+ values.length === 1 && !rawKey.endsWith("[]") ? values[0] : values;
156
+ }
157
+ else if (Array.isArray(existingValues)) {
158
+ existingValues.push(...values);
159
+ }
160
+ else {
161
+ // Convert to array and merge
162
+ result[key] = [existingValues, ...values];
163
+ }
164
+ }
165
+ return result;
166
+ }
167
+ //
168
+ //
106
169
  // Type Guards
107
170
  //
108
171
  /**
@@ -128,6 +191,7 @@ function createLambdaRequest(event, context) {
128
191
  let url;
129
192
  let method;
130
193
  let protocol;
194
+ let query;
131
195
  let remoteAddress;
132
196
  const headers = { ...event.headers };
133
197
  if (isFunctionUrlEvent(event)) {
@@ -138,6 +202,10 @@ function createLambdaRequest(event, context) {
138
202
  method = event.requestContext.http.method;
139
203
  protocol = event.requestContext.http.protocol.split("/")[0].toLowerCase();
140
204
  remoteAddress = event.requestContext.http.sourceIp;
205
+ // Parse query string with proper multi-value and bracket notation support
206
+ if (event.rawQueryString) {
207
+ query = parseQueryString(event.rawQueryString);
208
+ }
141
209
  // Normalize cookies into Cookie header if not already present
142
210
  if (event.cookies && event.cookies.length > 0 && !headers.cookie) {
143
211
  headers.cookie = event.cookies.join("; ");
@@ -145,7 +213,13 @@ function createLambdaRequest(event, context) {
145
213
  }
146
214
  else if (isApiGatewayV1Event(event)) {
147
215
  // API Gateway REST API v1 format
216
+ // Use multiValueQueryStringParameters for proper array support
217
+ const multiValueParams = event.multiValueQueryStringParameters;
148
218
  const queryParams = event.queryStringParameters;
219
+ if (multiValueParams && Object.keys(multiValueParams).length > 0) {
220
+ query = buildQueryFromMultiValue(multiValueParams);
221
+ }
222
+ // Build URL with query string
149
223
  if (queryParams && Object.keys(queryParams).length > 0) {
150
224
  const queryString = new URLSearchParams(queryParams).toString();
151
225
  url = `${event.path}?${queryString}`;
@@ -179,6 +253,7 @@ function createLambdaRequest(event, context) {
179
253
  lambdaEvent: event,
180
254
  method,
181
255
  protocol,
256
+ query,
182
257
  remoteAddress,
183
258
  url,
184
259
  });
@@ -975,7 +1050,9 @@ function createLambdaHandler(app, _options) {
975
1050
  {
976
1051
  status: 500,
977
1052
  title: "Internal Server Error",
978
- detail: error instanceof Error ? error.message : "Unknown error occurred",
1053
+ detail: error instanceof Error
1054
+ ? error.message
1055
+ : "Unknown error occurred",
979
1056
  },
980
1057
  ],
981
1058
  }),
@@ -1062,6 +1139,33 @@ const ensureProtocol = (url) => {
1062
1139
  return url;
1063
1140
  return HTTPS_PROTOCOL + url;
1064
1141
  };
1142
+ const extractHostname = (origin) => {
1143
+ try {
1144
+ const url = new URL(origin);
1145
+ return url.hostname;
1146
+ }
1147
+ catch {
1148
+ return undefined;
1149
+ }
1150
+ };
1151
+ const isOriginAllowed = (requestOrigin, allowed) => {
1152
+ const normalizedAllowed = ensureProtocol(allowed);
1153
+ const normalizedRequest = ensureProtocol(requestOrigin);
1154
+ const allowedHostname = extractHostname(normalizedAllowed);
1155
+ const requestHostname = extractHostname(normalizedRequest);
1156
+ if (!allowedHostname || !requestHostname) {
1157
+ return false;
1158
+ }
1159
+ // Exact match
1160
+ if (requestHostname === allowedHostname) {
1161
+ return true;
1162
+ }
1163
+ // Subdomain match
1164
+ if (requestHostname.endsWith(`.${allowedHostname}`)) {
1165
+ return true;
1166
+ }
1167
+ return false;
1168
+ };
1065
1169
  const dynamicOriginCallbackHandler = (origin) => {
1066
1170
  return (requestOrigin, callback) => {
1067
1171
  // Handle wildcard origin
@@ -1095,7 +1199,7 @@ const dynamicOriginCallbackHandler = (origin) => {
1095
1199
  if (allowed instanceof RegExp) {
1096
1200
  return allowed.test(requestOrigin);
1097
1201
  }
1098
- return requestOrigin.includes(allowed);
1202
+ return isOriginAllowed(requestOrigin, allowed);
1099
1203
  });
1100
1204
  if (isAllowed) {
1101
1205
  callback(null, true);
@@ -1366,7 +1470,7 @@ const logger$1 = logger$2.log;
1366
1470
  * Uses Symbol marker to survive prototype chain modifications from Express and dd-trace.
1367
1471
  */
1368
1472
  function isLambdaMockResponse(res) {
1369
- return res[JAYPIE_LAMBDA_MOCK] === true;
1473
+ return (res[JAYPIE_LAMBDA_MOCK] === true);
1370
1474
  }
1371
1475
  /**
1372
1476
  * Safely send a JSON response, avoiding dd-trace interception.