@arkyn/server 2.0.1-beta.0 → 2.0.1-beta.2

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.
Files changed (98) hide show
  1. package/dist/http/badResponses/badGateway.d.ts +11 -0
  2. package/dist/http/badResponses/badGateway.d.ts.map +1 -0
  3. package/dist/http/badResponses/badGateway.js +29 -0
  4. package/dist/http/badResponses/badRequest.d.ts +11 -0
  5. package/dist/http/badResponses/badRequest.d.ts.map +1 -0
  6. package/dist/http/badResponses/badRequest.js +29 -0
  7. package/dist/http/badResponses/conflict.d.ts +11 -0
  8. package/dist/http/badResponses/conflict.d.ts.map +1 -0
  9. package/dist/http/badResponses/conflict.js +29 -0
  10. package/dist/http/badResponses/forbidden.d.ts +11 -0
  11. package/dist/http/badResponses/forbidden.d.ts.map +1 -0
  12. package/dist/http/badResponses/forbidden.js +29 -0
  13. package/dist/http/badResponses/notFound.d.ts +11 -0
  14. package/dist/http/badResponses/notFound.d.ts.map +1 -0
  15. package/dist/http/badResponses/notFound.js +29 -0
  16. package/dist/http/badResponses/notImplemented.d.ts +11 -0
  17. package/dist/http/badResponses/notImplemented.d.ts.map +1 -0
  18. package/dist/http/badResponses/notImplemented.js +29 -0
  19. package/dist/http/badResponses/serverError.d.ts +11 -0
  20. package/dist/http/badResponses/serverError.d.ts.map +1 -0
  21. package/dist/http/badResponses/serverError.js +29 -0
  22. package/dist/http/badResponses/unauthorized.d.ts +11 -0
  23. package/dist/http/badResponses/unauthorized.d.ts.map +1 -0
  24. package/dist/http/badResponses/unauthorized.js +29 -0
  25. package/dist/http/badResponses/unprocessableEntity.d.ts +16 -0
  26. package/dist/http/badResponses/unprocessableEntity.d.ts.map +1 -0
  27. package/dist/http/badResponses/unprocessableEntity.js +33 -0
  28. package/dist/http/httpDebug.d.ts +3 -0
  29. package/dist/http/httpDebug.d.ts.map +1 -0
  30. package/dist/http/httpDebug.js +15 -0
  31. package/dist/http/successResponses/created.d.ts +11 -0
  32. package/dist/http/successResponses/created.d.ts.map +1 -0
  33. package/dist/http/successResponses/created.js +29 -0
  34. package/dist/http/successResponses/found.d.ts +11 -0
  35. package/dist/http/successResponses/found.d.ts.map +1 -0
  36. package/dist/http/successResponses/found.js +29 -0
  37. package/dist/http/successResponses/noContent.d.ts +10 -0
  38. package/dist/http/successResponses/noContent.d.ts.map +1 -0
  39. package/dist/http/successResponses/noContent.js +27 -0
  40. package/dist/http/successResponses/success.d.ts +11 -0
  41. package/dist/http/successResponses/success.d.ts.map +1 -0
  42. package/dist/http/successResponses/success.js +29 -0
  43. package/dist/http/successResponses/updated.d.ts +11 -0
  44. package/dist/http/successResponses/updated.d.ts.map +1 -0
  45. package/dist/http/successResponses/updated.js +29 -0
  46. package/dist/index.d.ts +20 -0
  47. package/dist/index.d.ts.map +1 -0
  48. package/dist/index.js +19 -0
  49. package/dist/services/decodeRequestBody.d.ts +17 -0
  50. package/dist/services/decodeRequestBody.d.ts.map +1 -0
  51. package/dist/services/decodeRequestBody.js +42 -0
  52. package/dist/services/errorHandler.d.ts +3 -0
  53. package/dist/services/errorHandler.d.ts.map +1 -0
  54. package/dist/services/errorHandler.js +51 -0
  55. package/dist/services/formParse.d.ts +42 -0
  56. package/dist/services/formParse.d.ts.map +1 -0
  57. package/dist/services/formParse.js +54 -0
  58. package/dist/services/getScopedParams.d.ts +28 -0
  59. package/dist/services/getScopedParams.d.ts.map +1 -0
  60. package/dist/services/getScopedParams.js +34 -0
  61. package/dist/services/sendFileToS3.d.ts +52 -0
  62. package/dist/services/sendFileToS3.d.ts.map +1 -0
  63. package/dist/services/sendFileToS3.js +143 -0
  64. package/package.json +2 -1
  65. package/src/http/badResponses/badGateway.ts +36 -0
  66. package/src/http/badResponses/badRequest.ts +36 -0
  67. package/src/http/badResponses/conflict.ts +36 -0
  68. package/src/http/badResponses/forbidden.ts +36 -0
  69. package/src/http/badResponses/notFound.ts +36 -0
  70. package/src/http/badResponses/notImplemented.ts +36 -0
  71. package/src/http/badResponses/serverError.ts +36 -0
  72. package/src/http/badResponses/unauthorized.ts +36 -0
  73. package/src/http/badResponses/unprocessableEntity.ts +48 -0
  74. package/src/http/httpDebug.ts +18 -0
  75. package/src/http/successResponses/created.ts +35 -0
  76. package/src/http/successResponses/found.ts +35 -0
  77. package/src/http/successResponses/noContent.ts +33 -0
  78. package/src/http/successResponses/success.ts +35 -0
  79. package/src/http/successResponses/updated.ts +35 -0
  80. package/src/index.ts +16 -18
  81. package/src/services/decodeRequestBody.ts +46 -0
  82. package/src/services/errorHandler.ts +55 -0
  83. package/src/services/formParse.ts +42 -1
  84. package/src/services/getScopedParams.ts +29 -2
  85. package/src/services/sendFileToS3.ts +81 -56
  86. package/src/helpers/globalErrorHandler.ts +0 -64
  87. package/src/httpBadResponses/badRequest.ts +0 -29
  88. package/src/httpBadResponses/conflict.ts +0 -29
  89. package/src/httpBadResponses/forbidden.ts +0 -29
  90. package/src/httpBadResponses/notFound.ts +0 -29
  91. package/src/httpBadResponses/serverError.ts +0 -29
  92. package/src/httpBadResponses/unauthorized.ts +0 -29
  93. package/src/httpBadResponses/unprocessableEntity.ts +0 -43
  94. package/src/httpResponses/created.ts +0 -35
  95. package/src/httpResponses/noContent.ts +0 -33
  96. package/src/httpResponses/success.ts +0 -35
  97. package/src/httpResponses/updated.ts +0 -35
  98. package/src/services/extractJsonFromRequest.ts +0 -30
@@ -0,0 +1,29 @@
1
+ class Updated {
2
+ body;
3
+ headers;
4
+ status;
5
+ statusText;
6
+ constructor(body, init) {
7
+ this.body = body;
8
+ this.headers = init?.headers || {};
9
+ this.status = init?.status || 200;
10
+ this.statusText = init?.statusText || "Resource updated successfully";
11
+ }
12
+ toResponse() {
13
+ const responseInit = {
14
+ headers: { "Content-Type": "application/json", ...this.headers },
15
+ status: this.status,
16
+ statusText: this.statusText,
17
+ };
18
+ return new Response(JSON.stringify(this.body), responseInit);
19
+ }
20
+ toJson() {
21
+ const responseInit = {
22
+ headers: this.headers,
23
+ status: this.status,
24
+ statusText: this.statusText,
25
+ };
26
+ return Response.json(this.body, responseInit);
27
+ }
28
+ }
29
+ export { Updated };
@@ -0,0 +1,20 @@
1
+ export { BadGateway } from "./http/badResponses/badGateway";
2
+ export { BadRequest } from "./http/badResponses/badRequest";
3
+ export { Conflict } from "./http/badResponses/conflict";
4
+ export { Forbidden } from "./http/badResponses/forbidden";
5
+ export { NotFound } from "./http/badResponses/notFound";
6
+ export { NotImplemented } from "./http/badResponses/notImplemented";
7
+ export { ServerError } from "./http/badResponses/serverError";
8
+ export { Unauthorized } from "./http/badResponses/unauthorized";
9
+ export { UnprocessableEntity } from "./http/badResponses/unprocessableEntity";
10
+ export { Created } from "./http/successResponses/created";
11
+ export { Found } from "./http/successResponses/found";
12
+ export { NoContent } from "./http/successResponses/noContent";
13
+ export { Success } from "./http/successResponses/success";
14
+ export { Updated } from "./http/successResponses/updated";
15
+ export { decodeRequestBody } from "./services/decodeRequestBody";
16
+ export { errorHandler } from "./services/errorHandler";
17
+ export { formParse } from "./services/formParse";
18
+ export { getScopedParams } from "./services/getScopedParams";
19
+ export { sendFileToS3 } from "./services/sendFileToS3";
20
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,gCAAgC,CAAC;AAC5D,OAAO,EAAE,UAAU,EAAE,MAAM,gCAAgC,CAAC;AAC5D,OAAO,EAAE,QAAQ,EAAE,MAAM,8BAA8B,CAAC;AACxD,OAAO,EAAE,SAAS,EAAE,MAAM,+BAA+B,CAAC;AAC1D,OAAO,EAAE,QAAQ,EAAE,MAAM,8BAA8B,CAAC;AACxD,OAAO,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAC;AACpE,OAAO,EAAE,WAAW,EAAE,MAAM,iCAAiC,CAAC;AAC9D,OAAO,EAAE,YAAY,EAAE,MAAM,kCAAkC,CAAC;AAChE,OAAO,EAAE,mBAAmB,EAAE,MAAM,yCAAyC,CAAC;AAE9E,OAAO,EAAE,OAAO,EAAE,MAAM,iCAAiC,CAAC;AAC1D,OAAO,EAAE,KAAK,EAAE,MAAM,+BAA+B,CAAC;AACtD,OAAO,EAAE,SAAS,EAAE,MAAM,mCAAmC,CAAC;AAC9D,OAAO,EAAE,OAAO,EAAE,MAAM,iCAAiC,CAAC;AAC1D,OAAO,EAAE,OAAO,EAAE,MAAM,iCAAiC,CAAC;AAE1D,OAAO,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAC;AACjE,OAAO,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AACvD,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACjD,OAAO,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAC7D,OAAO,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,19 @@
1
+ export { BadGateway } from "./http/badResponses/badGateway";
2
+ export { BadRequest } from "./http/badResponses/badRequest";
3
+ export { Conflict } from "./http/badResponses/conflict";
4
+ export { Forbidden } from "./http/badResponses/forbidden";
5
+ export { NotFound } from "./http/badResponses/notFound";
6
+ export { NotImplemented } from "./http/badResponses/notImplemented";
7
+ export { ServerError } from "./http/badResponses/serverError";
8
+ export { Unauthorized } from "./http/badResponses/unauthorized";
9
+ export { UnprocessableEntity } from "./http/badResponses/unprocessableEntity";
10
+ export { Created } from "./http/successResponses/created";
11
+ export { Found } from "./http/successResponses/found";
12
+ export { NoContent } from "./http/successResponses/noContent";
13
+ export { Success } from "./http/successResponses/success";
14
+ export { Updated } from "./http/successResponses/updated";
15
+ export { decodeRequestBody } from "./services/decodeRequestBody";
16
+ export { errorHandler } from "./services/errorHandler";
17
+ export { formParse } from "./services/formParse";
18
+ export { getScopedParams } from "./services/getScopedParams";
19
+ export { sendFileToS3 } from "./services/sendFileToS3";
@@ -0,0 +1,17 @@
1
+ import type { DecodeRequestBodyFunction } from "@arkyn/types";
2
+ /**
3
+ * Decodes the body of an incoming request into a JavaScript object.
4
+ *
5
+ * This function attempts to parse the request body in the following order:
6
+ * 1. Tries to parse the body as JSON.
7
+ * 2. If JSON parsing fails, attempts to parse the body as URL-encoded form data.
8
+ * 3. If both parsing attempts fail, logs the errors and returns an empty object.
9
+ *
10
+ * @param req - The incoming request object containing the body to decode.
11
+ * @returns A promise that resolves to the decoded data as a JavaScript object.
12
+ *
13
+ * @throws Logs errors to the console if the request body cannot be read or parsed.
14
+ */
15
+ declare const decodeRequestBody: DecodeRequestBodyFunction;
16
+ export { decodeRequestBody };
17
+ //# sourceMappingURL=decodeRequestBody.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decodeRequestBody.d.ts","sourceRoot":"","sources":["../../src/services/decodeRequestBody.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,cAAc,CAAC;AAE9D;;;;;;;;;;;;GAYG;AAEH,QAAA,MAAM,iBAAiB,EAAE,yBA2BxB,CAAC;AAEF,OAAO,EAAE,iBAAiB,EAAE,CAAC"}
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Decodes the body of an incoming request into a JavaScript object.
3
+ *
4
+ * This function attempts to parse the request body in the following order:
5
+ * 1. Tries to parse the body as JSON.
6
+ * 2. If JSON parsing fails, attempts to parse the body as URL-encoded form data.
7
+ * 3. If both parsing attempts fail, logs the errors and returns an empty object.
8
+ *
9
+ * @param req - The incoming request object containing the body to decode.
10
+ * @returns A promise that resolves to the decoded data as a JavaScript object.
11
+ *
12
+ * @throws Logs errors to the console if the request body cannot be read or parsed.
13
+ */
14
+ const decodeRequestBody = async (req) => {
15
+ let data;
16
+ try {
17
+ const arrayBuffer = await req.arrayBuffer();
18
+ const text = new TextDecoder().decode(arrayBuffer);
19
+ try {
20
+ data = JSON.parse(text);
21
+ }
22
+ catch (jsonError) {
23
+ try {
24
+ const formData = new URLSearchParams(text);
25
+ data = Object.fromEntries(formData.entries());
26
+ }
27
+ catch (formDataError) {
28
+ console.error("Failed to extract data from request:", {
29
+ jsonError,
30
+ formDataError,
31
+ });
32
+ data = {};
33
+ }
34
+ }
35
+ }
36
+ catch (error) {
37
+ console.error("Failed to read request body:", error);
38
+ data = {};
39
+ }
40
+ return data;
41
+ };
42
+ export { decodeRequestBody };
@@ -0,0 +1,3 @@
1
+ declare function errorHandler(error: any): Response | undefined;
2
+ export { errorHandler };
3
+ //# sourceMappingURL=errorHandler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errorHandler.d.ts","sourceRoot":"","sources":["../../src/services/errorHandler.ts"],"names":[],"mappings":"AAgBA,iBAAS,YAAY,CAAC,KAAK,EAAE,GAAG,wBAoC/B;AAED,OAAO,EAAE,YAAY,EAAE,CAAC"}
@@ -0,0 +1,51 @@
1
+ import { BadGateway } from "../http/badResponses/badGateway";
2
+ import { BadRequest } from "../http/badResponses/badRequest";
3
+ import { Conflict } from "../http/badResponses/conflict";
4
+ import { Forbidden } from "../http/badResponses/forbidden";
5
+ import { NotFound } from "../http/badResponses/notFound";
6
+ import { NotImplemented } from "../http/badResponses/notImplemented";
7
+ import { ServerError } from "../http/badResponses/serverError";
8
+ import { Unauthorized } from "../http/badResponses/unauthorized";
9
+ import { UnprocessableEntity } from "../http/badResponses/unprocessableEntity";
10
+ import { Created } from "../http/successResponses/created";
11
+ import { Found } from "../http/successResponses/found";
12
+ import { NoContent } from "../http/successResponses/noContent";
13
+ import { Success } from "../http/successResponses/success";
14
+ import { Updated } from "../http/successResponses/updated";
15
+ function errorHandler(error) {
16
+ switch (true) {
17
+ case error instanceof Response:
18
+ return error;
19
+ case error instanceof Found:
20
+ return error.toResponse();
21
+ case error instanceof Created:
22
+ return error.toResponse();
23
+ case error instanceof Updated:
24
+ return error.toResponse();
25
+ case error instanceof Success:
26
+ return error.toResponse();
27
+ case error instanceof NoContent:
28
+ return error.toResponse();
29
+ }
30
+ switch (true) {
31
+ case error instanceof BadGateway:
32
+ return error.toResponse();
33
+ case error instanceof BadRequest:
34
+ return error.toResponse();
35
+ case error instanceof Conflict:
36
+ return error.toResponse();
37
+ case error instanceof Forbidden:
38
+ return error.toResponse();
39
+ case error instanceof NotFound:
40
+ return error.toResponse();
41
+ case error instanceof NotImplemented:
42
+ return error.toResponse();
43
+ case error instanceof ServerError:
44
+ return error.toResponse();
45
+ case error instanceof Unauthorized:
46
+ return error.toResponse();
47
+ case error instanceof UnprocessableEntity:
48
+ return error.toResponse();
49
+ }
50
+ }
51
+ export { errorHandler };
@@ -0,0 +1,42 @@
1
+ import type { FormParseProps, FormParseReturnType } from "@arkyn/types";
2
+ /**
3
+ * Parses form data using a Zod schema and returns the result.
4
+ *
5
+ * @template T - A type that extends `FormParseProps`.
6
+ *
7
+ * @param {T} param0 - An array containing the form data and the Zod schema.
8
+ * @param {T[0]} param0[0] - The form data to be validated.
9
+ * @param {T[1]} param0[1] - The Zod schema used for validation.
10
+ *
11
+ * @returns {FormParseReturnType<T>} An object containing the validation result.
12
+ * - If validation fails, it includes:
13
+ * - `success`: A boolean indicating the validation status (false).
14
+ * - `fieldErrors`: An object mapping field names to their respective error messages.
15
+ * - `fields`: The original form data.
16
+ * - If validation succeeds, it includes:
17
+ * - `success`: A boolean indicating the validation status (true).
18
+ * - `data`: The parsed and validated data.
19
+ *
20
+ * @example
21
+ * ```typescript
22
+ * import { z } from "zod";
23
+ *
24
+ * const schema = z.object({
25
+ * name: z.string().min(1, "Name is required"),
26
+ * age: z.number().min(18, "Must be at least 18"),
27
+ * });
28
+ *
29
+ * const formData = { name: "", age: 17 };
30
+ *
31
+ * const result = formParse([formData, schema]);
32
+ *
33
+ * if (!result.success) {
34
+ * console.log(result.fieldErrors); // { name: "Name is required", age: "Must be at least 18" }
35
+ * } else {
36
+ * console.log(result.data); // Parsed data
37
+ * }
38
+ * ```
39
+ */
40
+ declare function formParse<T extends FormParseProps>([formData, schema,]: T): FormParseReturnType<T>;
41
+ export { formParse };
42
+ //# sourceMappingURL=formParse.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"formParse.d.ts","sourceRoot":"","sources":["../../src/services/formParse.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAExE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAEH,iBAAS,SAAS,CAAC,CAAC,SAAS,cAAc,EAAE,CAC3C,QAAQ,EACR,MAAM,EACP,EAAE,CAAC,GAAG,mBAAmB,CAAC,CAAC,CAAC,CAoB5B;AAED,OAAO,EAAE,SAAS,EAAE,CAAC"}
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Parses form data using a Zod schema and returns the result.
3
+ *
4
+ * @template T - A type that extends `FormParseProps`.
5
+ *
6
+ * @param {T} param0 - An array containing the form data and the Zod schema.
7
+ * @param {T[0]} param0[0] - The form data to be validated.
8
+ * @param {T[1]} param0[1] - The Zod schema used for validation.
9
+ *
10
+ * @returns {FormParseReturnType<T>} An object containing the validation result.
11
+ * - If validation fails, it includes:
12
+ * - `success`: A boolean indicating the validation status (false).
13
+ * - `fieldErrors`: An object mapping field names to their respective error messages.
14
+ * - `fields`: The original form data.
15
+ * - If validation succeeds, it includes:
16
+ * - `success`: A boolean indicating the validation status (true).
17
+ * - `data`: The parsed and validated data.
18
+ *
19
+ * @example
20
+ * ```typescript
21
+ * import { z } from "zod";
22
+ *
23
+ * const schema = z.object({
24
+ * name: z.string().min(1, "Name is required"),
25
+ * age: z.number().min(18, "Must be at least 18"),
26
+ * });
27
+ *
28
+ * const formData = { name: "", age: 17 };
29
+ *
30
+ * const result = formParse([formData, schema]);
31
+ *
32
+ * if (!result.success) {
33
+ * console.log(result.fieldErrors); // { name: "Name is required", age: "Must be at least 18" }
34
+ * } else {
35
+ * console.log(result.data); // Parsed data
36
+ * }
37
+ * ```
38
+ */
39
+ function formParse([formData, schema,]) {
40
+ const zodResponse = schema.safeParse(formData);
41
+ if (zodResponse.success === false) {
42
+ const errorsArray = Object.entries(zodResponse.error.formErrors.fieldErrors);
43
+ const errorsObject = Object.fromEntries(errorsArray.map((item) => [item[0], item[1]?.[0] || "Error"]));
44
+ return {
45
+ success: zodResponse.success,
46
+ fieldErrors: errorsObject,
47
+ fields: formData,
48
+ };
49
+ }
50
+ else {
51
+ return { success: zodResponse.success, data: zodResponse.data };
52
+ }
53
+ }
54
+ export { formParse };
@@ -0,0 +1,28 @@
1
+ import type { GetScopedParamsFunction } from "@arkyn/types";
2
+ /**
3
+ * Extracts and returns scoped query parameters from a request URL.
4
+ *
5
+ * @param request - The incoming request object containing the URL.
6
+ * @param scope - An optional string representing the scope prefix for filtering query parameters.
7
+ * If no scope is provided, all query parameters are returned.
8
+ *
9
+ * @returns A `URLSearchParams` object containing the scoped query parameters.
10
+ * If a scope is provided, only parameters with keys starting with the scope
11
+ * (e.g., `scope:key`) are included, and the scope prefix is removed from the keys.
12
+ * If no scope is provided, all query parameters are returned as-is.
13
+ *
14
+ * @example
15
+ * // Example 1: No scope provided
16
+ * const request = { url: "https://example.com?key1=value1&key2=value2" };
17
+ * const params = getScopedParams(request);
18
+ * console.log(params.toString()); // Output: "key1=value1&key2=value2"
19
+ *
20
+ * @example
21
+ * // Example 2: Scope provided
22
+ * const request = { url: "https://example.com?scope:key1=value1&scope:key2=value2&key3=value3" };
23
+ * const params = getScopedParams(request, "scope");
24
+ * console.log(params.toString()); // Output: "key1=value1&key2=value2"
25
+ */
26
+ declare const getScopedParams: GetScopedParamsFunction;
27
+ export { getScopedParams };
28
+ //# sourceMappingURL=getScopedParams.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"getScopedParams.d.ts","sourceRoot":"","sources":["../../src/services/getScopedParams.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,cAAc,CAAC;AAE5D;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,QAAA,MAAM,eAAe,EAAE,uBAWtB,CAAC;AAEF,OAAO,EAAE,eAAe,EAAE,CAAC"}
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Extracts and returns scoped query parameters from a request URL.
3
+ *
4
+ * @param request - The incoming request object containing the URL.
5
+ * @param scope - An optional string representing the scope prefix for filtering query parameters.
6
+ * If no scope is provided, all query parameters are returned.
7
+ *
8
+ * @returns A `URLSearchParams` object containing the scoped query parameters.
9
+ * If a scope is provided, only parameters with keys starting with the scope
10
+ * (e.g., `scope:key`) are included, and the scope prefix is removed from the keys.
11
+ * If no scope is provided, all query parameters are returned as-is.
12
+ *
13
+ * @example
14
+ * // Example 1: No scope provided
15
+ * const request = { url: "https://example.com?key1=value1&key2=value2" };
16
+ * const params = getScopedParams(request);
17
+ * console.log(params.toString()); // Output: "key1=value1&key2=value2"
18
+ *
19
+ * @example
20
+ * // Example 2: Scope provided
21
+ * const request = { url: "https://example.com?scope:key1=value1&scope:key2=value2&key3=value3" };
22
+ * const params = getScopedParams(request, "scope");
23
+ * console.log(params.toString()); // Output: "key1=value1&key2=value2"
24
+ */
25
+ const getScopedParams = (request, scope = "") => {
26
+ const url = new URL(request.url);
27
+ if (scope === "")
28
+ return url.searchParams;
29
+ const scopedSearchParams = Array.from(url.searchParams.entries())
30
+ .filter(([key]) => key.startsWith(`${scope}:`))
31
+ .map(([key, value]) => [key.replace(`${scope}:`, ""), value]);
32
+ return new URLSearchParams(scopedSearchParams);
33
+ };
34
+ export { getScopedParams };
@@ -0,0 +1,52 @@
1
+ import type { SendFileToS3Function } from "@arkyn/types";
2
+ /**
3
+ * Handles file uploads to an AWS S3 bucket. This function processes a file
4
+ * from a multipart form request, validates and optionally compresses the file,
5
+ * and uploads it to S3. It supports image-specific operations such as resizing
6
+ * validation and quality reduction.
7
+ *
8
+ * @param request - The HTTP request containing the multipart form data.
9
+ * @param awsS3Config - Configuration object for AWS S3, including bucket name,
10
+ * region, and credentials.
11
+ * @param config - Optional configuration object for file handling.
12
+ *
13
+ * @param config.fileName - The name of the form field containing the file. Defaults to `"file"`.
14
+ * @param config.maxPartSize - The maximum size (in bytes) for each part of the file. Defaults to `5_000_000`.
15
+ * @param config.reduceImageQuality - The quality percentage for image compression. Defaults to `100`.
16
+ * @param config.validateImageSize - Whether to validate the image dimensions. Defaults to `false`.
17
+ * @param config.validateImageMessage - The error message template for invalid image dimensions.
18
+ * Defaults to `"Invalid dimensions {{width}}px x {{height}}px"`.
19
+ *
20
+ * @returns A promise that resolves to an object containing the uploaded file's URL
21
+ * or an error message if validation fails.
22
+ *
23
+ * @throws {BadRequest} If no file is uploaded.
24
+ *
25
+ * @example
26
+ * ```typescript
27
+ * const awsS3Config = {
28
+ * AWS_S3_BUCKET: "my-bucket",
29
+ * AWS_REGION: "us-east-1",
30
+ * AWS_ACCESS_KEY_ID: "my-access-key",
31
+ * AWS_SECRET_ACCESS_KEY: "my-secret-key",
32
+ * };
33
+ *
34
+ * const config = {
35
+ * fileName: "upload",
36
+ * maxPartSize: 10_000_000,
37
+ * reduceImageQuality: 80,
38
+ * validateImageSize: true,
39
+ * validateImageMessage: "Invalid dimensions {{width}}px x {{height}}px",
40
+ * };
41
+ *
42
+ * const response = await sendFileToS3(request, awsS3Config, config);
43
+ * if (response.error) {
44
+ * console.error(response.error);
45
+ * } else {
46
+ * console.log("File uploaded to:", response.url);
47
+ * }
48
+ * ```
49
+ */
50
+ declare const sendFileToS3: SendFileToS3Function;
51
+ export { sendFileToS3 };
52
+ //# sourceMappingURL=sendFileToS3.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sendFileToS3.d.ts","sourceRoot":"","sources":["../../src/services/sendFileToS3.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAa,oBAAoB,EAAE,MAAM,cAAc,CAAC;AA8CpE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AAEH,QAAA,MAAM,YAAY,EAAE,oBAkFnB,CAAC;AAEF,OAAO,EAAE,YAAY,EAAE,CAAC"}
@@ -0,0 +1,143 @@
1
+ import { generateId } from "@arkyn/shared";
2
+ import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
3
+ import { unstable_composeUploadHandlers as composeUploadHandlers, unstable_createFileUploadHandler as createFileUploadHandler, unstable_parseMultipartFormData as parseMultipartFormData, } from "@remix-run/node";
4
+ import fs from "fs";
5
+ import sharp from "sharp";
6
+ import { BadRequest } from "../http/badResponses/badRequest";
7
+ import { getScopedParams } from "./getScopedParams";
8
+ async function s3Upload(fileStream, contentType, awsConfig) {
9
+ const uploadParams = {
10
+ Bucket: awsConfig.AWS_S3_BUCKET,
11
+ Key: `uploads/${generateId("text", "v4")}`,
12
+ Body: fileStream,
13
+ ContentType: contentType,
14
+ };
15
+ const s3Client = new S3Client({
16
+ region: awsConfig.AWS_REGION,
17
+ credentials: {
18
+ accessKeyId: awsConfig.AWS_ACCESS_KEY_ID,
19
+ secretAccessKey: awsConfig.AWS_SECRET_ACCESS_KEY,
20
+ },
21
+ });
22
+ const command = new PutObjectCommand(uploadParams);
23
+ try {
24
+ await s3Client.send(command);
25
+ }
26
+ catch (error) {
27
+ console.error(error);
28
+ }
29
+ return {
30
+ location: `https://${awsConfig.AWS_S3_BUCKET}.s3.amazonaws.com/${uploadParams.Key}`,
31
+ };
32
+ }
33
+ /**
34
+ * Handles file uploads to an AWS S3 bucket. This function processes a file
35
+ * from a multipart form request, validates and optionally compresses the file,
36
+ * and uploads it to S3. It supports image-specific operations such as resizing
37
+ * validation and quality reduction.
38
+ *
39
+ * @param request - The HTTP request containing the multipart form data.
40
+ * @param awsS3Config - Configuration object for AWS S3, including bucket name,
41
+ * region, and credentials.
42
+ * @param config - Optional configuration object for file handling.
43
+ *
44
+ * @param config.fileName - The name of the form field containing the file. Defaults to `"file"`.
45
+ * @param config.maxPartSize - The maximum size (in bytes) for each part of the file. Defaults to `5_000_000`.
46
+ * @param config.reduceImageQuality - The quality percentage for image compression. Defaults to `100`.
47
+ * @param config.validateImageSize - Whether to validate the image dimensions. Defaults to `false`.
48
+ * @param config.validateImageMessage - The error message template for invalid image dimensions.
49
+ * Defaults to `"Invalid dimensions {{width}}px x {{height}}px"`.
50
+ *
51
+ * @returns A promise that resolves to an object containing the uploaded file's URL
52
+ * or an error message if validation fails.
53
+ *
54
+ * @throws {BadRequest} If no file is uploaded.
55
+ *
56
+ * @example
57
+ * ```typescript
58
+ * const awsS3Config = {
59
+ * AWS_S3_BUCKET: "my-bucket",
60
+ * AWS_REGION: "us-east-1",
61
+ * AWS_ACCESS_KEY_ID: "my-access-key",
62
+ * AWS_SECRET_ACCESS_KEY: "my-secret-key",
63
+ * };
64
+ *
65
+ * const config = {
66
+ * fileName: "upload",
67
+ * maxPartSize: 10_000_000,
68
+ * reduceImageQuality: 80,
69
+ * validateImageSize: true,
70
+ * validateImageMessage: "Invalid dimensions {{width}}px x {{height}}px",
71
+ * };
72
+ *
73
+ * const response = await sendFileToS3(request, awsS3Config, config);
74
+ * if (response.error) {
75
+ * console.error(response.error);
76
+ * } else {
77
+ * console.log("File uploaded to:", response.url);
78
+ * }
79
+ * ```
80
+ */
81
+ const sendFileToS3 = async (request, awsS3Config, config) => {
82
+ const fileName = config?.fileName || "file";
83
+ const maxPartSize = config?.maxPartSize || 5_000_000;
84
+ const reduceImageQuality = config?.reduceImageQuality || 100;
85
+ const validateImageSize = config?.validateImageSize || false;
86
+ const validateImageMessage = config?.validateImageMessage ||
87
+ "Invalid dimensions {{width}}px x {{height}}px";
88
+ const uploadHandler = composeUploadHandlers(createFileUploadHandler({
89
+ maxPartSize,
90
+ file: ({ filename }) => filename,
91
+ }));
92
+ const formData = await parseMultipartFormData(request, uploadHandler);
93
+ const file = formData.get(fileName);
94
+ if (!file)
95
+ throw new BadRequest("No file uploaded");
96
+ const filterParams = getScopedParams(request);
97
+ const width = filterParams.get("w");
98
+ const height = filterParams.get("h");
99
+ const reduceQuality = filterParams.get("reduceQuality");
100
+ const quality = reduceQuality ? +reduceQuality : reduceImageQuality;
101
+ const isImage = file.type.startsWith("image");
102
+ if (isImage && width && height && validateImageSize) {
103
+ const image = sharp(file.getFilePath());
104
+ const metadata = await image.metadata();
105
+ if (metadata.width && metadata.height) {
106
+ const widthDiff = Math.abs(metadata.width - +width);
107
+ const heightDiff = Math.abs(metadata.height - +height);
108
+ if (widthDiff > 10 || heightDiff > 10) {
109
+ return {
110
+ error: validateImageMessage
111
+ .replace("{{width}}", width)
112
+ .replace("{{height}}", height),
113
+ };
114
+ }
115
+ }
116
+ }
117
+ if (isImage) {
118
+ let image = sharp(file.getFilePath());
119
+ if (file.type === "image/jpeg") {
120
+ image = image.jpeg({ quality });
121
+ }
122
+ else if (file.type === "image/png") {
123
+ image = image.png({ quality });
124
+ }
125
+ else if (file.type === "image/webp") {
126
+ image = image.webp({ quality });
127
+ }
128
+ const compressedFilePath = file.getFilePath() + "_compressed";
129
+ await image.toFile(compressedFilePath);
130
+ file.getFilePath = () => compressedFilePath;
131
+ const streamFile = fs.createReadStream(file.getFilePath());
132
+ const apiResponse = await s3Upload(streamFile, file.type, awsS3Config);
133
+ fs.unlink(compressedFilePath, (err) => {
134
+ if (err)
135
+ console.error(`Delete image error: ${err}`);
136
+ });
137
+ return { url: apiResponse.location };
138
+ }
139
+ const streamFile = fs.createReadStream(file.getFilePath());
140
+ const apiResponse = await s3Upload(streamFile, file.type, awsS3Config);
141
+ return { url: apiResponse.location };
142
+ };
143
+ export { sendFileToS3 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arkyn/server",
3
- "version": "2.0.1-beta.0",
3
+ "version": "2.0.1-beta.2",
4
4
  "main": "./dist/bundle.js",
5
5
  "module": "./src/index.ts",
6
6
  "type": "module",
@@ -12,6 +12,7 @@
12
12
  },
13
13
  "dependencies": {
14
14
  "@arkyn/server": "*",
15
+ "@arkyn/shared": "*",
15
16
  "@aws-sdk/client-s3": "^3.775.0",
16
17
  "@remix-run/node": "^2.16.2",
17
18
  "sharp": "^0.33.5"
@@ -0,0 +1,36 @@
1
+ import { httpDebug } from "../httpDebug";
2
+
3
+ class BadGateway {
4
+ body: any;
5
+ cause?: any;
6
+ status: number = 502;
7
+ statusText: string;
8
+
9
+ constructor(message: string, cause?: any) {
10
+ this.body = { name: "BadGateway", message: message };
11
+ this.statusText = message;
12
+ this.cause = cause ? JSON.stringify(cause) : undefined;
13
+ httpDebug("BadGateway", this.body, this.cause);
14
+ }
15
+
16
+ toResponse() {
17
+ const responseInit: ResponseInit = {
18
+ headers: { "Content-Type": "application/json" },
19
+ status: this.status,
20
+ statusText: this.statusText,
21
+ };
22
+
23
+ return new Response(JSON.stringify(this.body), responseInit);
24
+ }
25
+
26
+ toJson() {
27
+ const responseInit: ResponseInit = {
28
+ status: this.status,
29
+ statusText: this.statusText,
30
+ };
31
+
32
+ return Response.json(this.body, responseInit);
33
+ }
34
+ }
35
+
36
+ export { BadGateway };
@@ -0,0 +1,36 @@
1
+ import { httpDebug } from "../httpDebug";
2
+
3
+ class BadRequest {
4
+ body: any;
5
+ cause?: any;
6
+ status: number = 400;
7
+ statusText: string;
8
+
9
+ constructor(message: string, cause?: any) {
10
+ this.body = { name: "BadRequest", message: message };
11
+ this.statusText = message;
12
+ this.cause = cause ? JSON.stringify(cause) : undefined;
13
+ httpDebug("BadRequest", this.body, this.cause);
14
+ }
15
+
16
+ toResponse() {
17
+ const responseInit: ResponseInit = {
18
+ headers: { "Content-Type": "application/json" },
19
+ status: this.status,
20
+ statusText: this.statusText,
21
+ };
22
+
23
+ return new Response(JSON.stringify(this.body), responseInit);
24
+ }
25
+
26
+ toJson() {
27
+ const responseInit: ResponseInit = {
28
+ status: this.status,
29
+ statusText: this.statusText,
30
+ };
31
+
32
+ return Response.json(this.body, responseInit);
33
+ }
34
+ }
35
+
36
+ export { BadRequest };