Skip to content

Commit

Permalink
chore: CR fixers v2
Browse files Browse the repository at this point in the history
  • Loading branch information
bartoszherba committed Nov 22, 2024
1 parent c336dc9 commit 4f3dced
Show file tree
Hide file tree
Showing 16 changed files with 297 additions and 52 deletions.
18 changes: 18 additions & 0 deletions .changeset/hot-maps-unite.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,24 @@ export const upload = (context) => {
};
```

You can also provide a function that returns configuration options based on the request. This is useful when you need to enable/disable file uploads dynamically:

```ts
createServer(
{
// ... other config
},
{
fileUpload: (req) => ({
enabled: req.headers["x-enable-upload"] === "true",
// other options can also be configured dynamically
}),
}
);
```

This allows you to control file upload behavior on a per-request basis. For example, you could enable uploads only for authenticated requests or requests with specific headers.

Available options for the new `fileUpload` property in the `createServer` function are:

- `enabled`: (boolean) Enable/disable file upload functionality. Default: `true`. If you set it to `false`, the middleware will not initialize file upload functionality and the `req.files` object will be empty. That can be useful if you do not want to handle file uploads in your app and want to avoid unnecessary processing.
Expand Down
2 changes: 2 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,7 @@ module.exports = {
extends: "@vue-storefront/eslint-config-integrations",
rules: {
"class-methods-use-this": "off",
"@typescript-eslint/no-restricted-types": "off",
"@typescript-eslint/ban-types": "off",
},
};
107 changes: 107 additions & 0 deletions docs/content/3.middleware/2.guides/2.getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,114 @@ With our middleware file set up, we can use `ts-node-dev` to run our application
}
```

## Configuration Options

The `createServer` function accepts a second parameter for configuring various middleware options:

```ts [src/index.ts]
const app = await createServer(
{ integrations: config.integrations },
{
// CORS configuration
cors: {
origin: "http://localhost:3000",
credentials: true,
},
// Body parser configuration
bodyParser: {
limit: "50mb",
},
// Cookie parser configuration
cookieParser: {
secret: "secret",
},
// File upload configuration
fileUpload: {
enabled: true, // Enable/disable file upload functionality
maxFileSize: 5242880, // Maximum file size in bytes (default: 5MB)
maxFiles: 5, // Maximum number of files per upload
allowedMimeTypes: ["image/*", "application/pdf"], // Allowed file types
fieldNames: [], // Accepted form field names for file uploads
},
}
);
```

### CORS Configuration

Configure Cross-Origin Resource Sharing (CORS) settings. By default, `http://localhost:4000` is included in the allowed origins.

### Body Parser Configuration

Configure the body-parser middleware settings for handling request bodies.

### Cookie Parser Configuration

Configure the cookie-parser middleware settings for handling cookies.

### File Upload Configuration

Configure file upload handling for `multipart/form-data` requests. You can either provide static options or a function that returns configuration based on the request:

```ts
fileUpload: (req) => ({
enabled: req.headers["x-enable-upload"] === "true",
maxFileSize: 5242880,
maxFiles: 5,
allowedMimeTypes: ["image/*", "application/pdf"],
fieldNames: [],
})
```

Available options:
- `enabled`: Enable/disable file upload functionality (default: `true`)
- `maxFileSize`: Maximum file size in bytes (default: 5MB)
- `maxFiles`: Maximum number of files per upload (default: 5)
- `allowedMimeTypes`: Array of allowed MIME types (default: `["image/*", "application/pdf"]`)
- `fieldNames`: Array of accepted form field names for file uploads (default: `[]`)

When file uploads are enabled, uploaded files are available in the `req.files` object within your API endpoints:

```ts
export const upload = (context) => {
const { files } = context.req;
// Handle uploaded files
return Promise.resolve({
status: 200,
message: "ok",
});
};
```

You can also dynamically control file upload behavior on a per-request basis. This is particularly useful when you want to enable uploads only for specific scenarios, such as:
- Authenticated requests
- Requests with specific headers
- Requests from certain origins
- Different file size limits for different endpoints

Here's an example of dynamic configuration based on request headers:

```ts [src/index.ts]
const app = await createServer(
{ integrations: config.integrations },
{
fileUpload: (req) => ({
enabled: req.headers["x-enable-upload"] === "true",
maxFileSize: req.headers["x-upload-size"] ?
parseInt(req.headers["x-upload-size"]) :
5242880,
maxFiles: 5,
allowedMimeTypes: ["image/*", "application/pdf"],
fieldNames: [],
}),
}
);
```

In this example:
- File uploads are only enabled when the `x-enable-upload: true` header is present
- The maximum file size can be controlled via the `x-upload-size` header
- Other options remain static but could also be made dynamic based on your needs

## Adding Integrations

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ const testingExtension = {
]);
},
},
hooks(req, res, alokai) {
hooks(_req, _res, alokai) {
const logger = getLogger(alokai);
logger.info("hooks");
return {
Expand Down
48 changes: 48 additions & 0 deletions packages/middleware/__tests__/integration/upload.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,52 @@ describe("[Integration] Create server", () => {
expect(body.files).toBeUndefined();
expect(body.message).toBe("ok");
});

// add test for options provided from the request
it("should allow to provide options from the request", async () => {
app = await createServer(
{
integrations: {
test_integration: {
location: "./__tests__/integration/bootstrap/serverWithUpload",
configuration: {},
},
},
},
{
fileUpload: (req) => ({
enabled: req.headers["x-enable-upload"] === "true",
}),
}
);

// Test with uploads enabled via header
const uploadEnabled = await request(app)
.post("/test_integration/upload")
.set("Content-Type", "multipart/form-data")
.set("x-enable-upload", "true")
.attach("files", Buffer.from("test file content"), "test.jpg")
.expect(200);

expect(uploadEnabled.body.files).toEqual([
{
fieldname: "files",
originalname: "test.jpg",
encoding: "7bit",
mimetype: "image/jpeg",
buffer: expect.any(Object),
size: 17,
},
]);

// Test with uploads disabled via header
const uploadDisabled = await request(app)
.post("/test_integration/upload")
.set("Content-Type", "multipart/form-data")
.set("x-enable-upload", "false")
.attach("files", Buffer.from("test file content"), "test.jpg")
.expect(200);

expect(uploadDisabled.body.files).toBeUndefined();
});
});
25 changes: 14 additions & 11 deletions packages/middleware/__tests__/unit/services/fileUpload.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ import express from "express";
import multer from "multer";
import { createMulterMiddleware } from "../../../src/services/fileUpload";

// Mock multer
jest.mock("fileUpload", () => {
jest.mock("multer", () => {
const mockMulter = jest.fn(() => ({
fields: jest.fn(),
any: jest.fn(),
Expand All @@ -22,23 +21,27 @@ describe("configureFileUpload", () => {
});

it("should not configure upload when enabled is false", () => {
createMulterMiddleware(app, { enabled: false });
expect(app.use).not.toHaveBeenCalled();
const middleware = createMulterMiddleware({ enabled: false });
expect(middleware).toBeUndefined();
expect(multer).not.toHaveBeenCalled();
});

it("should configure upload with default options when enabled", () => {
createMulterMiddleware(app, { enabled: true });
const mockStorage = {};
(multer.memoryStorage as jest.Mock).mockReturnValue(mockStorage);

createMulterMiddleware({ enabled: true });

expect(multer.memoryStorage).toHaveBeenCalled();
expect(multer).toHaveBeenCalledWith(
expect.objectContaining({
storage: mockStorage,
limits: {
fileSize: 5 * 1024 * 1024,
files: 5,
},
})
);
expect(app.use).toHaveBeenCalled();
});

it("should configure upload with custom options", () => {
Expand All @@ -49,7 +52,7 @@ describe("configureFileUpload", () => {
allowedMimeTypes: ["image/jpeg"],
};

createMulterMiddleware(app, customOptions);
createMulterMiddleware(customOptions);

expect(multer).toHaveBeenCalledWith(
expect.objectContaining({
Expand All @@ -62,7 +65,7 @@ describe("configureFileUpload", () => {
});

it("should use fields() when fieldNames are provided", () => {
createMulterMiddleware(app, {
createMulterMiddleware({
enabled: true,
fieldNames: ["avatar", "document"],
});
Expand All @@ -78,7 +81,7 @@ describe("configureFileUpload", () => {
});

it("should use any() when no fieldNames are provided", () => {
createMulterMiddleware(app, { enabled: true });
createMulterMiddleware({ enabled: true });

const multerInstance = (multer as unknown as jest.Mock).mock.results[0]
.value;
Expand All @@ -89,7 +92,7 @@ describe("configureFileUpload", () => {
let fileFilter: (req: any, file: any, cb: any) => void;

beforeEach(() => {
createMulterMiddleware(app, {
createMulterMiddleware({
enabled: true,
allowedMimeTypes: ["image/*", "application/pdf"],
});
Expand All @@ -98,7 +101,7 @@ describe("configureFileUpload", () => {

it("should accept files when no mime types are specified", () => {
const cb = jest.fn();
createMulterMiddleware(app, { enabled: true, allowedMimeTypes: [] });
createMulterMiddleware({ enabled: true, allowedMimeTypes: [] });
fileFilter = (multer as unknown as jest.Mock).mock.calls[1][0].fileFilter;

fileFilter(null, { mimetype: "anything" }, cb);
Expand Down
6 changes: 3 additions & 3 deletions packages/middleware/src/createServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import type {
CreateServerOptions,
} from "./types";
import { LoggerManager, injectMetadata, lockLogger } from "./logger";

import { prepareFileUpload } from "./handlers/prepareFileUpload";
import {
prepareApiFunction,
prepareErrorHandler,
Expand All @@ -25,7 +25,6 @@ import {
} from "./handlers";
import { createTerminusOptions } from "./terminus";
import { prepareLogger } from "./handlers/prepareLogger";
import { createMulterMiddleware } from "./services/fileUpload";

const defaultCorsOptions: CreateServerOptions["cors"] = {
credentials: true,
Expand All @@ -51,7 +50,7 @@ async function createServer<

const app = express();

app.use(createMulterMiddleware(options.fileUpload));
// app.use(createMulterMiddleware(options.fileUpload));
app.use(express.json(options.bodyParser));
app.use(
options.cookieParser
Expand Down Expand Up @@ -90,6 +89,7 @@ async function createServer<
app.all(
"/:integrationName/:extensionName?/:functionName",
validateParams(integrations),
prepareFileUpload(options),
prepareLogger(loggerManager),
prepareApiFunction(integrations),
prepareErrorHandler(integrations),
Expand Down
4 changes: 2 additions & 2 deletions packages/middleware/src/errors/defaultErrorHandler.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Request } from "express";
import { getAgnosticStatusCode } from "../helpers";
import type { ResponseWithAlokaiLocals } from "../types";
import type { AlokaiResponse } from "../types";

type ClientSideError = {
message?: string;
Expand All @@ -16,7 +16,7 @@ type ClientSideError = {
export const defaultErrorHandler = (
error: ClientSideError,
req: Request,
res: ResponseWithAlokaiLocals
res: AlokaiResponse
) => {
const status = getAgnosticStatusCode(error);
res.status(status);
Expand Down
7 changes: 2 additions & 5 deletions packages/middleware/src/handlers/callApiFunction/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
import type { Request } from "express";
import { LogScope } from "../../types";
import { getLogger, injectMetadata } from "../../logger";
import type { ResponseWithAlokaiLocals } from "../../types";
import type { AlokaiResponse } from "../../types";

export async function callApiFunction(
req: Request,
res: ResponseWithAlokaiLocals
) {
export async function callApiFunction(req: Request, res: AlokaiResponse) {
const { apiFunction, args, errorHandler } = res.locals;

try {
Expand Down
4 changes: 2 additions & 2 deletions packages/middleware/src/handlers/prepareArguments/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import type { Request, NextFunction } from "express";
import type { ResponseWithAlokaiLocals } from "../../types";
import type { AlokaiResponse } from "../../types";

export function prepareArguments(
req: Request,
res: ResponseWithAlokaiLocals,
res: AlokaiResponse,
next: NextFunction
) {
const { method, query, body } = req;
Expand Down
Loading

0 comments on commit 4f3dced

Please sign in to comment.