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

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 (106) 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/successResponses/created.d.ts +11 -0
  29. package/dist/http/successResponses/created.d.ts.map +1 -0
  30. package/dist/http/successResponses/created.js +29 -0
  31. package/dist/http/successResponses/found.d.ts +11 -0
  32. package/dist/http/successResponses/found.d.ts.map +1 -0
  33. package/dist/http/successResponses/found.js +29 -0
  34. package/dist/http/successResponses/noContent.d.ts +10 -0
  35. package/dist/http/successResponses/noContent.d.ts.map +1 -0
  36. package/dist/http/successResponses/noContent.js +27 -0
  37. package/dist/http/successResponses/success.d.ts +11 -0
  38. package/dist/http/successResponses/success.d.ts.map +1 -0
  39. package/dist/http/successResponses/success.js +29 -0
  40. package/dist/http/successResponses/updated.d.ts +11 -0
  41. package/dist/http/successResponses/updated.d.ts.map +1 -0
  42. package/dist/http/successResponses/updated.js +29 -0
  43. package/dist/index.d.ts +23 -0
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.js +22 -0
  46. package/dist/services/decodeRequestBody.d.ts +17 -0
  47. package/dist/services/decodeRequestBody.d.ts.map +1 -0
  48. package/dist/services/decodeRequestBody.js +42 -0
  49. package/dist/services/errorHandler.d.ts +3 -0
  50. package/dist/services/errorHandler.d.ts.map +1 -0
  51. package/dist/services/errorHandler.js +51 -0
  52. package/dist/services/formParse.d.ts +42 -0
  53. package/dist/services/formParse.d.ts.map +1 -0
  54. package/dist/services/formParse.js +54 -0
  55. package/dist/services/getCaller.d.ts +6 -0
  56. package/dist/services/getCaller.d.ts.map +1 -0
  57. package/dist/services/getCaller.js +29 -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/httpDebug.d.ts +3 -0
  62. package/dist/services/httpDebug.d.ts.map +1 -0
  63. package/dist/services/httpDebug.js +18 -0
  64. package/dist/services/schemaValidator.d.ts +13 -0
  65. package/dist/services/schemaValidator.d.ts.map +1 -0
  66. package/dist/services/schemaValidator.js +51 -0
  67. package/dist/services/sendFileToS3.d.ts +52 -0
  68. package/dist/services/sendFileToS3.d.ts.map +1 -0
  69. package/dist/services/sendFileToS3.js +143 -0
  70. package/package.json +4 -2
  71. package/src/http/badResponses/badGateway.ts +36 -0
  72. package/src/http/badResponses/badRequest.ts +36 -0
  73. package/src/http/badResponses/conflict.ts +36 -0
  74. package/src/http/badResponses/forbidden.ts +36 -0
  75. package/src/http/badResponses/notFound.ts +36 -0
  76. package/src/http/badResponses/notImplemented.ts +36 -0
  77. package/src/http/badResponses/serverError.ts +36 -0
  78. package/src/http/badResponses/unauthorized.ts +36 -0
  79. package/src/http/badResponses/unprocessableEntity.ts +48 -0
  80. package/src/http/successResponses/created.ts +35 -0
  81. package/src/http/successResponses/found.ts +35 -0
  82. package/src/http/successResponses/noContent.ts +33 -0
  83. package/src/http/successResponses/success.ts +35 -0
  84. package/src/http/successResponses/updated.ts +35 -0
  85. package/src/index.ts +19 -18
  86. package/src/services/decodeRequestBody.ts +46 -0
  87. package/src/services/errorHandler.ts +55 -0
  88. package/src/services/formParse.ts +42 -1
  89. package/src/services/getCaller.ts +37 -0
  90. package/src/services/getScopedParams.ts +29 -2
  91. package/src/services/httpDebug.ts +23 -0
  92. package/src/services/schemaValidator.ts +66 -0
  93. package/src/services/sendFileToS3.ts +81 -56
  94. package/src/helpers/globalErrorHandler.ts +0 -64
  95. package/src/httpBadResponses/badRequest.ts +0 -29
  96. package/src/httpBadResponses/conflict.ts +0 -29
  97. package/src/httpBadResponses/forbidden.ts +0 -29
  98. package/src/httpBadResponses/notFound.ts +0 -29
  99. package/src/httpBadResponses/serverError.ts +0 -29
  100. package/src/httpBadResponses/unauthorized.ts +0 -29
  101. package/src/httpBadResponses/unprocessableEntity.ts +0 -43
  102. package/src/httpResponses/created.ts +0 -35
  103. package/src/httpResponses/noContent.ts +0 -33
  104. package/src/httpResponses/success.ts +0 -35
  105. package/src/httpResponses/updated.ts +0 -35
  106. package/src/services/extractJsonFromRequest.ts +0 -30
package/src/index.ts CHANGED
@@ -1,23 +1,24 @@
1
- // http bad responses
2
- export { BadRequestError } from "./httpBadResponses/badRequest";
3
- export { ConflictError } from "./httpBadResponses/conflict";
4
- export { ForbiddenError } from "./httpBadResponses/forbidden";
5
- export { NotFoundError } from "./httpBadResponses/notFound";
6
- export { ServerError } from "./httpBadResponses/serverError";
7
- export { UnauthorizedError } from "./httpBadResponses/unauthorized";
8
- export { UnprocessableEntityError } from "./httpBadResponses/unprocessableEntity";
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";
9
10
 
10
- // http responses
11
- export { Created } from "./httpResponses/created";
12
- export { NoContent } from "./httpResponses/noContent";
13
- export { Success } from "./httpResponses/success";
14
- export { Updated } from "./httpResponses/updated";
11
+ export { Created } from "./http/successResponses/created";
12
+ export { Found } from "./http/successResponses/found";
13
+ export { NoContent } from "./http/successResponses/noContent";
14
+ export { Success } from "./http/successResponses/success";
15
+ export { Updated } from "./http/successResponses/updated";
15
16
 
16
- // helpers
17
- export { globalErrorHandler } from "./helpers/globalErrorHandler";
18
-
19
- // services
20
- export { extractJsonFromRequest } from "./services/extractJsonFromRequest";
17
+ export { decodeRequestBody } from "./services/decodeRequestBody";
18
+ export { errorHandler } from "./services/errorHandler";
21
19
  export { formParse } from "./services/formParse";
20
+ export { getCaller } from "./services/getCaller";
22
21
  export { getScopedParams } from "./services/getScopedParams";
22
+ export { httpDebug } from "./services/httpDebug";
23
23
  export { sendFileToS3 } from "./services/sendFileToS3";
24
+ export { SchemaValidator } from "./services/schemaValidator";
@@ -0,0 +1,46 @@
1
+ import type { DecodeRequestBodyFunction } from "@arkyn/types";
2
+
3
+ /**
4
+ * Decodes the body of an incoming request into a JavaScript object.
5
+ *
6
+ * This function attempts to parse the request body in the following order:
7
+ * 1. Tries to parse the body as JSON.
8
+ * 2. If JSON parsing fails, attempts to parse the body as URL-encoded form data.
9
+ * 3. If both parsing attempts fail, logs the errors and returns an empty object.
10
+ *
11
+ * @param req - The incoming request object containing the body to decode.
12
+ * @returns A promise that resolves to the decoded data as a JavaScript object.
13
+ *
14
+ * @throws Logs errors to the console if the request body cannot be read or parsed.
15
+ */
16
+
17
+ const decodeRequestBody: DecodeRequestBodyFunction = async (req) => {
18
+ let data: any;
19
+
20
+ try {
21
+ const arrayBuffer = await req.arrayBuffer();
22
+ const text = new TextDecoder().decode(arrayBuffer);
23
+
24
+ try {
25
+ data = JSON.parse(text);
26
+ } catch (jsonError) {
27
+ try {
28
+ const formData = new URLSearchParams(text);
29
+ data = Object.fromEntries(formData.entries());
30
+ } catch (formDataError) {
31
+ console.error("Failed to extract data from request:", {
32
+ jsonError,
33
+ formDataError,
34
+ });
35
+ data = {};
36
+ }
37
+ }
38
+ } catch (error) {
39
+ console.error("Failed to read request body:", error);
40
+ data = {};
41
+ }
42
+
43
+ return data;
44
+ };
45
+
46
+ export { decodeRequestBody };
@@ -0,0 +1,55 @@
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
+
11
+ import { Created } from "../http/successResponses/created";
12
+ import { Found } from "../http/successResponses/found";
13
+ import { NoContent } from "../http/successResponses/noContent";
14
+ import { Success } from "../http/successResponses/success";
15
+ import { Updated } from "../http/successResponses/updated";
16
+
17
+ function errorHandler(error: any) {
18
+ switch (true) {
19
+ case error instanceof Response:
20
+ return error;
21
+ case error instanceof Found:
22
+ return error.toResponse();
23
+ case error instanceof Created:
24
+ return error.toResponse();
25
+ case error instanceof Updated:
26
+ return error.toResponse();
27
+ case error instanceof Success:
28
+ return error.toResponse();
29
+ case error instanceof NoContent:
30
+ return error.toResponse();
31
+ }
32
+
33
+ switch (true) {
34
+ case error instanceof BadGateway:
35
+ return error.toResponse();
36
+ case error instanceof BadRequest:
37
+ return error.toResponse();
38
+ case error instanceof Conflict:
39
+ return error.toResponse();
40
+ case error instanceof Forbidden:
41
+ return error.toResponse();
42
+ case error instanceof NotFound:
43
+ return error.toResponse();
44
+ case error instanceof NotImplemented:
45
+ return error.toResponse();
46
+ case error instanceof ServerError:
47
+ return error.toResponse();
48
+ case error instanceof Unauthorized:
49
+ return error.toResponse();
50
+ case error instanceof UnprocessableEntity:
51
+ return error.toResponse();
52
+ }
53
+ }
54
+
55
+ export { errorHandler };
@@ -1,6 +1,45 @@
1
1
  import type { FormParseProps, FormParseReturnType } from "@arkyn/types";
2
2
 
3
- export function formParse<T extends FormParseProps>([
3
+ /**
4
+ * Parses form data using a Zod schema and returns the result.
5
+ *
6
+ * @template T - A type that extends `FormParseProps`.
7
+ *
8
+ * @param {T} param0 - An array containing the form data and the Zod schema.
9
+ * @param {T[0]} param0[0] - The form data to be validated.
10
+ * @param {T[1]} param0[1] - The Zod schema used for validation.
11
+ *
12
+ * @returns {FormParseReturnType<T>} An object containing the validation result.
13
+ * - If validation fails, it includes:
14
+ * - `success`: A boolean indicating the validation status (false).
15
+ * - `fieldErrors`: An object mapping field names to their respective error messages.
16
+ * - `fields`: The original form data.
17
+ * - If validation succeeds, it includes:
18
+ * - `success`: A boolean indicating the validation status (true).
19
+ * - `data`: The parsed and validated data.
20
+ *
21
+ * @example
22
+ * ```typescript
23
+ * import { z } from "zod";
24
+ *
25
+ * const schema = z.object({
26
+ * name: z.string().min(1, "Name is required"),
27
+ * age: z.number().min(18, "Must be at least 18"),
28
+ * });
29
+ *
30
+ * const formData = { name: "", age: 17 };
31
+ *
32
+ * const result = formParse([formData, schema]);
33
+ *
34
+ * if (!result.success) {
35
+ * console.log(result.fieldErrors); // { name: "Name is required", age: "Must be at least 18" }
36
+ * } else {
37
+ * console.log(result.data); // Parsed data
38
+ * }
39
+ * ```
40
+ */
41
+
42
+ function formParse<T extends FormParseProps>([
4
43
  formData,
5
44
  schema,
6
45
  ]: T): FormParseReturnType<T> {
@@ -24,3 +63,5 @@ export function formParse<T extends FormParseProps>([
24
63
  return { success: zodResponse.success, data: zodResponse.data };
25
64
  }
26
65
  }
66
+
67
+ export { formParse };
@@ -0,0 +1,37 @@
1
+ import path from "path";
2
+
3
+ function getCaller() {
4
+ // Diretório raiz do projeto
5
+ const projectRoot = process.cwd();
6
+
7
+ // Captura a stack trace
8
+ const error = new Error();
9
+ const stackLines = error.stack?.split("\n").map((line) => line.trim()) || [];
10
+
11
+ let callerInfo = "Unknown caller";
12
+ let functionName = "Unknown function";
13
+
14
+ for (const line of stackLines) {
15
+ // Ignora chamadas dentro de @arkyn/server
16
+ if (!line.includes("@arkyn/server")) {
17
+ // Captura diferentes formatos de stack trace
18
+ const match = line.match(/at (.+?) \((.+?)\)/) || line.match(/at (.+)/);
19
+ if (match) {
20
+ functionName = match[1].split(" ")[0]; // Nome da função
21
+ let fullPath = match[2] || match[1]; // Caminho absoluto do arquivo
22
+
23
+ // Transforma caminho absoluto em relativo ao projeto
24
+ if (fullPath.startsWith(projectRoot)) {
25
+ fullPath = path.relative(projectRoot, fullPath);
26
+ }
27
+
28
+ callerInfo = fullPath;
29
+ break;
30
+ }
31
+ }
32
+ }
33
+
34
+ return { functionName, callerInfo };
35
+ }
36
+
37
+ export { getCaller };
@@ -1,4 +1,31 @@
1
- function getScopedParams(request: Request, scope: string = "") {
1
+ import type { GetScopedParamsFunction } from "@arkyn/types";
2
+
3
+ /**
4
+ * Extracts and returns scoped query parameters from a request URL.
5
+ *
6
+ * @param request - The incoming request object containing the URL.
7
+ * @param scope - An optional string representing the scope prefix for filtering query parameters.
8
+ * If no scope is provided, all query parameters are returned.
9
+ *
10
+ * @returns A `URLSearchParams` object containing the scoped query parameters.
11
+ * If a scope is provided, only parameters with keys starting with the scope
12
+ * (e.g., `scope:key`) are included, and the scope prefix is removed from the keys.
13
+ * If no scope is provided, all query parameters are returned as-is.
14
+ *
15
+ * @example
16
+ * // Example 1: No scope provided
17
+ * const request = { url: "https://example.com?key1=value1&key2=value2" };
18
+ * const params = getScopedParams(request);
19
+ * console.log(params.toString()); // Output: "key1=value1&key2=value2"
20
+ *
21
+ * @example
22
+ * // Example 2: Scope provided
23
+ * const request = { url: "https://example.com?scope:key1=value1&scope:key2=value2&key3=value3" };
24
+ * const params = getScopedParams(request, "scope");
25
+ * console.log(params.toString()); // Output: "key1=value1&key2=value2"
26
+ */
27
+
28
+ const getScopedParams: GetScopedParamsFunction = (request, scope = "") => {
2
29
  const url = new URL(request.url);
3
30
  if (scope === "") return url.searchParams;
4
31
 
@@ -9,6 +36,6 @@ function getScopedParams(request: Request, scope: string = "") {
9
36
  .map(([key, value]) => [key.replace(`${scope}:`, ""), value]);
10
37
 
11
38
  return new URLSearchParams(scopedSearchParams);
12
- }
39
+ };
13
40
 
14
41
  export { getScopedParams };
@@ -0,0 +1,23 @@
1
+ import { getCaller } from "../services/getCaller";
2
+
3
+ function httpDebug(name: string, body: any, cause?: any) {
4
+ const isDebugMode =
5
+ process.env.NODE_ENV === "development" ||
6
+ process.env?.SHOW_ERRORS_IN_CONSOLE === "true";
7
+
8
+ if (isDebugMode) {
9
+ const reset = "\x1b[0m";
10
+ const cyan = "\x1b[36m";
11
+
12
+ const debugName = `${cyan}[ARKYN-DEBUG]${reset}`;
13
+ const { callerInfo, functionName } = getCaller();
14
+
15
+ console.log(`${debugName} ${name} initialized`);
16
+ console.log(`${debugName} Caller Function: ${functionName}`);
17
+ console.log(`${debugName} Caller Location: ${callerInfo}`);
18
+ console.log(`${debugName} Body:`, body);
19
+ if (cause) console.log(`[ARKYN-DEBUG] Cause:`, cause);
20
+ }
21
+ }
22
+
23
+ export { httpDebug };
@@ -0,0 +1,66 @@
1
+ import { Schema, z } from "zod";
2
+
3
+ import { ServerError } from "../http/badResponses/serverError";
4
+ import { UnprocessableEntity } from "../http/badResponses/unprocessableEntity";
5
+ import { formParse } from "./formParse";
6
+ import { getCaller } from "./getCaller";
7
+ import { httpDebug } from "./httpDebug";
8
+
9
+ function formatErrorMessage(error: z.ZodError) {
10
+ const title = "Error validating:";
11
+ const lines = error.issues.map(
12
+ ({ path, message }) => `-> ${path.join(".")}: ${message}`
13
+ );
14
+
15
+ return [title, ...lines].join("\n");
16
+ }
17
+
18
+ class SchemaValidator<T extends Schema> {
19
+ functionName: string;
20
+ callerInfo: string;
21
+
22
+ constructor(readonly schema: T) {
23
+ const { callerInfo, functionName } = getCaller();
24
+ this.callerInfo = callerInfo;
25
+ this.functionName = functionName;
26
+ }
27
+
28
+ isValid(data: any): boolean {
29
+ return this.schema.safeParse(data).success;
30
+ }
31
+
32
+ safeValidate(data: any): z.SafeParseReturnType<z.infer<T>, z.infer<T>> {
33
+ return this.schema.safeParse(data);
34
+ }
35
+
36
+ validate(data: any): z.infer<T> {
37
+ try {
38
+ return this.schema.parse(data);
39
+ } catch (error: any) {
40
+ throw new ServerError(formatErrorMessage(error));
41
+ }
42
+ }
43
+
44
+ formValidate(data: any, message?: string): z.infer<T> {
45
+ const formParsed = formParse([data, this.schema]);
46
+
47
+ if (!formParsed.success) {
48
+ httpDebug("UnprocessableEntity", formParsed);
49
+ const firstErrorKey = Object.keys(formParsed.fieldErrors)[0];
50
+
51
+ throw new UnprocessableEntity(
52
+ {
53
+ fields: formParsed.fields,
54
+ fieldErrors: formParsed.fieldErrors,
55
+ data: { scrollTo: firstErrorKey },
56
+ message,
57
+ },
58
+ false
59
+ );
60
+ }
61
+
62
+ return formParsed.data;
63
+ }
64
+ }
65
+
66
+ export { SchemaValidator };
@@ -1,62 +1,34 @@
1
+ import { generateId } from "@arkyn/shared";
2
+ import type { AwsConfig, SendFileToS3Function } from "@arkyn/types";
1
3
  import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
2
4
  import {
3
5
  unstable_composeUploadHandlers as composeUploadHandlers,
4
6
  unstable_createFileUploadHandler as createFileUploadHandler,
5
7
  unstable_parseMultipartFormData as parseMultipartFormData,
6
- type ActionFunctionArgs,
7
8
  type NodeOnDiskFile,
8
9
  } from "@remix-run/node";
9
- import { randomUUID } from "crypto";
10
10
  import fs from "fs";
11
11
  import sharp from "sharp";
12
- import { BadRequestError } from "../httpBadResponses/badRequest";
12
+ import { BadRequest } from "../http/badResponses/badRequest";
13
13
  import { getScopedParams } from "./getScopedParams";
14
14
 
15
- type AWSConfig = {
16
- AWS_REGION: string;
17
- AWS_ACCESS_KEY_ID: string;
18
- AWS_SECRET_ACCESS_KEY: string;
19
- AWS_S3_BUCKET: string;
20
- };
21
-
22
- type Config = {
23
- maxPartSize?: number;
24
- fileName?: string;
25
- };
26
-
27
- async function s3_upload(
15
+ async function s3Upload(
28
16
  fileStream: fs.ReadStream,
29
17
  contentType: string,
30
- awsS3Config: AWSConfig
18
+ awsConfig: AwsConfig
31
19
  ) {
32
- const {
33
- AWS_ACCESS_KEY_ID,
34
- AWS_REGION,
35
- AWS_S3_BUCKET,
36
- AWS_SECRET_ACCESS_KEY,
37
- } = awsS3Config;
38
-
39
- const filePath = fileStream.path;
40
- let fileName = "";
41
-
42
- if (typeof filePath === "string") {
43
- fileName = filePath.split("/").pop() || "";
44
- } else {
45
- fileName = randomUUID();
46
- }
47
-
48
20
  const uploadParams = {
49
- Bucket: AWS_S3_BUCKET,
50
- Key: `uploads/${Date.now()}-${fileName.split(" ").join("-")}`,
21
+ Bucket: awsConfig.AWS_S3_BUCKET,
22
+ Key: `uploads/${generateId("text", "v4")}`,
51
23
  Body: fileStream,
52
24
  ContentType: contentType,
53
25
  };
54
26
 
55
27
  const s3Client = new S3Client({
56
- region: AWS_REGION,
28
+ region: awsConfig.AWS_REGION,
57
29
  credentials: {
58
- accessKeyId: AWS_ACCESS_KEY_ID,
59
- secretAccessKey: AWS_SECRET_ACCESS_KEY,
30
+ accessKeyId: awsConfig.AWS_ACCESS_KEY_ID,
31
+ secretAccessKey: awsConfig.AWS_SECRET_ACCESS_KEY,
60
32
  },
61
33
  });
62
34
 
@@ -69,20 +41,71 @@ async function s3_upload(
69
41
  }
70
42
 
71
43
  return {
72
- location: `https://${AWS_S3_BUCKET}.s3.amazonaws.com/${uploadParams.Key}`,
44
+ location: `https://${awsConfig.AWS_S3_BUCKET}.s3.amazonaws.com/${uploadParams.Key}`,
73
45
  };
74
46
  }
75
47
 
76
- async function sendFileToS3(
77
- props: ActionFunctionArgs,
78
- awsS3Config: AWSConfig,
79
- config: Config = {
80
- maxPartSize: 5_000_000,
81
- fileName: "file",
82
- }
83
- ) {
84
- const { fileName = "file", maxPartSize = 5_000_000 } = config;
85
- const { request } = props;
48
+ /**
49
+ * Handles file uploads to an AWS S3 bucket. This function processes a file
50
+ * from a multipart form request, validates and optionally compresses the file,
51
+ * and uploads it to S3. It supports image-specific operations such as resizing
52
+ * validation and quality reduction.
53
+ *
54
+ * @param request - The HTTP request containing the multipart form data.
55
+ * @param awsS3Config - Configuration object for AWS S3, including bucket name,
56
+ * region, and credentials.
57
+ * @param config - Optional configuration object for file handling.
58
+ *
59
+ * @param config.fileName - The name of the form field containing the file. Defaults to `"file"`.
60
+ * @param config.maxPartSize - The maximum size (in bytes) for each part of the file. Defaults to `5_000_000`.
61
+ * @param config.reduceImageQuality - The quality percentage for image compression. Defaults to `100`.
62
+ * @param config.validateImageSize - Whether to validate the image dimensions. Defaults to `false`.
63
+ * @param config.validateImageMessage - The error message template for invalid image dimensions.
64
+ * Defaults to `"Invalid dimensions {{width}}px x {{height}}px"`.
65
+ *
66
+ * @returns A promise that resolves to an object containing the uploaded file's URL
67
+ * or an error message if validation fails.
68
+ *
69
+ * @throws {BadRequest} If no file is uploaded.
70
+ *
71
+ * @example
72
+ * ```typescript
73
+ * const awsS3Config = {
74
+ * AWS_S3_BUCKET: "my-bucket",
75
+ * AWS_REGION: "us-east-1",
76
+ * AWS_ACCESS_KEY_ID: "my-access-key",
77
+ * AWS_SECRET_ACCESS_KEY: "my-secret-key",
78
+ * };
79
+ *
80
+ * const config = {
81
+ * fileName: "upload",
82
+ * maxPartSize: 10_000_000,
83
+ * reduceImageQuality: 80,
84
+ * validateImageSize: true,
85
+ * validateImageMessage: "Invalid dimensions {{width}}px x {{height}}px",
86
+ * };
87
+ *
88
+ * const response = await sendFileToS3(request, awsS3Config, config);
89
+ * if (response.error) {
90
+ * console.error(response.error);
91
+ * } else {
92
+ * console.log("File uploaded to:", response.url);
93
+ * }
94
+ * ```
95
+ */
96
+
97
+ const sendFileToS3: SendFileToS3Function = async (
98
+ request,
99
+ awsS3Config,
100
+ config
101
+ ) => {
102
+ const fileName = config?.fileName || "file";
103
+ const maxPartSize = config?.maxPartSize || 5_000_000;
104
+ const reduceImageQuality = config?.reduceImageQuality || 100;
105
+ const validateImageSize = config?.validateImageSize || false;
106
+ const validateImageMessage =
107
+ config?.validateImageMessage ||
108
+ "Invalid dimensions {{width}}px x {{height}}px";
86
109
 
87
110
  const uploadHandler = composeUploadHandlers(
88
111
  createFileUploadHandler({
@@ -94,18 +117,18 @@ async function sendFileToS3(
94
117
  const formData = await parseMultipartFormData(request, uploadHandler);
95
118
  const file = formData.get(fileName) as unknown as NodeOnDiskFile;
96
119
 
97
- if (!file) throw new BadRequestError("No file uploaded");
120
+ if (!file) throw new BadRequest("No file uploaded");
98
121
 
99
122
  const filterParams = getScopedParams(request);
100
123
  const width = filterParams.get("w");
101
124
  const height = filterParams.get("h");
102
125
 
103
126
  const reduceQuality = filterParams.get("reduceQuality");
104
- const quality = reduceQuality ? +reduceQuality : 100;
127
+ const quality = reduceQuality ? +reduceQuality : reduceImageQuality;
105
128
 
106
129
  const isImage = file.type.startsWith("image");
107
130
 
108
- if (isImage && width && height) {
131
+ if (isImage && width && height && validateImageSize) {
109
132
  const image = sharp(file.getFilePath());
110
133
  const metadata = await image.metadata();
111
134
 
@@ -115,7 +138,9 @@ async function sendFileToS3(
115
138
 
116
139
  if (widthDiff > 10 || heightDiff > 10) {
117
140
  return {
118
- error: `Formato inválido ${metadata.width}px x ${metadata.height}px`,
141
+ error: validateImageMessage
142
+ .replace("{{width}}", width)
143
+ .replace("{{height}}", height),
119
144
  };
120
145
  }
121
146
  }
@@ -138,7 +163,7 @@ async function sendFileToS3(
138
163
  file.getFilePath = () => compressedFilePath;
139
164
 
140
165
  const streamFile = fs.createReadStream(file.getFilePath());
141
- const apiResponse = await s3_upload(streamFile, file.type, awsS3Config);
166
+ const apiResponse = await s3Upload(streamFile, file.type, awsS3Config);
142
167
 
143
168
  fs.unlink(compressedFilePath, (err) => {
144
169
  if (err) console.error(`Delete image error: ${err}`);
@@ -148,9 +173,9 @@ async function sendFileToS3(
148
173
  }
149
174
 
150
175
  const streamFile = fs.createReadStream(file.getFilePath());
151
- const apiResponse = await s3_upload(streamFile, file.type, awsS3Config);
176
+ const apiResponse = await s3Upload(streamFile, file.type, awsS3Config);
152
177
 
153
178
  return { url: apiResponse.location };
154
- }
179
+ };
155
180
 
156
181
  export { sendFileToS3 };
@@ -1,64 +0,0 @@
1
- import { badRequest, BadRequestError } from "../httpBadResponses/badRequest";
2
- import { conflict, ConflictError } from "../httpBadResponses/conflict";
3
- import { forbidden, ForbiddenError } from "../httpBadResponses/forbidden";
4
- import { notFound, NotFoundError } from "../httpBadResponses/notFound";
5
- import { serverError } from "../httpBadResponses/serverError";
6
- import {
7
- unauthorized,
8
- UnauthorizedError,
9
- } from "../httpBadResponses/unauthorized";
10
- import {
11
- unprocessableEntity,
12
- UnprocessableEntityError,
13
- } from "../httpBadResponses/unprocessableEntity";
14
- import { created, Created } from "../httpResponses/created";
15
- import { noContent, NoContent } from "../httpResponses/noContent";
16
- import { success, Success } from "../httpResponses/success";
17
- import { updated, Updated } from "../httpResponses/updated";
18
-
19
- const globalErrorHandler = (error: any) => {
20
- // If the error is not an instance of Error, return the response
21
- switch (true) {
22
- case error instanceof Response:
23
- return error;
24
- case error instanceof Created:
25
- return created(error.body, error.init);
26
- case error instanceof Updated:
27
- return updated(error.body, error.init);
28
- case error instanceof Success:
29
- return success(error.body, error.init);
30
- case error instanceof NoContent:
31
- return noContent(error.init);
32
- }
33
-
34
- const showConsoleError =
35
- process.env.NODE_ENV === "development" ||
36
- process.env?.SHOW_ERRORS_IN_CONSOLE === "true";
37
-
38
- // If showConsoleError is true, log the error to the console
39
- if (showConsoleError) console.error(error);
40
-
41
- // If the error is an instance of BadRequestError, ForbiddenError, ConflictError, UnauthorizedError, NotFoundError, or UnprocessableEntityError, return the error
42
- switch (true) {
43
- case error instanceof BadRequestError:
44
- return badRequest(error);
45
- case error instanceof ForbiddenError:
46
- return forbidden(error);
47
- case error instanceof ConflictError:
48
- return conflict(error);
49
- case error instanceof UnauthorizedError:
50
- return unauthorized(error);
51
- case error instanceof NotFoundError:
52
- return notFound(error);
53
- case error instanceof UnprocessableEntityError:
54
- return unprocessableEntity(error);
55
- default:
56
- return serverError({
57
- message: error?.message || "Server error | Message not found",
58
- name: "Server Error",
59
- cause: error.cause || "Unknown",
60
- });
61
- }
62
- };
63
-
64
- export { globalErrorHandler };
@@ -1,29 +0,0 @@
1
- function badRequest(error: BadRequestError) {
2
- return new Response(
3
- JSON.stringify({
4
- status: 400,
5
- success: false,
6
- name: error.name,
7
- message: error.message,
8
- }),
9
- {
10
- status: 400,
11
- statusText: "Bad Request",
12
- headers: { "Content-Type": "application/json" },
13
- }
14
- );
15
- }
16
-
17
- class BadRequestError {
18
- name: string;
19
- message: string;
20
- cause?: any;
21
-
22
- constructor(message: string, cause?: any) {
23
- this.name = "BadRequestError";
24
- this.message = message;
25
- this.cause = cause ? JSON.stringify(cause) : undefined;
26
- }
27
- }
28
-
29
- export { badRequest, BadRequestError };