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

Feat/6 fastify plugin #49

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions libs/sdk/fastify/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { FastifyRequest, FastifyReply, FastifyPlugin } from "fastify";
import fp from "fastify-plugin";
import PriciSdk from "@prici/sdk";
import { FieldStateResult } from "@prici/shared-remult";

export interface PriciPluginOptions {
sdk: PriciSdk;
fieldId?: string;
errorMessage?: string;
incrementAmount?: number;
getAccountId?: (req: FastifyRequest) => string | Promise<string>;
getFieldId?: (req: FastifyRequest) => string | Promise<string>;
getError?: (
req: FastifyRequest,
fieldStateResult?: FieldStateResult
) => string | Promise<string>;
getIncrementAmount?: (req: FastifyRequest) => number;
}

const priciPlugin: FastifyPlugin<PriciPluginOptions> = fp<PriciPluginOptions>(
async (fastify, options) => {
const opts = {
getAccountId: async (req: FastifyRequest) =>
(req as any).accountId ||
(req as any).account?.id ||
(req as any).user?.account ||
(req as any).user?.tenant,
getFieldId: async (req: FastifyRequest) =>
options.fieldId || (req as any).fieldId,
getError: async (req: FastifyRequest) =>
options.errorMessage || options.sdk.defaultErrorMessage,
getIncrementAmount: () => options.incrementAmount,
...options,
};

fastify.addHook("onRequest", async (request, reply) => {
const [accountId, fieldId] = await Promise.all([
opts.getAccountId(request),
opts.getFieldId(request),
]);

if (!(accountId && fieldId)) {
return;
}

const result = await opts.sdk.getFieldState(accountId, fieldId);

if (!result.isAllowed) {
const errorMessage = await opts.getError(request, result);
reply.code(402).send({
message: errorMessage,
});
return reply;
}

request.priciValues = {
accountId,
fieldId,
incrementAmount: opts.getIncrementAmount(request),
};
});

fastify.addHook("onResponse", async (request, reply) => {
if (reply.statusCode.toString().startsWith("2") && request.priciValues) {
const { accountId, fieldId, incrementAmount } = request.priciValues;

try {
await opts.sdk.incrementField(accountId, fieldId, incrementAmount);
} catch (error) {
fastify.log.error("Failed to increment field", error);
}
}
});
},
{
name: "fastify-prici",
}
);

declare module "fastify" {
interface FastifyRequest {
priciValues?: {
accountId: string;
fieldId: string;
incrementAmount?: number;
};
}
}

export default priciPlugin;
207 changes: 207 additions & 0 deletions libs/sdk/fastify/tests/fastifyPlugin.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
import test, { describe } from "node:test";
import assert from "node:assert";
import Fastify from "fastify";
import { FieldKind, initialize } from "../../index";
import priciPlugin from "../index";

describe("priciPlugin", async () => {
await describe("plugin", async () => {
test("should register without error", async () => {
const fastify = Fastify();
const sdk = initialize();

await fastify.register(priciPlugin, { sdk });
await fastify.ready();
});

test("should skip when accountId and fieldId are not available", async (context) => {
const fastify = Fastify();
const sdk = initialize();
const getFieldStateSpy = context.mock.fn(async () => ({
isAllowed: true,
state: {
targetLimit: 1,
kind: FieldKind.Number,
currentValue: 0,
},
}));
sdk.getFieldState = getFieldStateSpy;

await fastify.register(priciPlugin, { sdk });

fastify.get("/", async () => ({ test: "fastify plugin" }));

const response = await fastify.inject({
method: "GET",
url: "/",
});

assert.strictEqual(getFieldStateSpy.mock.callCount(), 0);
assert.strictEqual(response.statusCode, 200);
});

test("should use getAccountId and getFieldId from options", async (context) => {
const fastify = Fastify();
const sdk = initialize();
sdk.getFieldState = context.mock.fn(async () => ({
isAllowed: true,
state: {
targetLimit: 1,
kind: FieldKind.Number,
currentValue: 0,
},
}));

const getAccountId = context.mock.fn(() => "accountId");
const getFieldId = context.mock.fn(() => "fieldId");

await fastify.register(priciPlugin, {
sdk,
getAccountId,
getFieldId,
});

fastify.get("/", async () => ({ test: "fastify plugin" }));

await fastify.inject({
method: "GET",
url: "/",
});

assert.strictEqual(getAccountId.mock.callCount(), 1);
assert.strictEqual(getFieldId.mock.callCount(), 1);
});

test("should return 402 when not allowed", async (context) => {
const fastify = Fastify();
const sdk = initialize();
sdk.getFieldState = context.mock.fn(async () => ({
isAllowed: false,
state: {
targetLimit: 1,
kind: FieldKind.Number,
currentValue: 1,
},
}));

await fastify.register(priciPlugin, {
sdk,
fieldId: "1",
getAccountId: () => "1",
errorMessage: "Permissions error",
});

fastify.get("/", async () => ({ test: "fastify plugin" }));

const response = await fastify.inject({
method: "GET",
url: "/",
});

assert.strictEqual(response.statusCode, 402);
assert.deepStrictEqual(JSON.parse(response.payload), {
message: "Permissions error",
});
});

test("should not call incrementField when response status code is not 2xx", async (context) => {
const fastify = Fastify();
const sdk = initialize();
sdk.getFieldState = context.mock.fn(async () => ({
isAllowed: true,
state: {
targetLimit: 1,
kind: FieldKind.Number,
currentValue: 0,
},
}));

const incrementFieldSpy = context.mock.fn(async () => ({}));
sdk.incrementField = incrementFieldSpy;

await fastify.register(priciPlugin, {
sdk,
fieldId: "1",
getAccountId: () => "1",
});

fastify.get("/", async () => {
throw new Error("Test error");
});

await fastify.inject({
method: "GET",
url: "/",
});

assert.strictEqual(incrementFieldSpy.mock.callCount(), 0);
});

test("should call incrementField when response status code is 2xx", async (context) => {
const fastify = Fastify();
const sdk = initialize();
sdk.getFieldState = context.mock.fn(async () => ({
isAllowed: true,
state: {
targetLimit: 1,
kind: FieldKind.Number,
currentValue: 0,
},
}));

const incrementFieldSpy = context.mock.fn(async () => ({}));
sdk.incrementField = incrementFieldSpy;

await fastify.register(priciPlugin, {
sdk,
fieldId: "1",
getAccountId: () => "1",
});

fastify.get("/", async () => ({ test: "fastify plugin" }));

await fastify.inject({
method: "GET",
url: "/",
});

assert.strictEqual(incrementFieldSpy.mock.callCount(), 1);
});

test("should handle incrementField error", async (context) => {
const fastify = Fastify();
const sdk = initialize();
const logSpy = context.mock.fn();

fastify.log.error = logSpy;

sdk.getFieldState = async () => ({
isAllowed: true,
state: {
targetLimit: 1,
kind: FieldKind.Number,
currentValue: 0,
},
});

sdk.incrementField = async () => {
throw new Error("Increment error");
};

await fastify.register(priciPlugin, {
sdk,
fieldId: "1",
getAccountId: () => "1",
});

fastify.get("/", async () => ({ test: "fastify plugin" }));

await fastify.inject({
method: "GET",
url: "/",
});

assert.strictEqual(logSpy.mock.callCount(), 1);
});
});
});
8 changes: 8 additions & 0 deletions libs/sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,19 @@
"remult": "^0.25.7"
},
"peerDependencies": {
"fastify": "^4.26.0",
"fastify-plugin": "^5.0.1",
"@nestjs/common": "^9.0.0 || ^10.0.0"
},
"peerDependenciesMeta": {
"@nestjs/common": {
"optional": true
},
"fastify": {
"optional": true
},
"fastify-plugin": {
"optional": true
}
},
"publishConfig": {
Expand Down
Loading