Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New handlers: refactored, Web API and AWS Lambda support #380

Draft
wants to merge 61 commits into
base: main
Choose a base branch
from

Conversation

witoszekdev
Copy link
Member

@witoszekdev witoszekdev commented Jan 14, 2025

Description

This PR introduces new handlers for Fetch Web API and AWS Lambda and refactors existing code so that it can easily allow adding new platforms in the future.

After the change handlers code is splits them into 3 parts:

  1. Platform Adapter - class that is responsible of:
    • parsing received requests from specific platform format (e.g. Next.js request, Web API Request) into a common format
    • and preparing response from common format into platform specific format (e.g. Next.js response, Web API Response)
  2. Handler use cases - class that represents business logic specific to handler, has a single method that returns a result
    • e.g. register use case: readers all required headers from request, validates it, saves to apl, returns success or failure
  3. Handler factory method - function that creates a handler instance with all dependencies injected, each platform has separate folder, for example:
    • src/handlers/nextjs - For handlers that use Next.js platform adapter
    • src/handlers/fetch-api - For handlers that use Fetch API platform adapter

This way we'll be able to easily add new platforms and handlers without changing existing code.

Currently in this PR:

  • Next.js - existing implementation, shouldn't be changed
  • Web API - new implementation
  • AWS Lambda event - new implementation

Examples and docs

Usage examples:

https://github.com/witoszekdev/saleor-app-hono-aws-lambda-template
https://github.com/witoszekdev/saleor-app-hono-deno-template
https://github.com/witoszekdev/saleor-app-hono-cf-pages-template
saleor/saleor-app-template#267

Docs:

saleor/saleor-docs#1437

Breaking changes?

SaleorWebhook accepts onError and formatErrorResponse parameters callbacks, now they won't pass res object in Next.js runtime, since these methods weren't supposed to send responses

@witoszekdev witoszekdev requested review from a team and lkostrowski January 14, 2025 17:58
Copy link

changeset-bot bot commented Jan 14, 2025

🦋 Changeset detected

Latest commit: 534b64f

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@saleor/app-sdk Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@witoszekdev witoszekdev changed the base branch from main to add-edge-support January 14, 2025 17:59
@witoszekdev witoszekdev marked this pull request as draft January 14, 2025 17:59
Copy link
Member

@lkostrowski lkostrowski left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be easier for me to review if I already see docs with examples. In case of SDK public API is more important than the code

@@ -0,0 +1,5 @@
---
"@saleor/app-sdk": minor
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please make it major since it has breaking changes

"@saleor/app-sdk": minor
---

Added `handlers/fetch-api` which adds support for frameworks that use [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please remember to add full changeset and docs

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines +153 to +157
"./handlers/next-app-router": {
"types": "./handlers/fetch-api/index.d.ts",
"import": "./handlers/fetch-api/index.mjs",
"require": "./handlers/fetch-api/index.js"
},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

won't it have the same names as fetch api? It would be confusing, probably better to also add alias on export

so next-app-router file can include export { FetchApi as NextAppRouterWebhook } from '../fetch-api' or something like this

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would be easier if we see docs at this point

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is fine, since exported names do not reference Fetch API in any way, names are the same as in Next.js handlers except they are in a different folder. Our exports are:

  • createAppRegisterHandler
  • createManifestHandler
  • createProtectedHandler
  • SaleorSyncWebhook
  • SaleorAsyncWebhook

Copy link
Member

@lkostrowski lkostrowski left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

another partial review ;)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is "fetch middleware"? If this is a middleware using Fetch Request API, I don't think we should call it that way. Maybe just "middleware"?

@@ -0,0 +1,5 @@
export type SaleorRequest = Request & { context?: Record<string, any> };
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probably you should avoid any in favor of unknown

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please add tests to all middlewares


const debug = createFetchMiddlewareDebug("withRegisteredSaleorDomainHeader");

export const withRegisteredSaleorDomainHeader: FetchMiddleware = (handler) => async (request) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we are actually checking "saleorApiUrl", not domain, and it 1.0.0 we will drop domain checking

);
}

const authData = await saleorApp?.apl.get(saleorApiUrl);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe can we extract this? Middleware checks header but also checks if auth data exists. And doesnt do anything with it. It doesnt event attach it to request, so it's cached


const debug = createFetchMiddlewareDebug("withSaleorDomainPresent");

export const withSaleorDomainPresent: FetchMiddleware = (handler) => async (request) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can remove this, in v1 we can remove domain header, so no need to check this

Comment on lines +122 to +125
const checksToRun = [
this.adapterMiddleware.withMethod(["POST"]),
this.adapterMiddleware.withSaleorDomainPresent(),
];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Im not sure if middleware should work as "check" only. If you check against e.g. existence of AuthData, you can pass Request and middleware can enrich it with context - eg attach AuthData there

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think modifying Request object is a good idea. It would be like adding new properties to globals like Array or Window 😬 These objects are meant to be immutable.

Adding context is usually done by web frameworks that use Fetch API by creating new Context object and passing it everywhere, or by using AsyncLocalStorage. Example in Hono.

We could do this, but then we would need to have some context mechanism of our own. We could also use AsyncLocalStorage, but this is also problematic since runtimes like Cloudflare Workers, or Deno require enabling Node compatibility layer to do this :/

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Modifying request isn't anything new. It's not about modifying global, but instance. It's not about Array.prototype.foo by (new Array()).foo

Context you mentioned is a platform context.
What I mean is request context. It can be achieved several ways, eg with async_hooks, modifying request or adding another argument (like createWebhookHandler(req, res, ctx)). Or how TRPC middleware works.

You can also read eg https://expressjs.com/en/guide/using-middleware.html

What I want to achieve is to allow middleware to attach data for next middlewares or handlers and compose the rich request in the end.

For example, if your handler fetches AuthData, it can attach it to request (pass to next step), instead of fetching it again in next middleware/handler

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I think we are talking about different things here and maybe the name middleware in this place is wrong and confusing 😅

These "middlewares" do not have any data we would want to pass as context, since they don't do any asynchronous actions.

This specific register action has a concept of "context" in its config, where you can attach to different events, e.g.:

  /**
   * Run right after Saleor calls this endpoint
   */
  onRequestStart?(
    request: RequestType,
    context: {
      authToken?: string;
      saleorDomain?: string;
      saleorApiUrl?: string;
      respondWithError: CallbackErrorHandler;
    }
  ): Promise<void>;

Config shape is the same as previous Next.js actions in app-sdk, it just has different Request object type depending on platform

createProtectedHandler also has its own context:

export type ProtectedHandlerContext = {
  baseUrl: string;
  authData: AuthData;
  user: TokenUserPayload;
};

Webhook handlers also have context (no change from previous public API):

export type SaleorWebhookHandler<TPayload = unknown, TExtras = {}> = (
  req: Request,
  ctx: WebhookContext<TPayload> & TExtras
) => Response | Promise<Response>;

As for fetch-middleware, please see my comment: #380 (comment).

Comment on lines +166 to +167
// TODO: We should strictly require `saleorApiUrl` instead of
// relying on `saleor-domain` that is deprecated
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comment is stale

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why? the comment is still valid because we are not doing strict check on saleorApiUrl for compatibility reasons (I didn't change bussiness logic, rather I just copied our old code and adapted it to work with our new adapter abstraction).

We can make another breaking change and just drop the Saleor-Domain usage altogether imho

@witoszekdev
Copy link
Member Author

witoszekdev commented Jan 27, 2025

@lkostrowski Public API usage doesn't change, the change made to existing Next.js handlers are meant to be drop-in and non-breaking. New handlers for Web API and Lambda have the same signature as Next.js handlers, they only differ when accepting a request object.

The only breaking change is removing one parameter, which I think was a mistake in our API design, since we were passing res object from Next.js in place where we didn't want API consumers to return a custom response, but rather to return an object that is then mapped by app-sdk to a response. Calling res multiple times is not supported.

I've prepared examples here:

And docs here: saleor/saleor-docs#1437

@witoszekdev
Copy link
Member Author

witoszekdev commented Jan 27, 2025

@lkostrowski one more thing: fetch-middleware was a leftover from old PR that I resumed work on and I actually didn't intend on including these changes. Instead I've created PlatformAdapterMiddleware class that does most of the features of these middlewares.

I'm also not sure if we even want to add these middlewares to begin with? They are not used in other places in app-sdk. Previously we had them because they were passed to retes.

I also don't think these are used in any of our apps, since most of them use higher level abstractions by using /handlers. I think this is a better approach, since using low-level middlewares could expose users of app-sdk to malicious requests if not all validations are used in their application code. I think it's much easier to rely on a handler that does all the validation for you.

Copy link

codecov bot commented Jan 27, 2025

Codecov Report

Attention: Patch coverage is 44.09654% with 857 lines in your changes missing coverage. Please review.

Project coverage is 62.71%. Comparing base (cd902b5) to head (534b64f).

Files with missing lines Patch % Lines
src/handlers/shared/protected-action-validator.ts 8.10% 136 Missing ⚠️
src/handlers/actions/register-action-handler.ts 76.78% 75 Missing ⚠️
.../handlers/platforms/aws-lambda/platform-adapter.ts 2.00% 49 Missing ⚠️
...c/handlers/platforms/fetch-api/platform-adapter.ts 2.00% 49 Missing ⚠️
src/handlers/shared/generic-saleor-webhook.ts 68.00% 40 Missing ⚠️
...middleware/with-registered-saleor-domain-header.ts 0.00% 38 Missing and 1 partial ⚠️
src/handlers/shared/saleor-webhook-validator.ts 81.16% 29 Missing ⚠️
...s/platforms/aws-lambda/create-protected-handler.ts 3.57% 27 Missing ⚠️
...aws-lambda/saleor-webhooks/saleor-async-webhook.ts 3.70% 26 Missing ⚠️
.../fetch-api/saleor-webhooks/saleor-async-webhook.ts 3.70% 26 Missing ⚠️
... and 28 more
Additional details and impacted files
@@             Coverage Diff             @@
##             main     #380       +/-   ##
===========================================
- Coverage   73.37%   62.71%   -10.67%     
===========================================
  Files          79      116       +37     
  Lines        3061     4197     +1136     
  Branches      457      572      +115     
===========================================
+ Hits         2246     2632      +386     
- Misses        810     1544      +734     
- Partials        5       21       +16     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

Copy link
Contributor

Released snapshot build with @dev tag in npm with version: 0.0.0-pr-20250127113254.

Install it with:

pnpm add @saleor/[email protected]

@witoszekdev
Copy link
Member Author

This PR will be split into multiple smaller ones for easier review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants