Skip to content

Commit

Permalink
feat: namespaced extensions
Browse files Browse the repository at this point in the history
  • Loading branch information
bartoszherba committed Jan 9, 2024
1 parent d2ce487 commit 8ee7114
Show file tree
Hide file tree
Showing 10 changed files with 361 additions and 26 deletions.
2 changes: 2 additions & 0 deletions docs/content/3.middleware/2.guides/3.extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export const integrations = {
...extensions, // don't forget to add existing extensions
{
name: 'extension-name',
isNamespaced: `[true/false:default]`
extendApiMethods: { /* ... */ },
extendApp: (app) => { /* ... */ },
hooks: () => { /* ... */ }
Expand Down Expand Up @@ -49,6 +50,7 @@ const extension = {
```

- `name` - a unique name for your extension,
- `isNamespaced` - defines if the extension should be namespaced. Namespaced extensions are registered under `/{integration-name}/{extension-name}` endpoint in opposition to non-namespaced extensions which are registered under `/{integration-name}` endpoint. Default value is `false`. Extensions without a namespace can potentially override existing endpoints, so it's recommended to use namespaced extensions whenever possible.
- `extendApiMethods` - overrides an integration's API Client to modify default behavior or add new API endpoints
- `extendApp` - gives you access to the Express.js app
- `hooks` - defines lifecycle hooks of API-client
Expand Down
5 changes: 3 additions & 2 deletions docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
"@microsoft/api-documenter": "^7.13.30",
"@microsoft/api-extractor": "^7.18.1",
"@types/node": "^18",
"nuxt": "^3.6.2"
"nuxt": "^3.6.2",
"nuxt-gtag": "^1.1.2"
},
"dependencies": {
"@stackblitz/sdk": "^1.9.0",
Expand All @@ -27,4 +28,4 @@
"resolutions": {
"@nuxt/content": "^2.8.0"
}
}
}
241 changes: 240 additions & 1 deletion docs/yarn.lock

Large diffs are not rendered by default.

43 changes: 43 additions & 0 deletions packages/middleware/__tests__/integration/createServer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,18 @@ describe("[Integration] Create server", () => {
},
},
},
{
name: "my-namespaced-extension",
isNamespaced: true,
extendApiMethods: {
myFunc(context) {
return context.api.error();
},
myFuncNamespaced(context) {
return context.api.success();
},
},
},
];
},
},
Expand Down Expand Up @@ -141,4 +153,35 @@ describe("[Integration] Create server", () => {
},
});
});

it("should make a call to a namespaced method", async () => {
expect.assertions(2);

const { status, text } = await request(app)
.post("/test_integration/my-namespaced-extension/myFuncNamespaced")
.send([]);

const response = JSON.parse(text);
// This is the result of the original "success" function from the integration
const apiMethodResult = await success();

expect(status).toEqual(200);
expect(response).toEqual(apiMethodResult);
});

it("namespaced extension should be not merged to the shared api", async () => {
expect.assertions(2);

const { status, text } = await request(app)
.post("/test_integration/myFunc")
.send([]);

const response = JSON.parse(text);
// This is the result of the original "success" function from the integration
const apiMethodResult = await success();

// If merged, the response would be { message: "error", error: true, status: 404 }
expect(status).toEqual(200);
expect(response).toEqual(apiMethodResult);
});
});
56 changes: 45 additions & 11 deletions packages/middleware/src/apiClientFactory/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const apiClientFactory = <
function createApiClient(
this: CallableContext<ALL_FUNCTIONS>,
config,
customApi = {}
customApi: Record<string, any> = {}
) {
const rawExtensions: ApiClientExtension<ALL_FUNCTIONS>[] =
this?.middleware?.extensions || [];
Expand All @@ -36,11 +36,6 @@ const apiClientFactory = <
hooks(this?.middleware?.req, this?.middleware?.res)
);

const extendedApis = rawExtensions.reduce(
(prev, { extendApiMethods }) => ({ ...prev, ...extendApiMethods }),
customApi
);

const _config = lifecycles
.filter((extension): extension is ExtensionHookWith<"beforeCreate"> =>
isFunction(extension?.beforeCreate)
Expand Down Expand Up @@ -102,6 +97,26 @@ const apiClientFactory = <

const isApiFactory = typeof apiOrApiFactory === "function";
const api = isApiFactory ? apiOrApiFactory(settings) : apiOrApiFactory;

const namespacedExtensions: Record<string, any> = {};
let sharedExtensions = customApi;

// If the extension is namespaced, we need to merge the extended api methods into the namespace
// Otherwise, we can just merge the extended api methods into the api
rawExtensions.forEach((extension) => {
if (extension.isNamespaced) {
namespacedExtensions[extension.name] = {
...(namespacedExtensions?.[extension.name] ?? {}),
...extension.extendApiMethods,
};
} else {
sharedExtensions = {
...sharedExtensions,
...extension.extendApiMethods,
};
}
});

/**
* FIXME IN-3487
*
Expand All @@ -113,18 +128,37 @@ const apiClientFactory = <
*
* `MiddlewareContext` requires `req` and `res` to be required, not optional, hence the error.
*/
// @ts-expect-error see above
const integrationApi = applyContextToApi(api, context, extensionHooks);
const extensionsApi = applyContextToApi(
extendedApis ?? {},
const integrationApi = applyContextToApi(
api,
// @ts-expect-error see above
context,
extensionHooks
);

const sharedExtensionsApi = applyContextToApi(
sharedExtensions,
// @ts-expect-error see above
context,
extensionHooks
);

const namespacedApi = {};

for (const [namespace, extension] of Object.entries(
namespacedExtensions
)) {
namespacedApi[namespace] = applyContextToApi(
extension,
// @ts-expect-error see above
context,
extensionHooks
);
}

const mergedApi = {
...integrationApi,
...extensionsApi,
...sharedExtensionsApi,
...namespacedApi,
} as ALL_FUNCTIONS;

// api methods haven't been invoked yet, so we still have time to add them to the context
Expand Down
4 changes: 2 additions & 2 deletions packages/middleware/src/createServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,14 @@ async function createServer<
consola.success("Integrations loaded!");

app.post(
"/:integrationName/:functionName",
"/:integrationName/:extensionName?/:functionName",
prepareApiFunction(integrations),
prepareErrorHandler(integrations),
prepareArguments,
callApiFunction
);
app.get(
"/:integrationName/:functionName",
"/:integrationName/:extensionName?/:functionName",
prepareApiFunction(integrations),
prepareErrorHandler(integrations),
prepareArguments,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/**
* Resolves the api function from the apiClient based on the extensionName and functionName parameters.
*
* @param apiClient
* @param reqParams
*/
export const getApiFunction = (
apiClient: any,
functionName: string,
extensionName?: string
) => {
return (
apiClient?.api?.[extensionName]?.[functionName] ??
apiClient?.api?.[functionName]
);
};
12 changes: 8 additions & 4 deletions packages/middleware/src/handlers/prepareApiFunction/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import type { RequestHandler } from "express";
import { IntegrationsLoaded, MiddlewareContext } from "../../types";
import { getApiFunction } from "./getApiFunction";

export function prepareApiFunction(
integrations: IntegrationsLoaded
): RequestHandler {
return (req, res, next) => {
const { integrationName, functionName } = req.params;
const { integrationName, extensionName, functionName } = req.params;

if (!integrations || !integrations[integrationName]) {
res.status(404);
Expand Down Expand Up @@ -74,9 +75,12 @@ export function prepareApiFunction(
...initConfig,
});

const apiFunction = apiClientInstance.api[functionName];

res.locals.apiFunction = apiFunction;
// Pick the function from the namespaced if it exists, otherwise pick it from the shared integration
res.locals.apiFunction = getApiFunction(
apiClientInstance,
functionName,
extensionName
);

next();
};
Expand Down
3 changes: 2 additions & 1 deletion packages/middleware/src/types/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ export interface ApiClientExtensionHooks<C = any> {

export interface ApiClientExtension<API = any, CONTEXT = any> {
name: string;
extendApiMethods?: ExtendApiMethod<API, CONTEXT>;
isNamespaced?: boolean;
extendApiMethods?: ExtendApiMethod<API, CONTEXT> | [key: string];
extendApp?: ({
app,
configuration,
Expand Down
5 changes: 0 additions & 5 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -7853,11 +7853,6 @@ is-unicode-supported@^0.1.0:
resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7"
integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==

is-unicode-supported@^1.3.0:
version "1.3.0"
resolved "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz#d824984b616c292a2e198207d4a609983842f714"
integrity sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==

is-utf8@^0.2.0, is-utf8@^0.2.1:
version "0.2.1"
resolved "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72"
Expand Down

0 comments on commit 8ee7114

Please sign in to comment.