diff --git a/lib/near-bindgen.d.ts b/lib/near-bindgen.d.ts index fedc06b55..67de788c8 100644 --- a/lib/near-bindgen.d.ts +++ b/lib/near-bindgen.d.ts @@ -25,6 +25,23 @@ export declare function call(options: { privateFunction?: boolean; payableFunction?: boolean; }): DecoratorFunction; +/** + * The interface that a middleware has to implement in order to be used as a middleware function/class. + */ +interface Middleware> { + /** + * The method that gets called with the same arguments that are passed to the function it is wrapping. + * + * @param args - Arguments that will be passed to the function - immutable. + */ + call(...args: Arguments): void; +} +/** + * Tells the SDK to apply an array of passed in middleware to the function execution. + * + * @param middlewares - The middlewares to be executed. + */ +export declare function middleware>(...middlewares: Middleware[]): DecoratorFunction; /** * Extends this class with the methods needed to make the contract storable/serializable and readable/deserializable to and from the blockchain. * Also tells the SDK to capture and expose all view, call and initialize functions. diff --git a/lib/near-bindgen.js b/lib/near-bindgen.js index 7d8d2cbac..91fa8bf35 100644 --- a/lib/near-bindgen.js +++ b/lib/near-bindgen.js @@ -40,6 +40,29 @@ export function call({ privateFunction = false, payableFunction = false, }) { }; }; } +/** + * Tells the SDK to apply an array of passed in middleware to the function execution. + * + * @param middlewares - The middlewares to be executed. + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function middleware(...middlewares) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return function (_target, _key, descriptor) { + const originalMethod = descriptor.value; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + descriptor.value = function (...args) { + try { + middlewares.forEach((middleware) => middleware.call(...args)); + } + catch (error) { + throw new Error(error); + } + return originalMethod.apply(this, args); + }; + }; +} export function NearBindgen({ requireInit = false, serializer = serialize, deserializer = deserialize, }) { // eslint-disable-next-line @typescript-eslint/no-explicit-any return (target) => { diff --git a/src/near-bindgen.ts b/src/near-bindgen.ts index fab5df0e4..44196db61 100644 --- a/src/near-bindgen.ts +++ b/src/near-bindgen.ts @@ -89,6 +89,50 @@ export function call({ }; } +/** + * The interface that a middleware has to implement in order to be used as a middleware function/class. + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +interface Middleware> { + /** + * The method that gets called with the same arguments that are passed to the function it is wrapping. + * + * @param args - Arguments that will be passed to the function - immutable. + */ + call(...args: Arguments): void; +} + +/** + * Tells the SDK to apply an array of passed in middleware to the function execution. + * + * @param middlewares - The middlewares to be executed. + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function middleware>( + ...middlewares: Middleware[] +): DecoratorFunction { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return function any>( + _target: object, + _key: string | symbol, + descriptor: TypedPropertyDescriptor + ): void { + const originalMethod = descriptor.value; + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + descriptor.value = function (...args: Arguments): ReturnType { + try { + middlewares.forEach((middleware) => middleware.call(...args)); + } catch (error) { + throw new Error(error); + } + + return originalMethod.apply(this, args); + }; + }; +} + /** * Extends this class with the methods needed to make the contract storable/serializable and readable/deserializable to and from the blockchain. * Also tells the SDK to capture and expose all view, call and initialize functions. diff --git a/tests/__tests__/test-middlewares.ava.js b/tests/__tests__/test-middlewares.ava.js new file mode 100644 index 000000000..47223a152 --- /dev/null +++ b/tests/__tests__/test-middlewares.ava.js @@ -0,0 +1,84 @@ +import { Worker } from "near-workspaces"; +import test from "ava"; + +test.beforeEach(async (t) => { + // Init the worker and start a Sandbox server + const worker = await Worker.init(); + + // Prepare sandbox for tests, create accounts, deploy contracts, etx. + const root = worker.rootAccount; + + // Deploy the contract. + const middlewares = await root.devDeploy("build/middlewares.wasm"); + + // Create the init args. + const args = JSON.stringify({ randomData: "anything" }); + // Capture the result of the init function call. + const result = await middlewares.callRaw(middlewares, "init", args); + + // Extract the logs. + const { logs } = result.result.receipts_outcome[0].outcome; + // Create the expected logs. + const expectedLogs = [`Log from middleware: ${args}`]; + + // Check for correct logs. + t.deepEqual(logs, expectedLogs); + + // Create test users + const ali = await root.createSubAccount("ali"); + + // Save state for test runs + t.context.worker = worker; + t.context.accounts = { root, middlewares, ali }; +}); + +test.afterEach.always(async (t) => { + await t.context.worker.tearDown().catch((error) => { + console.log("Failed to tear down the worker:", error); + }); +}); + +test("The middleware logs with call functions", async (t) => { + const { ali, middlewares } = t.context.accounts; + + // Create the arguments which will be passed to the function. + const args = JSON.stringify({ id: "1", text: "hello" }); + // Call the function. + const result = await ali.callRaw(middlewares, "add", args); + // Extract the logs. + const { logs } = result.result.receipts_outcome[0].outcome; + // Create the expected logs. + const expectedLogs = [`Log from middleware: ${args}`]; + + t.deepEqual(logs, expectedLogs); +}); + +test("The middleware logs with view functions", async (t) => { + const { ali, middlewares } = t.context.accounts; + + // Create the arguments which will be passed to the function. + const args = JSON.stringify({ id: "1", accountId: "hello" }); + // Call the function. + const result = await ali.callRaw(middlewares, "get", args); + // Extract the logs. + const { logs } = result.result.receipts_outcome[0].outcome; + // Create the expected logs. + const expectedLogs = [`Log from middleware: ${args}`]; + + t.deepEqual(logs, expectedLogs); +}); + +test("The middleware logs with private functions", async (t) => { + const { ali, middlewares } = t.context.accounts; + + // Create the arguments which will be passed to the function. + const args = { id: "test", accountId: "tset" }; + // Call the function. + const result = await ali.callRaw(middlewares, "get_private", ""); + // Extract the logs. + const { logs } = result.result.receipts_outcome[0].outcome; + // Create the expected logs. + const expectedLogs = [`Log from middleware: ${args}`]; + + t.deepEqual(logs, expectedLogs); +}); diff --git a/tests/package.json b/tests/package.json index 5bc3d5fd0..93e17a788 100644 --- a/tests/package.json +++ b/tests/package.json @@ -28,6 +28,7 @@ "build:private": "near-sdk-js build src/decorators/private.ts build/private.wasm", "build:bigint-serialization": "near-sdk-js build src/bigint-serialization.ts build/bigint-serialization.wasm", "build:date-serialization": "near-sdk-js build src/date-serialization.ts build/date-serialization.wasm", + "build:middlewares": "near-sdk-js build src/middlewares.ts build/middlewares.wasm", "test": "ava", "test:context-api": "ava __tests__/test_context_api.ava.js", "test:math-api": "ava __tests__/test_math_api.ava.js", @@ -49,7 +50,8 @@ "test:private": "ava __tests__/decorators/private.ava.js", "test:bigint-serialization": "ava __tests__/test-bigint-serialization.ava.js", "test:date-serialization": "ava __tests__/test-date-serialization.ava.js", - "test:serialization": "ava __tests__/test-serialization.ava.js" + "test:serialization": "ava __tests__/test-serialization.ava.js", + "test:middlewares": "ava __tests__/test-middlewares.ava.js" }, "author": "Near Inc ", "license": "Apache-2.0", diff --git a/tests/src/middlewares.ts b/tests/src/middlewares.ts new file mode 100644 index 000000000..980b44da5 --- /dev/null +++ b/tests/src/middlewares.ts @@ -0,0 +1,45 @@ +import { NearBindgen, near, call, view } from "near-sdk-js"; +import { initialize, middleware } from "../../lib/near-bindgen"; + +@NearBindgen({ requireInit: true }) +export class Contract { + @initialize({}) + @middleware({ + call: (args) => near.log(`Log from middleware: ${args}`), + }) + // eslint-disable-next-line @typescript-eslint/no-empty-function + init({ randomData: _ }: { randomData: string }) {} + + @call({}) + @middleware({ + call: (args) => near.log(`Log from middleware: ${args}`), + }) + // eslint-disable-next-line @typescript-eslint/no-empty-function + add({ id: _, text: _t }: { id: string; text: string }) {} + + @view({}) + @middleware({ + call: (args) => near.log(`Log from middleware: ${args}`), + }) + get({ id, accountId }: { id: string; accountId: string }): { + id: string; + accountId: string; + } { + return { id: accountId, accountId: id }; + } + + @view({}) + get_private(): { id: string; accountId: string } { + return this.getFromPrivate({ id: "test", accountId: "tset" }); + } + + @middleware({ + call: (args) => near.log(`Log from middleware: ${args}`), + }) + getFromPrivate({ id, accountId }: { id: string; accountId: string }): { + id: string; + accountId: string; + } { + return { id, accountId }; + } +}