diff --git a/lib/build-tools/near-bindgen-exporter.js b/lib/build-tools/near-bindgen-exporter.js index 7abffb2d5..18b0910ea 100644 --- a/lib/build-tools/near-bindgen-exporter.js +++ b/lib/build-tools/near-bindgen-exporter.js @@ -1,4 +1,6 @@ import * as t from "@babel/types"; +import signal from "signale"; +const { Signale } = signal; /** * A list of supported method types/decorators. */ @@ -238,7 +240,9 @@ export default function () { // Insert the method override into the class declaration. path.insertAfter(createDeclaration(classNode.id, child.key.name, methodType)); if (verbose) { - console.log(`Babel ${child.key.name} method export done`); + new Signale({ + scope: "near-bindgen-exporter", + }).info(`Babel ${child.key.name} method export done.`); } } } diff --git a/lib/cli/cli.js b/lib/cli/cli.js index 132b6c92c..e10292dae 100755 --- a/lib/cli/cli.js +++ b/lib/cli/cli.js @@ -30,7 +30,7 @@ export async function buildCom(source, target, { verbose = false }) { const TARGET_DIR = dirname(target); const TARGET_EXT = target.split(".").pop(); const TARGET_FILE_NAME = basename(target, `.${TARGET_EXT}`); - const signale = new Signale({ scope: "build", interactive: true }); + const signale = new Signale({ scope: "build", interactive: !verbose }); if (TARGET_EXT !== "wasm") { signale.error(`Unsupported target ${TARGET_EXT}, make sure target ends with .wasm!`); process.exit(1); @@ -94,7 +94,7 @@ async function createHeaderFileWithQjsc(rollupTarget, qjscTarget, verbose = fals async function createMethodsHeaderFile(rollupTarget, verbose = false) { const buildPath = path.dirname(rollupTarget); if (verbose) { - console.log(rollupTarget); + new Signale({ scope: "method-header" }).info(rollupTarget); } const mod = await import(`${PROJECT_DIR}/${rollupTarget}`); const exportNames = Object.keys(mod); diff --git a/lib/cli/utils.js b/lib/cli/utils.js index 725944ba1..f2967311d 100644 --- a/lib/cli/utils.js +++ b/lib/cli/utils.js @@ -1,22 +1,25 @@ import childProcess from "child_process"; import { promisify } from "util"; +import signal from "signale"; +const { Signale } = signal; const exec = promisify(childProcess.exec); export async function executeCommand(command, verbose = false) { + const signale = new Signale({ scope: "exec", interactive: !verbose }); if (verbose) { - console.log(command); + signale.info(`Running command: ${command}`); } try { const { stdout, stderr } = await exec(command); if (stderr && verbose) { - console.error(stderr); + signale.error(stderr); } if (verbose) { - console.log(stdout); + signale.info(`Command output: ${stdout}`); } return stdout.trim(); } catch (error) { - console.log(error); + signale.error(error); process.exit(1); } } diff --git a/lib/near-bindgen.d.ts b/lib/near-bindgen.d.ts index fedc06b55..f5eda6284 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. + */ + (...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..e6aa747a5 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(...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/build-tools/near-bindgen-exporter.ts b/src/build-tools/near-bindgen-exporter.ts index bcafae19c..94a0a0b19 100644 --- a/src/build-tools/near-bindgen-exporter.ts +++ b/src/build-tools/near-bindgen-exporter.ts @@ -1,6 +1,9 @@ import { PluginPass } from "@babel/core"; import { Node, Visitor } from "@babel/traverse"; import * as t from "@babel/types"; +import signal from "signale"; + +const { Signale } = signal; /** * A list of supported method types/decorators. @@ -388,7 +391,9 @@ export default function (): { visitor: Visitor } { ); if (verbose) { - console.log(`Babel ${child.key.name} method export done`); + new Signale({ + scope: "near-bindgen-exporter", + }).info(`Babel ${child.key.name} method export done.`); } } } diff --git a/src/cli/cli.ts b/src/cli/cli.ts index 5f1a7f84c..67609daff 100755 --- a/src/cli/cli.ts +++ b/src/cli/cli.ts @@ -43,7 +43,7 @@ export async function buildCom( const TARGET_DIR = dirname(target); const TARGET_EXT = target.split(".").pop(); const TARGET_FILE_NAME = basename(target, `.${TARGET_EXT}`); - const signale = new Signale({ scope: "build", interactive: true }); + const signale = new Signale({ scope: "build", interactive: !verbose }); if (TARGET_EXT !== "wasm") { signale.error( @@ -143,7 +143,9 @@ async function createMethodsHeaderFile(rollupTarget: string, verbose = false) { const buildPath = path.dirname(rollupTarget); if (verbose) { - console.log(rollupTarget); + new Signale({scope: "method-header"}).info( +rollupTarget + ) } const mod = await import(`${PROJECT_DIR}/${rollupTarget}`); diff --git a/src/cli/utils.ts b/src/cli/utils.ts index 40d59b896..4c8f2e6e1 100644 --- a/src/cli/utils.ts +++ b/src/cli/utils.ts @@ -1,5 +1,8 @@ import childProcess from "child_process"; import { promisify } from "util"; +import signal from "signale" + +const {Signale} = signal; const exec = promisify(childProcess.exec); @@ -7,24 +10,26 @@ export async function executeCommand( command: string, verbose = false ): Promise { + const signale = new Signale({scope: "exec", interactive: !verbose}) + if (verbose) { - console.log(command); + signale.info(`Running command: ${command}`); } try { const { stdout, stderr } = await exec(command); if (stderr && verbose) { - console.error(stderr); + signale.error(stderr); } if (verbose) { - console.log(stdout); + signale.info(`Command output: ${stdout}`); } return stdout.trim(); } catch (error) { - console.log(error); + signale.error(error); process.exit(1); } } diff --git a/src/near-bindgen.ts b/src/near-bindgen.ts index fab5df0e4..c52392f88 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. + */ + (...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(...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..b0679c33e --- /dev/null +++ b/tests/__tests__/test-middlewares.ava.js @@ -0,0 +1,99 @@ +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 two middleware 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_two", args); + // Extract the logs. + const { logs } = result.result.receipts_outcome[0].outcome; + // Create the expected logs. + const expectedLogs = [`Log from middleware: ${args}`, "Second log!"]; + + 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..f6b966350 --- /dev/null +++ b/tests/src/middlewares.ts @@ -0,0 +1,49 @@ +import { NearBindgen, near, call, view } from "near-sdk-js"; +import { initialize, middleware } from "../../lib/near-bindgen"; + +@NearBindgen({ requireInit: true }) +export class Contract { + @initialize({}) + @middleware((...args) => near.log(`Log from middleware: ${args}`)) + // eslint-disable-next-line @typescript-eslint/no-empty-function + init({ randomData: _ }: { randomData: string }) {} + + @call({}) + @middleware((...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((...args) => near.log(`Log from middleware: ${args}`)) + get({ id, accountId }: { id: string; accountId: string }): { + id: string; + accountId: string; + } { + return { id: accountId, accountId: id }; + } + + @view({}) + @middleware( + (...args) => near.log(`Log from middleware: ${args}`), + () => near.log("Second log!") + ) + get_two({ 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((args) => near.log(`Log from middleware: ${args}`)) + getFromPrivate({ id, accountId }: { id: string; accountId: string }): { + id: string; + accountId: string; + } { + return { id, accountId }; + } +}